]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
lib/ldap: add helpers for retrieving sets of objects by DN
authorArran Cudbard-Bell <a.cudbardb@freeradius.org>
Thu, 16 Jul 2026 22:22:31 +0000 (18:22 -0400)
committerArran Cudbard-Bell <a.cudbardb@freeradius.org>
Thu, 16 Jul 2026 22:23:02 +0000 (18:23 -0400)
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.

src/lib/ldap/base.h
src/lib/ldap/connection.c
src/lib/ldap/directory.c
src/lib/ldap/util.c
src/listen/ldap_sync/proto_ldap_sync_ldap.c

index 380173123689a43c20dd57657bd7b0a9a9513c2c..ba085fcc4e626d6e86076a88d83426929aa2a630 100644 (file)
@@ -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());
 
index 08496a73204a22b2fc121b842a1d1e33845ead88..3f8ba73f64fbb684b12a84209160c57dbe375b96 100644 (file)
@@ -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){
index 6c94b2e7f270b8cc2f05cdc4f1e6843bf3ab7682..d73738d3e6c294559e966bc8755c96334361beab 100644 (file)
@@ -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;
index fe4986ced7c32fae48d7da6a3c708692ceb76d3f..bba74ad4a0e744cf779aabb7371436d84a3938d6 100644 (file)
@@ -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 `(|(<dn_attr>=<dn>)...)`, 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
index 296a22125657b9b1ad63bb8633842b8abdc8fcf8..570eaaa6e953dbf1e6c7ef40132fcd9718eb98ed 100644 (file)
@@ -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;