From: Todd C. Miller Date: Sat, 13 Nov 2010 18:53:37 +0000 (-0500) Subject: Merge in ordered LDAP entry support from Andreas Mueller. X-Git-Tag: SUDO_1_7_5~110 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=18023610d3b716ba034e05d8ffb747fe0e981598;p=sudo Merge in ordered LDAP entry support from Andreas Mueller. --HG-- branch : 1.7 --- diff --git a/ldap.c b/ldap.c index c99f03c1c..d21598f1d 100644 --- a/ldap.c +++ b/ldap.c @@ -116,11 +116,6 @@ #define SUDO_LDAP_SSL 1 #define SUDO_LDAP_STARTTLS 2 -struct ldap_result_list { - struct ldap_result_list *next; - LDAPMessage *result; -}; - /* The TIMEFILTER_LENGTH includes the filter itself plus the global AND wrapped around the user filter and the time filter when timed entries are used. The length is computed as follows: @@ -130,6 +125,41 @@ struct ldap_result_list { */ #define TIMEFILTER_LENGTH 114 +/* + * 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. + * XXX - should probably be a tailq since we do appends + */ +struct ldap_search_list { + LDAP *ldap; + LDAPMessage *searchresult; + struct ldap_search_list *next; +}; + +/* + * 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; + int allowed; +}; + +/* + * 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; + int nentries; +}; + struct ldap_config_table { const char *conf_str; /* config file string */ short type; /* CONF_BOOL, CONF_INT, CONF_STR */ @@ -143,7 +173,7 @@ struct ldap_config_list_str { char val[1]; }; -/* ldap configuration structure */ +/* LDAP configuration structure */ static struct ldap_config { int port; int version; @@ -252,6 +282,18 @@ static struct ldap_config_table ldap_conf_table[] = { { NULL } }; +/* XXX - reorder code to avoid protos */ +static struct ldap_result *sudo_ldap_result_alloc __P((void)); +static struct ldap_search_list *sudo_ldap_result_last_search __P((struct ldap_result *lres)); +static void sudo_ldap_result_free __P((struct ldap_result *lres)); +static struct ldap_search_list *sudo_ldap_result_add_search + __P((struct ldap_result *lres, LDAP *ldap, LDAPMessage *searchresult)); +static struct ldap_entry_wrapper *sudo_ldap_result_add_entry + __P((struct ldap_result *lres, LDAPMessage *entry)); +static int ldap_entry_compare __P((const void *a, const void *b)); +static LDAPMessage *sudo_ldap_result_get_entry __P((struct ldap_result *lres, + int idx)); + /* sudo_nss implementation */ static int sudo_ldap_open __P((struct sudo_nss *nss)); static int sudo_ldap_close __P((struct sudo_nss *nss)); @@ -267,6 +309,24 @@ static int sudo_ldap_display_bound_defaults __P((struct sudo_nss *nss, static int sudo_ldap_display_privs __P((struct sudo_nss *nss, struct passwd *pw, struct lbuf *lbuf)); +/* ldap retrieval functions using sudo_nss */ +static struct ldap_result *sudo_ldap_result_get __P((struct sudo_nss *nss, + struct passwd *pw)); +static void sudo_ldap_result_free_nss __P((struct sudo_nss *nss)); + +/* + * LDAP sudo_nss handle. + * We store the connection to the LDAP server, the cached ldap_result object + * (if any), and the name of the user the query was performed for. + * If a new query is launched with sudo_ldap_result_get() that specifies a + * different user, the old cached result is freed before the new query is run. + */ +struct sudo_ldap_handle { + LDAP *ld; + struct ldap_result *result; + char *username; +}; + struct sudo_nss sudo_nss_ldap = { &sudo_nss_ldap, NULL, @@ -533,7 +593,7 @@ sudo_ldap_check_user_netgroup(ld, entry, user) if (netgr_matches(val, NULL, NULL, user)) ret = TRUE; DPRINTF(("ldap sudoUser netgroup '%s' ... %s", val, - ret ? "MATCH!" : "not"), 2); + ret ? "MATCH!" : "not"), 2 + ((ret) ? 0 : 1)); } ldap_value_free_len(bv); /* cleanup */ @@ -914,16 +974,15 @@ done: } /* - * builds together a filter to check against ldap + * Builds up a filter to check against LDAP. */ static char * sudo_ldap_build_pass1(pw) struct passwd *pw; { struct group *grp; + char *buf, timebuffer[TIMEFILTER_LENGTH]; size_t sz; - char *buf; - char timebuffer[TIMEFILTER_LENGTH]; int i; /* Start with (|(sudoUser=USERNAME)(sudoUser=ALL)) + NUL */ @@ -996,6 +1055,28 @@ sudo_ldap_build_pass1(pw) return(buf); } +/* + * Builds up a filter to check against netgroup entries in LDAP. + */ +static char * +sudo_ldap_build_pass2() +{ + char *buf, timebuffer[TIMEFILTER_LENGTH]; + + if (ldap_conf.timed) { + /* + * If timed, use a global AND clause that has the time limit as + * as the second leg. + */ + easprintf(&buf, "(&(sudoUser=+*)%s)", timebuffer); + } else { + /* No time limit, just the netgroup selection. */ + buf = estrdup("sudoUser=+*"); + } + + return(buf); +} + /* * Map yes/true/on to TRUE, no/false/off to FALSE, else -1 */ @@ -1321,7 +1402,8 @@ sudo_ldap_display_defaults(nss, pw, lbuf) { struct berval **bv, **p; struct ldap_config_list_str *base; - LDAP *ld = (LDAP *) nss->handle; + struct sudo_ldap_handle *handle = nss->handle; + LDAP *ld = handle->ld; LDAPMessage *entry, *result; char *prefix; int rc, count = 0; @@ -1511,7 +1593,19 @@ sudo_ldap_display_entry_long(ld, entry, lbuf) lbuf_append(lbuf, "\n", NULL); } - /* get the Command Values from the entry */ + /* + * Display order attribute if present. This attribute is single valued, + * so there is no need for a loop. + */ + bv = ldap_get_values_len(ld, entry, "sudoOrder"); + if (bv != NULL) { + if (*bv != NULL) { + lbuf_append(lbuf, " Order: ", (*bv)->bv_val, "\n", NULL); + } + ldap_value_free_len(bv); + } + + /* Get the command values from the entry. */ bv = ldap_get_values_len(ld, entry, "sudoCommand"); if (bv != NULL) { lbuf_append(lbuf, " Commands:\n", NULL); @@ -1534,55 +1628,27 @@ sudo_ldap_display_privs(nss, pw, lbuf) 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; + struct sudo_ldap_handle *handle = nss->handle; + LDAP *ld = handle->ld; + struct ldap_result*lres; + LDAPMessage *entry; + int i, 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)) { + DPRINTF(("ldap search for command list"), 1); + lres = sudo_ldap_result_get(nss, pw); - 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); + /* Display all matching entries. */ + for (i = 0; i < lres->nentries; i++) { + entry = sudo_ldap_result_get_entry(lres, i); + if (long_list) + count += sudo_ldap_display_entry_long(ld, entry, lbuf); + else + count += sudo_ldap_display_entry_short(ld, entry, lbuf); } + done: return(count); } @@ -1592,55 +1658,31 @@ 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 */ + struct sudo_ldap_handle *handle = nss->handle; + LDAP *ld = handle->ld; + struct ldap_result *lres; + LDAPMessage *entry; + int i, found = FALSE; 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)) { + goto done; - found = TRUE; - break; - } - } - ldap_msgfree(result); + /* the sudo_ldap_result_get method returns all nodes that are match + the user and the host */ + DPRINTF(("ldap search for command list"), 1); + lres = sudo_ldap_result_get(nss, pw); + + /* display all the entries that match */ + for (found = FALSE, i = 0; (!found) && (i < lres->nentries); i++) { + entry = sudo_ldap_result_get_entry(lres, i); + if (sudo_ldap_check_command(ld, entry, NULL) && + sudo_ldap_check_runas(ld, entry)) { + found = TRUE; + goto done; } - efree(filt); } +done: if (found) printf("%s%s%s\n", safe_cmnd ? safe_cmnd : user_cmnd, user_args ? " " : "", user_args ? user_args : ""); @@ -1855,6 +1897,7 @@ sudo_ldap_open(nss) { LDAP *ld; int rc, ldapnoinit = FALSE; + struct sudo_ldap_handle *handle; if (!sudo_ldap_read_config()) return(-1); @@ -1915,7 +1958,13 @@ sudo_ldap_open(nss) if (sudo_ldap_bind_s(ld) != 0) return(-1); - nss->handle = ld; + /* Create a handle container. */ + handle = emalloc(sizeof(struct sudo_ldap_handle)); + handle->ld = ld; + handle->result = NULL; + handle->username = NULL; + nss->handle = handle; + return(0); } @@ -1924,9 +1973,10 @@ 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 */ + struct sudo_ldap_handle *handle = nss->handle; + LDAP *ld = handle->ld; + LDAPMessage *entry, *result; + int rc; if (ld == NULL) return(-1); @@ -1957,58 +2007,48 @@ sudo_ldap_lookup(nss, ret, pwflag) int ret; int pwflag; { - struct ldap_config_list_str *base; - LDAP *ld = (LDAP *) nss->handle; - LDAPMessage *entry, *result, *matching_entry = NULL; - char *filt; - int do_netgr, rc, allowed = UNSPEC; - int setenv_implied; + struct sudo_ldap_handle *handle = nss->handle; + LDAP *ld = handle->ld; + LDAPMessage *entry; + int i, rc, setenv_implied; int ldap_user_matches = FALSE, ldap_host_matches = FALSE; struct passwd *pw = list_pw ? list_pw : sudo_user.pw; - struct ldap_result_list *rl, *results = NULL; + struct ldap_result *lres = NULL; if (ld == NULL) return(ret); + /* Fetch list of sudoRole entries that match user and host. */ + lres = sudo_ldap_result_get(nss, pw); + + /* + * The following queries are only determine whether or not a + * password is required, so the order of the entries doesn't matter. + */ if (pwflag) { + DPRINTF(("perform search for pwflag %d", pwflag), 1); + int matched = FALSE; int doauth = UNSPEC; enum def_tupple pwcheck = (pwflag == -1) ? never : sudo_defs_table[pwflag].sd_un.tuple; - for (allowed = 0, do_netgr = 0; !allowed && 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)) { - allowed = 1; - break; /* end foreach */ - } - } - } - ldap_msgfree(result); + for (i = 0; i < lres->nentries; i++) { + ldap_user_matches = TRUE; + ldap_host_matches = TRUE; + entry = sudo_ldap_result_get_entry(lres, i); + 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 = TRUE; + break; } - efree(filt); } - if (allowed || user_uid == 0) { + if (matched || user_uid == 0) { SET(ret, VALIDATE_OK); CLR(ret, VALIDATE_NOT_OK); if (def_authenticate) { @@ -2032,6 +2072,103 @@ sudo_ldap_lookup(nss, ret, pwflag) goto done; } + DPRINTF(("searching LDAP for sudoers entries"), 1); + + setenv_implied = FALSE; + for (i = 0; i < lres->nentries; i++) { + ldap_user_matches = TRUE; + ldap_host_matches = TRUE; + entry = sudo_ldap_result_get_entry(lres, i); + if (sudo_ldap_check_runas(ld, entry) && + /* verify command match */ + (rc = sudo_ldap_check_command(ld, entry, &setenv_implied)) != UNSPEC + ) { + /* We have a match, set allowed flag based on rc. */ + DPRINTF(("Command %sallowed", rc == TRUE ? "" : "NOT "), 1); + lres->entries[i].allowed = rc; + } + } + + /* The matching entry with the highest order should now be applied. */ + /* XXX - will this choose an entry with a non-maching runas user? */ + entry = sudo_ldap_result_get_entry(lres, 0); + DPRINTF(("LDAP entry: %p", entry), 1); + if (lres->entries[0].allowed == TRUE) { + /* Apply any options. */ + sudo_ldap_check_command(ld, entry, &setenv_implied); + 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 */ + SET(ret, VALIDATE_OK); + CLR(ret, VALIDATE_NOT_OK); + } else { + SET(ret, VALIDATE_NOT_OK); + CLR(ret, VALIDATE_OK); + } + + /* XXX - if we did not jump to the label "done", then some of + the code below is tautological. i.e. ldap_user_matches and + ldap_host_matches will be set (otherwise we would not + have selected the entry) */ +done: + DPRINTF(("done with LDAP searches"), 1); + DPRINTF(("user_matches=%d", ldap_user_matches), 1); + DPRINTF(("host_matches=%d", ldap_host_matches), 1); + + if (!ISSET(ret, VALIDATE_OK)) { + /* No matching entries. */ + 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); +} + +/* + * Perform the LDAP query for the user or return a cached query if + * there is one for this user. + */ +static struct ldap_result * +sudo_ldap_result_get(nss, pw) + struct sudo_nss *nss; + struct passwd *pw; +{ + struct sudo_ldap_handle *handle = nss->handle; + struct ldap_config_list_str *base; + struct ldap_result *lres; + LDAPMessage *entry, *result; + LDAP *ld = handle->ld; + int do_netgr, rc; + char *filt; + + /* + * If we already have a cached result, return it so we don't have to + * have to contact the LDAP server again. + */ + if (handle->result) { + if (strcmp(pw->pw_name, handle->username) == 0) { + DPRINTF(("reusing previous result (user %s) with %d entries", + handle->username, handle->result->nentries), 1); + return(handle->result); + } + /* User mismatch, cached result cannot be used. */ + DPRINTF(("removing result (user %s), new search (user %s)", + handle->username, pw->pw_name), 1); + sudo_ldap_result_free_nss(nss); + } + /* * Okay - time to search for anything that matches this user * Lets limit it to only two queries of the LDAP server @@ -2045,12 +2182,17 @@ sudo_ldap_lookup(nss, ret, pwflag) * 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. + * + * 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. */ - setenv_implied = FALSE; - for (do_netgr = 0; allowed != FALSE && do_netgr < 2; do_netgr++) { - filt = do_netgr ? estrdup("sudoUser=+*") : sudo_ldap_build_pass1(pw); + lres = sudo_ldap_result_alloc(); + for (do_netgr = 0; do_netgr < 2; do_netgr++) { + filt = do_netgr ? sudo_ldap_build_pass2() : sudo_ldap_build_pass1(pw); DPRINTF(("ldap search '%s'", filt), 1); - for (base = ldap_conf.base; allowed != FALSE && base != NULL; base = base->next) { + for (base = ldap_conf.base; base != NULL; base = base->next) { + DPRINTF(("searching from base '%s'", base->val), 1); result = NULL; rc = ldap_search_ext_s(ld, base->val, LDAP_SCOPE_SUBTREE, filt, NULL, 0, NULL, NULL, NULL, 0, &result); @@ -2059,99 +2201,74 @@ sudo_ldap_lookup(nss, ret, pwflag) continue; } - /* Add result to list for later free()ing. */ - rl = emalloc(sizeof(*rl)); - rl->result = result; - rl->next = results; - results = rl; - - /* parse each entry returned from this most recent search */ + /* Add the seach result to list of search results. */ + DPRINTF(("adding search result"), 1); + sudo_ldap_result_add_search(lres, ld, result); 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); - if (rc == FALSE) { - /* Command explicitly denied, we are done. */ - allowed = FALSE; - break; - } else if (rc == TRUE && allowed == UNSPEC) { - /* Command allowed, no other matches yet. */ - allowed = TRUE; - matching_entry = entry; - } + if ((!do_netgr || + sudo_ldap_check_user_netgroup(ld, entry, pw->pw_name)) && + sudo_ldap_check_host(ld, entry)) { + sudo_ldap_result_add_entry(lres, entry); } } + DPRINTF(("result now has %d entries", lres->nentries), 1); } efree(filt); } - if (allowed == TRUE) { - SET(ret, VALIDATE_OK); - CLR(ret, VALIDATE_NOT_OK); - /* Set options based on matching entry. */ - if (setenv_implied) - def_setenv = TRUE; - sudo_ldap_parse_options(ld, matching_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 */ - } else if (allowed == FALSE) { - SET(ret, VALIDATE_NOT_OK); - CLR(ret, VALIDATE_OK); - } + /* Sort the entries by the sudoOrder attribute. */ + DPRINTF(("sorting remaining %d entries", lres->nentries), 1); + qsort(lres->entries, lres->nentries, sizeof(lres->entries[0]), + ldap_entry_compare); - /* Free all results. */ - while ((rl = results) != NULL) { - results = results->next; - ldap_msgfree(rl->result); - efree(rl); - } + /* Store everything in the sudo_nss handle. */ + handle->result = lres; + handle->username = estrdup(pw->pw_name); -done: - DPRINTF(("user_matches=%d", ldap_user_matches), 1); - DPRINTF(("host_matches=%d", ldap_host_matches), 1); + return(lres); +} - if (!ISSET(ret, VALIDATE_OK)) { - /* we do not have a match */ - if (pwflag && list_pw == NULL) - SET(ret, FLAG_NO_CHECK); +/* + * Free the ldap result structure in the sudo_nss handle. + */ +static void +sudo_ldap_result_free_nss(nss) + struct sudo_nss *nss; +{ + struct sudo_ldap_handle *handle = nss->handle; + + if (handle->result != NULL) { + DPRINTF(("removing reusable search result"), 1); + sudo_ldap_result_free(handle->result); + if (handle->username) { + efree(handle->username); + handle->username = NULL; + } + handle->result = NULL; } - 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 + * Shut down the 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); + struct sudo_ldap_handle *handle = nss->handle; + + if (handle != NULL) { + /* Free the result before unbinding; it may use the LDAP connection. */ + sudo_ldap_result_free_nss(nss); + + /* 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. */ + efree(nss->handle); nss->handle = NULL; } return(0); @@ -2166,3 +2283,199 @@ sudo_ldap_parse(nss) { return(0); } + +/* + * Create a new sudo_ldap_result structure. + */ +static struct ldap_result * +sudo_ldap_result_alloc() +{ + struct ldap_result *result; + + result = emalloc(sizeof(*result)); + result->searches = NULL; + result->nentries = 0; + result->entries = NULL; + return(result); +} + +/* + * Free the ldap result structure + */ +static void +sudo_ldap_result_free(lres) + struct ldap_result *lres; +{ + struct ldap_search_list *s; + + if (lres != NULL) { + if (lres->nentries) { + efree(lres->entries); + lres->entries = NULL; + } + if (lres->searches) { + while ((s = lres->searches) != NULL) { + ldap_msgfree(s->searchresult); + lres->searches = s->next; + efree(s); + } + } + efree(lres); + } +} + +/* + * Add a search result to the ldap_result structure. + */ +static struct ldap_search_list * +sudo_ldap_result_add_search(lres, ldap, searchresult) + struct ldap_result *lres; + LDAP *ldap; + LDAPMessage *searchresult; +{ + struct ldap_search_list *s, *news; + + news = emalloc(sizeof(struct ldap_search_list)); + news->next = NULL; + news->ldap = ldap; + news->searchresult = searchresult; + + /* Add entry to the end of the chain (XXX - tailq instead?). */ + if (lres->searches) { + for (s = lres->searches; s->next != NULL; s = s->next) + continue; + s->next = news; + } else { + lres->searches = news; + } + return(news); +} + +/* + * Add an entry to the result structure. + */ +static struct ldap_entry_wrapper * +sudo_ldap_result_add_entry(lres, entry) + struct ldap_result *lres; + LDAPMessage *entry; +{ + struct ldap_search_list *last; + struct berval **bv; + double order = 0.0; + char *ep; + + /* Determine whether the entry has the sudoOrder attribute. */ + last = sudo_ldap_result_last_search(lres); + bv = ldap_get_values_len(last->ldap, entry, "sudoOrder"); + if (bv != NULL) { + if (ldap_count_values_len(bv) > 0) { + /* Get the value of this attribute, 0 if not present. */ + DPRINTF(("order attribute raw: %s", (*bv)->bv_val), 1); + order = strtod((*bv)->bv_val, &ep); + if (ep == (*bv)->bv_val || *ep != '\0') { + warningx("invalid sudoOrder attribute: %s", (*bv)->bv_val); + order = 0.0; + } + DPRINTF(("order attribute: %f", order), 1); + } + ldap_value_free_len(bv); + } + + /* Allocate a new entry_wrapper, fill it in and append to the array. */ + /* XXX - realloc each time can be expensive, preallocate? */ + lres->nentries++; + lres->entries = erealloc3(lres->entries, lres->nentries, + sizeof(lres->entries[0])); + lres->entries[lres->nentries - 1].entry = entry; + lres->entries[lres->nentries - 1].order = order; + lres->entries[lres->nentries - 1].allowed = UNSPEC; + + return(&lres->entries[lres->nentries - 1]); +} + +/* + * Find the last entry in the list of searches, usually the + * one currently being used to add entries. + * XXX - use a tailq instead? + */ +static struct ldap_search_list * +sudo_ldap_result_last_search(lres) + struct ldap_result *lres; +{ + struct ldap_search_list *result = lres->searches; + + if (result) { + while (result->next) + result = result->next; + } + return(result); +} + +#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, searchresult) + LDAP *ldap; + LDAPMessage *searchresult; +{ + /* + * 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. + */ + struct ldap_result *result = sudo_ldap_result_alloc(); + + /* + * Build a new list node for the search result, this creates the + * list node. + */ + struct ldap_search_list *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. + */ + LDAPMessage *entry; + LDAP_FOREACH(entry, last->ldap, last->searchresult) { + sudo_ldap_result_add_entry(result, entry); + } + DPRINTF(("sudo_ldap_result_from_search: %d entries found", + result->nentries), 2); + return(result); +} +#endif + +/* + * Sort comparison function for ldap_entry_wrapper structures. + */ +static int +ldap_entry_compare(a, b) + const void *a; + const void *b; +{ + const struct ldap_entry_wrapper *aw = a; + const struct ldap_entry_wrapper *bw = b; + + return(aw->order < bw->order ? -1 : + (aw->order > bw->order ? 1 : 0)); +} + +/* + * Get an entry by number (index) with bounds checking. + */ +static LDAPMessage * +sudo_ldap_result_get_entry(lres, idx) + struct ldap_result *lres; + int idx; +{ + if (idx < 0 || idx >= lres->nentries) + return(NULL); + return(lres->entries[idx].entry); +} diff --git a/schema.ActiveDirectory b/schema.ActiveDirectory index fb758ac3c..cfdc7cbb9 100644 --- a/schema.ActiveDirectory +++ b/schema.ActiveDirectory @@ -196,6 +196,25 @@ name: sudoNotAfter schemaIDGUID:: xJhSt/Yd3RGJPTB1VtiVkw== objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=X +dn: CN=sudoOrder,CN=Schema,CN=Configuration,DC=X +changetype: add +objectClass: top +objectClass: attributeSchema +cn: sudoOrder +distinguishedName: CN=sudoOrder,CN=Schema,CN=Configuration,DC=X +instanceType: 4 +attributeID: 1.3.6.1.4.1.15953.9.1.10 +attributeSyntax: 1.3.6.1.4.1.1466.115.121.1.27 +isSingleValued: TRUE +showInAdvancedViewOnly: TRUE +adminDisplayName: sudoOrder +adminDescription: an integer to order the sudoRole entries +oMSyntax: 22 +lDAPDisplayName: sudoOrder +name: sudoOrder +schemaIDGUID:: xJhSt/Yd3RGJPTB1VtiVkw== +objectCategory: CN=Attribute-Schema,CN=Schema,CN=Configuration,DC=X + dn: changetype: modify add: schemaUpdateNow @@ -222,6 +241,7 @@ mayContain: sudoRunAsGroup mayContain: sudoUser mayContain: sudoNotBefore mayContain: sudoNotAfter +mayContain: sudoOrder rDNAttID: cn showInAdvancedViewOnly: FALSE adminDisplayName: sudoRole diff --git a/schema.OpenLDAP b/schema.OpenLDAP index 5da4c2b82..e14f7ad1b 100644 --- a/schema.OpenLDAP +++ b/schema.OpenLDAP @@ -61,9 +61,16 @@ attributetype ( 1.3.6.1.4.1.15953.9.1.9 ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 ) +attributeTypes ( 1.3.6.1.4.1.15953.9.1.10 + NAME 'sudoOrder' + DESC 'an integer to order the sudoRole entries' + EQUALITY integerMatch + ORDERING integerOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 ) + objectclass ( 1.3.6.1.4.1.15953.9.2.1 NAME 'sudoRole' SUP top STRUCTURAL DESC 'Sudoer Entries' MUST ( cn ) - MAY ( sudoUser $ sudoHost $ sudoCommand $ sudoRunAs $ sudoRunAsUser $ sudoRunAsGroup $ sudoOption $ + MAY ( sudoUser $ sudoHost $ sudoCommand $ sudoRunAs $ sudoRunAsUser $ sudoRunAsGroup $ sudoOption $ sudoOrder $ description ) ) diff --git a/schema.iPlanet b/schema.iPlanet index 0cb00388e..8382960a8 100644 --- a/schema.iPlanet +++ b/schema.iPlanet @@ -8,4 +8,5 @@ attributeTypes: ( 1.3.6.1.4.1.15953.9.1.6 NAME 'sudoRunAsUser' DESC 'User(s) imp attributeTypes: ( 1.3.6.1.4.1.15953.9.1.7 NAME 'sudoRunAsGroup' DESC 'Group(s) impersonated by sudo' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 X-ORIGIN 'SUDO' ) attributeTypes: ( 1.3.6.1.4.1.15953.9.1.8 NAME 'sudoNotBefore' DESC 'Start of time interval for which the entry is valid' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 ) attributeTypes: ( 1.3.6.1.4.1.15953.9.1.9 NAME 'sudoNotAfter' DESC 'End of time interval for which the entry is valid' EQUALITY generalizedTimeMatch ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 ) -objectClasses: ( 1.3.6.1.4.1.15953.9.2.1 NAME 'sudoRole' SUP top STRUCTURAL DESC 'Sudoer Entries' MUST ( cn ) MAY ( sudoUser $ sudoHost $ sudoCommand $ sudoRunAs $ sudoRunAsUser $ sudoRunAsGroup $ sudoOption $ description ) X-ORIGIN 'SUDO' ) +attributeTypes: ( 1.3.6.1.4.1.15953.9.1.10 NAME 'sudoOrder' DESC 'an integer to order the sudoRole entries' EQUALITY integerMatch ORDERING integerOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 ) +objectClasses: ( 1.3.6.1.4.1.15953.9.2.1 NAME 'sudoRole' SUP top STRUCTURAL DESC 'Sudoer Entries' MUST ( cn ) MAY ( sudoUser $ sudoHost $ sudoCommand $ sudoRunAs $ sudoRunAsUser $ sudoRunAsGroup $ sudoOption $ sudoOrder $ description ) X-ORIGIN 'SUDO' ) diff --git a/sudoers.ldap.pod b/sudoers.ldap.pod index 4622bc773..1b26d395f 100644 --- a/sudoers.ldap.pod +++ b/sudoers.ldap.pod @@ -150,6 +150,15 @@ A timestamp in the form C that indicates end of validity of this C. If multiple B entries are present, the last one is used. +=item B + +The sudoRole entries retrieved from the LDAP directory have no +inherent order. The B attribute is an integer that will +be used to sort the matching entries. This allows to more closely +mimic the behaviour of the sudoers file, where the of the entries +does have an influence on the result. If the B attribute +is not present, a value of 0 is assumed. + =back Each component listed above should contain a single value, but there @@ -324,7 +333,7 @@ in which case they are queried in the order specified. =item B on/true/yes/off/false/no -Whether or not to evaluate the sudoNotBefore and sudoNotAfter +Whether or not to evaluate the B and B attributes that implement time-dependent sudoers entries. =item B debug_level @@ -742,12 +751,19 @@ C line in C and restart B. ORDERING generalizedTimeOrderingMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.24 ) + attributeTypes ( 1.3.6.1.4.1.15953.9.1.10 + NAME 'sudoOrder' + DESC 'an integer to order the sudoRole entries' + EQUALITY integerMatch + ORDERING integerOrderingMatch + SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 ) + objectclass ( 1.3.6.1.4.1.15953.9.2.1 NAME 'sudoRole' SUP top STRUCTURAL DESC 'Sudoer Entries' MUST ( cn ) MAY ( sudoUser $ sudoHost $ sudoCommand $ sudoRunAs $ sudoRunAsUser $ sudoRunAsGroup $ sudoOption $ sudoNotBefore $ sudoNotAfter $ - description ) + sudoOrder $ description ) ) =head1 SEE ALSO