From: Arran Cudbard-Bell Date: Thu, 16 Jul 2026 22:22:31 +0000 (-0400) Subject: lib/ldap: add helpers for retrieving sets of objects by DN X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9d006b335551de3a6b5ccdbfe9ba2f67a237c7ab;p=thirdparty%2Ffreeradius-server.git lib/ldap: add helpers for retrieving sets of objects by DN fr_ldap_directory_common_base_find resolves DNs to a covering naming context using a hash table built when the rootDSE is parsed, and fr_ldap_filter_afrom_dn_list builds the matching filter using the directory's dn_attr, which fr_ldap_directory_alloc now defaults to entryDN before discovery completes. fr_ldap_berval_to_string_list converts berval values to a NULL terminated string list carved from a single talloc pool. --- diff --git a/src/lib/ldap/base.h b/src/lib/ldap/base.h index 38017312368..ba085fcc4e6 100644 --- a/src/lib/ldap/base.h +++ b/src/lib/ldap/base.h @@ -211,10 +211,12 @@ typedef struct { fr_ldap_sync_type_t sync_type; //!< What kind of LDAP sync this directory supports. char const *dn_attr; //!< Attribute to match an entry's DN in a search filter. - ///< RFC 5020 entryDN where supported, distinguishedName + ///< Defaults to RFC 5020 entryDN, distinguishedName ///< on Active Directory and Samba. char const **naming_contexts; //!< Databases served by this directory. + fr_hash_table_t *naming_contexts_ht; //!< For resolving DNs to the naming context + ///< containing them. } fr_ldap_directory_t; /** Connection configuration @@ -839,6 +841,10 @@ int fr_ldap_control_add_session_tracking(fr_ldap_connection_t *conn, request_t "namingContexts", \ NULL } +fr_ldap_directory_t *fr_ldap_directory_alloc(TALLOC_CTX *ctx); + +char const *fr_ldap_directory_common_base_find(fr_ldap_directory_t const *directory, char const * const *dn_list); + int fr_ldap_directory_result_parse(fr_ldap_directory_t *directory, LDAP *handle, LDAPMessage *result, char const *name); @@ -954,6 +960,8 @@ size_t fr_ldap_util_normalise_dn(char *out, char const *in); char *fr_ldap_berval_to_string(TALLOC_CTX *ctx, struct berval const *in); +char const **fr_ldap_berval_to_string_list(TALLOC_CTX *ctx, struct berval **values, int count, size_t extra); + uint8_t *fr_ldap_berval_to_bin(TALLOC_CTX *ctx, struct berval const *in); int fr_ldap_parse_url_extensions(LDAPControl **sss, size_t sss_len, char *extensions[]); @@ -972,6 +980,9 @@ int fr_ldap_dn_box_escape(fr_value_box_t *vb, UNUSED void *uctx); int fr_ldap_filter_box_escape(fr_value_box_t *vb, UNUSED void *uctx); +char *fr_ldap_filter_afrom_dn_list(TALLOC_CTX *ctx, char const *dn_attr, char const *filter, + char const * const *dn_list); + int fr_ldap_filter_to_tmpl(TALLOC_CTX *ctx, tmpl_rules_t const *t_rules, char const **sub, size_t sublen, tmpl_t **out) CC_HINT(nonnull()); diff --git a/src/lib/ldap/connection.c b/src/lib/ldap/connection.c index 08496a73204..3f8ba73f64f 100644 --- a/src/lib/ldap/connection.c +++ b/src/lib/ldap/connection.c @@ -974,7 +974,7 @@ fr_ldap_thread_trunk_t *fr_thread_ldap_trunk_get(fr_ldap_thread_t *thread, char * connection init callbacks run, populated asynchronously * by the rootDSE query enqueued below. */ - MEM(new->directory = talloc_zero(new, fr_ldap_directory_t)); + new->directory = fr_ldap_directory_alloc(new); new->trunk = trunk_alloc(new, thread->el, &(trunk_io_funcs_t){ diff --git a/src/lib/ldap/directory.c b/src/lib/ldap/directory.c index 6c94b2e7f27..d73738d3e6c 100644 --- a/src/lib/ldap/directory.c +++ b/src/lib/ldap/directory.c @@ -49,6 +49,22 @@ static fr_table_num_sorted_t const fr_ldap_directory_type_table[] = { }; static size_t fr_ldap_directory_type_table_len = NUM_ELEMENTS(fr_ldap_directory_type_table); +/** Hash a naming context, case insensitively + * + */ +static uint32_t _naming_context_hash(void const *data) +{ + return fr_hash_case_string(data); +} + +/** Compare two naming contexts, case insensitively + * + */ +static int8_t _naming_context_cmp(void const *one, void const *two) +{ + return CMP(strcasecmp(one, two), 0); +} + int fr_ldap_directory_result_parse(fr_ldap_directory_t *directory, LDAP *handle, LDAPMessage *result, char const *name) { @@ -186,12 +202,10 @@ found: break; case FR_LDAP_DIRECTORY_EDIRECTORY: - directory->dn_attr = "entryDN"; directory->cleartext_password = false; break; default: - directory->dn_attr = "entryDN"; directory->cleartext_password = true; break; } @@ -235,14 +249,85 @@ found: num = ldap_count_values_len(values); MEM(directory->naming_contexts = talloc_array(directory, char const *, num)); + MEM(directory->naming_contexts_ht = fr_hash_table_alloc(directory, _naming_context_hash, + _naming_context_cmp, NULL)); for (i = 0; i < num; i++) { directory->naming_contexts[i] = fr_ldap_berval_to_string(directory, values[i]); + fr_hash_table_insert(directory->naming_contexts_ht, directory->naming_contexts[i]); } ldap_value_free_len(values); return 0; } +/** Allocate a directory structure with defaults + * + * dn_attr defaults to the RFC 5020 entryDN attribute, overridden when + * parsing the rootDSE detects a directory which doesn't implement entryDN. + */ +fr_ldap_directory_t *fr_ldap_directory_alloc(TALLOC_CTX *ctx) +{ + fr_ldap_directory_t *directory; + + MEM(directory = talloc_zero(ctx, fr_ldap_directory_t)); + directory->dn_attr = "entryDN"; + + return directory; +} + +/** Find the naming context which contains a set of DNs + * + * Looks up successively shorter suffixes of each DN in the hash table of + * naming contexts (database suffixes) built when the rootDSE was parsed, + * and returns the naming context containing every DN. A search with the + * returned base covers all the DNs. + * + * @param[in] directory Directory discovery results, providing the naming contexts. + * @param[in] dn_list NULL terminated list of DNs to cover, no empty strings. + * @return + * - The matching naming context. + * - NULL if the directory hasn't been discovered yet, or no single + * naming context contains every DN. + */ +char const *fr_ldap_directory_common_base_find(fr_ldap_directory_t const *directory, char const * const *dn_list) +{ + char const *common = NULL; + char const * const *dn_p; + + if (!directory->naming_contexts_ht) return NULL; + + for (dn_p = dn_list; *dn_p; dn_p++) { + char const *context = NULL; + char const *p = *dn_p; + + /* + * Check successively shorter suffixes of the DN, + * starting after each RDN separator. + */ + while (p) { + context = fr_hash_table_find(directory->naming_contexts_ht, p); + if (context) break; + + p = strchr(p, ','); + if (p) p++; + } + if (!context) return NULL; + + /* + * Lookups return the stored string, so pointer + * comparison is enough to check every DN resolved + * to the same naming context. + */ + if (!common) { + common = context; + continue; + } + if (common != context) return NULL; + } + + return common; +} + /** Parse results of search on rootDSE to gather data on LDAP server * * @param[in] handle on which the query was run. @@ -286,7 +371,7 @@ int fr_ldap_trunk_directory_init_async(fr_ldap_thread_trunk_t *ttrunk) return 0; } -/** Async extract useful information from the rootDSE of the LDAP server +/** Asynchronously extract useful information from the rootDSE of the LDAP server * * This version is for a single connection rather than a connection trunk * @@ -300,8 +385,7 @@ int fr_ldap_conn_directory_alloc_async(fr_ldap_connection_t *ldap_conn) int msgid; static char const *attrs[] = LDAP_DIRECTORY_ATTRS; - ldap_conn->directory = talloc_zero(ldap_conn, fr_ldap_directory_t); - if (!ldap_conn->directory) return -1; + ldap_conn->directory = fr_ldap_directory_alloc(ldap_conn); if (fr_ldap_search_async(&msgid, NULL, ldap_conn, "", LDAP_SCOPE_BASE, "(objectclass=*)", attrs, NULL, NULL) != LDAP_PROC_SUCCESS) return -1; diff --git a/src/lib/ldap/util.c b/src/lib/ldap/util.c index fe4986ced7c..bba74ad4a0e 100644 --- a/src/lib/ldap/util.c +++ b/src/lib/ldap/util.c @@ -497,6 +497,48 @@ int fr_ldap_parse_url_extensions(LDAPControl **sss, size_t sss_len, char *extens return (sss_end - sss_p); } +/** Convert a list of bervals to a NULL terminated list of talloced strings + * + * A list variant of fr_ldap_berval_to_string. Where talloc_pooled_object is + * available the pointer array and every string are allocated from a single + * talloc pool, so building the list costs one malloc. Zero length values + * are skipped. + * + * @param[in] ctx to parent the list. + * @param[in] values to copy. + * @param[in] count Number of values. + * @param[in] extra Leading pointer array entries to leave NULL, for the + * caller to fill with strings not copied into the pool. + * @return NULL terminated array of \0 terminated strings. + */ +char const **fr_ldap_berval_to_string_list(TALLOC_CTX *ctx, struct berval **values, int count, size_t extra) +{ + char const **list; + size_t num = extra; + int i; + +#ifdef HAVE_TALLOC_ZERO_POOLED_OBJECT + { + size_t strings_size = 0; + + for (i = 0; i < count; i++) { + strings_size += values[i]->bv_len + 1; + } + MEM(list = _talloc_zero_pooled_object(ctx, sizeof(char const *) * (count + extra + 1), + "char const *[]", count, strings_size)); + } +#else + MEM(list = talloc_zero_array(ctx, char const *, count + extra + 1)); +#endif + + for (i = 0; i < count; i++) { + if (values[i]->bv_len == 0) continue; + MEM(list[num++] = fr_ldap_berval_to_string(list, values[i])); + } + + return list; +} + /** Convert a berval to a talloced string * * The ldap_get_values function is deprecated, and ldap_get_values_len @@ -628,6 +670,48 @@ size_t fr_ldap_common_dn(char const *full, char const *part) return f_len - p_len; } +/** Build a filter matching a set of objects by DN + * + * Produces `(|(=)...)`, ANDed with filter if one is given. + * DN values are escaped. + * + * @param[in] ctx to allocate the filter string in. + * @param[in] dn_attr Attribute which matches an object's own DN, + * e.g. entryDN or distinguishedName. + * @param[in] filter Optional filter to AND with the DN set, may be NULL. + * @param[in] dn_list NULL terminated list of DNs to match, no empty strings. + * @return The filter string. + */ +char *fr_ldap_filter_afrom_dn_list(TALLOC_CTX *ctx, char const *dn_attr, char const *filter, + char const * const *dn_list) +{ + char *out; + char const * const *dn_p; + + MEM(out = talloc_typed_strdup(ctx, "(|")); + for (dn_p = dn_list; *dn_p; dn_p++) { + char *escaped; + size_t len; + + len = (strlen(*dn_p) * 3) + 1; + MEM(escaped = talloc_array(ctx, char, len)); + fr_ldap_filter_escape_func(NULL, escaped, len, *dn_p, NULL); + MEM(out = talloc_asprintf_append_buffer(out, "(%s=%s)", dn_attr, escaped)); + talloc_free(escaped); + } + MEM(out = talloc_strdup_append_buffer(out, ")")); + + if (filter && *filter) { + char *combined; + + MEM(combined = talloc_typed_asprintf(ctx, "(&%s%s)", filter, out)); + talloc_free(out); + return combined; + } + + return out; +} + /** Combine filters and tokenize to a tmpl * * @param ctx To allocate combined filter in diff --git a/src/listen/ldap_sync/proto_ldap_sync_ldap.c b/src/listen/ldap_sync/proto_ldap_sync_ldap.c index 296a2212565..570eaaa6e95 100644 --- a/src/listen/ldap_sync/proto_ldap_sync_ldap.c +++ b/src/listen/ldap_sync/proto_ldap_sync_ldap.c @@ -1248,7 +1248,6 @@ static void _proto_ldap_socket_open_connected(connection_t *conn, UNUSED connect * Allocate the directory structure and send the query */ dir_ctx->msgid = fr_ldap_conn_directory_alloc_async(ldap_conn); - if (dir_ctx->msgid < 0) { talloc_free(dir_ctx); goto connection_failed;