/*
 * Copyright (c) 2003-2012 Todd C. Miller <Todd.Miller@courtesan.com>
 * Copyright (c) 2011 Daniel Kopecek <dkopecek@redhat.com>
 *
 * This code is derived from software contributed by Aaron Spangler.
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <config.h>

#include <sys/types.h>
#include <sys/time.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <stdio.h>
#ifdef STDC_HEADERS
# include <stdlib.h>
# include <stddef.h>
#else
# ifdef HAVE_STDLIB_H
#  include <stdlib.h>
# endif
#endif /* STDC_HEADERS */
#ifdef HAVE_STRING_H
# include <string.h>
#endif /* HAVE_STRING_H */
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif /* HAVE_STRINGS_H */
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif /* HAVE_UNISTD_H */
#if TIME_WITH_SYS_TIME
# include <time.h>
#endif
#ifdef HAVE_DLOPEN
# include <dlfcn.h>
#else
# include "compat/dlfcn.h"
#endif
#include <ctype.h>
#include <pwd.h>
#include <grp.h>

#include <errno.h>
#include <stdint.h>

#include "sudoers.h"
#include "parse.h"
#include "lbuf.h"
#include "sudo_debug.h"

/* SSSD <--> SUDO interface - do not change */
struct sss_sudo_attr {
    char *name;
    char **values;
    unsigned int num_values;
};

struct sss_sudo_rule {
    unsigned int num_attrs;
    struct sss_sudo_attr *attrs;
};

struct sss_sudo_result {
    unsigned int num_rules;
    struct sss_sudo_rule *rules;
};

typedef int  (*sss_sudo_send_recv_t)(uid_t, const char*, const char*,
                                     uint32_t*, struct sss_sudo_result**);

typedef int  (*sss_sudo_send_recv_defaults_t)(uid_t, const char*, uint32_t*,
                                              char**, struct sss_sudo_result**);

typedef void (*sss_sudo_free_result_t)(struct sss_sudo_result*);

typedef int  (*sss_sudo_get_values_t)(struct sss_sudo_rule*, const char*,
                                      char***);

typedef void (*sss_sudo_free_values_t)(char**);

/* sudo_nss implementation */

struct sudo_sss_handle {
    char *domainname;
    struct passwd *pw;
    void *ssslib;
    sss_sudo_send_recv_t fn_send_recv;
    sss_sudo_send_recv_defaults_t fn_send_recv_defaults;
    sss_sudo_free_result_t fn_free_result;
    sss_sudo_get_values_t fn_get_values;
    sss_sudo_free_values_t fn_free_values;
};

static int sudo_sss_open(struct sudo_nss *nss);
static int sudo_sss_close(struct sudo_nss *nss);
static int sudo_sss_parse(struct sudo_nss *nss);
static void sudo_sss_parse_options(struct sudo_sss_handle *handle,
				   struct sss_sudo_rule *rule);
static int sudo_sss_setdefs(struct sudo_nss *nss);
static int sudo_sss_lookup(struct sudo_nss *nss, int ret, int pwflag);
static int sudo_sss_display_cmnd(struct sudo_nss *nss, struct passwd *pw);
static int sudo_sss_display_defaults(struct sudo_nss *nss, struct passwd *pw,
				     struct lbuf *lbuf);

static int sudo_sss_display_bound_defaults(struct sudo_nss *nss,
					   struct passwd *pw, struct lbuf *lbuf);

static int sudo_sss_display_privs(struct sudo_nss *nss, struct passwd *pw,
				  struct lbuf *lbuf);


static struct sss_sudo_result *sudo_sss_result_get(struct sudo_nss *nss,
						   struct passwd *pw,
						   uint32_t *state);

static void
sudo_sss_attrcpy(struct sss_sudo_attr *dst, const struct sss_sudo_attr *src)
{
     int i;
     debug_decl(sudo_sss_attrcpy, SUDO_DEBUG_SSSD)

     sudo_debug_printf(SUDO_DEBUG_DEBUG, "dst=%p, src=%p", dst, src);
     sudo_debug_printf(SUDO_DEBUG_INFO, "emalloc: cnt=%d", src->num_values);

     dst->name = estrdup(src->name);
     dst->num_values = src->num_values;
     dst->values = emalloc2(dst->num_values, sizeof(char *));

     for (i = 0; i < dst->num_values; ++i)
	  dst->values[i] = estrdup(src->values[i]);

     debug_return;
}

static void
sudo_sss_rulecpy(struct sss_sudo_rule *dst, const struct sss_sudo_rule *src)
{
     int i;
     debug_decl(sudo_sss_rulecpy, SUDO_DEBUG_SSSD)

     sudo_debug_printf(SUDO_DEBUG_DEBUG, "dst=%p, src=%p", dst, src);
     sudo_debug_printf(SUDO_DEBUG_INFO, "emalloc: cnt=%d", src->num_attrs);

     dst->num_attrs = src->num_attrs;
     dst->attrs = emalloc2(dst->num_attrs, sizeof(struct sss_sudo_attr));

     for (i = 0; i < dst->num_attrs; ++i)
	  sudo_sss_attrcpy(dst->attrs + i, src->attrs + i);

     debug_return;
}

#define _SUDO_SSS_FILTER_INCLUDE 0
#define _SUDO_SSS_FILTER_EXCLUDE 1

#define _SUDO_SSS_STATE_HOSTMATCH 0x01
#define _SUDO_SSS_STATE_USERMATCH 0x02

static struct sss_sudo_result *
sudo_sss_filter_result(struct sudo_sss_handle *handle,
    struct sss_sudo_result *in_res,
    int (*filterp)(struct sudo_sss_handle *, struct sss_sudo_rule *, void *),
    int act, void *filterp_arg)
{
    struct sss_sudo_result *out_res;
    int i, l, r;
    debug_decl(sudo_sss_filter_result, SUDO_DEBUG_SSSD)

    sudo_debug_printf(SUDO_DEBUG_DEBUG, "in_res=%p, count=%u, act=%s",
	in_res, in_res->num_rules,
	act == _SUDO_SSS_FILTER_EXCLUDE ? "EXCLUDE" : "INCLUDE");

    if (in_res == NULL)
	debug_return_ptr(NULL);

    sudo_debug_printf(SUDO_DEBUG_DEBUG, "emalloc: cnt=%d", in_res->num_rules);

    out_res = emalloc(sizeof(struct sss_sudo_result));
    out_res->rules = in_res->num_rules > 0 ?
	emalloc2(in_res->num_rules, sizeof(struct sss_sudo_rule)) : NULL;
    out_res->num_rules = 0;

    for (i = l = 0; i < in_res->num_rules; ++i) {
	 r = filterp(handle, in_res->rules + i, filterp_arg);

	 if (( r && act == _SUDO_SSS_FILTER_INCLUDE) ||
	     (!r && act == _SUDO_SSS_FILTER_EXCLUDE)) {
	    sudo_debug_printf(SUDO_DEBUG_DEBUG,
		"COPY (%s): %p[%u] => %p[%u] (= %p)",
		act == _SUDO_SSS_FILTER_EXCLUDE ? "not excluded" : "included",
		in_res->rules, i, out_res->rules, l, in_res->rules + i);

	    sudo_sss_rulecpy(out_res->rules + l, in_res->rules + i);
	    ++l;
	}
    }

    if (l < in_res->num_rules) {
	sudo_debug_printf(SUDO_DEBUG_DEBUG,
	    "reallocating result: %p (count: %u -> %u)", out_res->rules,
	    in_res->num_rules, l);
	out_res->rules = erealloc3(out_res->rules, l, sizeof(struct sss_sudo_rule));
    }

    out_res->num_rules = l;

    debug_return_ptr(out_res);
}

struct sudo_nss sudo_nss_sss = {
    &sudo_nss_sss,
    NULL,
    sudo_sss_open,
    sudo_sss_close,
    sudo_sss_parse,
    sudo_sss_setdefs,
    sudo_sss_lookup,
    sudo_sss_display_cmnd,
    sudo_sss_display_defaults,
    sudo_sss_display_bound_defaults,
    sudo_sss_display_privs
};

/* sudo_nss implementation */
// ok
static int sudo_sss_open(struct sudo_nss *nss)
{
    struct sudo_sss_handle *handle;
    static const char path[] = _PATH_SSSD_LIB"/libsss_sudo.so";
    debug_decl(sudo_sss_open, SUDO_DEBUG_SSSD);

    /* Create a handle container. */
    handle = emalloc(sizeof(struct sudo_sss_handle));

    /* Load symbols */
    handle->ssslib = dlopen(path, RTLD_LAZY);
    if (handle->ssslib == NULL) {
	warningx(_("Unable to dlopen %s: %s"), path, dlerror());
	warningx(_("Unable to initialize SSS source. Is SSSD installed on your machine?"));
	debug_return_int(EFAULT);
    }

    handle->fn_send_recv = dlsym(handle->ssslib, "sss_sudo_send_recv");
    if (handle->fn_send_recv == NULL) {
	warningx(_("unable to find symbol \"%s\" in %s"), path,
	   "sss_sudo_send_recv");
	debug_return_int(EFAULT);
    }

    handle->fn_send_recv_defaults =
	dlsym(handle->ssslib, "sss_sudo_send_recv_defaults");
    if (handle->fn_send_recv_defaults == NULL) {
	warningx(_("unable to find symbol \"%s\" in %s"), path,
	   "sss_sudo_send_recv_defaults");
	debug_return_int(EFAULT);
    }

    handle->fn_free_result = dlsym(handle->ssslib, "sss_sudo_free_result");
    if (handle->fn_free_result == NULL) {
	warningx(_("unable to find symbol \"%s\" in %s"), path,
	   "sss_sudo_free_result");
	debug_return_int(EFAULT);
    }

    handle->fn_get_values = dlsym(handle->ssslib, "sss_sudo_get_values");
    if (handle->fn_get_values == NULL) {
	warningx(_("unable to find symbol \"%s\" in %s"), path,
	   "sss_sudo_get_values");
	debug_return_int(EFAULT);
    }

    handle->fn_free_values = dlsym(handle->ssslib, "sss_sudo_free_values");
    if (handle->fn_free_values == NULL) {
	warningx(_("unable to find symbol \"%s\" in %s"), path,
	   "sss_sudo_free_values");
	debug_return_int(EFAULT);
    }

    handle->domainname = NULL;
    handle->pw = sudo_user.pw;
    nss->handle = handle;

    sudo_debug_printf(SUDO_DEBUG_DEBUG, "handle=%p", handle);

    debug_return_int(0);
}

// ok
static int sudo_sss_close(struct sudo_nss *nss)
{
    struct sudo_sss_handle *handle;
    debug_decl(sudo_sss_close, SUDO_DEBUG_SSSD);

    if (nss && nss->handle) {
	handle = nss->handle;
	dlclose(handle->ssslib);
    }

    efree(nss->handle);
    debug_return_int(0);
}

// ok
static int sudo_sss_parse(struct sudo_nss *nss)
{
    debug_decl(sudo_sss_parse, SUDO_DEBUG_SSSD);
    debug_return_int(0);
}

static int sudo_sss_setdefs(struct sudo_nss *nss)
{
    struct sudo_sss_handle *handle = nss->handle;

    struct sss_sudo_result *sss_result;
    struct sss_sudo_rule   *sss_rule;
    uint32_t sss_error;
    int i;
    debug_decl(sudo_sss_setdefs, SUDO_DEBUG_SSSD);

    if (handle == NULL)
	debug_return_int(-1);

    sudo_debug_printf(SUDO_DEBUG_DIAG, "Looking for cn=defaults");

    if (handle->fn_send_recv_defaults(handle->pw->pw_uid, handle->pw->pw_name,
				      &sss_error, &handle->domainname,
				      &sss_result) != 0) {
	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "handle->fn_send_recv_defaults: != 0, sss_error=%u", sss_error);
	debug_return_int(-1);
    }

    if (sss_error == ENOENT) {
	sudo_debug_printf(SUDO_DEBUG_INFO, "The user was not found in SSSD.");
	debug_return_int(-1);
    } else if(sss_error != 0) {
	sudo_debug_printf(SUDO_DEBUG_INFO, "sss_error=%u\n", sss_error);
	debug_return_int(-1);
    }

    for (i = 0; i < sss_result->num_rules; ++i) {
	 sudo_debug_printf(SUDO_DEBUG_DIAG,
	    "Parsing cn=defaults, %d/%d", i, sss_result->num_rules);
	 sss_rule = sss_result->rules + i;
	 sudo_sss_parse_options(handle, sss_rule);
    }

    handle->fn_free_result(sss_result);
    debug_return_int(0);
}

static int sudo_sss_checkpw(struct sudo_nss *nss, struct passwd *pw)
{
    struct sudo_sss_handle *handle = nss->handle;
    debug_decl(sudo_sss_checkpw, SUDO_DEBUG_SSSD);

    if (pw->pw_name != handle->pw->pw_name ||
	pw->pw_uid  != handle->pw->pw_uid) {
	sudo_debug_printf(SUDO_DEBUG_DIAG,
	    "Requested name or uid don't match the initial once, reinitializing...");
	handle->pw = pw;

	if (sudo_sss_setdefs(nss) != 0)
	    debug_return_int(-1);
    }

     debug_return_int(0);
}

static int
sudo_sss_check_runas_user(struct sudo_sss_handle *handle, struct sss_sudo_rule *sss_rule)
{
    char **val_array = NULL;
    char *val;
    int ret = false, i;
    debug_decl(sudo_sss_check_runas_user, SUDO_DEBUG_SSSD);

    if (!runas_pw)
	debug_return_int(UNSPEC);

    /* get the runas user from the entry */
    switch (handle->fn_get_values(sss_rule, "sudoRunAsUser", &val_array)) {
    case 0:
	break;
    case ENOENT:
	sudo_debug_printf(SUDO_DEBUG_INFO, "No result. Trying old style (sudoRunAs)");

	/* try old style */
	switch (handle->fn_get_values(sss_rule, "sudoRunAs", &val_array)) {
	case 0:
	    break;
	case ENOENT:
	    sudo_debug_printf(SUDO_DEBUG_INFO, "No result. Matching against runas_default");
	    /*
	     * If there are no runas entries, match runas_default against
	     * what the user specified on the command line.
	     */
	    return !strcasecmp(runas_pw->pw_name, def_runas_default);
	default:
	    sudo_debug_printf(SUDO_DEBUG_INFO, "handle->fn_get_values(sudoRunAs): != 0");
	    debug_return_int(UNSPEC);
	}
	break;
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO, "handle->fn_get_values(sudoRunAsUser): != 0");
	debug_return_int(UNSPEC);
    }

    /*
     * BUG:
     *
     * if runas is not specified on the command line, the only information
     * as to which user to run as is in the runas_default option.  We should
     * check to see if we have the local option present.  Unfortunately we
     * don't parse these options until after this routine says yes or no.
     * The query has already returned, so we could peek at the attribute
     * values here though.
     *
     * For now just require users to always use -u option unless its set
     * in the global defaults. This behaviour is no different than the global
     * /etc/sudoers.
     *
     * Sigh - maybe add this feature later
     */

    /* walk through values returned, looking for a match */
    for (i = 0; val_array[i] != NULL && !ret; ++i) {
	val = val_array[i];

	sudo_debug_printf(SUDO_DEBUG_DEBUG, "val[%d]=%s", i, val);

	switch (val[0]) {
	case '+':
	    sudo_debug_printf(SUDO_DEBUG_DEBUG, "netgr_");
	    if (netgr_matches(val, NULL, NULL, runas_pw->pw_name)) {
		sudo_debug_printf(SUDO_DEBUG_DEBUG, "=> match");
		ret = true;
	    }
	    break;
	case '%':
	    sudo_debug_printf(SUDO_DEBUG_DEBUG, "usergr_");
	    if (usergr_matches(val, runas_pw->pw_name, runas_pw)) {
		sudo_debug_printf(SUDO_DEBUG_DEBUG, "=> match");
		ret = true;
	    }
	    break;
	case 'A':
	    if (strcmp(val, "ALL") == 0) {
		sudo_debug_printf(SUDO_DEBUG_DEBUG, "ALL => match");
		ret = true;
		break;
	    }
	    /* FALLTHROUGH */
	    sudo_debug_printf(SUDO_DEBUG_DEBUG, "FALLTHROUGH");
	default:
	    if (strcasecmp(val, runas_pw->pw_name) == 0) {
		sudo_debug_printf(SUDO_DEBUG_DEBUG,
		    "%s == %s (pw_name) => match", val, runas_pw->pw_name);
		ret = true;
	    }
	    break;
	}

	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "sssd/ldap sudoRunAsUser '%s' ... %s", val, ret ? "MATCH!" : "not");
    }

    handle->fn_free_values(val_array); /* cleanup */

    debug_return_int(ret);
}

static int
sudo_sss_check_runas_group(struct sudo_sss_handle *handle, struct sss_sudo_rule *rule)
{
    char **val_array = NULL;
    char *val;
    int ret = false, i;
    debug_decl(sudo_sss_check_runas_group, SUDO_DEBUG_SSSD);

    /* runas_gr is only set if the user specified the -g flag */
    if (!runas_gr)
	debug_return_int(UNSPEC);

    /* get the values from the entry */
    switch (handle->fn_get_values(rule, "sudoRunAsGroup", &val_array)) {
    case 0:
	break;
    case ENOENT:
	sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	debug_return_int(false);
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "handle->fn_get_values(sudoRunAsGroup): != 0");
	debug_return_int(UNSPEC);
    }

    /* walk through values returned, looking for a match */
    for (i = 0; val_array[i] != NULL; ++i) {
	val = val_array[i];
	sudo_debug_printf(SUDO_DEBUG_DEBUG, "val[%d]=%s", i, val);

	if (strcmp(val, "ALL") == 0 || group_matches(val, runas_gr))
	    ret = true;

	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "sssd/ldap sudoRunAsGroup '%s' ... %s", val, ret ? "MATCH!" : "not");
    }

    handle->fn_free_values(val_array);

    debug_return_int(ret);
}

/*
 * Walk through search results and return true if we have a runas match,
 * else false.  RunAs info is optional.
 */
static int
sudo_sss_check_runas(struct sudo_sss_handle *handle, struct sss_sudo_rule *rule)
{
    int ret;
    debug_decl(sudo_sss_check_runas, SUDO_DEBUG_SSSD);

    if (rule == NULL)
	 debug_return_int(false);

    ret = sudo_sss_check_runas_user(handle, rule) != false &&
	 sudo_sss_check_runas_group(handle, rule) != false;

    debug_return_int(ret);
}

static int
sudo_sss_check_host(struct sudo_sss_handle *handle, struct sss_sudo_rule *rule)
{
    char **val_array, *val;
    int ret = false, i;
    debug_decl(sudo_sss_check_host, SUDO_DEBUG_SSSD);

    if (rule == NULL)
	debug_return_int(ret);

    /* get the values from the rule */
    switch (handle->fn_get_values(rule, "sudoHost", &val_array))
    {
    case 0:
	break;
    case ENOENT:
	sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	debug_return_int(false);
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO, "handle->fn_get_values(sudoHost): != 0");
	debug_return_int(ret);
    }

    /* walk through values */
    for (i = 0; val_array[i] != NULL; ++i) {
	val = val_array[i];
	sudo_debug_printf(SUDO_DEBUG_DEBUG, "val[%d]=%s", i, val);

	/* match any or address or netgroup or hostname */
	if (!strcmp(val, "ALL") || addr_matches(val) ||
	    netgr_matches(val, user_host, user_shost, NULL) ||
	    hostname_matches(user_shost, user_host, val))
	    ret = true;

	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "sssd/ldap sudoHost '%s' ... %s", val, ret ? "MATCH!" : "not");
    }

    handle->fn_free_values(val_array);

    debug_return_int(ret);
}

static int
sudo_sss_result_filterp(struct sudo_sss_handle *handle,
    struct sss_sudo_rule *rule, void *unused)
{
    (void)unused;
    debug_decl(sudo_sss_result_filterp, SUDO_DEBUG_SSSD);

    if (sudo_sss_check_host(handle, rule))
	debug_return_int(1);
    else
	debug_return_int(0);
}

static struct sss_sudo_result *
sudo_sss_result_get(struct sudo_nss *nss, struct passwd *pw, uint32_t *state)
{
    struct sudo_sss_handle *handle = nss->handle;
    struct sss_sudo_result *u_sss_result, *f_sss_result;
    uint32_t sss_error = 0, ret;
    debug_decl(sudo_sss_result_get, SUDO_DEBUG_SSSD);

    if (sudo_sss_checkpw(nss, pw) != 0)
	debug_return_ptr(NULL);

    sudo_debug_printf(SUDO_DEBUG_DIAG, "  username=%s", handle->pw->pw_name);
    sudo_debug_printf(SUDO_DEBUG_DIAG, "domainname=%s", handle->domainname);

    u_sss_result = f_sss_result = NULL;

    ret = handle->fn_send_recv(handle->pw->pw_uid, handle->pw->pw_name,
	handle->domainname, &sss_error, &u_sss_result);

    switch (ret) {
    case 0:
	switch (sss_error) {
	case 0:
	    if (u_sss_result != NULL) {
		if (state != NULL) {
		    sudo_debug_printf(SUDO_DEBUG_DEBUG, "state |= USERMATCH");
		    *state |= _SUDO_SSS_STATE_USERMATCH;
		}
		sudo_debug_printf(SUDO_DEBUG_INFO, "Received %u rule(s)",
		    u_sss_result->num_rules);
	    } else {
		sudo_debug_printf(SUDO_DEBUG_INFO,
		    "Internal error: u_sss_result == NULL && sss_error == 0");
		debug_return_ptr(NULL);
	    }
	    break;
	case ENOENT:
	    sudo_debug_printf(SUDO_DEBUG_INFO, "The user was not found in SSSD.");
	default:
	    sudo_debug_printf(SUDO_DEBUG_INFO, "sss_error=%u\n", sss_error);
	    debug_return_ptr(NULL);
	}
	break;
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "handle->fn_send_recv: != 0: ret=%d", ret);
	debug_return_ptr(NULL);
    }

    f_sss_result = sudo_sss_filter_result(handle, u_sss_result,
	sudo_sss_result_filterp, _SUDO_SSS_FILTER_INCLUDE, NULL);

    if (f_sss_result != NULL) {
	if (f_sss_result->num_rules > 0) {
	    if (state != NULL) {
		sudo_debug_printf(SUDO_DEBUG_DEBUG, "state |= HOSTMATCH");
		*state |= _SUDO_SSS_STATE_HOSTMATCH;
	    }
	}
    }

    sudo_debug_printf(SUDO_DEBUG_DEBUG,
	"u_sss_result=(%p, %u) => f_sss_result=(%p, %u)", u_sss_result,
	u_sss_result->num_rules, f_sss_result, f_sss_result->num_rules);

    handle->fn_free_result(u_sss_result);

    debug_return_ptr(f_sss_result);
}

/*
 * Search for boolean "option" in sudoOption.
 * Returns true if found and allowed, false if negated, else UNSPEC.
 */
static int
sudo_sss_check_bool(struct sudo_sss_handle *handle, struct sss_sudo_rule *rule,
    char *option)
{
    char ch, *var, **val_array = NULL;
    int i, ret = UNSPEC;
    debug_decl(sudo_sss_check_bool, SUDO_DEBUG_SSSD);

    if (rule == NULL)
	debug_return_int(ret);

    switch (handle->fn_get_values(rule, "sudoOption", &val_array)) {
    case 0:
	break;
    case ENOENT:
	sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	debug_return_int(ret);
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO, "handle->fn_get_values: != 0");
	debug_return_int(ret);
    }

    /* walk through options */
    for (i = 0; val_array[i] != NULL; ++i) {
	var = val_array[i];
	sudo_debug_printf(SUDO_DEBUG_INFO, "sssd/ldap sudoOption: '%s'", var);

	if ((ch = *var) == '!')
	    var++;
	if (strcmp(var, option) == 0)
	    ret = (ch != '!');
    }

    handle->fn_free_values(val_array);

    debug_return_int(ret);
}

/*
 * Walk through search results and return true if we have a command match,
 * false if disallowed and UNSPEC if not matched.
 */
static int
sudo_sss_check_command(struct sudo_sss_handle *handle,
    struct sss_sudo_rule *rule, int *setenv_implied)
{
    char **val_array = NULL, *val;
    char *allowed_cmnd, *allowed_args;
    int i, foundbang, ret = UNSPEC;
    debug_decl(sudo_sss_check_command, SUDO_DEBUG_SSSD);

    if (rule == NULL)
	debug_return_int(ret);

    switch (handle->fn_get_values(rule, "sudoCommand", &val_array)) {
    case 0:
	break;
    case ENOENT:
	sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	debug_return_int(ret);
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO, "handle->fn_get_values: != 0");
	debug_return_int(ret);
    }

    for (i = 0; val_array[i] != NULL && ret != false; ++i) {
	val = val_array[i];

	sudo_debug_printf(SUDO_DEBUG_DEBUG, "val[%d]=%s", i, val);

	/* Match against ALL ? */
	if (!strcmp(val, "ALL")) {
	    ret = true;
	    if (setenv_implied != NULL)
		*setenv_implied = true;
	    sudo_debug_printf(SUDO_DEBUG_INFO,
		"sssd/ldap sudoCommand '%s' ... MATCH!", val);
	    continue;
	}

	/* check for !command */
	if (*val == '!') {
	    foundbang = true;
	    allowed_cmnd = estrdup(1 + val);	/* !command */
	} else {
	    foundbang = false;
	    allowed_cmnd = estrdup(val);	/* command */
	}

	/* split optional args away from command */
	allowed_args = strchr(allowed_cmnd, ' ');
	if (allowed_args)
	    *allowed_args++ = '\0';

	/* check the command like normal */
	if (command_matches(allowed_cmnd, allowed_args)) {
	    /*
	     * If allowed (no bang) set ret but keep on checking.
	     * If disallowed (bang), exit loop.
	     */
	    ret = foundbang ? false : true;
	}

	sudo_debug_printf(SUDO_DEBUG_INFO, "sssd/ldap sudoCommand '%s' ... %s",
	    val, ret == true ? "MATCH!" : "not");
	efree(allowed_cmnd);	/* cleanup */
    }

    handle->fn_free_values(val_array); /* more cleanup */

    debug_return_int(ret);
}

static void
sudo_sss_parse_options(struct sudo_sss_handle *handle, struct sss_sudo_rule *rule)
{
    int i;
    char op, *v, *val;
    char **val_array = NULL;
    debug_decl(sudo_sss_parse_options, SUDO_DEBUG_SSSD);

    if (rule == NULL)
	debug_return;

    switch (handle->fn_get_values(rule, "sudoOption", &val_array)) {
    case 0:
	break;
    case ENOENT:
	sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	debug_return;
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO, "handle->fn_get_values(sudoOption): != 0");
	debug_return;
    }

    /* walk through options */
    for (i = 0; val_array[i] != NULL; i++) {
	sudo_debug_printf(SUDO_DEBUG_INFO, "sssd/ldap sudoOption: '%s'",
	 val_array[i]);
	v = estrdup(val_array[i]);

	/* check for equals sign past first char */
	val = strchr(v, '=');
	if (val > v) {
	    *val++ = '\0';	/* split on = and truncate var */
	    op = *(val - 2);	/* peek for += or -= cases */
	    if (op == '+' || op == '-') {
		*(val - 2) = '\0';	/* found, remove extra char */
		/* case var+=val or var-=val */
		set_default(v, val, (int) op);
	    } else {
		/* case var=val */
		set_default(v, val, true);
	    }
	} else if (*v == '!') {
	    /* case !var Boolean False */
	    set_default(v + 1, NULL, false);
	} else {
	    /* case var Boolean True */
	    set_default(v, NULL, true);
	}
	efree(v);
    }

    handle->fn_free_values(val_array);
    debug_return;
}

static int
sudo_sss_lookup(struct sudo_nss *nss, int ret, int pwflag)
{
    int rc, setenv_implied;

    struct sudo_sss_handle *handle = nss->handle;
    struct sss_sudo_result *sss_result = NULL;
    struct sss_sudo_rule   *rule;
    uint32_t i, state = 0;
    debug_decl(sudo_sss_lookup, SUDO_DEBUG_SSSD);

    /* Fetch list of sudoRole entries that match user and host. */
    sss_result = sudo_sss_result_get(nss, sudo_user.pw, &state);

    /*
     * The following queries are only determine whether or not a
     * password is required, so the order of the entries doesn't matter.
     */
    if (pwflag) {
	int doauth = UNSPEC;
	int matched = UNSPEC;
	enum def_tuple pwcheck =
	    (pwflag == -1) ? never : sudo_defs_table[pwflag].sd_un.tuple;

	sudo_debug_printf(SUDO_DEBUG_INFO, "perform search for pwflag %d", pwflag);
	if (sss_result != NULL) {
	    for (i = 0; i < sss_result->num_rules; i++) {
		rule = sss_result->rules + i;
		if ((pwcheck == any && doauth != false) ||
		    (pwcheck == all && doauth == false)) {
		    doauth = sudo_sss_check_bool(handle, rule, "authenticate");
		}
		/* Only check the command when listing another user. */
		if (user_uid == 0 || list_pw == NULL ||
		    user_uid == list_pw->pw_uid ||
		    sudo_sss_check_command(handle, rule, NULL)) {
		    matched = true;
		    break;
		}
	    }
	}
	if (matched || user_uid == 0) {
	    SET(ret, VALIDATE_OK);
	    CLR(ret, VALIDATE_NOT_OK);
	    if (def_authenticate) {
		switch (pwcheck) {
		    case always:
			SET(ret, FLAG_CHECK_USER);
			break;
		    case all:
		    case any:
			if (doauth == false)
			    def_authenticate = false;
			break;
		    case never:
			def_authenticate = false;
			break;
		    default:
			break;
		}
	    }
	}
	goto done;
    }

    sudo_debug_printf(SUDO_DEBUG_DIAG,
	"searching SSSD/LDAP for sudoers entries");

    setenv_implied = false;
    if (sss_result != NULL) {
	for (i = 0; i < sss_result->num_rules; i++) {
	    rule = sss_result->rules + i;
	    if (!sudo_sss_check_runas(handle, rule))
		continue;
	    rc = sudo_sss_check_command(handle, rule, &setenv_implied);
	    if (rc != UNSPEC) {
		/* We have a match. */
		sudo_debug_printf(SUDO_DEBUG_DIAG, "Command %sallowed",
		    rc == true ? "" : "NOT ");
		if (rc == true) {
		    sudo_debug_printf(SUDO_DEBUG_DEBUG, "SSSD rule: %p", rule);
		    /* Apply entry-specific options. */
		    if (setenv_implied)
			def_setenv = true;
		    sudo_sss_parse_options(handle, rule);
#ifdef HAVE_SELINUX
		    /* Set role and type if not specified on command line. */
		    if (user_role == NULL)
			user_role = def_role;
		    if (user_type == NULL)
			user_type = def_type;
#endif /* HAVE_SELINUX */
		    SET(ret, VALIDATE_OK);
		    CLR(ret, VALIDATE_NOT_OK);
		} else {
		    SET(ret, VALIDATE_NOT_OK);
		    CLR(ret, VALIDATE_OK);
		}
		break;
	    }
	}
    }
done:
    sudo_debug_printf(SUDO_DEBUG_DIAG, "Done with LDAP searches");

    if (!ISSET(ret, VALIDATE_OK)) {
	/* No matching entries. */
	if (pwflag && list_pw == NULL)
	    SET(ret, FLAG_NO_CHECK);
    }

    if (state & _SUDO_SSS_STATE_USERMATCH)
	CLR(ret, FLAG_NO_USER);
    if (state & _SUDO_SSS_STATE_HOSTMATCH)
	CLR(ret, FLAG_NO_HOST);

    sudo_debug_printf(SUDO_DEBUG_DEBUG, "sudo_sss_lookup(%d)=0x%02x",
     pwflag, ret);

    debug_return_int(ret);
}

static int
sudo_sss_display_cmnd(struct sudo_nss *nss, struct passwd *pw)
{
    struct sudo_sss_handle *handle = nss->handle;
    struct sss_sudo_result *sss_result = NULL;
    struct sss_sudo_rule *rule;
    int i, found = false;
    debug_decl(sudo_sss_display_cmnd, SUDO_DEBUG_SSSD);

    if (handle == NULL)
	goto done;

    if (sudo_sss_checkpw(nss, pw) != 0)
	debug_return_int(-1);

    /*
     * The sudo_sss_result_get() function returns all nodes that match
     * the user and the host.
     */
    sudo_debug_printf(SUDO_DEBUG_DIAG, "sssd/ldap search for command list");
    sss_result = sudo_sss_result_get(nss, pw, NULL);

    if (sss_result == NULL)
	goto done;

    for (i = 0; i < sss_result->num_rules; i++) {
	rule = sss_result->rules + i;
	if (sudo_sss_check_command(handle, rule, NULL) &&
	    sudo_sss_check_runas(handle, rule)) {
	    found = true;
	    goto done;
	}
    }

done:
    if (found)
	printf("%s%s%s\n", safe_cmnd ? safe_cmnd : user_cmnd,
	    user_args ? " " : "", user_args ? user_args : "");

    if (sss_result != NULL)
	handle->fn_free_result(sss_result);

    debug_return_int(!found);
}

static int
sudo_sss_display_defaults(struct sudo_nss *nss, struct passwd *pw,
    struct lbuf *lbuf)
{
    struct sudo_sss_handle *handle = nss->handle;

    struct sss_sudo_rule *rule;
    struct sss_sudo_result *sss_result = NULL;

    uint32_t sss_error = 0;

    char *prefix, *val, **val_array = NULL;
    int count = 0, i, j;

    debug_decl(sudo_sss_display_defaults, SUDO_DEBUG_SSSD);

    if (handle == NULL)
	goto done;

    if (handle->fn_send_recv_defaults(pw->pw_uid, pw->pw_name,
				    &sss_error, &handle->domainname,
				    &sss_result) != 0) {
	sudo_debug_printf(SUDO_DEBUG_INFO, "handle->fn_send_recv_defaults: !=0, sss_error=%u", sss_error);
	goto done;
    }

    if (sss_error == ENOENT) {
	sudo_debug_printf(SUDO_DEBUG_INFO, "The user was not found in SSSD.");
	goto done;
    } else if(sss_error != 0) {
	sudo_debug_printf(SUDO_DEBUG_INFO, "sss_error=%u\n", sss_error);
	goto done;
    }

    handle->pw = pw;

    for (i = 0; i < sss_result->num_rules; ++i) {
	rule = sss_result->rules + i;

	switch (handle->fn_get_values(rule, "sudoOption", &val_array)) {
	case 0:
	    break;
	case ENOENT:
	    sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	    continue;
	default:
	    sudo_debug_printf(SUDO_DEBUG_INFO, "handle->fn_get_values: != 0");
	    continue;
	}

	if (lbuf->len == 0 || isspace((unsigned char)lbuf->buf[lbuf->len - 1]))
	    prefix = "    ";
	else
	    prefix = ", ";

	for (j = 0; val_array[j] != NULL; ++j) {
	    val = val_array[j];
	    lbuf_append(lbuf, "%s%s", prefix, val);
	    prefix = ", ";
	    count++;
	}

	handle->fn_free_values(val_array);
	val_array = NULL;
    }

    handle->fn_free_result(sss_result);
done:
    debug_return_int(count);
}

// ok
static int
sudo_sss_display_bound_defaults(struct sudo_nss *nss,
    struct passwd *pw, struct lbuf *lbuf)
{
    debug_decl(sudo_sss_display_bound_defaults, SUDO_DEBUG_SSSD);
    debug_return_int(0);
}

static int
sudo_sss_display_entry_long(struct sudo_sss_handle *handle,
    struct sss_sudo_rule *rule, struct lbuf *lbuf)
{
    char **val_array = NULL;
    int count = 0, i;
    debug_decl(sudo_sss_display_entry_long, SUDO_DEBUG_SSSD);

    /* get the RunAsUser Values from the entry */
    lbuf_append(lbuf, "    RunAsUsers: ");
    switch (handle->fn_get_values(rule, "sudoRunAsUser", &val_array)) {
    case 0:
	for (i = 0; val_array[i] != NULL; ++i)
	    lbuf_append(lbuf, "%s%s", i != 0 ? ", " : "", val_array[i]);
	handle->fn_free_values(val_array);
	break;
    case ENOENT:
	switch (handle->fn_get_values(rule, "sudoRunAs", &val_array)) {
	case 0:
	    for (i = 0; val_array[i] != NULL; ++i)
		 lbuf_append(lbuf, "%s%s", i != 0 ? ", " : "", val_array[i]);
	    handle->fn_free_values(val_array);
	    break;
	case ENOENT:
	    sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	    lbuf_append(lbuf, "%s", def_runas_default);
	    break;
	default:
	    sudo_debug_printf(SUDO_DEBUG_INFO, "handle->fn_get_values(sudoRunAs): != 0");
	    debug_return_int(count);
	}
	break;
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO, "handle->fn_get_values(sudoRunAsUser): != 0");
	debug_return_int(count);
    }
    lbuf_append(lbuf, "\n");

    /* get the RunAsGroup Values from the entry */
    switch (handle->fn_get_values(rule, "sudoRunAsGroup", &val_array)) {
    case 0:
	lbuf_append(lbuf, "    RunAsGroups: ");
	for (i = 0; val_array[i] != NULL; ++i)
	     lbuf_append(lbuf, "%s%s", i != 0 ? ", " : "", val_array[i]);
	handle->fn_free_values(val_array);
	lbuf_append(lbuf, "\n");
	break;
    case ENOENT:
	sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	break;
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "handle->fn_get_values(sudoRunAsGroup): != 0");
	debug_return_int(count);
    }

    /* get the Option Values from the entry */
    switch (handle->fn_get_values(rule, "sudoOption", &val_array)) {
    case 0:
	lbuf_append(lbuf, "    Options: ");
	for (i = 0; val_array[i] != NULL; ++i)
	     lbuf_append(lbuf, "%s%s", i != 0 ? ", " : "", val_array[i]);
	handle->fn_free_values(val_array);
	lbuf_append(lbuf, "\n");
	break;
    case ENOENT:
	sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	break;
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO, "handle->fn_get_values(sudoOption): != 0");
	debug_return_int(count);
    }

    /* Get the command values from the entry. */
    switch (handle->fn_get_values(rule, "sudoCommand", &val_array)) {
    case 0:
	lbuf_append(lbuf, _("    Commands:\n"));
	for (i = 0; val_array[i] != NULL; ++i) {
	     lbuf_append(lbuf, "\t%s\n", val_array[i]);
	     count++;
	}
	handle->fn_free_values(val_array);
	break;
    case ENOENT:
	sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	break;
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "handle->fn_get_values(sudoCommand): != 0");
	debug_return_int(count);
    }

    debug_return_int(count);
}

static int
sudo_sss_display_entry_short(struct sudo_sss_handle *handle,
    struct sss_sudo_rule *rule, struct lbuf *lbuf)
{
    char **val_array = NULL;
    int count = 0, i;
    debug_decl(sudo_sss_display_entry_short, SUDO_DEBUG_SSSD);

    lbuf_append(lbuf, "    (");

    /* get the RunAsUser Values from the entry */
    switch (handle->fn_get_values(rule, "sudoRunAsUser", &val_array)) {
    case 0:
	for (i = 0; val_array[i] != NULL; ++i)
	     lbuf_append(lbuf, "%s%s", i != 0 ? ", " : "", val_array[i]);
	handle->fn_free_values(val_array);
	break;
    case ENOENT:
	sudo_debug_printf(SUDO_DEBUG_INFO, "No result. Trying old style (sudoRunAs).");
	/* try old style */
	switch (handle->fn_get_values(rule, "sudoRunAs", &val_array)) {
	case 0:
	    for (i = 0; val_array[i] != NULL; ++i)
		 lbuf_append(lbuf, "%s%s", i != 0 ? ", " : "", val_array[i]);
	    handle->fn_free_values(val_array);
	    break;
	case ENOENT:
	    sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	    lbuf_append(lbuf, "%s", def_runas_default);
	    break;
	default:
	    sudo_debug_printf(SUDO_DEBUG_INFO,
		"handle->fn_get_values(sudoRunAs): != 0");
	    debug_return_int(count);
	}
	break;
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "handle->fn_get_values(sudoRunAsUser): != 0");
	debug_return_int(count);
    }

    /* get the RunAsGroup Values from the entry */
    switch (handle->fn_get_values(rule, "sudoRunAsGroup", &val_array)) {
    case 0:
	lbuf_append(lbuf, " : ");
	for (i = 0; val_array[i] != NULL; ++i)
	     lbuf_append(lbuf, "%s%s", i != 0 ? ", " : "", val_array[i]);
	handle->fn_free_values(val_array);
	break;
    case ENOENT:
	sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	break;
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO, "handle->fn_get_values(sudoRunAsGroup): != 0");
	debug_return_int(count);
    }

    lbuf_append(lbuf, ") ");

    /* get the Option Values from the entry */
    switch (handle->fn_get_values(rule, "sudoOption", &val_array)) {
    case 0:
	for (i = 0; val_array[i] != NULL; ++i) {
	    char *cp = val_array[i];
	    if (*cp == '!')
		cp++;
	    if (strcmp(cp, "authenticate") == 0)
		lbuf_append(lbuf, val_array[i][0] == '!' ?
			    "NOPASSWD: " : "PASSWD: ");
	    else if (strcmp(cp, "noexec") == 0)
		lbuf_append(lbuf, val_array[i][0] == '!' ?
			    "EXEC: " : "NOEXEC: ");
	    else if (strcmp(cp, "setenv") == 0)
		lbuf_append(lbuf, val_array[i][0] == '!' ?
			    "NOSETENV: " : "SETENV: ");
	}
	handle->fn_free_values(val_array);
	break;
    case ENOENT:
	sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	break;
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "handle->fn_get_values(sudoOption): != 0");
	debug_return_int(count);
    }

    /* get the Command Values from the entry */
    switch (handle->fn_get_values(rule, "sudoCommand", &val_array)) {
    case 0:
	for (i = 0; val_array[i] != NULL; ++i) {
	    lbuf_append(lbuf, "%s%s", i != 0 ? ", " : "", val_array[i]);
	    count++;
	}
	handle->fn_free_values(val_array);
	break;
    case ENOENT:
	sudo_debug_printf(SUDO_DEBUG_INFO, "No result.");
	break;
    default:
	sudo_debug_printf(SUDO_DEBUG_INFO,
	    "handle->fn_get_values(sudoCommand): != 0");
	debug_return_int(count);
    }
    lbuf_append(lbuf, "\n");

    debug_return_int(count);
}

static int
sudo_sss_display_privs(struct sudo_nss *nss, struct passwd *pw,
    struct lbuf *lbuf)
{
    struct sudo_sss_handle *handle = nss->handle;

    struct sss_sudo_result *sss_result = NULL;
    struct sss_sudo_rule *rule;
    unsigned int i, count = 0;
    debug_decl(sudo_sss_display_privs, SUDO_DEBUG_SSSD);

    if (handle == NULL)
	debug_return_int(-1);
    if (sudo_sss_checkpw(nss, pw) != 0)
	debug_return_int(-1);

    sudo_debug_printf(SUDO_DEBUG_INFO, "sssd/ldap search for command list");

    sss_result = sudo_sss_result_get(nss, pw, NULL);

    if (sss_result == NULL)
	debug_return_int(count);

    /* Display all matching entries. */
    for (i = 0; i < sss_result->num_rules; ++i) {
	rule = sss_result->rules + i;
	if (long_list)
	    count += sudo_sss_display_entry_long(handle, rule, lbuf);
	else
	    count += sudo_sss_display_entry_short(handle, rule, lbuf);
    }

    if (sss_result != NULL)
	handle->fn_free_result(sss_result);

    debug_return_int(count);
}
