diff options
Diffstat (limited to 'plugins/sudoers/ldap.c')
-rw-r--r-- | plugins/sudoers/ldap.c | 2050 |
1 files changed, 2050 insertions, 0 deletions
diff --git a/plugins/sudoers/ldap.c b/plugins/sudoers/ldap.c new file mode 100644 index 0000000..bc2baec --- /dev/null +++ b/plugins/sudoers/ldap.c @@ -0,0 +1,2050 @@ +/* + * Copyright (c) 2003-2018 Todd C. Miller <Todd.Miller@sudo.ws> + * + * 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. + */ + +/* + * This is an open source non-commercial project. Dear PVS-Studio, please check it. + * PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com + */ + +#include <config.h> + +#include <sys/types.h> +#include <sys/time.h> +#include <sys/stat.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#ifdef HAVE_STRING_H +# include <string.h> +#endif /* HAVE_STRING_H */ +#ifdef HAVE_STRINGS_H +# include <strings.h> +#endif /* HAVE_STRINGS_H */ +#include <unistd.h> +#include <time.h> +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <pwd.h> +#include <grp.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 +#endif /* HAVE_LDAP_SASL_INTERACTIVE_BIND_S */ + +#include "sudoers.h" +#include "gram.h" +#include "sudo_lbuf.h" +#include "sudo_ldap.h" +#include "sudo_ldap_conf.h" +#include "sudo_dso.h" + +#ifndef LDAP_OPT_RESULT_CODE +# define LDAP_OPT_RESULT_CODE LDAP_OPT_ERROR_NUMBER +#endif + +#ifndef LDAP_OPT_SUCCESS +# define LDAP_OPT_SUCCESS LDAP_SUCCESS +#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 +# ifdef HAVE_LDAP_SEARCH_ST +# define ldap_search_ext_s(a, b, c, d, e, f, g, h, i, j, k) \ + ldap_search_st(a, b, c, d, e, f, i, k) +# else +# 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 +#endif + +#define LDAP_FOREACH(var, ld, res) \ + for ((var) = ldap_first_entry((ld), (res)); \ + (var) != NULL; \ + (var) = ldap_next_entry((ld), (var))) + +/* The TIMEFILTER_LENGTH is the length of the filter when timed entries + are used. The length is computed as follows: + 81 for the filter itself + + 2 * 17 for the now timestamp +*/ +#define TIMEFILTER_LENGTH 115 + +/* + * The ldap_search structure implements a linked list of ldap and + * search result pointers, which allows us to remove them after + * all search results have been combined in memory. + */ +struct ldap_search_result { + STAILQ_ENTRY(ldap_search_result) entries; + LDAP *ldap; + LDAPMessage *searchresult; +}; +STAILQ_HEAD(ldap_search_list, ldap_search_result); + +/* + * The ldap_entry_wrapper structure is used to implement sorted result entries. + * A double is used for the order to allow for insertion of new entries + * without having to renumber everything. + * Note: there is no standard floating point type in LDAP. + * As a result, some LDAP servers will only allow an integer. + */ +struct ldap_entry_wrapper { + LDAPMessage *entry; + double order; +}; + +/* + * The ldap_result structure contains the list of matching searches as + * well as an array of all result entries sorted by the sudoOrder attribute. + */ +struct ldap_result { + struct ldap_search_list searches; + struct ldap_entry_wrapper *entries; + unsigned int allocated_entries; + unsigned int nentries; +}; +#define ALLOCATION_INCREMENT 100 + +/* + * The ldap_netgroup structure implements a singly-linked tail queue of + * netgroups a user is a member of when querying netgroups directly. + */ +struct ldap_netgroup { + STAILQ_ENTRY(ldap_netgroup) entries; + char *name; +}; +STAILQ_HEAD(ldap_netgroup_list, ldap_netgroup); + +/* + * LDAP sudo_nss handle. + * We store the connection to the LDAP server and the passwd struct of the + * user the last query was performed for. + */ +struct sudo_ldap_handle { + LDAP *ld; + struct passwd *pw; + struct sudoers_parse_tree parse_tree; +}; + +#ifdef HAVE_LDAP_INITIALIZE +static char * +sudo_ldap_join_uri(struct ldap_config_str_list *uri_list) +{ + struct ldap_config_str *uri; + size_t len = 0; + char *buf = NULL; + debug_decl(sudo_ldap_join_uri, SUDOERS_DEBUG_LDAP) + + STAILQ_FOREACH(uri, uri_list, entries) { + if (ldap_conf.ssl_mode == SUDO_LDAP_STARTTLS) { + if (strncasecmp(uri->val, "ldaps://", 8) == 0) { + sudo_warnx(U_("starttls not supported when using ldaps")); + ldap_conf.ssl_mode = SUDO_LDAP_SSL; + } + } + len += strlen(uri->val) + 1; + } + if (len == 0 || (buf = malloc(len)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + } else { + char *cp = buf; + + STAILQ_FOREACH(uri, uri_list, entries) { + cp += strlcpy(cp, uri->val, len - (cp - buf)); + *cp++ = ' '; + } + cp[-1] = '\0'; + } + debug_return_str(buf); +} +#endif /* HAVE_LDAP_INITIALIZE */ + +/* + * Wrapper for ldap_create() or ldap_init() that handles + * SSL/TLS initialization as well. + * Returns LDAP_SUCCESS on success, else non-zero. + */ +static int +sudo_ldap_init(LDAP **ldp, const char *host, int port) +{ + LDAP *ld; + int ret = LDAP_CONNECT_ERROR; + debug_decl(sudo_ldap_init, SUDOERS_DEBUG_LDAP) + +#ifdef HAVE_LDAPSSL_INIT + if (ldap_conf.ssl_mode != SUDO_LDAP_CLEAR) { + const int defsecure = ldap_conf.ssl_mode == SUDO_LDAP_SSL; + DPRINTF2("ldapssl_clientauth_init(%s, %s)", + ldap_conf.tls_certfile ? ldap_conf.tls_certfile : "NULL", + ldap_conf.tls_keyfile ? ldap_conf.tls_keyfile : "NULL"); + ret = ldapssl_clientauth_init(ldap_conf.tls_certfile, NULL, + ldap_conf.tls_keyfile != NULL, ldap_conf.tls_keyfile, NULL); + /* + * Starting with version 5.0, Mozilla-derived LDAP SDKs require + * the cert and key paths to be a directory, not a file. + * If the user specified a file and it fails, try the parent dir. + */ + if (ret != LDAP_SUCCESS) { + bool retry = false; + if (ldap_conf.tls_certfile != NULL) { + char *cp = strrchr(ldap_conf.tls_certfile, '/'); + if (cp != NULL && strncmp(cp + 1, "cert", 4) == 0) { + *cp = '\0'; + retry = true; + } + } + if (ldap_conf.tls_keyfile != NULL) { + char *cp = strrchr(ldap_conf.tls_keyfile, '/'); + if (cp != NULL && strncmp(cp + 1, "key", 3) == 0) { + *cp = '\0'; + retry = true; + } + } + if (retry) { + DPRINTF2("retry ldapssl_clientauth_init(%s, %s)", + ldap_conf.tls_certfile ? ldap_conf.tls_certfile : "NULL", + ldap_conf.tls_keyfile ? ldap_conf.tls_keyfile : "NULL"); + ret = ldapssl_clientauth_init(ldap_conf.tls_certfile, NULL, + ldap_conf.tls_keyfile != NULL, ldap_conf.tls_keyfile, NULL); + } + } + if (ret != LDAP_SUCCESS) { + sudo_warnx(U_("unable to initialize SSL cert and key db: %s"), + ldapssl_err2string(ret)); + if (ldap_conf.tls_certfile == NULL) + sudo_warnx(U_("you must set TLS_CERT in %s to use SSL"), + path_ldap_conf); + goto done; + } + + DPRINTF2("ldapssl_init(%s, %d, %d)", host, port, defsecure); + if ((ld = ldapssl_init(host, port, defsecure)) != NULL) + ret = LDAP_SUCCESS; + } else +#elif defined(HAVE_LDAP_SSL_INIT) && defined(HAVE_LDAP_SSL_CLIENT_INIT) + if (ldap_conf.ssl_mode == SUDO_LDAP_SSL) { + int sslrc; + ret = ldap_ssl_client_init(ldap_conf.tls_keyfile, ldap_conf.tls_keypw, + 0, &sslrc); + if (ret != LDAP_SUCCESS) { + sudo_warnx("ldap_ssl_client_init(): %s (SSL reason code %d)", + ldap_err2string(ret), sslrc); + goto done; + } + DPRINTF2("ldap_ssl_init(%s, %d, NULL)", host, port); + if ((ld = ldap_ssl_init((char *)host, port, NULL)) != NULL) + ret = LDAP_SUCCESS; + } else +#endif + { +#ifdef HAVE_LDAP_CREATE + DPRINTF2("ldap_create()"); + if ((ret = ldap_create(&ld)) != LDAP_SUCCESS) + goto done; + DPRINTF2("ldap_set_option(LDAP_OPT_HOST_NAME, %s)", host); + ret = ldap_set_option(ld, LDAP_OPT_HOST_NAME, host); +#else + DPRINTF2("ldap_init(%s, %d)", host, port); + if ((ld = ldap_init((char *)host, port)) == NULL) + goto done; + ret = LDAP_SUCCESS; +#endif + } + + *ldp = ld; +done: + debug_return_int(ret); +} + +/* + * Wrapper for ldap_get_values_len() that fills in the response code + * on error. + */ +static struct berval ** +sudo_ldap_get_values_len(LDAP *ld, LDAPMessage *entry, char *attr, int *rc) +{ + struct berval **bval; + + bval = ldap_get_values_len(ld, entry, attr); + if (bval == NULL) { + int optrc = ldap_get_option(ld, LDAP_OPT_RESULT_CODE, rc); + if (optrc != LDAP_OPT_SUCCESS) + *rc = optrc; + } else { + *rc = LDAP_SUCCESS; + } + return bval; +} + +/* + * Walk through search results and return true if we have a matching + * non-Unix group (including netgroups), else false. + */ +static int +sudo_ldap_check_non_unix_group(LDAP *ld, LDAPMessage *entry, struct passwd *pw) +{ + struct berval **bv, **p; + bool ret = false; + char *val; + int rc; + debug_decl(sudo_ldap_check_non_unix_group, SUDOERS_DEBUG_LDAP) + + if (!entry) + debug_return_bool(ret); + + /* get the values from the entry */ + bv = sudo_ldap_get_values_len(ld, entry, "sudoUser", &rc); + if (bv == NULL) { + if (rc == LDAP_NO_MEMORY) + debug_return_int(-1); + debug_return_bool(false); + } + + /* walk through values */ + for (p = bv; *p != NULL && !ret; p++) { + val = (*p)->bv_val; + if (*val == '+') { + if (netgr_matches(val, def_netgroup_tuple ? user_runhost : NULL, + def_netgroup_tuple ? user_srunhost : NULL, pw->pw_name)) + ret = true; + DPRINTF2("ldap sudoUser netgroup '%s' ... %s", val, + ret ? "MATCH!" : "not"); + } else { + if (group_plugin_query(pw->pw_name, val + 2, pw)) + ret = true; + DPRINTF2("ldap sudoUser non-Unix group '%s' ... %s", val, + ret ? "MATCH!" : "not"); + } + } + + ldap_value_free_len(bv); /* cleanup */ + + debug_return_bool(ret); +} + +/* + * Extract the dn from an entry and return the first rdn from it. + */ +static char * +sudo_ldap_get_first_rdn(LDAP *ld, LDAPMessage *entry) +{ +#ifdef HAVE_LDAP_STR2DN + char *dn, *rdn = NULL; + LDAPDN tmpDN; + debug_decl(sudo_ldap_get_first_rdn, SUDOERS_DEBUG_LDAP) + + if ((dn = ldap_get_dn(ld, entry)) == NULL) + debug_return_str(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); + debug_return_str(rdn); +#else + char *dn, **edn; + debug_decl(sudo_ldap_get_first_rdn, SUDOERS_DEBUG_LDAP) + + if ((dn = ldap_get_dn(ld, entry)) == NULL) + debug_return_str(NULL); + edn = ldap_explode_dn(dn, 1); + ldap_memfree(dn); + debug_return_str(edn ? edn[0] : NULL); +#endif +} + +/* + * Read sudoOption and fill in the defaults list. + * This is used to parse the cn=defaults entry. + */ +static bool +sudo_ldap_parse_options(LDAP *ld, LDAPMessage *entry, struct defaults_list *defs) +{ + struct berval **bv, **p; + char *cn, *cp, *source = NULL; + bool ret = false; + int rc; + debug_decl(sudo_ldap_parse_options, SUDOERS_DEBUG_LDAP) + + bv = sudo_ldap_get_values_len(ld, entry, "sudoOption", &rc); + if (bv == NULL) { + if (rc == LDAP_NO_MEMORY) + debug_return_bool(false); + debug_return_bool(true); + } + + /* Use sudoRole in place of file name in defaults. */ + cn = sudo_ldap_get_first_rdn(ld, entry); + if (asprintf(&cp, "sudoRole %s", cn ? cn : "UNKNOWN") == -1) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + if ((source = rcstr_dup(cp)) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + free(cp); + goto done; + } + + /* Walk through options, appending to defs. */ + for (p = bv; *p != NULL; p++) { + char *var, *val; + int op; + + op = sudo_ldap_parse_option((*p)->bv_val, &var, &val); + if (!sudo_ldap_add_default(var, val, op, source, defs)) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + goto done; + } + } + + ret = true; + +done: + rcstr_delref(source); + if (cn) + ldap_memfree(cn); + ldap_value_free_len(bv); + + debug_return_bool(ret); +} + +/* + * Build an LDAP timefilter. + * + * Stores a filter in the buffer that makes sure only entries + * are selected that have a sudoNotBefore in the past and a + * sudoNotAfter in the future, i.e. a filter of the following + * structure (spaced out a little more for better readability: + * + * (& + * (| + * (!(sudoNotAfter=*)) + * (sudoNotAfter>__now__) + * ) + * (| + * (!(sudoNotBefore=*)) + * (sudoNotBefore<__now__) + * ) + * ) + * + * If either the sudoNotAfter or sudoNotBefore attributes are missing, + * no time restriction shall be imposed. + */ +static bool +sudo_ldap_timefilter(char *buffer, size_t buffersize) +{ + struct tm *tp; + time_t now; + char timebuffer[sizeof("20120727121554.0Z")]; + int len = -1; + debug_decl(sudo_ldap_timefilter, SUDOERS_DEBUG_LDAP) + + /* Make sure we have a formatted timestamp for __now__. */ + time(&now); + if ((tp = gmtime(&now)) == NULL) { + sudo_warn(U_("unable to get GMT time")); + goto done; + } + + /* Format the timestamp according to the RFC. */ + if (strftime(timebuffer, sizeof(timebuffer), "%Y%m%d%H%M%S.0Z", tp) == 0) { + sudo_warnx(U_("unable to format timestamp")); + goto done; + } + + /* Build filter. */ + len = snprintf(buffer, buffersize, "(&(|(!(sudoNotAfter=*))(sudoNotAfter>=%s))(|(!(sudoNotBefore=*))(sudoNotBefore<=%s)))", + timebuffer, timebuffer); + if (len <= 0 || (size_t)len >= buffersize) { + sudo_warnx(U_("internal error, %s overflow"), __func__); + errno = EOVERFLOW; + len = -1; + } + +done: + debug_return_bool(len != -1); +} + +/* + * Builds up a filter to search for default settings + */ +static char * +sudo_ldap_build_default_filter(void) +{ + char *filt; + debug_decl(sudo_ldap_build_default_filter, SUDOERS_DEBUG_LDAP) + + if (!ldap_conf.search_filter) + debug_return_str(strdup("cn=defaults")); + + if (asprintf(&filt, "(&%s(cn=defaults))", ldap_conf.search_filter) == -1) + debug_return_str(NULL); + + debug_return_str(filt); +} + +/* + * Determine length of query value after escaping characters + * as per RFC 4515. + */ +static size_t +sudo_ldap_value_len(const char *value) +{ + const char *s; + size_t len = 0; + + for (s = value; *s != '\0'; s++) { + switch (*s) { + case '\\': + case '(': + case ')': + case '*': + len += 2; + break; + } + } + len += (size_t)(s - value); + return len; +} + +/* + * Like strlcat() but escapes characters as per RFC 4515. + */ +static size_t +sudo_ldap_value_cat(char *dst, const char *src, size_t size) +{ + char *d = dst; + const char *s = src; + size_t n = size; + size_t dlen; + + /* Find the end of dst and adjust bytes left but don't go past end */ + while (n-- != 0 && *d != '\0') + d++; + dlen = d - dst; + n = size - dlen; + + if (n == 0) + return dlen + strlen(s); + while (*s != '\0') { + switch (*s) { + case '\\': + if (n < 3) + goto done; + *d++ = '\\'; + *d++ = '5'; + *d++ = 'c'; + n -= 3; + break; + case '(': + if (n < 3) + goto done; + *d++ = '\\'; + *d++ = '2'; + *d++ = '8'; + n -= 3; + break; + case ')': + if (n < 3) + goto done; + *d++ = '\\'; + *d++ = '2'; + *d++ = '9'; + n -= 3; + break; + case '*': + if (n < 3) + goto done; + *d++ = '\\'; + *d++ = '2'; + *d++ = 'a'; + n -= 3; + break; + default: + if (n < 1) + goto done; + *d++ = *s; + n--; + break; + } + s++; + } +done: + *d = '\0'; + while (*s != '\0') + s++; + return dlen + (s - src); /* count does not include NUL */ +} + +/* + * Like strdup() but escapes characters as per RFC 4515. + */ +static char * +sudo_ldap_value_dup(const char *src) +{ + char *dst; + size_t size; + + size = sudo_ldap_value_len(src) + 1; + dst = malloc(size); + if (dst == NULL) + return NULL; + + *dst = '\0'; + if (sudo_ldap_value_cat(dst, src, size) >= size) { + /* Should not be possible... */ + free(dst); + dst = NULL; + } + return dst; +} + +/* + * Check the netgroups list beginning at "start" for nesting. + * Parent nodes with a memberNisNetgroup that match one of the + * netgroups are added to the list and checked for further nesting. + * Return true on success or false if there was an internal overflow. + */ +static bool +sudo_netgroup_lookup_nested(LDAP *ld, char *base, struct timeval *timeout, + struct ldap_netgroup_list *netgroups, struct ldap_netgroup *start) +{ + LDAPMessage *entry, *result; + size_t filt_len; + char *filt; + int rc; + debug_decl(sudo_netgroup_lookup_nested, SUDOERS_DEBUG_LDAP); + + DPRINTF1("Checking for nested netgroups from netgroup_base '%s'", base); + do { + struct ldap_netgroup *ng, *old_tail; + + result = NULL; + old_tail = STAILQ_LAST(netgroups, ldap_netgroup, entries); + filt_len = strlen(ldap_conf.netgroup_search_filter) + 7; + for (ng = start; ng != NULL; ng = STAILQ_NEXT(ng, entries)) { + filt_len += sudo_ldap_value_len(ng->name) + 20; + } + if ((filt = malloc(filt_len)) == NULL) + goto oom; + CHECK_STRLCPY(filt, "(&", filt_len); + CHECK_STRLCAT(filt, ldap_conf.netgroup_search_filter, filt_len); + CHECK_STRLCAT(filt, "(|", filt_len); + for (ng = start; ng != NULL; ng = STAILQ_NEXT(ng, entries)) { + CHECK_STRLCAT(filt, "(memberNisNetgroup=", filt_len); + CHECK_LDAP_VCAT(filt, ng->name, filt_len); + CHECK_STRLCAT(filt, ")", filt_len); + } + CHECK_STRLCAT(filt, "))", filt_len); + DPRINTF1("ldap netgroup search filter: '%s'", filt); + rc = ldap_search_ext_s(ld, base, LDAP_SCOPE_SUBTREE, filt, + NULL, 0, NULL, NULL, timeout, 0, &result); + free(filt); + if (rc == LDAP_SUCCESS) { + LDAP_FOREACH(entry, ld, result) { + struct berval **bv; + + bv = sudo_ldap_get_values_len(ld, entry, "cn", &rc); + if (bv == NULL) { + if (rc == LDAP_NO_MEMORY) + goto oom; + } else { + /* Don't add a netgroup twice. */ + STAILQ_FOREACH(ng, netgroups, entries) { + /* Assumes only one cn per entry. */ + if (strcasecmp(ng->name, (*bv)->bv_val) == 0) + break; + } + if (ng == NULL) { + ng = malloc(sizeof(*ng)); + if (ng == NULL || + (ng->name = strdup((*bv)->bv_val)) == NULL) { + free(ng); + ldap_value_free_len(bv); + goto oom; + } +#ifdef __clang_analyzer__ + /* clang analyzer false positive */ + if (__builtin_expect(netgroups->stqh_last == NULL, 0)) + __builtin_trap(); +#endif + STAILQ_INSERT_TAIL(netgroups, ng, entries); + DPRINTF1("Found new netgroup %s for %s", ng->name, base); + } + ldap_value_free_len(bv); + } + } + } + ldap_msgfree(result); + + /* Check for nested netgroups in what we added. */ + start = old_tail ? STAILQ_NEXT(old_tail, entries) : STAILQ_FIRST(netgroups); + } while (start != NULL); + + debug_return_bool(true); +oom: + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + ldap_msgfree(result); + debug_return_bool(false); +overflow: + sudo_warnx(U_("internal error, %s overflow"), __func__); + free(filt); + debug_return_bool(false); +} + +/* + * Look up netgroups that the specified user is a member of. + * Appends new entries to the netgroups list. + * Return true on success or false if there was an internal overflow. + */ +static bool +sudo_netgroup_lookup(LDAP *ld, struct passwd *pw, + struct ldap_netgroup_list *netgroups) +{ + struct ldap_config_str *base; + struct ldap_netgroup *ng, *old_tail; + struct timeval tv, *tvp = NULL; + LDAPMessage *entry, *result = NULL; + const char *domain; + char *escaped_domain = NULL, *escaped_user = NULL; + char *escaped_host = NULL, *escaped_shost = NULL, *filt = NULL; + int filt_len, rc; + bool ret = false; + debug_decl(sudo_netgroup_lookup, SUDOERS_DEBUG_LDAP); + + if (ldap_conf.timeout > 0) { + tv.tv_sec = ldap_conf.timeout; + tv.tv_usec = 0; + tvp = &tv; + } + + /* Use NIS domain if set, else wildcard match. */ + domain = sudo_getdomainname(); + + /* Escape the domain, host names, and user name per RFC 4515. */ + if (domain != NULL) { + if ((escaped_domain = sudo_ldap_value_dup(domain)) == NULL) + goto oom; + } + if ((escaped_user = sudo_ldap_value_dup(pw->pw_name)) == NULL) + goto oom; + if (def_netgroup_tuple) { + escaped_host = sudo_ldap_value_dup(user_runhost); + if (user_runhost == user_srunhost) + escaped_shost = escaped_host; + else + escaped_shost = sudo_ldap_value_dup(user_srunhost); + if (escaped_host == NULL || escaped_shost == NULL) + goto oom; + } + + /* Build query, using NIS domain if it is set. */ + if (domain != NULL) { + if (escaped_host != escaped_shost) { + filt_len = asprintf(&filt, "(&%s(|" + "(nisNetgroupTriple=\\28,%s,%s\\29)" + "(nisNetgroupTriple=\\28%s,%s,%s\\29)" + "(nisNetgroupTriple=\\28%s,%s,%s\\29)" + "(nisNetgroupTriple=\\28,%s,\\29)" + "(nisNetgroupTriple=\\28%s,%s,\\29)" + "(nisNetgroupTriple=\\28%s,%s,\\29)))", + ldap_conf.netgroup_search_filter, escaped_user, escaped_domain, + escaped_shost, escaped_user, escaped_domain, + escaped_host, escaped_user, escaped_domain, escaped_user, + escaped_shost, escaped_user, escaped_host, escaped_user); + } else if (escaped_shost != NULL) { + filt_len = asprintf(&filt, "(&%s(|" + "(nisNetgroupTriple=\\28,%s,%s\\29)" + "(nisNetgroupTriple=\\28%s,%s,%s\\29)" + "(nisNetgroupTriple=\\28,%s,\\29)" + "(nisNetgroupTriple=\\28%s,%s,\\29)))", + ldap_conf.netgroup_search_filter, escaped_user, escaped_domain, + escaped_shost, escaped_user, escaped_domain, + escaped_user, escaped_shost, escaped_user); + } else { + filt_len = asprintf(&filt, "(&%s(|" + "(nisNetgroupTriple=\\28*,%s,%s\\29)" + "(nisNetgroupTriple=\\28*,%s,\\29)))", + ldap_conf.netgroup_search_filter, escaped_user, escaped_domain, + escaped_user); + } + } else { + if (escaped_host != escaped_shost) { + filt_len = asprintf(&filt, "(&%s(|" + "(nisNetgroupTriple=\\28,%s,*\\29)" + "(nisNetgroupTriple=\\28%s,%s,*\\29)" + "(nisNetgroupTriple=\\28%s,%s,*\\29)))", + ldap_conf.netgroup_search_filter, escaped_user, + escaped_shost, escaped_user, escaped_host, escaped_user); + } else if (escaped_shost != NULL) { + filt_len = asprintf(&filt, "(&%s(|" + "(nisNetgroupTriple=\\28,%s,*\\29)" + "(nisNetgroupTriple=\\28%s,%s,*\\29)))", + ldap_conf.netgroup_search_filter, escaped_user, + escaped_shost, escaped_user); + } else { + filt_len = asprintf(&filt, + "(&%s(|(nisNetgroupTriple=\\28*,%s,*\\29)))", + ldap_conf.netgroup_search_filter, escaped_user); + } + } + if (filt_len == -1) + goto oom; + DPRINTF1("ldap netgroup search filter: '%s'", filt); + + STAILQ_FOREACH(base, &ldap_conf.netgroup_base, entries) { + DPRINTF1("searching from netgroup_base '%s'", base->val); + rc = ldap_search_ext_s(ld, base->val, LDAP_SCOPE_SUBTREE, filt, + NULL, 0, NULL, NULL, tvp, 0, &result); + if (rc != LDAP_SUCCESS) { + DPRINTF1("ldap netgroup search failed: %s", ldap_err2string(rc)); + ldap_msgfree(result); + result = NULL; + continue; + } + + old_tail = STAILQ_LAST(netgroups, ldap_netgroup, entries); + LDAP_FOREACH(entry, ld, result) { + struct berval **bv; + + bv = sudo_ldap_get_values_len(ld, entry, "cn", &rc); + if (bv == NULL) { + if (rc == LDAP_NO_MEMORY) + goto oom; + } else { + /* Don't add a netgroup twice. */ + STAILQ_FOREACH(ng, netgroups, entries) { + /* Assumes only one cn per entry. */ + if (strcasecmp(ng->name, (*bv)->bv_val) == 0) + break; + } + if (ng == NULL) { + ng = malloc(sizeof(*ng)); + if (ng == NULL || + (ng->name = strdup((*bv)->bv_val)) == NULL) { + free(ng); + ldap_value_free_len(bv); + goto oom; + } + STAILQ_INSERT_TAIL(netgroups, ng, entries); + DPRINTF1("Found new netgroup %s for %s", ng->name, + base->val); + } + ldap_value_free_len(bv); + } + } + ldap_msgfree(result); + result = NULL; + + /* Check for nested netgroups in what we added. */ + ng = old_tail ? STAILQ_NEXT(old_tail, entries) : STAILQ_FIRST(netgroups); + if (ng != NULL) { + if (!sudo_netgroup_lookup_nested(ld, base->val, tvp, netgroups, ng)) + goto done; + } + } + ret = true; + goto done; + +oom: + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); +done: + free(escaped_domain); + free(escaped_user); + free(escaped_host); + if (escaped_host != escaped_shost) + free(escaped_shost); + free(filt); + ldap_msgfree(result); + debug_return_bool(ret); +} + +/* + * Builds up a filter to check against LDAP. + */ +static char * +sudo_ldap_build_pass1(LDAP *ld, struct passwd *pw) +{ + char *buf, timebuffer[TIMEFILTER_LENGTH + 1], gidbuf[MAX_UID_T_LEN + 1]; + struct ldap_netgroup_list netgroups; + struct ldap_netgroup *ng = NULL; + struct gid_list *gidlist; + struct group_list *grlist; + struct group *grp; + size_t sz = 0; + int i; + debug_decl(sudo_ldap_build_pass1, SUDOERS_DEBUG_LDAP) + + STAILQ_INIT(&netgroups); + + /* If there is a filter, allocate space for the global AND. */ + if (ldap_conf.timed || ldap_conf.search_filter) + sz += 3; + + /* Add LDAP search filter if present. */ + if (ldap_conf.search_filter) + sz += strlen(ldap_conf.search_filter); + + /* Then add (|(sudoUser=USERNAME)(sudoUser=ALL)) + NUL */ + sz += 29 + sudo_ldap_value_len(pw->pw_name); + + /* Add space for primary and supplementary groups and gids */ + if ((grp = sudo_getgrgid(pw->pw_gid)) != NULL) { + sz += 12 + sudo_ldap_value_len(grp->gr_name); + } + sz += 13 + MAX_UID_T_LEN; + if ((grlist = sudo_get_grlist(pw)) != NULL) { + for (i = 0; i < grlist->ngroups; i++) { + if (grp != NULL && strcasecmp(grlist->groups[i], grp->gr_name) == 0) + continue; + sz += 12 + sudo_ldap_value_len(grlist->groups[i]); + } + } + if ((gidlist = sudo_get_gidlist(pw, ENTRY_TYPE_ANY)) != NULL) { + for (i = 0; i < gidlist->ngids; i++) { + if (pw->pw_gid == gidlist->gids[i]) + continue; + sz += 13 + MAX_UID_T_LEN; + } + } + + /* Add space for user netgroups if netgroup_base specified. */ + if (!STAILQ_EMPTY(&ldap_conf.netgroup_base)) { + DPRINTF1("Looking up netgroups for %s", pw->pw_name); + if (sudo_netgroup_lookup(ld, pw, &netgroups)) { + STAILQ_FOREACH(ng, &netgroups, entries) { + sz += 14 + strlen(ng->name); + } + } else { + /* sudo_netgroup_lookup() failed, clean up. */ + while ((ng = STAILQ_FIRST(&netgroups)) != NULL) { + STAILQ_REMOVE_HEAD(&netgroups, entries); + free(ng->name); + free(ng); + } + } + } + + /* If timed, add space for time limits. */ + if (ldap_conf.timed) + sz += TIMEFILTER_LENGTH; + if ((buf = malloc(sz)) == NULL) + goto bad; + *buf = '\0'; + + /* + * If timed or using a search filter, start a global AND clause to + * contain the search filter, search criteria, and time restriction. + */ + if (ldap_conf.timed || ldap_conf.search_filter) + CHECK_STRLCPY(buf, "(&", sz); + + if (ldap_conf.search_filter) + CHECK_STRLCAT(buf, ldap_conf.search_filter, sz); + + /* Global OR + sudoUser=user_name filter */ + CHECK_STRLCAT(buf, "(|(sudoUser=", sz); + CHECK_LDAP_VCAT(buf, pw->pw_name, sz); + CHECK_STRLCAT(buf, ")", sz); + + /* Append primary group and gid */ + if (grp != NULL) { + CHECK_STRLCAT(buf, "(sudoUser=%", sz); + CHECK_LDAP_VCAT(buf, grp->gr_name, sz); + CHECK_STRLCAT(buf, ")", sz); + } + (void) snprintf(gidbuf, sizeof(gidbuf), "%u", (unsigned int)pw->pw_gid); + CHECK_STRLCAT(buf, "(sudoUser=%#", sz); + CHECK_STRLCAT(buf, gidbuf, sz); + CHECK_STRLCAT(buf, ")", sz); + + /* Append supplementary groups and gids */ + if (grlist != NULL) { + for (i = 0; i < grlist->ngroups; i++) { + if (grp != NULL && strcasecmp(grlist->groups[i], grp->gr_name) == 0) + continue; + CHECK_STRLCAT(buf, "(sudoUser=%", sz); + CHECK_LDAP_VCAT(buf, grlist->groups[i], sz); + CHECK_STRLCAT(buf, ")", sz); + } + } + if (gidlist != NULL) { + for (i = 0; i < gidlist->ngids; i++) { + if (pw->pw_gid == gidlist->gids[i]) + continue; + (void) snprintf(gidbuf, sizeof(gidbuf), "%u", + (unsigned int)gidlist->gids[i]); + CHECK_STRLCAT(buf, "(sudoUser=%#", sz); + CHECK_STRLCAT(buf, gidbuf, sz); + CHECK_STRLCAT(buf, ")", sz); + } + } + + /* Done with groups. */ + if (gidlist != NULL) + sudo_gidlist_delref(gidlist); + if (grlist != NULL) + sudo_grlist_delref(grlist); + if (grp != NULL) + sudo_gr_delref(grp); + + /* Add netgroups (if any), freeing the list as we go. */ + while ((ng = STAILQ_FIRST(&netgroups)) != NULL) { + STAILQ_REMOVE_HEAD(&netgroups, entries); + CHECK_STRLCAT(buf, "(sudoUser=+", sz); + CHECK_LDAP_VCAT(buf, ng->name, sz); + CHECK_STRLCAT(buf, ")", sz); + free(ng->name); + free(ng); + } + + /* Add ALL to list and end the global OR. */ + CHECK_STRLCAT(buf, "(sudoUser=ALL)", sz); + + /* Add the time restriction, or simply end the global OR. */ + if (ldap_conf.timed) { + CHECK_STRLCAT(buf, ")", sz); /* closes the global OR */ + if (!sudo_ldap_timefilter(timebuffer, sizeof(timebuffer))) + goto bad; + CHECK_STRLCAT(buf, timebuffer, sz); + } else if (ldap_conf.search_filter) { + CHECK_STRLCAT(buf, ")", sz); /* closes the global OR */ + } + CHECK_STRLCAT(buf, ")", sz); /* closes the global OR or the global AND */ + + debug_return_str(buf); +overflow: + sudo_warnx(U_("internal error, %s overflow"), __func__); + if (ng != NULL) { + /* Overflow while traversing netgroups. */ + free(ng->name); + free(ng); + } + errno = EOVERFLOW; +bad: + while ((ng = STAILQ_FIRST(&netgroups)) != NULL) { + STAILQ_REMOVE_HEAD(&netgroups, entries); + free(ng->name); + free(ng); + } + free(buf); + debug_return_str(NULL); +} + +/* + * Builds up a filter to check against non-Unix group + * entries in LDAP, including netgroups. + */ +static char * +sudo_ldap_build_pass2(void) +{ + char *filt, timebuffer[TIMEFILTER_LENGTH + 1]; + bool query_netgroups = def_use_netgroups; + int len; + debug_decl(sudo_ldap_build_pass2, SUDOERS_DEBUG_LDAP) + + /* No need to query netgroups if using netgroup_base. */ + if (!STAILQ_EMPTY(&ldap_conf.netgroup_base)) + query_netgroups = false; + + /* Short circuit if no netgroups and no non-Unix groups. */ + if (!query_netgroups && !def_group_plugin) { + errno = ENOENT; + debug_return_str(NULL); + } + + if (ldap_conf.timed) { + if (!sudo_ldap_timefilter(timebuffer, sizeof(timebuffer))) + debug_return_str(NULL); + } + + /* + * Match all sudoUsers beginning with '+' or '%:'. + * If a search filter or time restriction is specified, + * those get ANDed in to the expression. + */ + if (query_netgroups && def_group_plugin) { + len = asprintf(&filt, "%s%s(|(sudoUser=+*)(sudoUser=%%:*))%s%s", + (ldap_conf.timed || ldap_conf.search_filter) ? "(&" : "", + ldap_conf.search_filter ? ldap_conf.search_filter : "", + ldap_conf.timed ? timebuffer : "", + (ldap_conf.timed || ldap_conf.search_filter) ? ")" : ""); + } else { + len = asprintf(&filt, "(&%s(sudoUser=*)(sudoUser=%s*)%s)", + ldap_conf.search_filter ? ldap_conf.search_filter : "", + query_netgroups ? "+" : "%:", + ldap_conf.timed ? timebuffer : ""); + } + if (len == -1) + filt = NULL; + + debug_return_str(filt); +} + +static char * +berval_iter(void **vp) +{ + struct berval **bv = *vp; + + *vp = bv + 1; + return *bv ? (*bv)->bv_val : NULL; +} + +static bool +ldap_to_sudoers(LDAP *ld, struct ldap_result *lres, + struct userspec_list *ldap_userspecs) +{ + struct userspec *us; + struct member *m; + unsigned int i; + int rc; + debug_decl(ldap_to_sudoers, SUDOERS_DEBUG_LDAP) + + /* We only have a single userspec */ + if ((us = calloc(1, sizeof(*us))) == NULL) + goto oom; + TAILQ_INIT(&us->users); + TAILQ_INIT(&us->privileges); + STAILQ_INIT(&us->comments); + TAILQ_INSERT_TAIL(ldap_userspecs, us, entries); + + /* The user has already matched, use ALL as wildcard. */ + if ((m = calloc(1, sizeof(*m))) == NULL) + goto oom; + m->type = ALL; + TAILQ_INSERT_TAIL(&us->users, m, entries); + + /* Treat each sudoRole as a separate privilege. */ + for (i = 0; i < lres->nentries; i++) { + LDAPMessage *entry = lres->entries[i].entry; + struct berval **cmnds = NULL, **hosts = NULL; + struct berval **runasusers = NULL, **runasgroups = NULL; + struct berval **opts = NULL, **notbefore = NULL, **notafter = NULL; + struct privilege *priv = NULL; + char *cn = NULL; + + /* Ignore sudoRole without sudoCommand. */ + cmnds = sudo_ldap_get_values_len(ld, entry, "sudoCommand", &rc); + if (cmnds == NULL) { + if (rc == LDAP_NO_MEMORY) + goto cleanup; + continue; + } + + /* Get the entry's dn for long format printing. */ + if ((cn = sudo_ldap_get_first_rdn(ld, entry)) == NULL) + goto cleanup; + + /* Get sudoHost */ + hosts = sudo_ldap_get_values_len(ld, entry, "sudoHost", &rc); + if (rc == LDAP_NO_MEMORY) + goto cleanup; + + /* Get sudoRunAsUser / sudoRunAsGroup */ + runasusers = sudo_ldap_get_values_len(ld, entry, "sudoRunAsUser", &rc); + if (runasusers == NULL) { + if (rc != LDAP_NO_MEMORY) + runasusers = sudo_ldap_get_values_len(ld, entry, "sudoRunAs", &rc); + if (rc == LDAP_NO_MEMORY) + goto cleanup; + } + runasgroups = sudo_ldap_get_values_len(ld, entry, "sudoRunAsGroup", &rc); + if (rc == LDAP_NO_MEMORY) + goto cleanup; + + /* Get sudoNotBefore / sudoNotAfter */ + notbefore = sudo_ldap_get_values_len(ld, entry, "sudoNotBefore", &rc); + if (rc == LDAP_NO_MEMORY) + goto cleanup; + notafter = sudo_ldap_get_values_len(ld, entry, "sudoNotAfter", &rc); + if (rc == LDAP_NO_MEMORY) + goto cleanup; + + /* Parse sudoOptions. */ + opts = sudo_ldap_get_values_len(ld, entry, "sudoOption", &rc); + if (rc == LDAP_NO_MEMORY) + goto cleanup; + + priv = sudo_ldap_role_to_priv(cn, hosts, runasusers, runasgroups, + cmnds, opts, notbefore ? notbefore[0]->bv_val : NULL, + notafter ? notafter[0]->bv_val : NULL, false, true, berval_iter); + + cleanup: + if (cn != NULL) + ldap_memfree(cn); + if (cmnds != NULL) + ldap_value_free_len(cmnds); + if (hosts != NULL) + ldap_value_free_len(hosts); + if (runasusers != NULL) + ldap_value_free_len(runasusers); + if (runasgroups != NULL) + ldap_value_free_len(runasgroups); + if (opts != NULL) + ldap_value_free_len(opts); + if (notbefore != NULL) + ldap_value_free_len(notbefore); + if (notafter != NULL) + ldap_value_free_len(notafter); + + if (priv == NULL) + goto oom; + TAILQ_INSERT_TAIL(&us->privileges, priv, entries); + } + + debug_return_bool(true); + +oom: + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + free_userspecs(ldap_userspecs); + debug_return_bool(false); +} + +#ifdef HAVE_LDAP_SASL_INTERACTIVE_BIND_S +typedef unsigned int (*sudo_gss_krb5_ccache_name_t)(unsigned int *minor_status, const char *name, const char **old_name); +static sudo_gss_krb5_ccache_name_t sudo_gss_krb5_ccache_name; + +static int +sudo_set_krb5_ccache_name(const char *name, const char **old_name) +{ + int ret = 0; + unsigned int junk; + static bool initialized; + debug_decl(sudo_set_krb5_ccache_name, SUDOERS_DEBUG_LDAP) + + if (!initialized) { + sudo_gss_krb5_ccache_name = (sudo_gss_krb5_ccache_name_t) + sudo_dso_findsym(SUDO_DSO_DEFAULT, "gss_krb5_ccache_name"); + initialized = true; + } + + /* + * Try to use gss_krb5_ccache_name() if possible. + * We also need to set KRB5CCNAME since some LDAP libs may not use + * gss_krb5_ccache_name(). + */ + if (sudo_gss_krb5_ccache_name != NULL) { + ret = sudo_gss_krb5_ccache_name(&junk, name, old_name); + } else { + /* No gss_krb5_ccache_name(), fall back on KRB5CCNAME. */ + if (old_name != NULL) + *old_name = sudo_getenv("KRB5CCNAME"); + } + if (name != NULL && *name != '\0') { + if (sudo_setenv("KRB5CCNAME", name, true) == -1) + ret = -1; + } else { + if (sudo_unsetenv("KRB5CCNAME") == -1) + ret = -1; + } + + debug_return_int(ret); +} + +/* + * Make a copy of the credential cache file specified by KRB5CCNAME + * which must be readable by the user. The resulting cache file + * is root-owned and will be removed after authenticating via SASL. + */ +static char * +sudo_krb5_copy_cc_file(const char *old_ccname) +{ + int nfd, ofd = -1; + ssize_t nread, nwritten = -1; + static char new_ccname[sizeof(_PATH_TMP) + sizeof("sudocc_XXXXXXXX") - 1]; + char buf[10240], *ret = NULL; + debug_decl(sudo_krb5_copy_cc_file, SUDOERS_DEBUG_LDAP) + + old_ccname = sudo_krb5_ccname_path(old_ccname); + if (old_ccname != NULL) { + /* Open credential cache as user to prevent stolen creds. */ + if (!set_perms(PERM_USER)) + goto done; + ofd = open(old_ccname, O_RDONLY|O_NONBLOCK); + if (!restore_perms()) + goto done; + + if (ofd != -1) { + (void) fcntl(ofd, F_SETFL, 0); + if (sudo_lock_file(ofd, SUDO_LOCK)) { + snprintf(new_ccname, sizeof(new_ccname), "%s%s", + _PATH_TMP, "sudocc_XXXXXXXX"); + nfd = mkstemp(new_ccname); + if (nfd != -1) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "copy ccache %s -> %s", old_ccname, new_ccname); + while ((nread = read(ofd, buf, sizeof(buf))) > 0) { + ssize_t off = 0; + do { + nwritten = write(nfd, buf + off, nread - off); + if (nwritten == -1) { + sudo_warn("error writing to %s", new_ccname); + goto write_error; + } + off += nwritten; + } while (off < nread); + } + if (nread == -1) + sudo_warn("unable to read %s", new_ccname); +write_error: + close(nfd); + if (nread != -1 && nwritten != -1) { + ret = new_ccname; /* success! */ + } else { + unlink(new_ccname); /* failed */ + } + } else { + sudo_warn("unable to create temp file %s", new_ccname); + } + } + } else { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "unable to open %s", old_ccname); + } + } +done: + if (ofd != -1) + close(ofd); + debug_return_str(ret); +} + +static int +sudo_ldap_sasl_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; + int ret = LDAP_SUCCESS; + debug_decl(sudo_ldap_sasl_interact, SUDOERS_DEBUG_LDAP) + + for (; interact->id != SASL_CB_LIST_END; interact++) { + if (interact->id != SASL_CB_USER) { + sudo_warnx("sudo_ldap_sasl_interact: unexpected interact id %lu", + interact->id); + ret = LDAP_PARAM_ERROR; + break; + } + + 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 = strdup(interact->result); + if (interact->result == NULL) { + ret = LDAP_NO_MEMORY; + break; + } +#endif /* SASL_VERSION_MAJOR < 2 */ + DPRINTF2("sudo_ldap_sasl_interact: SASL_CB_USER %s", + (const char *)interact->result); + } + debug_return_int(ret); +} +#endif /* HAVE_LDAP_SASL_INTERACTIVE_BIND_S */ + +/* + * Create a new sudo_ldap_result structure. + */ +static struct ldap_result * +sudo_ldap_result_alloc(void) +{ + struct ldap_result *result; + debug_decl(sudo_ldap_result_alloc, SUDOERS_DEBUG_LDAP) + + result = calloc(1, sizeof(*result)); + if (result != NULL) + STAILQ_INIT(&result->searches); + + debug_return_ptr(result); +} + +/* + * Free the ldap result structure + */ +static void +sudo_ldap_result_free(struct ldap_result *lres) +{ + struct ldap_search_result *s; + debug_decl(sudo_ldap_result_free, SUDOERS_DEBUG_LDAP) + + if (lres != NULL) { + if (lres->nentries) { + free(lres->entries); + lres->entries = NULL; + } + while ((s = STAILQ_FIRST(&lres->searches)) != NULL) { + STAILQ_REMOVE_HEAD(&lres->searches, entries); + ldap_msgfree(s->searchresult); + free(s); + } + free(lres); + } + debug_return; +} + +/* + * Add a search result to the ldap_result structure. + */ +static struct ldap_search_result * +sudo_ldap_result_add_search(struct ldap_result *lres, LDAP *ldap, + LDAPMessage *searchresult) +{ + struct ldap_search_result *news; + debug_decl(sudo_ldap_result_add_search, SUDOERS_DEBUG_LDAP) + + /* Create new entry and add it to the end of the chain. */ + news = calloc(1, sizeof(*news)); + if (news != NULL) { + news->ldap = ldap; + news->searchresult = searchresult; + STAILQ_INSERT_TAIL(&lres->searches, news, entries); + } + + debug_return_ptr(news); +} + +/* + * Connect to the LDAP server specified by ld. + * Returns LDAP_SUCCESS on success, else non-zero. + */ +static int +sudo_ldap_bind_s(LDAP *ld) +{ + int ret; + debug_decl(sudo_ldap_bind_s, SUDOERS_DEBUG_LDAP) + +#ifdef HAVE_LDAP_SASL_INTERACTIVE_BIND_S + if (ldap_conf.rootuse_sasl == true || + (ldap_conf.rootuse_sasl != false && ldap_conf.use_sasl == true)) { + const char *old_ccname = NULL; + const char *new_ccname = ldap_conf.krb5_ccname; + const char *tmp_ccname = NULL; + void *auth_id = ldap_conf.rootsasl_auth_id ? + ldap_conf.rootsasl_auth_id : ldap_conf.sasl_auth_id; + int rc; + + /* Make temp copy of the user's credential cache as needed. */ + if (ldap_conf.krb5_ccname == NULL && user_ccname != NULL) { + new_ccname = tmp_ccname = sudo_krb5_copy_cc_file(user_ccname); + if (tmp_ccname == NULL) { + /* XXX - fatal error */ + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "unable to copy user ccache %s", user_ccname); + } + } + + if (new_ccname != NULL) { + rc = sudo_set_krb5_ccache_name(new_ccname, &old_ccname); + if (rc == 0) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "set ccache name %s -> %s", + old_ccname ? old_ccname : "(none)", new_ccname); + } else { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "sudo_set_krb5_ccache_name() failed: %d", rc); + } + } + ret = ldap_sasl_interactive_bind_s(ld, ldap_conf.binddn, + ldap_conf.sasl_mech, NULL, NULL, LDAP_SASL_QUIET, + sudo_ldap_sasl_interact, auth_id); + if (new_ccname != NULL) { + rc = sudo_set_krb5_ccache_name(old_ccname ? old_ccname : "", NULL); + if (rc == 0) { + sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, + "restore ccache name %s -> %s", new_ccname, + old_ccname ? old_ccname : "(none)"); + } else { + sudo_debug_printf(SUDO_DEBUG_WARN|SUDO_DEBUG_LINENO, + "sudo_set_krb5_ccache_name() failed: %d", rc); + } + /* Remove temporary copy of user's credential cache. */ + if (tmp_ccname != NULL) + unlink(tmp_ccname); + } + if (ret != LDAP_SUCCESS) { + sudo_warnx("ldap_sasl_interactive_bind_s(): %s", + ldap_err2string(ret)); + goto done; + } + DPRINTF1("ldap_sasl_interactive_bind_s() ok"); + } 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); + + ret = ldap_sasl_bind_s(ld, ldap_conf.binddn, LDAP_SASL_SIMPLE, &bv, + NULL, NULL, NULL); + if (ret != LDAP_SUCCESS) { + sudo_warnx("ldap_sasl_bind_s(): %s", ldap_err2string(ret)); + goto done; + } + DPRINTF1("ldap_sasl_bind_s() ok"); + } +#else + { + ret = ldap_simple_bind_s(ld, ldap_conf.binddn, ldap_conf.bindpw); + if (ret != LDAP_SUCCESS) { + sudo_warnx("ldap_simple_bind_s(): %s", ldap_err2string(ret)); + goto done; + } + DPRINTF1("ldap_simple_bind_s() ok"); + } +#endif +done: + debug_return_int(ret); +} + +/* + * Shut down the LDAP connection. + */ +static int +sudo_ldap_close(struct sudo_nss *nss) +{ + struct sudo_ldap_handle *handle = nss->handle; + debug_decl(sudo_ldap_close, SUDOERS_DEBUG_LDAP) + + if (handle != NULL) { + /* Unbind and close the LDAP connection. */ + if (handle->ld != NULL) { + ldap_unbind_ext_s(handle->ld, NULL, NULL); + handle->ld = NULL; + } + + /* Free the handle container. */ + if (handle->pw != NULL) + sudo_pw_delref(handle->pw); + free_parse_tree(&handle->parse_tree); + free(handle); + nss->handle = NULL; + } + debug_return_int(0); +} + +/* + * Open a connection to the LDAP server. + * Returns 0 on success and non-zero on failure. + */ +static int +sudo_ldap_open(struct sudo_nss *nss) +{ + LDAP *ld; + int rc = -1; + bool ldapnoinit = false; + struct sudo_ldap_handle *handle; + debug_decl(sudo_ldap_open, SUDOERS_DEBUG_LDAP) + + if (nss->handle != NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: called with non-NULL handle %p", __func__, nss->handle); + sudo_ldap_close(nss); + } + + if (!sudo_ldap_read_config()) + goto done; + + /* Prevent reading of user ldaprc and system defaults. */ + if (sudo_getenv("LDAPNOINIT") == NULL) { + if (sudo_setenv("LDAPNOINIT", "1", true) == 0) + ldapnoinit = true; + } + + /* Set global LDAP options */ + if (sudo_ldap_set_options_global() != LDAP_SUCCESS) + goto done; + + /* Connect to LDAP server */ +#ifdef HAVE_LDAP_INITIALIZE + if (!STAILQ_EMPTY(&ldap_conf.uri)) { + char *buf = sudo_ldap_join_uri(&ldap_conf.uri); + if (buf == NULL) + goto done; + DPRINTF2("ldap_initialize(ld, %s)", buf); + rc = ldap_initialize(&ld, buf); + free(buf); + } else +#endif + rc = sudo_ldap_init(&ld, ldap_conf.host, ldap_conf.port); + if (rc != LDAP_SUCCESS) { + sudo_warnx(U_("unable to initialize LDAP: %s"), ldap_err2string(rc)); + goto done; + } + + /* Set LDAP per-connection options */ + rc = sudo_ldap_set_options_conn(ld); + if (rc != LDAP_SUCCESS) + goto done; + + if (ldapnoinit) + (void) sudo_unsetenv("LDAPNOINIT"); + + 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) { + sudo_warnx("ldap_start_tls_s(): %s", ldap_err2string(rc)); + goto done; + } + DPRINTF1("ldap_start_tls_s() ok"); +#elif defined(HAVE_LDAP_SSL_CLIENT_INIT) && defined(HAVE_LDAP_START_TLS_S_NP) + int sslrc; + rc = ldap_ssl_client_init(ldap_conf.tls_keyfile, ldap_conf.tls_keypw, + 0, &sslrc); + if (rc != LDAP_SUCCESS) { + sudo_warnx("ldap_ssl_client_init(): %s (SSL reason code %d)", + ldap_err2string(rc), sslrc); + goto done; + } + rc = ldap_start_tls_s_np(ld, NULL); + if (rc != LDAP_SUCCESS) { + sudo_warnx("ldap_start_tls_s_np(): %s", ldap_err2string(rc)); + goto done; + } + DPRINTF1("ldap_start_tls_s_np() ok"); +#else + sudo_warnx(U_("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 */ + rc = sudo_ldap_bind_s(ld); + if (rc != LDAP_SUCCESS) + goto done; + + /* Create a handle container. */ + handle = calloc(1, sizeof(struct sudo_ldap_handle)); + if (handle == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + rc = -1; + goto done; + } + handle->ld = ld; + /* handle->pw = NULL; */ + init_parse_tree(&handle->parse_tree); + nss->handle = handle; + +done: + debug_return_int(rc == LDAP_SUCCESS ? 0 : -1); +} + +static int +sudo_ldap_getdefs(struct sudo_nss *nss) +{ + struct sudo_ldap_handle *handle = nss->handle; + struct timeval tv, *tvp = NULL; + struct ldap_config_str *base; + LDAPMessage *entry, *result = NULL; + char *filt = NULL; + int rc, ret = -1; + static bool cached; + debug_decl(sudo_ldap_getdefs, SUDOERS_DEBUG_LDAP) + + if (handle == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: called with NULL handle", __func__); + debug_return_int(-1); + } + + /* Use cached result if present. */ + if (cached) + debug_return_int(0); + + filt = sudo_ldap_build_default_filter(); + if (filt == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_int(-1); + } + DPRINTF1("Looking for cn=defaults: %s", filt); + + STAILQ_FOREACH(base, &ldap_conf.base, entries) { + LDAP *ld = handle->ld; + + if (ldap_conf.timeout > 0) { + tv.tv_sec = ldap_conf.timeout; + tv.tv_usec = 0; + tvp = &tv; + } + ldap_msgfree(result); + result = NULL; + rc = ldap_search_ext_s(ld, base->val, LDAP_SCOPE_SUBTREE, + filt, NULL, 0, NULL, NULL, tvp, 0, &result); + if (rc == LDAP_SUCCESS && (entry = ldap_first_entry(ld, result))) { + DPRINTF1("found:%s", ldap_get_dn(ld, entry)); + if (!sudo_ldap_parse_options(ld, entry, &handle->parse_tree.defaults)) + goto done; + } else { + DPRINTF1("no default options found in %s", base->val); + } + } + cached = true; + ret = 0; + +done: + ldap_msgfree(result); + free(filt); + + debug_return_int(ret); +} + +/* + * Comparison function for ldap_entry_wrapper structures, ascending order. + * This should match role_order_cmp() in parse_ldif.c. + */ +static int +ldap_entry_compare(const void *a, const void *b) +{ + const struct ldap_entry_wrapper *aw = a; + const struct ldap_entry_wrapper *bw = b; + debug_decl(ldap_entry_compare, SUDOERS_DEBUG_LDAP) + + debug_return_int(aw->order < bw->order ? -1 : + (aw->order > bw->order ? 1 : 0)); +} + +/* + * Return the last entry in the list of searches, usually the + * one currently being used to add entries. + */ +static struct ldap_search_result * +sudo_ldap_result_last_search(struct ldap_result *lres) +{ + debug_decl(sudo_ldap_result_last_search, SUDOERS_DEBUG_LDAP) + + debug_return_ptr(STAILQ_LAST(&lres->searches, ldap_search_result, entries)); +} + +/* + * Add an entry to the result structure. + */ +static struct ldap_entry_wrapper * +sudo_ldap_result_add_entry(struct ldap_result *lres, LDAPMessage *entry) +{ + struct ldap_search_result *last; + struct berval **bv; + double order = 0.0; + char *ep; + int rc; + debug_decl(sudo_ldap_result_add_entry, SUDOERS_DEBUG_LDAP) + + /* Determine whether the entry has the sudoOrder attribute. */ + last = sudo_ldap_result_last_search(lres); + if (last != NULL) { + bv = sudo_ldap_get_values_len(last->ldap, entry, "sudoOrder", &rc); + if (rc == LDAP_NO_MEMORY) { + /* XXX - return error */ + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + } else { + if (ldap_count_values_len(bv) > 0) { + /* Get the value of this attribute, 0 if not present. */ + DPRINTF2("order attribute raw: %s", (*bv)->bv_val); + order = strtod((*bv)->bv_val, &ep); + if (ep == (*bv)->bv_val || *ep != '\0') { + sudo_warnx(U_("invalid sudoOrder attribute: %s"), + (*bv)->bv_val); + order = 0.0; + } + DPRINTF2("order attribute: %f", order); + } + ldap_value_free_len(bv); + } + } + + /* + * Enlarge the array of entry wrappers as needed, preallocating blocks + * of 100 entries to save on allocation time. + */ + if (++lres->nentries > lres->allocated_entries) { + int allocated_entries = lres->allocated_entries + ALLOCATION_INCREMENT; + struct ldap_entry_wrapper *entries = reallocarray(lres->entries, + allocated_entries, sizeof(lres->entries[0])); + if (entries == NULL) + debug_return_ptr(NULL); + lres->allocated_entries = allocated_entries; + lres->entries = entries; + } + + /* Fill in the new entry and return it. */ + lres->entries[lres->nentries - 1].entry = entry; + lres->entries[lres->nentries - 1].order = order; + + debug_return_ptr(&lres->entries[lres->nentries - 1]); +} + +/* + * Perform the LDAP query for the user. The caller is responsible for + * freeing the result with sudo_ldap_result_free(). + */ +static struct ldap_result * +sudo_ldap_result_get(struct sudo_nss *nss, struct passwd *pw) +{ + struct sudo_ldap_handle *handle = nss->handle; + struct ldap_config_str *base; + struct ldap_result *lres; + struct timeval tv, *tvp = NULL; + LDAPMessage *entry, *result; + LDAP *ld = handle->ld; + char *filt = NULL; + int pass, rc; + debug_decl(sudo_ldap_result_get, SUDOERS_DEBUG_LDAP) + + /* + * 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 non- + * Unix groups, including netgroups. Then we take the non-Unix + * groups returned and try to match them against the username. + * + * Since we have to sort the possible entries before we make a + * decision, we perform the queries and store all of the results in + * an ldap_result object. The results are then sorted by sudoOrder. + */ + lres = sudo_ldap_result_alloc(); + if (lres == NULL) + goto oom; + for (pass = 0; pass < 2; pass++) { + filt = pass ? sudo_ldap_build_pass2() : sudo_ldap_build_pass1(ld, pw); + if (filt != NULL) { + DPRINTF1("ldap search '%s'", filt); + STAILQ_FOREACH(base, &ldap_conf.base, entries) { + DPRINTF1("searching from base '%s'", + base->val); + if (ldap_conf.timeout > 0) { + tv.tv_sec = ldap_conf.timeout; + tv.tv_usec = 0; + tvp = &tv; + } + result = NULL; + rc = ldap_search_ext_s(ld, base->val, LDAP_SCOPE_SUBTREE, filt, + NULL, 0, NULL, NULL, tvp, 0, &result); + if (rc != LDAP_SUCCESS) { + DPRINTF1("ldap search pass %d failed: %s", pass + 1, + ldap_err2string(rc)); + continue; + } + + /* Add the search result to list of search results. */ + DPRINTF1("adding search result"); + if (sudo_ldap_result_add_search(lres, ld, result) == NULL) + goto oom; + LDAP_FOREACH(entry, ld, result) { + if (pass != 0) { + /* Check non-unix group in 2nd pass. */ + switch (sudo_ldap_check_non_unix_group(ld, entry, pw)) { + case -1: + goto oom; + case false: + continue; + default: + break; + } + } + if (sudo_ldap_result_add_entry(lres, entry) == NULL) + goto oom; + } + DPRINTF1("result now has %d entries", lres->nentries); + } + free(filt); + filt = NULL; + } else if (errno != ENOENT) { + /* Out of memory? */ + goto oom; + } + } + + /* Sort the entries by the sudoOrder attribute. */ + if (lres->nentries != 0) { + DPRINTF1("sorting remaining %d entries", lres->nentries); + qsort(lres->entries, lres->nentries, sizeof(lres->entries[0]), + ldap_entry_compare); + } + + debug_return_ptr(lres); +oom: + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + free(filt); + sudo_ldap_result_free(lres); + debug_return_ptr(NULL); +} + +/* + * Perform LDAP query for user and host and convert to sudoers + * parse tree. + */ +static int +sudo_ldap_query(struct sudo_nss *nss, struct passwd *pw) +{ + struct sudo_ldap_handle *handle = nss->handle; + struct ldap_result *lres = NULL; + int ret = -1; + debug_decl(sudo_ldap_query, SUDOERS_DEBUG_LDAP) + + if (handle == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: called with NULL handle", __func__); + debug_return_int(-1); + } + + /* Use cached result if it matches pw. */ + if (handle->pw != NULL) { + if (pw == handle->pw) { + ret = 0; + goto done; + } + sudo_pw_delref(handle->pw); + handle->pw = NULL; + } + + /* Free old userspecs, if any. */ + free_userspecs(&handle->parse_tree.userspecs); + + DPRINTF1("%s: ldap search user %s, host %s", __func__, pw->pw_name, + user_runhost); + if ((lres = sudo_ldap_result_get(nss, pw)) == NULL) + goto done; + + /* Convert to sudoers parse tree. */ + if (!ldap_to_sudoers(handle->ld, lres, &handle->parse_tree.userspecs)) + goto done; + + /* Stash a ref to the passwd struct in the handle. */ + sudo_pw_addref(pw); + handle->pw = pw; + + ret = 0; + +done: + /* Cleanup. */ + sudo_ldap_result_free(lres); + if (ret == -1) + free_userspecs(&handle->parse_tree.userspecs); + debug_return_int(ret); +} + +/* + * Return the initialized (but empty) sudoers parse tree. + * The contents will be populated by the getdefs() and query() functions. + */ +static struct sudoers_parse_tree * +sudo_ldap_parse(struct sudo_nss *nss) +{ + struct sudo_ldap_handle *handle = nss->handle; + debug_decl(sudo_ldap_parse, SUDOERS_DEBUG_LDAP) + + if (handle == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR, + "%s: called with NULL handle", __func__); + debug_return_ptr(NULL); + } + + debug_return_ptr(&handle->parse_tree); +} + +#if 0 +/* + * Create an ldap_result from an LDAP search result. + * + * This function is currently not used anywhere, it is left here as + * an example of how to use the cached searches. + */ +static struct ldap_result * +sudo_ldap_result_from_search(LDAP *ldap, LDAPMessage *searchresult) +{ + struct ldap_search_result *last; + struct ldap_result *result; + LDAPMessage *entry; + + /* + * An ldap_result is built from several search results, which are + * organized in a list. The head of the list is maintained in the + * ldap_result structure, together with the wrappers that point + * to individual entries, this has to be initialized first. + */ + result = sudo_ldap_result_alloc(); + if (result == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_ptr(NULL); + } + + /* + * Build a new list node for the search result, this creates the + * list node. + */ + last = sudo_ldap_result_add_search(result, ldap, searchresult); + + /* + * Now add each entry in the search result to the array of of entries + * in the ldap_result object. + */ + LDAP_FOREACH(entry, last->ldap, last->searchresult) { + if (sudo_ldap_result_add_entry(result, entry) == NULL) { + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + sudo_ldap_result_free(result); + result = NULL; + break; + } + } + DPRINTF1("sudo_ldap_result_from_search: %d entries found", + result ? result->nentries : -1); + return result; +} +#endif + +/* sudo_nss implementation */ +struct sudo_nss sudo_nss_ldap = { + { NULL, NULL }, + sudo_ldap_open, + sudo_ldap_close, + sudo_ldap_parse, + sudo_ldap_query, + sudo_ldap_getdefs +}; |