/*
 * Copyright (c) 2003-2010 Todd C. Miller <Todd.Miller@courtesan.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 */
#if defined(HAVE_MALLOC_H) && !defined(STDC_HEADERS)
# include <malloc.h>
#endif /* HAVE_MALLOC_H && !STDC_HEADERS */
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif /* HAVE_UNISTD_H */
#include <ctype.h>
#include <pwd.h>
#include <grp.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#ifdef HAVE_LBER_H
# include <lber.h>
#endif
#include <ldap.h>
#if defined(HAVE_LDAP_SSL_H)
# include <ldap_ssl.h>
#elif defined(HAVE_MPS_LDAP_SSL_H)
# include <mps/ldap_ssl.h>
#endif
#ifdef HAVE_LDAP_SASL_INTERACTIVE_BIND_S
# ifdef HAVE_SASL_SASL_H
#  include <sasl/sasl.h>
# else
#  include <sasl.h>
# endif
# if HAVE_GSS_KRB5_CCACHE_NAME
#  if defined(HAVE_GSSAPI_GSSAPI_KRB5_H)
#   include <gssapi/gssapi.h>
#   include <gssapi/gssapi_krb5.h>
#  elif defined(HAVE_GSSAPI_GSSAPI_H)
#   include <gssapi/gssapi.h>
#  else
#   include <gssapi.h>
#  endif
# endif
#endif

#include "sudo.h"
#include "parse.h"
#include "lbuf.h"

#ifndef LDAP_OPT_SUCCESS
# define LDAP_OPT_SUCCESS LDAP_SUCCESS
#endif

#ifndef LDAPS_PORT
# define LDAPS_PORT 636
#endif

#if defined(HAVE_LDAP_SASL_INTERACTIVE_BIND_S) && !defined(LDAP_SASL_QUIET)
# define LDAP_SASL_QUIET	0
#endif

#ifndef HAVE_LDAP_UNBIND_EXT_S
#define ldap_unbind_ext_s(a, b, c)	ldap_unbind_s(a)
#endif

#ifndef HAVE_LDAP_SEARCH_EXT_S
#define ldap_search_ext_s(a, b, c, d, e, f, g, h, i, j, k)		\
	ldap_search_s(a, b, c, d, e, f, k)
#endif

#define LDAP_FOREACH(var, ld, res)					\
    for ((var) = ldap_first_entry((ld), (res));				\
	(var) != NULL;							\
	(var) = ldap_next_entry((ld), (var)))

#define	DPRINTF(args, level)	if (ldap_conf.debug >= level) warningx args

#define CONF_BOOL	0
#define CONF_INT	1
#define CONF_STR	2
#define CONF_LIST_STR	4

#define SUDO_LDAP_SSL		1
#define SUDO_LDAP_STARTTLS	2

struct ldap_config_table {
    const char *conf_str;	/* config file string */
    short type;			/* CONF_BOOL, CONF_INT, CONF_STR */
    short connected;		/* connection-specific value? */
    int opt_val;		/* LDAP_OPT_* (or -1 for sudo internal) */
    void *valp;			/* pointer into ldap_conf */
};

struct ldap_config_list_str {
    struct ldap_config_list_str *next;
    char val[1];
};

/* ldap configuration structure */
static struct ldap_config {
    int port;
    int version;
    int debug;
    int ldap_debug;
    int tls_checkpeer;
    int timelimit;
    int bind_timelimit;
    int use_sasl;
    int rootuse_sasl;
    int ssl_mode;
    char *host;
    struct ldap_config_list_str *uri;
    char *binddn;
    char *bindpw;
    char *rootbinddn;
    struct ldap_config_list_str *base;
    char *ssl;
    char *tls_cacertfile;
    char *tls_cacertdir;
    char *tls_random_file;
    char *tls_cipher_suite;
    char *tls_certfile;
    char *tls_keyfile;
    char *sasl_auth_id;
    char *rootsasl_auth_id;
    char *sasl_secprops;
    char *krb5_ccname;
} ldap_conf;

static struct ldap_config_table ldap_conf_table[] = {
    { "sudoers_debug", CONF_INT, FALSE, -1, &ldap_conf.debug },
    { "host", CONF_STR, FALSE, -1, &ldap_conf.host },
    { "port", CONF_INT, FALSE, -1, &ldap_conf.port },
    { "ssl", CONF_STR, FALSE, -1, &ldap_conf.ssl },
    { "sslpath", CONF_STR, FALSE, -1, &ldap_conf.tls_certfile },
    { "uri", CONF_LIST_STR, FALSE, -1, &ldap_conf.uri },
#ifdef LDAP_OPT_DEBUG_LEVEL
    { "debug", CONF_INT, FALSE, LDAP_OPT_DEBUG_LEVEL, &ldap_conf.ldap_debug },
#endif
#ifdef LDAP_OPT_PROTOCOL_VERSION
    { "ldap_version", CONF_INT, TRUE, LDAP_OPT_PROTOCOL_VERSION,
	&ldap_conf.version },
#endif
#ifdef LDAP_OPT_X_TLS_REQUIRE_CERT
    { "tls_checkpeer", CONF_BOOL, FALSE, LDAP_OPT_X_TLS_REQUIRE_CERT,
	&ldap_conf.tls_checkpeer },
#else
    { "tls_checkpeer", CONF_BOOL, FALSE, -1, &ldap_conf.tls_checkpeer },
#endif
#ifdef LDAP_OPT_X_TLS_CACERTFILE
    { "tls_cacertfile", CONF_STR, FALSE, LDAP_OPT_X_TLS_CACERTFILE,
	&ldap_conf.tls_cacertfile },
    { "tls_cacert", CONF_STR, FALSE, LDAP_OPT_X_TLS_CACERTFILE,
	&ldap_conf.tls_cacertfile },
#endif
#ifdef LDAP_OPT_X_TLS_CACERTDIR
    { "tls_cacertdir", CONF_STR, FALSE, LDAP_OPT_X_TLS_CACERTDIR,
	&ldap_conf.tls_cacertdir },
#endif
#ifdef LDAP_OPT_X_TLS_RANDOM_FILE
    { "tls_randfile", CONF_STR, FALSE, LDAP_OPT_X_TLS_RANDOM_FILE,
	&ldap_conf.tls_random_file },
#endif
#ifdef LDAP_OPT_X_TLS_CIPHER_SUITE
    { "tls_ciphers", CONF_STR, FALSE, LDAP_OPT_X_TLS_CIPHER_SUITE,
	&ldap_conf.tls_cipher_suite },
#endif
#ifdef LDAP_OPT_X_TLS_CERTFILE
    { "tls_cert", CONF_STR, FALSE, LDAP_OPT_X_TLS_CERTFILE,
	&ldap_conf.tls_certfile },
#else
    { "tls_cert", CONF_STR, FALSE, -1, &ldap_conf.tls_certfile },
#endif
#ifdef LDAP_OPT_X_TLS_KEYFILE
    { "tls_key", CONF_STR, FALSE, LDAP_OPT_X_TLS_KEYFILE,
	&ldap_conf.tls_keyfile },
#else
    { "tls_key", CONF_STR, FALSE, -1, &ldap_conf.tls_keyfile },
#endif
#ifdef LDAP_OPT_NETWORK_TIMEOUT
    { "bind_timelimit", CONF_INT, TRUE, -1 /* needs timeval, set manually */,
	&ldap_conf.bind_timelimit },
#elif defined(LDAP_X_OPT_CONNECT_TIMEOUT)
    { "bind_timelimit", CONF_INT, TRUE, LDAP_X_OPT_CONNECT_TIMEOUT,
	&ldap_conf.bind_timelimit },
#endif
    { "timelimit", CONF_INT, TRUE, LDAP_OPT_TIMELIMIT, &ldap_conf.timelimit },
    { "binddn", CONF_STR, FALSE, -1, &ldap_conf.binddn },
    { "bindpw", CONF_STR, FALSE, -1, &ldap_conf.bindpw },
    { "rootbinddn", CONF_STR, FALSE, -1, &ldap_conf.rootbinddn },
    { "sudoers_base", CONF_LIST_STR, FALSE, -1, &ldap_conf.base },
#ifdef HAVE_LDAP_SASL_INTERACTIVE_BIND_S
    { "use_sasl", CONF_BOOL, FALSE, -1, &ldap_conf.use_sasl },
    { "sasl_auth_id", CONF_STR, FALSE, -1, &ldap_conf.sasl_auth_id },
    { "rootuse_sasl", CONF_BOOL, FALSE, -1, &ldap_conf.rootuse_sasl },
    { "rootsasl_auth_id", CONF_STR, FALSE, -1, &ldap_conf.rootsasl_auth_id },
# ifdef LDAP_OPT_X_SASL_SECPROPS
    { "sasl_secprops", CONF_STR, TRUE, LDAP_OPT_X_SASL_SECPROPS,
	&ldap_conf.sasl_secprops },
# endif
    { "krb5_ccname", CONF_STR, FALSE, -1, &ldap_conf.krb5_ccname },
#endif /* HAVE_LDAP_SASL_INTERACTIVE_BIND_S */
    { NULL }
};

/* sudo_nss implementation */
static int sudo_ldap_open __P((struct sudo_nss *nss));
static int sudo_ldap_close __P((struct sudo_nss *nss));
static int sudo_ldap_parse __P((struct sudo_nss *nss));
static int sudo_ldap_setdefs __P((struct sudo_nss *nss));
static int sudo_ldap_lookup __P((struct sudo_nss *nss, int ret, int pwflag));
static int sudo_ldap_display_cmnd __P((struct sudo_nss *nss,
    struct passwd *pw));
static int sudo_ldap_display_defaults __P((struct sudo_nss *nss,
    struct passwd *pw, struct lbuf *lbuf));
static int sudo_ldap_display_bound_defaults __P((struct sudo_nss *nss,
    struct passwd *pw, struct lbuf *lbuf));
static int sudo_ldap_display_privs __P((struct sudo_nss *nss,
    struct passwd *pw, struct lbuf *lbuf));

struct sudo_nss sudo_nss_ldap = {
    &sudo_nss_ldap,
    NULL,
    sudo_ldap_open,
    sudo_ldap_close,
    sudo_ldap_parse,
    sudo_ldap_setdefs,
    sudo_ldap_lookup,
    sudo_ldap_display_cmnd,
    sudo_ldap_display_defaults,
    sudo_ldap_display_bound_defaults,
    sudo_ldap_display_privs
};

#ifdef HAVE_LDAP_CREATE
/*
 * Rebuild the hosts list and include a specific port for each host.
 * ldap_create() does not take a default port parameter so we must
 * append one if we want something other than LDAP_PORT.
 */
static void
sudo_ldap_conf_add_ports()
{

    char *host, *port, defport[13];
    char hostbuf[LINE_MAX * 2];

    hostbuf[0] = '\0';
    if (snprintf(defport, sizeof(defport), ":%d", ldap_conf.port) >= sizeof(defport))
	errorx(1, "sudo_ldap_conf_add_ports: port too large");

    for ((host = strtok(ldap_conf.host, " \t")); host; (host = strtok(NULL, " \t"))) {
	if (hostbuf[0] != '\0') {
	    if (strlcat(hostbuf, " ", sizeof(hostbuf)) >= sizeof(hostbuf))
		goto toobig;
	}

	if (strlcat(hostbuf, host, sizeof(hostbuf)) >= sizeof(hostbuf))
	    goto toobig;
	/* Append port if there is not one already. */
	if ((port = strrchr(host, ':')) == NULL ||
	    !isdigit((unsigned char)port[1])) {
	    if (strlcat(hostbuf, defport, sizeof(hostbuf)) >= sizeof(hostbuf))
		goto toobig;
	}
    }

    free(ldap_conf.host);
    ldap_conf.host = estrdup(hostbuf);
    return;

toobig:
    errorx(1, "sudo_ldap_conf_add_ports: out of space expanding hostbuf");
}
#endif

#ifndef HAVE_LDAP_INITIALIZE
/*
 * For each uri, convert to host:port pairs.  For ldaps:// enable SSL
 * Accepts: uris of the form ldap:/// or ldap://hostname:portnum/
 * where the trailing slash is optional.
 */
static int
sudo_ldap_parse_uri(uri_list)
    const struct ldap_config_list_str *uri_list;
{
    char *buf, *uri, *host, *cp, *port;
    char hostbuf[LINE_MAX];
    int nldap = 0, nldaps = 0;
    int rc = -1;

    do {
	buf = estrdup(uri_list->val);
	hostbuf[0] = '\0';
	for ((uri = strtok(buf, " \t")); uri != NULL; (uri = strtok(NULL, " \t"))) {
	    if (strncasecmp(uri, "ldap://", 7) == 0) {
		nldap++;
		host = uri + 7;
	    } else if (strncasecmp(uri, "ldaps://", 8) == 0) {
		nldaps++;
		host = uri + 8;
	    } else {
		warningx("unsupported LDAP uri type: %s", uri);
		goto done;
	    }

	    /* trim optional trailing slash */
	    if ((cp = strrchr(host, '/')) != NULL && cp[1] == '\0') {
		*cp = '\0';
	    }

	    if (hostbuf[0] != '\0') {
		if (strlcat(hostbuf, " ", sizeof(hostbuf)) >= sizeof(hostbuf))
		    goto toobig;
	    }

	    if (*host == '\0')
		host = "localhost";		/* no host specified, use localhost */

	    if (strlcat(hostbuf, host, sizeof(hostbuf)) >= sizeof(hostbuf))
		goto toobig;

	    /* If using SSL and no port specified, add port 636 */
	    if (nldaps) {
		if ((port = strrchr(host, ':')) == NULL ||
		    !isdigit((unsigned char)port[1]))
		    if (strlcat(hostbuf, ":636", sizeof(hostbuf)) >= sizeof(hostbuf))
			goto toobig;
	    }
	}
	if (hostbuf[0] == '\0') {
	    warningx("invalid uri: %s", uri_list);
	    goto done;
	}

	if (nldaps != 0) {
	    if (nldap != 0) {
		warningx("cannot mix ldap and ldaps URIs");
		goto done;
	    }
	    if (ldap_conf.ssl_mode == SUDO_LDAP_STARTTLS) {
		warningx("cannot mix ldaps and starttls");
		goto done;
	    }
	    ldap_conf.ssl_mode = SUDO_LDAP_SSL;
	}

	free(ldap_conf.host);
	ldap_conf.host = estrdup(hostbuf);
	efree(buf);
    } while ((uri_list = uri_list->next));

    buf = NULL;
    rc = 0;

done:
    efree(buf);
    return(rc);

toobig:
    errorx(1, "sudo_ldap_parse_uri: out of space building hostbuf");
}
#else
static char *
sudo_ldap_join_uri(uri_list)
    struct ldap_config_list_str *uri_list;
{
    struct ldap_config_list_str *uri;
    size_t len = 0;
    char *buf, *cp;

    /* Usually just a single entry. */
    if (uri_list->next == NULL)
	return(estrdup(uri_list->val));

    for (uri = uri_list; uri != NULL; uri = uri->next) {
	len += strlen(uri->val) + 1;
    }
    buf = cp = emalloc(len);
    buf[0] = '\0';
    for (uri = uri_list; uri != NULL; uri = uri->next) {
	cp += strlcpy(cp, uri->val, len - (cp - buf));
	*cp++ = ' ';
    }
    cp[-1] = '\0';
    return(buf);
}
#endif /* HAVE_LDAP_INITIALIZE */

static int
sudo_ldap_init(ldp, host, port)
    LDAP **ldp;
    const char *host;
    int port;
{
    LDAP *ld = NULL;
    int rc = LDAP_CONNECT_ERROR;

#ifdef HAVE_LDAPSSL_INIT
    if (ldap_conf.ssl_mode == SUDO_LDAP_SSL) {
	DPRINTF(("ldapssl_clientauth_init(%s, %s)",
	    ldap_conf.tls_certfile ? ldap_conf.tls_certfile : "NULL",
	    ldap_conf.tls_keyfile ? ldap_conf.tls_keyfile : "NULL"), 2);
	rc = ldapssl_clientauth_init(ldap_conf.tls_certfile, NULL,
	    ldap_conf.tls_keyfile != NULL, ldap_conf.tls_keyfile, NULL);
	/*
	 * Mozilla-derived SDKs have a bug starting with version 5.0
	 * where the path can no longer be a file name and must be a dir.
	 */
	if (rc != LDAP_SUCCESS) {
	    char *cp;
	    if (ldap_conf.tls_certfile) {
		cp = strrchr(ldap_conf.tls_certfile, '/');
		if (cp != NULL && strncmp(cp + 1, "cert", 4) == 0)
		    *cp = '\0';
	    }
	    if (ldap_conf.tls_keyfile) {
		cp = strrchr(ldap_conf.tls_keyfile, '/');
		if (cp != NULL && strncmp(cp + 1, "key", 3) == 0)
		    *cp = '\0';
	    }
	    DPRINTF(("ldapssl_clientauth_init(%s, %s)",
		ldap_conf.tls_certfile ? ldap_conf.tls_certfile : "NULL",
		ldap_conf.tls_keyfile ? ldap_conf.tls_keyfile : "NULL"), 2);
	    rc = ldapssl_clientauth_init(ldap_conf.tls_certfile, NULL,
		ldap_conf.tls_keyfile != NULL, ldap_conf.tls_keyfile, NULL);
	    if (rc != LDAP_SUCCESS) {
		warningx("unable to initialize SSL cert and key db: %s",
		    ldapssl_err2string(rc));
		goto done;
	    }
	}

	DPRINTF(("ldapssl_init(%s, %d, 1)", host, port), 2);
	if ((ld = ldapssl_init(host, port, 1)) != NULL)
	    rc = LDAP_SUCCESS;
    } else
#endif
    {
#ifdef HAVE_LDAP_CREATE
	DPRINTF(("ldap_create()"), 2);
	if ((rc = ldap_create(&ld)) != LDAP_SUCCESS)
	    goto done;
	DPRINTF(("ldap_set_option(LDAP_OPT_HOST_NAME, %s)", host), 2);
	rc = ldap_set_option(ld, LDAP_OPT_HOST_NAME, host);
#else
	DPRINTF(("ldap_init(%s, %d)", host, port), 2);
	if ((ld = ldap_init(host, port)) != NULL)
	    rc = LDAP_SUCCESS;
#endif
    }

done:
    *ldp = ld;
    return(rc);
}

/*
 * Walk through search results and return TRUE if we have a matching
 * netgroup, else FALSE.
 */
static int
sudo_ldap_check_user_netgroup(ld, entry, user)
    LDAP *ld;
    LDAPMessage *entry;
    char *user;
{
    struct berval **bv, **p;
    char *val;
    int ret = FALSE;

    if (!entry)
	return(ret);

    /* get the values from the entry */
    bv = ldap_get_values_len(ld, entry, "sudoUser");
    if (bv == NULL)
	return(ret);

    /* walk through values */
    for (p = bv; *p != NULL && !ret; p++) {
	val = (*p)->bv_val;
	/* match any */
	if (netgr_matches(val, NULL, NULL, user))
	    ret = TRUE;
	DPRINTF(("ldap sudoUser netgroup '%s' ... %s", val,
	    ret ? "MATCH!" : "not"), 2);
    }

    ldap_value_free_len(bv);	/* cleanup */

    return(ret);
}

/*
 * Walk through search results and return TRUE if we have a
 * host match, else FALSE.
 */
static int
sudo_ldap_check_host(ld, entry)
    LDAP *ld;
    LDAPMessage *entry;
{
    struct berval **bv, **p;
    char *val;
    int ret = FALSE;

    if (!entry)
	return(ret);

    /* get the values from the entry */
    bv = ldap_get_values_len(ld, entry, "sudoHost");
    if (bv == NULL)
	return(ret);

    /* walk through values */
    for (p = bv; *p != NULL && !ret; p++) {
	val = (*p)->bv_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;
	DPRINTF(("ldap sudoHost '%s' ... %s", val,
	    ret ? "MATCH!" : "not"), 2);
    }

    ldap_value_free_len(bv);	/* cleanup */

    return(ret);
}

static int
sudo_ldap_check_runas_user(ld, entry)
    LDAP *ld;
    LDAPMessage *entry;
{
    struct berval **bv, **p;
    char *val;
    int ret = FALSE;

    if (!runas_pw)
	return(UNSPEC);

    /* get the runas user from the entry */
    bv = ldap_get_values_len(ld, entry, "sudoRunAsUser");
    if (bv == NULL)
	bv = ldap_get_values_len(ld, entry, "sudoRunAs"); /* old style */

    /*
     * 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
     */

    /*
     * If there are no runas entries, match runas_default against
     * what the user specified on the command line.
     */
    if (bv == NULL)
	return(!strcasecmp(runas_pw->pw_name, def_runas_default));

    /* walk through values returned, looking for a match */
    for (p = bv; *p != NULL && !ret; p++) {
	val = (*p)->bv_val;
	switch (val[0]) {
	case '+':
	    if (netgr_matches(val, NULL, NULL, runas_pw->pw_name))
		ret = TRUE;
	    break;
	case '%':
	    if (usergr_matches(val, runas_pw->pw_name, runas_pw))
		ret = TRUE;
	    break;
	case 'A':
	    if (strcmp(val, "ALL") == 0) {
		ret = TRUE;
		break;
	    }
	    /* FALLTHROUGH */
	default:
	    if (strcasecmp(val, runas_pw->pw_name) == 0)
		ret = TRUE;
	    break;
	}
	DPRINTF(("ldap sudoRunAsUser '%s' ... %s", val,
	    ret ? "MATCH!" : "not"), 2);
    }

    ldap_value_free_len(bv);	/* cleanup */

    return(ret);
}

static int
sudo_ldap_check_runas_group(ld, entry)
    LDAP *ld;
    LDAPMessage *entry;
{
    struct berval **bv, **p;
    char *val;
    int ret = FALSE;

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

    /* get the values from the entry */
    bv = ldap_get_values_len(ld, entry, "sudoRunAsGroup");
    if (bv == NULL)
	return(ret);

    /* walk through values returned, looking for a match */
    for (p = bv; *p != NULL && !ret; p++) {
	val = (*p)->bv_val;
	if (strcmp(val, "ALL") == 0 || group_matches(val, runas_gr))
	    ret = TRUE;
	DPRINTF(("ldap sudoRunAsGroup '%s' ... %s", val,
	    ret ? "MATCH!" : "not"), 2);
    }

    ldap_value_free_len(bv);	/* cleanup */

    return(ret);
}

/*
 * Walk through search results and return TRUE if we have a runas match,
 * else FALSE.  RunAs info is optional.
 */
static int
sudo_ldap_check_runas(ld, entry)
    LDAP *ld;
    LDAPMessage *entry;
{
    int ret;

    if (!entry)
	return(FALSE);

    ret = sudo_ldap_check_runas_user(ld, entry) != FALSE &&
	sudo_ldap_check_runas_group(ld, entry) != FALSE;

    return(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_ldap_check_command(ld, entry, setenv_implied)
    LDAP *ld;
    LDAPMessage *entry;
    int *setenv_implied;
{
    struct berval **bv, **p;
    char *allowed_cmnd, *allowed_args, *val;
    int foundbang, ret = UNSPEC;

    if (!entry)
	return(ret);

    bv = ldap_get_values_len(ld, entry, "sudoCommand");
    if (bv == NULL)
	return(ret);

    for (p = bv; *p != NULL && ret != FALSE; p++) {
	val = (*p)->bv_val;
	/* Match against ALL ? */
	if (!strcmp(val, "ALL")) {
	    ret = TRUE;
	    if (setenv_implied != NULL)
		*setenv_implied = TRUE;
	    DPRINTF(("ldap sudoCommand '%s' ... MATCH!", val), 2);
	    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;
	}
	DPRINTF(("ldap sudoCommand '%s' ... %s", val,
	    ret == TRUE ? "MATCH!" : "not"), 2);

	efree(allowed_cmnd);	/* cleanup */
    }

    ldap_value_free_len(bv);	/* more cleanup */

    return(ret);
}

/*
 * Search for boolean "option" in sudoOption.
 * Returns TRUE if found and allowed, FALSE if negated, else UNSPEC.
 */
static int
sudo_ldap_check_bool(ld, entry, option)
    LDAP *ld;
    LDAPMessage *entry;
    char *option;
{
    struct berval **bv, **p;
    char ch, *var;
    int ret = UNSPEC;

    if (entry == NULL)
	return(UNSPEC);

    bv = ldap_get_values_len(ld, entry, "sudoOption");
    if (bv == NULL)
	return(ret);

    /* walk through options */
    for (p = bv; *p != NULL; p++) {
	var = (*p)->bv_val;;
	DPRINTF(("ldap sudoOption: '%s'", var), 2);

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

    ldap_value_free_len(bv);

    return(ret);
}

/*
 * Read sudoOption and modify the defaults as we go.  This is used once
 * from the cn=defaults entry and also once when a final sudoRole is matched.
 */
static void
sudo_ldap_parse_options(ld, entry)
    LDAP *ld;
    LDAPMessage *entry;
{
    struct berval **bv, **p;
    char op, *var, *val;

    if (entry == NULL)
	return;

    bv = ldap_get_values_len(ld, entry, "sudoOption");
    if (bv == NULL)
	return;

    /* walk through options */
    for (p = bv; *p != NULL; p++) {
	var = estrdup((*p)->bv_val);
	DPRINTF(("ldap sudoOption: '%s'", var), 2);

	/* check for equals sign past first char */
	val = strchr(var, '=');
	if (val > var) {
	    *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(var, val, (int) op);
	    } else {
		/* case var=val */
		set_default(var, val, TRUE);
	    }
	} else if (*var == '!') {
	    /* case !var Boolean False */
	    set_default(var + 1, NULL, FALSE);
	} else {
	    /* case var Boolean True */
	    set_default(var, NULL, TRUE);
	}
	efree(var);
    }

    ldap_value_free_len(bv);
}

/*
 * builds together a filter to check against ldap
 */
static char *
sudo_ldap_build_pass1(pw)
    struct passwd *pw;
{
    struct group *grp;
    size_t sz;
    char *buf;
    int i;

    /* Start with (|(sudoUser=USERNAME)(sudoUser=ALL)) + NUL */
    sz = 29 + strlen(pw->pw_name);

    /* Add space for groups */
    if ((grp = sudo_getgrgid(pw->pw_gid)) != NULL)
	sz += 12 + strlen(grp->gr_name);	/* primary group */
    for (i = 0; i < user_ngroups; i++) {
	if (user_groups[i] == pw->pw_gid)
	    continue;
	if ((grp = sudo_getgrgid(user_groups[i])) != NULL)
	    sz += 12 + strlen(grp->gr_name);	/* supplementary group */
    }
    buf = emalloc(sz);

    /* Global OR + sudoUser=user_name filter */
    (void) strlcpy(buf, "(|(sudoUser=", sz);
    (void) strlcat(buf, pw->pw_name, sz);
    (void) strlcat(buf, ")", sz);

    /* Append primary group */
    if ((grp = sudo_getgrgid(pw->pw_gid)) != NULL) {
	(void) strlcat(buf, "(sudoUser=%", sz);
	(void) strlcat(buf, grp->gr_name, sz);
	(void) strlcat(buf, ")", sz);
    }

    /* Append supplementary groups */
    for (i = 0; i < user_ngroups; i++) {
	if (user_groups[i] == pw->pw_gid)
	    continue;
	if ((grp = sudo_getgrgid(user_groups[i])) != NULL) {
	    (void) strlcat(buf, "(sudoUser=%", sz);
	    (void) strlcat(buf, grp->gr_name, sz);
	    (void) strlcat(buf, ")", sz);
	}
    }

    /* Add ALL to list and end the global OR */
    if (strlcat(buf, "(sudoUser=ALL))", sz) >= sz)
	errorx(1, "sudo_ldap_build_pass1 allocation mismatch");

    return(buf);
}

/*
 * Map yes/true/on to TRUE, no/false/off to FALSE, else -1
 */
static int
_atobool(s)
    const char *s;
{
    switch (*s) {
	case 'y':
	case 'Y':
	    if (strcasecmp(s, "yes") == 0)
		return(TRUE);
	    break;
	case 't':
	case 'T':
	    if (strcasecmp(s, "true") == 0)
		return(TRUE);
	    break;
	case 'o':
	case 'O':
	    if (strcasecmp(s, "on") == 0)
		return(TRUE);
	    if (strcasecmp(s, "off") == 0)
		return(FALSE);
	    break;
	case 'n':
	case 'N':
	    if (strcasecmp(s, "no") == 0)
		return(FALSE);
	    break;
	case 'f':
	case 'F':
	    if (strcasecmp(s, "false") == 0)
		return(FALSE);
	    break;
    }
    return(-1);
}

static void
sudo_ldap_read_secret(path)
    const char *path;
{
    FILE *fp;
    char buf[LINE_MAX], *cp;

    if ((fp = fopen(_PATH_LDAP_SECRET, "r")) != NULL) {
	if (fgets(buf, sizeof(buf), fp) != NULL) {
	    if ((cp = strchr(buf, '\n')) != NULL)
		*cp = '\0';
	    /* copy to bindpw and binddn */
	    efree(ldap_conf.bindpw);
	    ldap_conf.bindpw = estrdup(buf);
	    efree(ldap_conf.binddn);
	    ldap_conf.binddn = ldap_conf.rootbinddn;
	    ldap_conf.rootbinddn = NULL;
	}
	fclose(fp);
    }
}

static int
sudo_ldap_read_config()
{
    FILE *fp;
    char *cp, *keyword, *value;
    struct ldap_config_table *cur;

    /* defaults */
    ldap_conf.version = 3;
    ldap_conf.port = -1;
    ldap_conf.tls_checkpeer = -1;
    ldap_conf.timelimit = -1;
    ldap_conf.bind_timelimit = -1;
    ldap_conf.use_sasl = -1;
    ldap_conf.rootuse_sasl = -1;

    if ((fp = fopen(_PATH_LDAP_CONF, "r")) == NULL)
	return(FALSE);

    while ((cp = sudo_parseln(fp)) != NULL) {
	if (*cp == '\0')
	    continue;		/* skip empty line */

	/* split into keyword and value */
	keyword = cp;
	while (*cp && !isblank((unsigned char) *cp))
	    cp++;
	if (*cp)
	    *cp++ = '\0';	/* terminate keyword */

	/* skip whitespace before value */
	while (isblank((unsigned char) *cp))
	    cp++;
	value = cp;

	/* Look up keyword in config table. */
	for (cur = ldap_conf_table; cur->conf_str != NULL; cur++) {
	    if (strcasecmp(keyword, cur->conf_str) == 0) {
		switch (cur->type) {
		case CONF_BOOL:
		    *(int *)(cur->valp) = _atobool(value);
		    break;
		case CONF_INT:
		    *(int *)(cur->valp) = atoi(value);
		    break;
		case CONF_STR:
		    efree(*(char **)(cur->valp));
		    *(char **)(cur->valp) = estrdup(value);
		    break;
		case CONF_LIST_STR:
		    {
			struct ldap_config_list_str **p;
			size_t len = strlen(value);

			if (len > 0) {
			    p = (struct ldap_config_list_str **)cur->valp;
			    while (*p != NULL)
			    	p = &(*p)->next;
			    *p = emalloc(sizeof(struct ldap_config_list_str) + len);
			    memcpy((*p)->val, value, len + 1);
			    (*p)->next = NULL;
			}
		    }
		    break;
		}
		break;
	    }
	}
    }
    fclose(fp);

    if (!ldap_conf.host)
	ldap_conf.host = estrdup("localhost");

    if (ldap_conf.bind_timelimit > 0)
	ldap_conf.bind_timelimit *= 1000;	/* convert to ms */

    if (ldap_conf.debug > 1) {
	fprintf(stderr, "LDAP Config Summary\n");
	fprintf(stderr, "===================\n");
	if (ldap_conf.uri) {
	    struct ldap_config_list_str *uri = ldap_conf.uri;

	    do {
		fprintf(stderr, "uri              %s\n", uri->val);
	    } while ((uri = uri->next) != NULL);
	} else {
	    fprintf(stderr, "host             %s\n", ldap_conf.host ?
		ldap_conf.host : "(NONE)");
	    fprintf(stderr, "port             %d\n", ldap_conf.port);
	}
	fprintf(stderr, "ldap_version     %d\n", ldap_conf.version);
	if (ldap_conf.base) {
	    struct ldap_config_list_str *base = ldap_conf.base;

	    do {
		fprintf(stderr, "sudoers_base     %s\n", base->val);
	    } while ((base = base->next) != NULL);
	} else {
	    fprintf(stderr, "sudoers_base     %s\n",
		"(NONE) <---Sudo will ignore ldap)");
	}
	fprintf(stderr, "binddn           %s\n", ldap_conf.binddn ?
	    ldap_conf.binddn : "(anonymous)");
	fprintf(stderr, "bindpw           %s\n", ldap_conf.bindpw ?
	    ldap_conf.bindpw : "(anonymous)");
	if (ldap_conf.bind_timelimit > 0)
	    fprintf(stderr, "bind_timelimit   %d\n", ldap_conf.bind_timelimit);
	if (ldap_conf.timelimit > 0)
	    fprintf(stderr, "timelimit        %d\n", ldap_conf.timelimit);
	fprintf(stderr, "ssl              %s\n", ldap_conf.ssl ?
	    ldap_conf.ssl : "(no)");
	if (ldap_conf.tls_checkpeer != -1)
	    fprintf(stderr, "tls_checkpeer    %s\n", ldap_conf.tls_checkpeer ?
		"(yes)" : "(no)");
	if (ldap_conf.tls_cacertfile != NULL)
	    fprintf(stderr, "tls_cacertfile   %s\n", ldap_conf.tls_cacertfile);
	if (ldap_conf.tls_cacertdir != NULL)
	    fprintf(stderr, "tls_cacertdir    %s\n", ldap_conf.tls_cacertdir);
	if (ldap_conf.tls_random_file != NULL)
	    fprintf(stderr, "tls_random_file  %s\n", ldap_conf.tls_random_file);
	if (ldap_conf.tls_cipher_suite != NULL)
	    fprintf(stderr, "tls_cipher_suite %s\n", ldap_conf.tls_cipher_suite);
	if (ldap_conf.tls_certfile != NULL)
	    fprintf(stderr, "tls_certfile     %s\n", ldap_conf.tls_certfile);
	if (ldap_conf.tls_keyfile != NULL)
	    fprintf(stderr, "tls_keyfile      %s\n", ldap_conf.tls_keyfile);
#ifdef HAVE_LDAP_SASL_INTERACTIVE_BIND_S
	if (ldap_conf.use_sasl != -1) {
	    fprintf(stderr, "use_sasl         %s\n",
		ldap_conf.use_sasl ? "yes" : "no");
	    fprintf(stderr, "sasl_auth_id     %s\n", ldap_conf.sasl_auth_id ?
		ldap_conf.sasl_auth_id : "(NONE)");
	    fprintf(stderr, "rootuse_sasl     %d\n", ldap_conf.rootuse_sasl);
	    fprintf(stderr, "rootsasl_auth_id %s\n", ldap_conf.rootsasl_auth_id ?
		ldap_conf.rootsasl_auth_id : "(NONE)");
	    fprintf(stderr, "sasl_secprops    %s\n", ldap_conf.sasl_secprops ?
		ldap_conf.sasl_secprops : "(NONE)");
	    fprintf(stderr, "krb5_ccname      %s\n", ldap_conf.krb5_ccname ?
		ldap_conf.krb5_ccname : "(NONE)");
	}
#endif
	fprintf(stderr, "===================\n");
    }
    if (!ldap_conf.base)
	return(FALSE);		/* if no base is defined, ignore LDAP */

    /*
     * Interpret SSL option
     */
    if (ldap_conf.ssl != NULL) {
	if (strcasecmp(ldap_conf.ssl, "start_tls") == 0)
	    ldap_conf.ssl_mode = SUDO_LDAP_STARTTLS;
	else if (_atobool(ldap_conf.ssl))
	    ldap_conf.ssl_mode = SUDO_LDAP_SSL;
    }

#if defined(HAVE_LDAPSSL_SET_STRENGTH) && !defined(LDAP_OPT_X_TLS_REQUIRE_CERT)
    if (ldap_conf.tls_checkpeer != -1) {
	ldapssl_set_strength(NULL,
	    ldap_conf.tls_checkpeer ? LDAPSSL_AUTH_CERT : LDAPSSL_AUTH_WEAK);
    }
#endif

#ifndef HAVE_LDAP_INITIALIZE
    /* Convert uri list to host list if no ldap_initialize(). */
    if (ldap_conf.uri) {
	struct ldap_config_list_str *uri = ldap_conf.uri;
	if (sudo_ldap_parse_uri(uri) != 0)
	    return(FALSE);
	do {
	    ldap_conf.uri = uri->next;
	    efree(uri);
	} while ((uri = ldap_conf.uri));
	ldap_conf.port = LDAP_PORT;
    }
#endif

    if (!ldap_conf.uri) {
	/* Use port 389 for plaintext LDAP and port 636 for SSL LDAP */
	if (ldap_conf.port < 0)
	    ldap_conf.port =
		ldap_conf.ssl_mode == SUDO_LDAP_SSL ? LDAPS_PORT : LDAP_PORT;

#ifdef HAVE_LDAP_CREATE
	/*
	 * Cannot specify port directly to ldap_create(), each host must
	 * include :port to override the default.
	 */
	if (ldap_conf.port != LDAP_PORT)
	    sudo_ldap_conf_add_ports();
#endif
    }

    /* If rootbinddn set, read in /etc/ldap.secret if it exists. */
    if (ldap_conf.rootbinddn)
	sudo_ldap_read_secret(_PATH_LDAP_SECRET);

#ifdef HAVE_LDAP_SASL_INTERACTIVE_BIND_S
    /*
     * Make sure we can open the file specified by krb5_ccname.
     */
    if (ldap_conf.krb5_ccname != NULL) {
	if (strncasecmp(ldap_conf.krb5_ccname, "FILE:", 5) == 0 ||
	    strncasecmp(ldap_conf.krb5_ccname, "WRFILE:", 7) == 0) {
	    value = ldap_conf.krb5_ccname +
		(ldap_conf.krb5_ccname[4] == ':' ? 5 : 7);
	    if ((fp = fopen(value, "r")) != NULL) {
		DPRINTF(("using krb5 credential cache: %s", value), 1);
		fclose(fp);
	    } else {
		/* Can't open it, just ignore the entry. */
		DPRINTF(("unable to open krb5 credential cache: %s", value), 1);
		efree(ldap_conf.krb5_ccname);
		ldap_conf.krb5_ccname = NULL;
	    }
	}
    }
#endif
    return(TRUE);
}

/*
 * Extract the dn from an entry and return the first rdn from it.
 */
static char *
sudo_ldap_get_first_rdn(ld, entry)
    LDAP *ld;
    LDAPMessage *entry;
{
#ifdef HAVE_LDAP_STR2DN
    char *dn, *rdn = NULL;
    LDAPDN tmpDN;

    if ((dn = ldap_get_dn(ld, entry)) == NULL)
	return(NULL);
    if (ldap_str2dn(dn, &tmpDN, LDAP_DN_FORMAT_LDAP) == LDAP_SUCCESS) {
	ldap_rdn2str(tmpDN[0], &rdn, LDAP_DN_FORMAT_UFN);
	ldap_dnfree(tmpDN);
    }
    ldap_memfree(dn);
    return(rdn);
#else
    char *dn, **edn;

    if ((dn = ldap_get_dn(ld, entry)) == NULL)
	return(NULL);
    edn = ldap_explode_dn(dn, 1);
    ldap_memfree(dn);
    return(edn ? edn[0] : NULL);
#endif
}

/*
 * Fetch and display the global Options.
 */
static int
sudo_ldap_display_defaults(nss, pw, lbuf)
    struct sudo_nss *nss;
    struct passwd *pw;
    struct lbuf *lbuf;
{
    struct berval **bv, **p;
    struct ldap_config_list_str *base;
    LDAP *ld = (LDAP *) nss->handle;
    LDAPMessage *entry, *result;
    char *prefix;
    int rc, count = 0;

    if (ld == NULL)
	goto done;

    for (base = ldap_conf.base; base != NULL; base = base->next) {
	result = NULL;
	rc = ldap_search_ext_s(ld, base->val, LDAP_SCOPE_SUBTREE,
	    "cn=defaults", NULL, 0, NULL, NULL, NULL, 0, &result);
	if (rc == LDAP_SUCCESS && (entry = ldap_first_entry(ld, result))) {
	    bv = ldap_get_values_len(ld, entry, "sudoOption");
	    if (bv != NULL) {
		if (lbuf->len == 0 || isspace((unsigned char)lbuf->buf[lbuf->len - 1]))
		    prefix = "    ";
		else
		    prefix = ", ";
		for (p = bv; *p != NULL; p++) {
		    lbuf_append(lbuf, prefix, (*p)->bv_val, NULL);
		    prefix = ", ";
		    count++;
		}
		ldap_value_free_len(bv);
	    }
	}
	if (result)
	    ldap_msgfree(result);
    }
done:
    return(count);
}

/*
 * STUB
 */
static int
sudo_ldap_display_bound_defaults(nss, pw, lbuf)
    struct sudo_nss *nss;
    struct passwd *pw;
    struct lbuf *lbuf;
{
    return(0);
}

/*
 * Print a record in the short form, ala file sudoers.
 */
static int
sudo_ldap_display_entry_short(ld, entry, lbuf)
    LDAP *ld;
    LDAPMessage *entry;
    struct lbuf *lbuf;
{
    struct berval **bv, **p;
    int count = 0;

    lbuf_append(lbuf, "    (", NULL);

    /* get the RunAsUser Values from the entry */
    bv = ldap_get_values_len(ld, entry, "sudoRunAsUser");
    if (bv == NULL)
	bv = ldap_get_values_len(ld, entry, "sudoRunAs");
    if (bv != NULL) {
	for (p = bv; *p != NULL; p++) {
	    if (p != bv)
		lbuf_append(lbuf, ", ", NULL);
	    lbuf_append(lbuf, (*p)->bv_val, NULL);
	}
	ldap_value_free_len(bv);
    } else
	lbuf_append(lbuf, def_runas_default, NULL);

    /* get the RunAsGroup Values from the entry */
    bv = ldap_get_values_len(ld, entry, "sudoRunAsGroup");
    if (bv != NULL) {
	lbuf_append(lbuf, " : ", NULL);
	for (p = bv; *p != NULL; p++) {
	    if (p != bv)
		lbuf_append(lbuf, ", ", NULL);
	    lbuf_append(lbuf, (*p)->bv_val, NULL);
	}
	ldap_value_free_len(bv);
    }
    lbuf_append(lbuf, ") ", NULL);

    /* get the Option Values from the entry */
    bv = ldap_get_values_len(ld, entry, "sudoOption");
    if (bv != NULL) {
	char *cp, *tag;

	for (p = bv; *p != NULL; p++) {
	    cp = (*p)->bv_val;
	    if (*cp == '!')
		cp++;
	    tag = NULL;
	    if (strcmp(cp, "authenticate") == 0)
		tag = (*p)->bv_val[0] == '!' ?
		    "NOPASSWD: " : "PASSWD: ";
	    else if (strcmp(cp, "noexec") == 0)
		tag = (*p)->bv_val[0] == '!' ?
		    "EXEC: " : "NOEXEC: ";
	    else if (strcmp(cp, "setenv") == 0)
		tag = (*p)->bv_val[0] == '!' ?
		    "NOSETENV: " : "SETENV: ";
	    if (tag != NULL)
		lbuf_append(lbuf, tag, NULL);
	    /* XXX - ignores other options */
	}
	ldap_value_free_len(bv);
    }

    /* get the Command Values from the entry */
    bv = ldap_get_values_len(ld, entry, "sudoCommand");
    if (bv != NULL) {
	for (p = bv; *p != NULL; p++) {
	    if (p != bv)
		lbuf_append(lbuf, ", ", NULL);
	    lbuf_append(lbuf, (*p)->bv_val, NULL);
	    count++;
	}
	ldap_value_free_len(bv);
    }
    lbuf_append(lbuf, "\n", NULL);

    return(count);
}

/*
 * Print a record in the long form.
 */
static int
sudo_ldap_display_entry_long(ld, entry, lbuf)
    LDAP *ld;
    LDAPMessage *entry;
    struct lbuf *lbuf;
{
    struct berval **bv, **p;
    char *rdn;
    int count = 0;

    /* extract the dn, only show the first rdn */
    rdn = sudo_ldap_get_first_rdn(ld, entry);
    lbuf_append(lbuf, "\nLDAP Role: ", rdn ? rdn : "UNKNOWN", "\n", NULL);
    if (rdn)
	ldap_memfree(rdn);

    /* get the RunAsUser Values from the entry */
    lbuf_append(lbuf, "    RunAsUsers: ", NULL);
    bv = ldap_get_values_len(ld, entry, "sudoRunAsUser");
    if (bv == NULL)
	bv = ldap_get_values_len(ld, entry, "sudoRunAs");
    if (bv != NULL) {
	for (p = bv; *p != NULL; p++) {
	    if (p != bv)
		lbuf_append(lbuf, ", ", NULL);
	    lbuf_append(lbuf, (*p)->bv_val, NULL);
	}
	ldap_value_free_len(bv);
    } else
	lbuf_append(lbuf, def_runas_default, NULL);
    lbuf_append(lbuf, "\n", NULL);

    /* get the RunAsGroup Values from the entry */
    bv = ldap_get_values_len(ld, entry, "sudoRunAsGroup");
    if (bv != NULL) {
	lbuf_append(lbuf, "    RunAsGroups: ", NULL);
	for (p = bv; *p != NULL; p++) {
	    if (p != bv)
		lbuf_append(lbuf, ", ", NULL);
	    lbuf_append(lbuf, (*p)->bv_val, NULL);
	}
	ldap_value_free_len(bv);
	lbuf_append(lbuf, "\n", NULL);
    }

    /* get the Option Values from the entry */
    bv = ldap_get_values_len(ld, entry, "sudoOption");
    if (bv != NULL) {
	lbuf_append(lbuf, "    Options: ", NULL);
	for (p = bv; *p != NULL; p++) {
	    if (p != bv)
		lbuf_append(lbuf, ", ", NULL);
	    lbuf_append(lbuf, (*p)->bv_val, NULL);
	}
	ldap_value_free_len(bv);
	lbuf_append(lbuf, "\n", NULL);
    }

    /* get the Command Values from the entry */
    bv = ldap_get_values_len(ld, entry, "sudoCommand");
    if (bv != NULL) {
	lbuf_append(lbuf, "    Commands:\n", NULL);
	for (p = bv; *p != NULL; p++) {
	    lbuf_append(lbuf, "\t", (*p)->bv_val, "\n", NULL);
	    count++;
	}
	ldap_value_free_len(bv);
    }

    return(count);
}

/*
 * Like sudo_ldap_lookup(), except we just print entries.
 */
static int
sudo_ldap_display_privs(nss, pw, lbuf)
    struct sudo_nss *nss;
    struct passwd *pw;
    struct lbuf *lbuf;
{
    struct ldap_config_list_str *base;
    LDAP *ld = (LDAP *) nss->handle;
    LDAPMessage *entry, *result;
    char *filt;
    int rc, do_netgr, count = 0;

    if (ld == NULL)
	goto done;

    /*
     * Okay - time to search for anything that matches this user
     * Lets limit it to only two queries of the LDAP server
     *
     * The first pass will look by the username, groups, and
     * the keyword ALL.  We will then inspect the results that
     * came back from the query.  We don't need to inspect the
     * sudoUser in this pass since the LDAP server already scanned
     * it for us.
     *
     * The second pass will return all the entries that contain
     * user netgroups.  Then we take the netgroups returned and
     * try to match them against the username.
     */
    for (do_netgr = 0; do_netgr < 2; do_netgr++) {
	filt = do_netgr ? estrdup("sudoUser=+*") : sudo_ldap_build_pass1(pw);
	DPRINTF(("ldap search '%s'", filt), 1);
	for (base = ldap_conf.base; base != NULL; base = base->next) {
	    result = NULL;
	    rc = ldap_search_ext_s(ld, base->val, LDAP_SCOPE_SUBTREE, filt,
		NULL, 0, NULL, NULL, NULL, 0, &result);
	    if (rc != LDAP_SUCCESS)
		continue;	/* no entries for this pass */

	    /* print each matching entry */
	    LDAP_FOREACH(entry, ld, result) {
		if ((!do_netgr ||
		    sudo_ldap_check_user_netgroup(ld, entry, pw->pw_name)) &&
		    sudo_ldap_check_host(ld, entry)) {

		    if (long_list)
			count += sudo_ldap_display_entry_long(ld, entry, lbuf);
		    else
			count += sudo_ldap_display_entry_short(ld, entry, lbuf);
		}
	    }
	    ldap_msgfree(result);
	}
	efree(filt);
    }
done:
    return(count);
}

static int
sudo_ldap_display_cmnd(nss, pw)
    struct sudo_nss *nss;
    struct passwd *pw;
{
    struct ldap_config_list_str *base;
    LDAP *ld = (LDAP *) nss->handle;
    LDAPMessage *entry, *result;		/* used for searches */
    char *filt;					/* used to parse attributes */
    int rc, found, do_netgr;			/* temp/final return values */

    if (ld == NULL)
	return(1);

    /*
     * Okay - time to search for anything that matches this user
     * Lets limit it to only two queries of the LDAP server
     *
     * The first pass will look by the username, groups, and
     * the keyword ALL.  We will then inspect the results that
     * came back from the query.  We don't need to inspect the
     * sudoUser in this pass since the LDAP server already scanned
     * it for us.
     *
     * The second pass will return all the entries that contain
     * user netgroups.  Then we take the netgroups returned and
     * try to match them against the username.
     */
    for (found = FALSE, do_netgr = 0; !found && do_netgr < 2; do_netgr++) {
	filt = do_netgr ? estrdup("sudoUser=+*") : sudo_ldap_build_pass1(pw);
	DPRINTF(("ldap search '%s'", filt), 1);
	for (base = ldap_conf.base; base != NULL; base = base->next) {
	    result = NULL;
	    rc = ldap_search_ext_s(ld, base->val, LDAP_SCOPE_SUBTREE, filt,
		NULL, 0, NULL, NULL, NULL, 0, &result);
	    if (rc != LDAP_SUCCESS)
		continue;	/* no entries for this pass */

	    LDAP_FOREACH(entry, ld, result) {
		if ((!do_netgr ||
		    sudo_ldap_check_user_netgroup(ld, entry, pw->pw_name)) &&
		    sudo_ldap_check_host(ld, entry) &&
		    sudo_ldap_check_command(ld, entry, NULL) &&
		    sudo_ldap_check_runas(ld, entry)) {

		    found = TRUE;
		    break;
		}
	    }
	    ldap_msgfree(result);
	}
	efree(filt);
    }

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

#ifdef HAVE_LDAP_SASL_INTERACTIVE_BIND_S
static int
sudo_ldap_sasl_interact(ld, flags, _auth_id, _interact)
    LDAP *ld;
    unsigned int flags;
    void *_auth_id;
    void *_interact;
{
    char *auth_id = (char *)_auth_id;
    sasl_interact_t *interact = (sasl_interact_t *)_interact;

    for (; interact->id != SASL_CB_LIST_END; interact++) {
	if (interact->id != SASL_CB_USER)
	    return(LDAP_PARAM_ERROR);

	if (auth_id != NULL)
	    interact->result = auth_id;
	else if (interact->defresult != NULL)
	    interact->result = interact->defresult;
	else
	    interact->result = "";

	interact->len = strlen(interact->result);
#if SASL_VERSION_MAJOR < 2
	interact->result = estrdup(interact->result);
#endif /* SASL_VERSION_MAJOR < 2 */
    }
    return(LDAP_SUCCESS);
}
#endif /* HAVE_LDAP_SASL_INTERACTIVE_BIND_S */

/*
 * Set LDAP options based on the config table.
 */
static int
sudo_ldap_set_options(ld)
    LDAP *ld;
{
    struct ldap_config_table *cur;
    int rc;

    /* Set ber options */
#ifdef LBER_OPT_DEBUG_LEVEL
    if (ldap_conf.ldap_debug)
	ber_set_option(NULL, LBER_OPT_DEBUG_LEVEL, &ldap_conf.ldap_debug);
#endif

    /* Set simple LDAP options */
    for (cur = ldap_conf_table; cur->conf_str != NULL; cur++) {
	LDAP *conn;
	int ival;
	char *sval;

	if (cur->opt_val == -1)
	    continue;

	conn = cur->connected ? ld : NULL;
	switch (cur->type) {
	case CONF_BOOL:
	case CONF_INT:
	    ival = *(int *)(cur->valp);
	    if (ival >= 0) {
		rc = ldap_set_option(conn, cur->opt_val, &ival);
		if (rc != LDAP_OPT_SUCCESS) {
		    warningx("ldap_set_option: %s -> %d: %s",
			cur->conf_str, ival, ldap_err2string(rc));
		    return(-1);
		}
		DPRINTF(("ldap_set_option: %s -> %d", cur->conf_str, ival), 1);
	    }
	    break;
	case CONF_STR:
	    sval = *(char **)(cur->valp);
	    if (sval != NULL) {
		rc = ldap_set_option(conn, cur->opt_val, sval);
		if (rc != LDAP_OPT_SUCCESS) {
		    warningx("ldap_set_option: %s -> %s: %s",
			cur->conf_str, sval, ldap_err2string(rc));
		    return(-1);
		}
		DPRINTF(("ldap_set_option: %s -> %s", cur->conf_str, sval), 1);
	    }
	    break;
	}
    }

#ifdef LDAP_OPT_NETWORK_TIMEOUT
    /* Convert bind_timelimit to a timeval */
    if (ldap_conf.bind_timelimit > 0) {
	struct timeval tv;
	tv.tv_sec = ldap_conf.bind_timelimit / 1000;
	tv.tv_usec = 0;
	rc = ldap_set_option(ld, LDAP_OPT_NETWORK_TIMEOUT, &tv);
	if (rc != LDAP_OPT_SUCCESS) {
	    warningx("ldap_set_option(NETWORK_TIMEOUT, %ld): %s",
		(long)tv.tv_sec, ldap_err2string(rc));
	    return(-1);
	}
	DPRINTF(("ldap_set_option(LDAP_OPT_NETWORK_TIMEOUT, %ld)",
	    (long)tv.tv_sec), 1);
    }
#endif

#if defined(LDAP_OPT_X_TLS) && !defined(HAVE_LDAPSSL_INIT)
    if (ldap_conf.ssl_mode == SUDO_LDAP_SSL) {
	int val = LDAP_OPT_X_TLS_HARD;
	rc = ldap_set_option(ld, LDAP_OPT_X_TLS, &val);
	if (rc != LDAP_SUCCESS) {
	    warningx("ldap_set_option(LDAP_OPT_X_TLS, LDAP_OPT_X_TLS_HARD): %s",
		ldap_err2string(rc));
	    return(-1);
	}
	DPRINTF(("ldap_set_option(LDAP_OPT_X_TLS, LDAP_OPT_X_TLS_HARD)"), 1);
    }
#endif
    return(0);
}

/*
 * Connect to the LDAP server specified by ld
 */
static int
sudo_ldap_bind_s(ld)
    LDAP *ld;
{
    int rc;
#ifdef HAVE_LDAP_SASL_INTERACTIVE_BIND_S
    const char *old_ccname = user_ccname;
# ifdef HAVE_GSS_KRB5_CCACHE_NAME
    unsigned int status;
# endif
#endif

#ifdef HAVE_LDAP_SASL_INTERACTIVE_BIND_S
    if (ldap_conf.rootuse_sasl == TRUE ||
	(ldap_conf.rootuse_sasl != FALSE && ldap_conf.use_sasl == TRUE)) {
	void *auth_id = ldap_conf.rootsasl_auth_id ?
	    ldap_conf.rootsasl_auth_id : ldap_conf.sasl_auth_id;

	if (ldap_conf.krb5_ccname != NULL) {
# ifdef HAVE_GSS_KRB5_CCACHE_NAME
	    if (gss_krb5_ccache_name(&status, ldap_conf.krb5_ccname, &old_ccname)
		!= GSS_S_COMPLETE) {
		old_ccname = NULL;
		DPRINTF(("gss_krb5_ccache_name() failed: %d", status), 1);
	    }
# else
	    setenv("KRB5CCNAME", ldap_conf.krb5_ccname, TRUE);
# endif
	}
	rc = ldap_sasl_interactive_bind_s(ld, ldap_conf.binddn, "GSSAPI",
	    NULL, NULL, LDAP_SASL_QUIET, sudo_ldap_sasl_interact, auth_id);
	if (ldap_conf.krb5_ccname != NULL) {
# ifdef HAVE_GSS_KRB5_CCACHE_NAME
	    if (gss_krb5_ccache_name(&status, old_ccname, NULL) != GSS_S_COMPLETE)
		    DPRINTF(("gss_krb5_ccache_name() failed: %d", status), 1);
# else
	    if (old_ccname != NULL)
		setenv("KRB5CCNAME", old_ccname, TRUE);
	    else
		unsetenv("KRB5CCNAME");
# endif
	}
	if (rc != LDAP_SUCCESS) {
	    warningx("ldap_sasl_interactive_bind_s(): %s", ldap_err2string(rc));
	    return(-1);
	}
	DPRINTF(("ldap_sasl_interactive_bind_s() ok"), 1);
    } else
#endif /* HAVE_LDAP_SASL_INTERACTIVE_BIND_S */
#ifdef HAVE_LDAP_SASL_BIND_S
    {
	struct berval bv;

	bv.bv_val = ldap_conf.bindpw ? ldap_conf.bindpw : "";
	bv.bv_len = strlen(bv.bv_val);

	rc = ldap_sasl_bind_s(ld, ldap_conf.binddn, LDAP_SASL_SIMPLE, &bv,
	    NULL, NULL, NULL);
	if (rc != LDAP_SUCCESS) {
	    warningx("ldap_sasl_bind_s(): %s", ldap_err2string(rc));
	    return(-1);
	}
	DPRINTF(("ldap_sasl_bind_s() ok"), 1);
    }
#else
    {
	rc = ldap_simple_bind_s(ld, ldap_conf.binddn, ldap_conf.bindpw);
	if (rc != LDAP_SUCCESS) {
	    warningx("ldap_simple_bind_s(): %s", ldap_err2string(rc));
	    return(-1);
	}
	DPRINTF(("ldap_simple_bind_s() ok"), 1);
    }
#endif
    return(0);
}

/*
 * Open a connection to the LDAP server.
 * Returns 0 on success and non-zero on failure.
 */
static int
sudo_ldap_open(nss)
    struct sudo_nss *nss;
{
    LDAP *ld;
    int rc, ldapnoinit = FALSE;

    if (!sudo_ldap_read_config())
	return(-1);

    /* Prevent reading of user ldaprc and system defaults. */
    if (getenv("LDAPNOINIT") == NULL) {
	ldapnoinit = TRUE;
	setenv("LDAPNOINIT", "1", TRUE);
    }

    /* Connect to LDAP server */
#ifdef HAVE_LDAP_INITIALIZE
    if (ldap_conf.uri != NULL) {
	char *buf = sudo_ldap_join_uri(ldap_conf.uri);
	DPRINTF(("ldap_initialize(ld, %s)", buf), 2);
	rc = ldap_initialize(&ld, buf);
	efree(buf);
    } else
#endif
	rc = sudo_ldap_init(&ld, ldap_conf.host, ldap_conf.port);
    if (rc != LDAP_SUCCESS) {
	warningx("unable to initialize LDAP: %s", ldap_err2string(rc));
	return(-1);
    }

    if (ldapnoinit)
	unsetenv("LDAPNOINIT");

    /* Set LDAP options */
    if (sudo_ldap_set_options(ld) < 0)
	return(-1);

    if (ldap_conf.ssl_mode == SUDO_LDAP_STARTTLS) {
#if defined(HAVE_LDAP_START_TLS_S)
	rc = ldap_start_tls_s(ld, NULL, NULL);
	if (rc != LDAP_SUCCESS) {
	    warningx("ldap_start_tls_s(): %s", ldap_err2string(rc));
	    return(-1);
	}
	DPRINTF(("ldap_start_tls_s() ok"), 1);
#elif defined(HAVE_LDAP_SSL_CLIENT_INIT) && defined(HAVE_LDAP_START_TLS_S_NP)
	if (ldap_ssl_client_init(NULL, NULL, 0, &rc) != LDAP_SUCCESS) {
	    warningx("ldap_ssl_client_init(): %s", ldap_err2string(rc));
	    return(-1);
	}
	rc = ldap_start_tls_s_np(ld, NULL);
	if (rc != LDAP_SUCCESS) {
	    warningx("ldap_start_tls_s_np(): %s", ldap_err2string(rc));
	    return(-1);
	}
	DPRINTF(("ldap_start_tls_s_np() ok"), 1);
#else
	warningx("start_tls specified but LDAP libs do not support ldap_start_tls_s() or ldap_start_tls_s_np()");
#endif /* !HAVE_LDAP_START_TLS_S && !HAVE_LDAP_START_TLS_S_NP */
    }

    /* Actually connect */
    if (sudo_ldap_bind_s(ld) != 0)
	return(-1);

    nss->handle = ld;
    return(0);
}

static int
sudo_ldap_setdefs(nss)
    struct sudo_nss *nss;
{
    struct ldap_config_list_str *base;
    LDAP *ld = (LDAP *) nss->handle;
    LDAPMessage *entry, *result;		 /* used for searches */
    int rc;					 /* temp return value */

    if (ld == NULL)
	return(-1);

    for (base = ldap_conf.base; base != NULL; base = base->next) {
	result = NULL;
	rc = ldap_search_ext_s(ld, base->val, LDAP_SCOPE_SUBTREE,
	    "cn=defaults", NULL, 0, NULL, NULL, NULL, 0, &result);
	if (rc == LDAP_SUCCESS && (entry = ldap_first_entry(ld, result))) {
	    DPRINTF(("found:%s", ldap_get_dn(ld, entry)), 1);
	    sudo_ldap_parse_options(ld, entry);
	} else
	    DPRINTF(("no default options found in %s", base->val), 1);

	if (result)
	    ldap_msgfree(result);
    }

    return(0);
}

/*
 * like sudoers_lookup() - only LDAP style
 */
static int
sudo_ldap_lookup(nss, ret, pwflag)
    struct sudo_nss *nss;
    int ret;
    int pwflag;
{
    struct ldap_config_list_str *base;
    LDAP *ld = (LDAP *) nss->handle;
    LDAPMessage *entry, *result;
    char *filt;
    int do_netgr, rc, matched;
    int setenv_implied;
    int ldap_user_matches = FALSE, ldap_host_matches = FALSE;
    struct passwd *pw = list_pw ? list_pw : sudo_user.pw;

    if (ld == NULL)
	return(ret);

    if (pwflag) {
	int doauth = UNSPEC;
	enum def_tupple pwcheck = 
	    (pwflag == -1) ? never : sudo_defs_table[pwflag].sd_un.tuple;

	for (matched = 0, do_netgr = 0; !matched && do_netgr < 2; do_netgr++) {
	    filt = do_netgr ? estrdup("sudoUser=+*") : sudo_ldap_build_pass1(pw);
	    for (base = ldap_conf.base; base != NULL; base = base->next) {
		result = NULL;
		rc = ldap_search_ext_s(ld, base->val, LDAP_SCOPE_SUBTREE, filt,
		    NULL, 0, NULL, NULL, NULL, 0, &result);
		if (rc != LDAP_SUCCESS)
		    continue;

		LDAP_FOREACH(entry, ld, result) {
		    /* only verify netgroup matches in pass 2 */
		    if (do_netgr && !sudo_ldap_check_user_netgroup(ld, entry, pw->pw_name))
			continue;

		    ldap_user_matches = TRUE;
		    if (sudo_ldap_check_host(ld, entry)) {
			ldap_host_matches = TRUE;
			if ((pwcheck == any && doauth != FALSE) ||
			    (pwcheck == all && doauth == FALSE))
			    doauth = sudo_ldap_check_bool(ld, entry, "authenticate");
			/* Only check the command when listing another user. */
			if (user_uid == 0 || list_pw == NULL ||
			    user_uid == list_pw->pw_uid ||
			    sudo_ldap_check_command(ld, entry, NULL)) {
			    matched = 1;
			    break;	/* end foreach */
			}
		    }
		}
		ldap_msgfree(result);
	    }
	    efree(filt);
	}
	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;
    }

    /*
     * Okay - time to search for anything that matches this user
     * Lets limit it to only two queries of the LDAP server
     *
     * The first pass will look by the username, groups, and
     * the keyword ALL.  We will then inspect the results that
     * came back from the query.  We don't need to inspect the
     * sudoUser in this pass since the LDAP server already scanned
     * it for us.
     *
     * The second pass will return all the entries that contain
     * user netgroups.  Then we take the netgroups returned and
     * try to match them against the username.
     */
    setenv_implied = FALSE;
    for (matched = 0, do_netgr = 0; !matched && do_netgr < 2; do_netgr++) {
	filt = do_netgr ? estrdup("sudoUser=+*") : sudo_ldap_build_pass1(pw);
	DPRINTF(("ldap search '%s'", filt), 1);
	for (base = ldap_conf.base; base != NULL; base = base->next) {
	    result = NULL;
	    rc = ldap_search_ext_s(ld, base->val, LDAP_SCOPE_SUBTREE, filt,
		NULL, 0, NULL, NULL, NULL, 0, &result);
	    if (rc != LDAP_SUCCESS) {
		DPRINTF(("nothing found for '%s'", filt), 1);
		continue;
	    }

	    /* parse each entry returned from this most recent search */
	    LDAP_FOREACH(entry, ld, result) {
		DPRINTF(("found:%s", ldap_get_dn(ld, entry)), 1);
		if (
		/* first verify user netgroup matches - only if in pass 2 */
		    (!do_netgr || sudo_ldap_check_user_netgroup(ld, entry, pw->pw_name)) &&
		/* remember that user matched */
		    (ldap_user_matches = TRUE) &&
		/* verify host match */
		    sudo_ldap_check_host(ld, entry) &&
		/* remember that host matched */
		    (ldap_host_matches = TRUE) &&
		/* verify runas match */
		    sudo_ldap_check_runas(ld, entry) &&
		/* verify command match */
		    (rc = sudo_ldap_check_command(ld, entry, &setenv_implied)) != UNSPEC
		    ) {
		    /* We have a match! */
		    DPRINTF(("Command %sallowed", rc == TRUE ? "" : "NOT "), 1);
		    matched = TRUE;
		    if (rc == TRUE) {
			/* pick up any options */
			if (setenv_implied)
			    def_setenv = TRUE;
			sudo_ldap_parse_options(ld, entry);
#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 */
			/* make sure we don't reenter loop */
			SET(ret, VALIDATE_OK);
			CLR(ret, VALIDATE_NOT_OK);
		    } else {
			SET(ret, VALIDATE_NOT_OK);
			CLR(ret, VALIDATE_OK);
		    }
		    /* break from inside for loop */
		    break;
		}
	    }
	    ldap_msgfree(result);
	}
	efree(filt);
    }

done:
    DPRINTF(("user_matches=%d", ldap_user_matches), 1);
    DPRINTF(("host_matches=%d", ldap_host_matches), 1);

    if (!ISSET(ret, VALIDATE_OK)) {
	/* we do not have a match */
	if (pwflag && list_pw == NULL)
	    SET(ret, FLAG_NO_CHECK);
    }
    if (ldap_user_matches)
	CLR(ret, FLAG_NO_USER);
    if (ldap_host_matches)
	CLR(ret, FLAG_NO_HOST);
    DPRINTF(("sudo_ldap_lookup(%d)=0x%02x", pwflag, ret), 1);

    return(ret);
}

/*
 * shut down LDAP connection
 */
static int
sudo_ldap_close(nss)
    struct sudo_nss *nss;
{
    if (nss->handle != NULL) {
	ldap_unbind_ext_s((LDAP *) nss->handle, NULL, NULL);
	nss->handle = NULL;
    }
    return(0);
}

/*
 * STUB
 */
static int
sudo_ldap_parse(nss)
    struct sudo_nss *nss;
{
    return(0);
}
