]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
rlm_ldap: add profile search_mode option
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)
seq keeps one search per profile DN, applied in list order with the default profile first. bulk retrieves every profile with a single sorted search matching objects by DN, requiring base scope and sort_by, and auto picks bulk when both hold, otherwise seq.

raddb/mods-available/ldap
src/modules/rlm_ldap/profile.c
src/modules/rlm_ldap/rlm_ldap.c
src/modules/rlm_ldap/rlm_ldap.h

index 07161ade426d966b49fd14c491dc30e5ee5acd76..224252bc88562c93980792f142210e34dc937875 100644 (file)
@@ -580,6 +580,46 @@ ldap {
                #
 #              scope = 'base'
 
+               #
+               #  search_mode:: How profile objects are retrieved from the
+               #  directory.  May be `seq`, `bulk` or `auto`.
+               #
+               #  [options="header,autowidth"]
+               #  |===
+               #  | Mode   | Description
+               #  | seq    | One search per profile DN.  Profiles are applied in
+               #             the order they appear: the default profile first,
+               #             then the profiles listed on the user object.  With
+               #             a non-base `scope`, each search also retrieves the
+               #             profile's children (nested profiles).
+               #  | bulk   | A single search retrieves every profile, including
+               #             the default.  Profiles are applied in the order the
+               #             server returns them, so `sort_by` is required to
+               #             make the order deterministic.  Profiles are matched
+               #             by their exact DN (see `dn_attribute`), so `scope`
+               #             must be `base` and nested profiles are not
+               #             retrieved.
+               #  | auto   | `bulk` when `sort_by` is set and `scope` is `base`,
+               #             otherwise `seq`.
+               #  |===
+               #
+               #  How `search_mode`, `scope` and `sort_by` interact:
+               #
+               #  [options="header,autowidth"]
+               #  |===
+               #  | search_mode | scope      | sort_by | Result
+               #  | seq         | any        | unset   | One search per DN, applied in list order.
+               #  | seq         | one/sub    | unset   | As above, each search also returns nested profiles, in server order.
+               #  | seq         | one/sub    | set     | As above, nested profiles within each search sorted by the server.
+               #  | bulk        | base       | set     | One search for all DNs, applied in the server's sort order.
+               #  | bulk        | base       | unset   | Configuration error, `bulk` requires `sort_by`.
+               #  | bulk        | one/sub    | -       | Configuration error.
+               #  | auto        | base       | set     | Resolves to `bulk`.
+               #  | auto        | any other combination | | Resolves to `seq`.
+               #  |===
+               #
+#              search_mode = 'auto'
+
                #
                #  default:: The default profile. This may be a DN or an attribute
                #  reference.
index 103099f4a9c2061ef3d98f516261fee7502a3e17..a91e67ededdae92487a8dd29f31fe2936054f970 100644 (file)
@@ -44,11 +44,96 @@ typedef struct {
        fr_ldap_result_code_t   *ret;                   //!< Result of the query and applying the map.
        int                     *applied;               //!< Number of profiles applied.
        fr_ldap_query_t         *query;
-       char const              *dn;
+       char const              *dn;                    //!< DN of the profile object being retrieved.
+                                                       ///< NULL when all profiles are retrieved by one search.
        rlm_ldap_t const        *inst;
        fr_ldap_map_exp_t       const *expanded;
+       fr_ldap_thread_trunk_t  *ttrunk;                //!< Trunk used for profile searches.
+       char const              *filter;                //!< Filter to apply to profile searches.
+       char const * const      *next;                  //!< Next profile DN to search for (search_mode = seq).
 } ldap_profile_ctx_t;
 
+/** Apply the attribute map to a single profile entry
+ *
+ * @param[out] fallthrough     Whether processing should continue to the next profile entry.
+ * @param[in] request          Current request.
+ * @param[in] profile_ctx      Profile lookup state.
+ * @param[in] handle           libldap handle the result was received on.
+ * @param[in] entry            Profile object to apply.
+ */
+static void ldap_profile_entry_map(bool *fallthrough, request_t *request, ldap_profile_ctx_t *profile_ctx,
+                                  LDAP *handle, LDAPMessage *entry)
+{
+       int     ret;
+
+       // Set fallthrough to the configured default
+       *fallthrough = profile_ctx->inst->profile.fallthrough_def;
+
+       ret = fr_ldap_map_do(request, profile_ctx->inst->profile.check_attr, profile_ctx->inst->valuepair_attr,
+                          profile_ctx->expanded, entry);
+       if (ret < 0) {
+               if (profile_ctx->ret) *profile_ctx->ret = LDAP_RESULT_ERROR;
+       } else {
+               if (profile_ctx->applied) *profile_ctx->applied += ret;
+       }
+
+       if (profile_ctx->inst->profile.fallthrough_attr) {
+               struct berval           **values;
+               int                     count;
+               char                    *value;
+               xlat_exp_head_t         *cond_expr = NULL;
+               fr_value_box_list_t     res;
+
+               tmpl_rules_t const parse_rules = {
+                       .attr = {
+                               .dict_def = request->local_dict,
+                               .list_def = request_attr_request,
+                       },
+                       .xlat = {
+                               .runtime_el = unlang_interpret_event_list(request),
+                       },
+                       .at_runtime = true,
+               };
+
+               values = ldap_get_values_len(handle, entry, profile_ctx->inst->profile.fallthrough_attr);
+               count = ldap_count_values_len(values);
+               if (count == 0) goto free_values;
+               if (count > 1) {
+                       RWARN("%s returned more than 1 value.  Only evaluating the first.",
+                             profile_ctx->inst->profile.fallthrough_attr);
+               }
+               value = fr_ldap_berval_to_string(request, values[0]);
+
+               RDEBUG3("Parsing fallthrough condition %s", value);
+               if (xlat_tokenize_expression(request, &cond_expr,
+                                            &FR_SBUFF_IN(value, talloc_strlen(value)),
+                                            NULL, &parse_rules) < 0) {
+                       RPEDEBUG("Failed parsing '%s' value \"%s\"", profile_ctx->inst->profile.fallthrough_attr, value);
+                       goto free;
+               }
+
+               if (xlat_impure_func(cond_expr)) {
+                       fr_strerror_const("Fallthrough expression cannot depend on functions which call external databases");
+                       goto free;
+               }
+
+               RDEBUG2("Checking fallthrough condition %s", value);
+               fr_value_box_list_init(&res);
+               if (unlang_xlat_eval(request, &res, request, cond_expr) < 0) {
+                       RPEDEBUG("Failed evaluating condition");
+                       goto free;
+               }
+               *fallthrough = (fr_value_box_list_head(&res) && fr_value_box_is_truthy(fr_value_box_list_head(&res))) ? true : false;
+               fr_value_box_list_talloc_free(&res);
+               RDEBUG2("Fallthrough condition evaluated to %s", *fallthrough ? "true" : "false");
+       free:
+               talloc_free(value);
+               talloc_free(cond_expr);
+       free_values:
+               ldap_value_free_len(values);
+       }
+}
+
 /** Process the results of a profile lookup
  *
  */
@@ -57,11 +142,10 @@ static unlang_action_t ldap_map_profile_resume(request_t *request, void *uctx)
        ldap_profile_ctx_t      *profile_ctx = talloc_get_type_abort(uctx, ldap_profile_ctx_t);
        fr_ldap_query_t         *query = profile_ctx->query;
        LDAP                    *handle;
-       LDAPMessage             *entry = NULL;
+       LDAPMessage             *entry;
        int                     ldap_errno;
        char                    *dn = NULL;
-       int                     ret;
-       bool                    fallthrough;
+       bool                    fallthrough = true;
 
        /*
         *      Tell the caller what happened
@@ -74,8 +158,12 @@ static unlang_action_t ldap_map_profile_resume(request_t *request, void *uctx)
 
        case LDAP_RESULT_NO_RESULT:
        case LDAP_RESULT_BAD_DN:
-               RDEBUG2("Profile object \"%s\" not found", profile_ctx->dn);
-               goto finish;
+               if (profile_ctx->dn) {
+                       RDEBUG2("Profile object \"%s\" not found", profile_ctx->dn);
+               } else {
+                       RDEBUG2("No profile objects found");
+               }
+               goto next;
 
        default:
                goto finish;
@@ -101,95 +189,66 @@ static unlang_action_t ldap_map_profile_resume(request_t *request, void *uctx)
                        ldap_memfree(dn);
                }
 
-               // Set fallthrough to the configured default
-               fallthrough = profile_ctx->inst->profile.fallthrough_def;
-
                RINDENT();
-               ret = fr_ldap_map_do(request, profile_ctx->inst->profile.check_attr, profile_ctx->inst->valuepair_attr,
-                                  profile_ctx->expanded, entry);
-               if (ret < 0) {
-                       if (profile_ctx->ret) *profile_ctx->ret = LDAP_RESULT_ERROR;
-               } else {
-                       if (profile_ctx->applied) *profile_ctx->applied += ret;
-               }
-
-               if (profile_ctx->inst->profile.fallthrough_attr) {
-                       struct berval           **values;
-                       int                     count;
-                       char                    *value;
-                       xlat_exp_head_t         *cond_expr = NULL;
-                       fr_value_box_list_t     res;
-
-                       tmpl_rules_t const parse_rules = {
-                               .attr = {
-                                       .dict_def = request->local_dict,
-                                       .list_def = request_attr_request,
-                               },
-                               .xlat = {
-                                       .runtime_el = unlang_interpret_event_list(request),
-                               },
-                               .at_runtime = true,
-                       };
-
-                       values = ldap_get_values_len(handle, entry, profile_ctx->inst->profile.fallthrough_attr);
-                       count = ldap_count_values_len(values);
-                       if (count == 0) goto free_values;
-                       if (count > 1) {
-                               RWARN("%s returned more than 1 value.  Only evaluating the first.",
-                                     profile_ctx->inst->profile.fallthrough_attr);
-                       }
-                       value = fr_ldap_berval_to_string(request, values[0]);
-
-                       RDEBUG3("Parsing fallthrough condition %s", value);
-                       if (xlat_tokenize_expression(request, &cond_expr,
-                                                    &FR_SBUFF_IN(value, talloc_strlen(value)),
-                                                    NULL, &parse_rules) < 0) {
-                               RPEDEBUG("Failed parsing '%s' value \"%s\"", profile_ctx->inst->profile.fallthrough_attr, value);
-                               goto free;
-                       }
-
-                       if (xlat_impure_func(cond_expr)) {
-                               fr_strerror_const("Fallthrough expression cannot depend on functions which call external databases");
-                               goto free;
-                       }
-
-                       RDEBUG2("Checking fallthrough condition %s", value);
-                       fr_value_box_list_init(&res);
-                       if (unlang_xlat_eval(request, &res, request, cond_expr) < 0) {
-                               RPEDEBUG("Failed evaluating condition");
-                               goto free;
-                       }
-                       fallthrough = (fr_value_box_list_head(&res) && fr_value_box_is_truthy(fr_value_box_list_head(&res))) ? true : false;
-                       fr_value_box_list_talloc_free(&res);
-                       RDEBUG2("Fallthrough condition evaluated to %s", fallthrough ? "true" : "false");
-               free:
-                       talloc_free(value);
-                       talloc_free(cond_expr);
-               free_values:
-                       ldap_value_free_len(values);
-               }
-
-               entry = ldap_next_entry(handle, entry);
+               ldap_profile_entry_map(&fallthrough, request, profile_ctx, handle, entry);
                REXDENT();
                if (!fallthrough) break;
+
+               entry = ldap_next_entry(handle, entry);
        }
        REXDENT();
 
+       if (!fallthrough) goto finish;
+
+next:
+       /*
+        *      Chain the search for the next profile (search_mode = seq)
+        */
+       while (profile_ctx->next && *profile_ctx->next) {
+               LDAPControl     *serverctrls[] = { profile_ctx->inst->profile.obj_sort_ctrl, NULL };
+
+               TALLOC_FREE(profile_ctx->query);
+
+               profile_ctx->dn = *profile_ctx->next++;
+
+               if (unlang_function_repeat_set(request, ldap_map_profile_resume) < 0) {
+                       talloc_free(profile_ctx);
+                       return UNLANG_ACTION_FAIL;
+               }
+               return fr_ldap_trunk_search(profile_ctx, &profile_ctx->query, request, profile_ctx->ttrunk,
+                                           profile_ctx->dn, profile_ctx->inst->profile.obj_scope,
+                                           profile_ctx->filter, profile_ctx->expanded->attrs, serverctrls, NULL);
+       }
+
 finish:
        talloc_free(profile_ctx);
        return UNLANG_ACTION_CALCULATE_RESULT;
 }
 
-/** Cancel an in progress profile lookup
+/** Push the resume frame and start the first profile search
  *
+ * Cancellation is handled by the frame fr_ldap_trunk_search pushes above
+ * this one, which abandons the in-flight query and detaches it from the
+ * trunk request before the unwind frees profile_ctx.
  */
-static void ldap_map_profile_cancel(UNUSED request_t *request, UNUSED fr_signal_t action, void *uctx)
+static unlang_action_t ldap_profile_search_push(ldap_profile_ctx_t *profile_ctx, request_t *request,
+                                               char const *base, int scope, char const *filter)
 {
-       ldap_profile_ctx_t      *profile_ctx = talloc_get_type_abort(uctx, ldap_profile_ctx_t);
+       LDAPControl     *serverctrls[] = { profile_ctx->inst->profile.obj_sort_ctrl, NULL };
 
-       if (!profile_ctx->query || !profile_ctx->query->treq) return;
+       if (unlang_function_push(request,
+                                NULL,
+                                ldap_map_profile_resume,
+                                NULL, 0,
+                                UNLANG_SUB_FRAME,
+                                profile_ctx) < 0) {
+               talloc_free(profile_ctx);
+               return UNLANG_ACTION_FAIL;
+       }
 
-       trunk_request_signal_cancel(profile_ctx->query->treq);
+       return fr_ldap_trunk_search(profile_ctx, &profile_ctx->query, request, profile_ctx->ttrunk,
+                                   base, scope, filter,
+                                   profile_ctx->expanded->attrs, serverctrls, NULL);
 }
 
 /** Search for and apply an LDAP profile
@@ -214,7 +273,6 @@ unlang_action_t rlm_ldap_map_profile(fr_ldap_result_code_t *ret, int *applied,
                                     char const *dn, int scope, char const *filter, fr_ldap_map_exp_t const *expanded)
 {
        ldap_profile_ctx_t      *profile_ctx;
-       LDAPControl             *serverctrls[] = { inst->profile.obj_sort_ctrl, NULL };
 
        if (!dn || !*dn) return UNLANG_ACTION_CALCULATE_RESULT;
 
@@ -224,21 +282,85 @@ unlang_action_t rlm_ldap_map_profile(fr_ldap_result_code_t *ret, int *applied,
                .applied = applied,
                .dn = dn,
                .expanded = expanded,
-               .inst = inst
+               .inst = inst,
+               .ttrunk = ttrunk
        };
        if (ret) *ret = LDAP_RESULT_ERROR;
 
-       if (unlang_function_push(request,
-                                NULL,
-                                ldap_map_profile_resume,
-                                ldap_map_profile_cancel, ~FR_SIGNAL_CANCEL,
-                                UNLANG_SUB_FRAME,
-                                profile_ctx) < 0) {
-               talloc_free(profile_ctx);
-               return UNLANG_ACTION_FAIL;
+       return ldap_profile_search_push(profile_ctx, request, dn, scope, filter);
+}
+
+/** Search for and apply a set of LDAP profiles
+ *
+ * With search_mode = bulk, a single search retrieves every profile object, matching
+ * entries by DN using the attribute configured (or detected) for the directory,
+ * and profiles are applied in result order.
+ * With search_mode = seq, one search is run per profile DN, applied in list order.
+ *
+ * @param[out] ret             Where to write the result of the last query.
+ * @param[out] applied         Incremented by the number of profile maps applied.
+ * @param[in] inst             LDAP module instance.
+ * @param[in] request          Current request.
+ * @param[in] ttrunk           Trunk connection on which to run LDAP queries.
+ * @param[in] dn_list          NULL terminated list of profile object DNs to apply,
+ *                             in application order (default profile first).
+ *                             Must contain at least one DN, and no empty strings.
+ * @param[in] filter           to apply when looking up profiles.
+ * @param[in] expanded         Structure containing a list of xlat
+ *                             expanded attribute names and mapping information.
+ * @return An unlang_action_t.
+ */
+unlang_action_t rlm_ldap_map_profiles(fr_ldap_result_code_t *ret, int *applied,
+                                     rlm_ldap_t const *inst, request_t *request, fr_ldap_thread_trunk_t *ttrunk,
+                                     char const * const *dn_list, char const *filter,
+                                     fr_ldap_map_exp_t const *expanded)
+{
+       ldap_profile_ctx_t      *profile_ctx;
+
+       fr_assert(dn_list && *dn_list);
+
+       MEM(profile_ctx = talloc(unlang_interpret_frame_talloc_ctx(request), ldap_profile_ctx_t));
+       *profile_ctx = (ldap_profile_ctx_t) {
+               .ret = ret,
+               .applied = applied,
+               .expanded = expanded,
+               .inst = inst,
+               .ttrunk = ttrunk,
+               .filter = filter
+       };
+       if (ret) *ret = LDAP_RESULT_ERROR;
+
+       switch (inst->profile.search_mode) {
+       case LDAP_PROFILE_SEARCH_MODE_BULK:
+       {
+               char const      *base, *dn_attr;
+
+               dn_attr = inst->dn_attr;
+               if (!dn_attr) dn_attr = ttrunk->directory->dn_attr;
+               fr_assert(dn_attr);
+
+               base = fr_ldap_directory_common_base_find(ttrunk->directory, dn_list);
+               if (!base) {
+                       RWDEBUG("Retrieving profiles one at a time, no naming context contains every profile DN");
+                       goto seq;
+               }
+
+               return ldap_profile_search_push(profile_ctx, request, base, LDAP_SCOPE_SUB,
+                                               fr_ldap_filter_afrom_dn_list(profile_ctx, dn_attr, filter, dn_list));
        }
 
-       return fr_ldap_trunk_search(profile_ctx, &profile_ctx->query, request, ttrunk, dn,
-                                   scope, filter,
-                                   expanded->attrs, serverctrls, NULL);
+       case LDAP_PROFILE_SEARCH_MODE_SEQ:
+       seq:
+               profile_ctx->dn = dn_list[0];
+               profile_ctx->next = dn_list + 1;
+
+               return ldap_profile_search_push(profile_ctx, request, profile_ctx->dn, inst->profile.obj_scope, filter);
+
+       case LDAP_PROFILE_SEARCH_MODE_AUTO:
+               fr_assert_msg(false, "search mode auto should've been resolved at startup");
+               break;
+       }
+
+       talloc_free(profile_ctx);
+       return UNLANG_ACTION_FAIL;
 }
index 1d67c10bc5625052f78b863f12924c5d4f9a917e..f74b9b8b8cee3f07b7514aec652ce5e9af3acb69 100644 (file)
@@ -91,9 +91,18 @@ static const call_env_parser_t sasl_call_env[] = {
        CALL_ENV_TERMINATOR
 };
 
+static fr_table_num_sorted_t const profile_search_mode_table[] = {
+       { L("auto"),    LDAP_PROFILE_SEARCH_MODE_AUTO   },
+       { L("bulk"),    LDAP_PROFILE_SEARCH_MODE_BULK   },
+       { L("seq"),     LDAP_PROFILE_SEARCH_MODE_SEQ    }
+};
+static size_t profile_search_mode_table_len = NUM_ELEMENTS(profile_search_mode_table);
+
 static conf_parser_t profile_config[] = {
        { FR_CONF_OFFSET("scope", rlm_ldap_t, profile.obj_scope), .dflt = "base",
          .func = cf_table_parse_int, .uctx = &(cf_table_parse_ctx_t){ .table = fr_ldap_scope, .len = &fr_ldap_scope_len } },
+       { FR_CONF_OFFSET("search_mode", rlm_ldap_t, profile.search_mode), .dflt = "auto",
+         .func = cf_table_parse_int, .uctx = &(cf_table_parse_ctx_t){ .table = profile_search_mode_table, .len = &profile_search_mode_table_len } },
        { FR_CONF_OFFSET("attribute", rlm_ldap_t, profile.attr) },
        { FR_CONF_OFFSET("attribute_suspend", rlm_ldap_t, profile.attr_suspend) },
        { FR_CONF_OFFSET("check_attribute", rlm_ldap_t, profile.check_attr) },
@@ -1830,113 +1839,80 @@ static unlang_action_t CC_HINT(nonnull) mod_authorize_resume(unlang_result_t *p_
                }
                FALL_THROUGH;
 
-       case LDAP_AUTZ_DEFAULT_PROFILE:
-               /*
-                *      Apply ONE user profile, or a default user profile.
-                */
-               if (call_env->default_profile.type == FR_TYPE_STRING) {
-                       REPEAT_MOD_AUTHORIZE_RESUME;
-                       ret = rlm_ldap_map_profile(NULL, NULL, inst, request, autz_ctx->ttrunk,
-                                                  call_env->default_profile.vb_strvalue,
-                                                  inst->profile.obj_scope, NULL, &autz_ctx->expanded);
-                       switch (ret) {
-                       case UNLANG_ACTION_FAIL:
-                               p_result->rcode = RLM_MODULE_FAIL;
-                               goto finish;
-
-                       case UNLANG_ACTION_PUSHED_CHILD:
-                               autz_ctx->status = LDAP_AUTZ_POST_DEFAULT_PROFILE;
-                               return UNLANG_ACTION_PUSHED_CHILD;
-
-                       default:
-                               break;
-                       }
-               }
-               FALL_THROUGH;
-
-       case LDAP_AUTZ_POST_DEFAULT_PROFILE:
-               /*
-                *      Did we jump back her after applying the default profile?
-                */
-               if (autz_ctx->status == LDAP_AUTZ_POST_DEFAULT_PROFILE) autz_ctx->rcode = RLM_MODULE_UPDATED;
+       case LDAP_AUTZ_PROFILES:
+       {
+               struct berval   **values = NULL;
+               char const      *profile_attr = NULL;
+               bool            have_default = !fr_box_is_null(&call_env->default_profile);
+               int             count;
 
                /*
-                *      Apply a SET of user profiles.
+                *      Which set of user profiles to apply depends on the
+                *      user's access state.  The default profile always applies.
                 */
                switch (autz_ctx->access_state) {
                case LDAP_ACCESS_ALLOWED:
-                       if (inst->profile.attr) {
-                               int count;
-
-                               autz_ctx->profile_values = ldap_get_values_len(handle, autz_ctx->entry, inst->profile.attr);
-                               count = ldap_count_values_len(autz_ctx->profile_values);
-                               if (count > 0) {
-                                       RDEBUG2("Processing %i profile(s) found in attribute \"%s\"", count, inst->profile.attr);
-                                       if (RDEBUG_ENABLED3) {
-                                               for (struct berval **bv_p = autz_ctx->profile_values; *bv_p; bv_p++) {
-                                                       RDEBUG3("Will evaluate profile with DN \"%pV\"", fr_box_strvalue_len((*bv_p)->bv_val, (*bv_p)->bv_len));
-                                               }
-                                       }
-                               } else {
-                                       RDEBUG2("No profile(s) found in attribute \"%s\"", inst->profile.attr);
-                               }
-                       }
+                       profile_attr = inst->profile.attr;
                        break;
 
                case LDAP_ACCESS_SUSPENDED:
-                       if (inst->profile.attr_suspend) {
-                               int count;
-
-                               autz_ctx->profile_values = ldap_get_values_len(handle, autz_ctx->entry, inst->profile.attr_suspend);
-                               count = ldap_count_values_len(autz_ctx->profile_values);
-                               if (count > 0) {
-                                       RDEBUG2("Processing %i suspension profile(s) found in attribute \"%s\"", count, inst->profile.attr_suspend);
-                                       if (RDEBUG_ENABLED3) {
-                                               for (struct berval **bv_p = autz_ctx->profile_values; *bv_p; bv_p++) {
-                                                       RDEBUG3("Will evaluate suspenension profile with DN \"%pV\"",
-                                                               fr_box_strvalue_len((*bv_p)->bv_val, (*bv_p)->bv_len));
-                                               }
-                                       }
-                               } else {
-                                       RDEBUG2("No suspension profile(s) found in attribute \"%s\"", inst->profile.attr_suspend);
-                               }
-                       }
+                       profile_attr = inst->profile.attr_suspend;
                        break;
 
                case LDAP_ACCESS_DISALLOWED:
                        break;
                }
+               if (profile_attr) {
+                       values = ldap_get_values_len(handle, autz_ctx->entry, profile_attr);
+                       count = ldap_count_values_len(values);
+                       if (count > 0) {
+                               RDEBUG2("Processing %i profile(s) found in attribute \"%s\"", count, profile_attr);
+                       } else {
+                               RDEBUG2("No profile(s) found in attribute \"%s\"", profile_attr);
+                       }
+               } else {
+                       count = 0;
+               }
 
-               FALL_THROUGH;
+               if (!have_default && (count == 0)) break;
 
-       case LDAP_AUTZ_USER_PROFILE:
                /*
-                *      After each profile has been applied, execution will restart here.
-                *      Start by clearing the previously used value.
+                *      Build the list of profile DNs to apply, the default
+                *      profile first, then the profiles from the user object.
                 */
-               if (autz_ctx->profile_value) {
-                       TALLOC_FREE(autz_ctx->profile_value);
-                       autz_ctx->rcode = RLM_MODULE_UPDATED;   /* We're back here after applying a profile successfully */
+               autz_ctx->profile_dn_list = fr_ldap_berval_to_string_list(autz_ctx, values, count, have_default ? 1 : 0);
+               if (have_default) autz_ctx->profile_dn_list[0] = call_env->default_profile.vb_strvalue;
+               if (values) ldap_value_free_len(values);
+
+               if (!autz_ctx->profile_dn_list[0]) break;
+
+               if (RDEBUG_ENABLED3) {
+                       for (char const **dn_p = autz_ctx->profile_dn_list; *dn_p; dn_p++) {
+                               RDEBUG3("Will evaluate profile with DN \"%s\"", *dn_p);
+                       }
                }
 
-               if (autz_ctx->profile_values && autz_ctx->profile_values[autz_ctx->value_idx]) {
-                       autz_ctx->profile_value = fr_ldap_berval_to_string(autz_ctx, autz_ctx->profile_values[autz_ctx->value_idx++]);
-                       REPEAT_MOD_AUTHORIZE_RESUME;
-                       ret = rlm_ldap_map_profile(NULL, NULL, inst, request, autz_ctx->ttrunk, autz_ctx->profile_value,
-                                                  inst->profile.obj_scope, autz_ctx->call_env->profile_filter.vb_strvalue, &autz_ctx->expanded);
-                       switch (ret) {
-                       case UNLANG_ACTION_FAIL:
-                               p_result->rcode = RLM_MODULE_FAIL;
-                               goto finish;
+               REPEAT_MOD_AUTHORIZE_RESUME;
+               ret = rlm_ldap_map_profiles(NULL, &autz_ctx->profiles_applied, inst, request, autz_ctx->ttrunk,
+                                           autz_ctx->profile_dn_list, call_env->profile_filter.vb_strvalue,
+                                           &autz_ctx->expanded);
+               switch (ret) {
+               case UNLANG_ACTION_FAIL:
+                       p_result->rcode = RLM_MODULE_FAIL;
+                       goto finish;
 
-                       case UNLANG_ACTION_PUSHED_CHILD:
-                               autz_ctx->status = LDAP_AUTZ_USER_PROFILE;
-                               return UNLANG_ACTION_PUSHED_CHILD;
+               case UNLANG_ACTION_PUSHED_CHILD:
+                       autz_ctx->status = LDAP_AUTZ_POST_PROFILES;
+                       return UNLANG_ACTION_PUSHED_CHILD;
 
-                       default:
-                               break;
-                       }
+               default:
+                       break;
                }
+       }
+       FALL_THROUGH;
+
+       case LDAP_AUTZ_POST_PROFILES:
+               if (autz_ctx->profiles_applied > 0) autz_ctx->rcode = RLM_MODULE_UPDATED;
                break;
        }
 
@@ -1962,7 +1938,6 @@ static void mod_authorize_cancel(module_ctx_t const *mctx, UNUSED request_t *req
 static int autz_ctx_free(ldap_autz_ctx_t *autz_ctx)
 {
        talloc_free(autz_ctx->expanded.ctx);
-       if (autz_ctx->profile_values) ldap_value_free_len(autz_ctx->profile_values);
        return 0;
 }
 
@@ -2766,15 +2741,44 @@ static int mod_instantiate(module_inst_ctx_t const *mctx)
        SSS_CONTROL_BUILD(user)
        SSS_CONTROL_BUILD(profile)
 
+       /*
+        *      Bulk retrieval matches profile objects by their exact DN,
+        *      so subtree (non-base scope) semantics cannot be preserved.
+        */
+       switch (inst->profile.search_mode) {
+       case LDAP_PROFILE_SEARCH_MODE_AUTO:
+               if (inst->profile.obj_sort_ctrl && (inst->profile.obj_scope == LDAP_SCOPE_BASE)) {
+                       inst->profile.search_mode = LDAP_PROFILE_SEARCH_MODE_BULK;
+               } else {
+                       inst->profile.search_mode = LDAP_PROFILE_SEARCH_MODE_SEQ;
+               }
+               break;
+
+       case LDAP_PROFILE_SEARCH_MODE_BULK:
+               if (inst->profile.obj_scope != LDAP_SCOPE_BASE) {
+                       cf_log_err(conf, "'profile.search_mode = bulk' requires 'profile.scope = base'");
+                       return -1;
+               }
+               if (!inst->profile.obj_sort_ctrl) {
+                       cf_log_err(conf, "'profile.search_mode = bulk' requires 'profile.sort_by', "
+                                  "else profile evaluation order is non-deterministic");
+                       return -1;
+               }
+               break;
+
+       case LDAP_PROFILE_SEARCH_MODE_SEQ:
+               break;
+       }
+
        if (inst->handle_config.tls_require_cert_str) {
                /*
                 *      Convert cert strictness to enumerated constants
                 */
                inst->handle_config.tls_require_cert = fr_table_value_by_str(fr_ldap_tls_require_cert,
-                                                             inst->handle_config.tls_require_cert_str, -1);
+                                                                            inst->handle_config.tls_require_cert_str, -1);
                if (inst->handle_config.tls_require_cert < 0) {
                        cf_log_err(conf, "Invalid 'tls.require_cert' value \"%s\", expected 'never', "
-                                     "'demand', 'allow', 'try' or 'hard'", inst->handle_config.tls_require_cert_str);
+                                  "'demand', 'allow', 'try' or 'hard'", inst->handle_config.tls_require_cert_str);
                        return -1;
                }
        }
index e907a7e8e8e8f15bfd24188bf10bf93cdb91deb7..49f630a657b4af837584a153423c131bb88554d2 100644 (file)
 #include <freeradius-devel/server/module_rlm.h>
 #include <freeradius-devel/ldap/base.h>
 
+/** How profile objects are retrieved from the directory
+ *
+ */
+typedef enum {
+       LDAP_PROFILE_SEARCH_MODE_AUTO = 0,              //!< Resolved at instantiation, LDAP_PROFILE_SEARCH_MODE_BULK
+                                                       ///< when server side sorting is configured and the search
+                                                       ///< scope is base, otherwise LDAP_PROFILE_SEARCH_MODE_SEQ.
+       LDAP_PROFILE_SEARCH_MODE_SEQ,                   //!< One search per profile DN, applied in list order.
+       LDAP_PROFILE_SEARCH_MODE_BULK                   //!< A single search retrieving every profile object,
+                                                       ///< applied in result order.  Requires base scope.
+} ldap_profile_search_mode_t;
+
 typedef struct {
        /*
         *      Options
@@ -109,6 +121,8 @@ typedef struct {
         */
        struct {
                int             obj_scope;                      //!< Search scope.
+               ldap_profile_search_mode_t search_mode;         //!< Whether profiles are retrieved one at a time or
+                                                               ///< with a single search.
                char const      *attr;                          //!< Attribute that identifies profiles to apply. May appear
                                                                //!< in userobj or groupobj.
                char const      *attr_suspend;                  //!< Attribute that identifies profiles to apply when the user's
@@ -185,9 +199,8 @@ typedef enum {
        LDAP_AUTZ_POST_EDIR,
 #endif
        LDAP_AUTZ_MAP,
-       LDAP_AUTZ_DEFAULT_PROFILE,
-       LDAP_AUTZ_POST_DEFAULT_PROFILE,
-       LDAP_AUTZ_USER_PROFILE,
+       LDAP_AUTZ_PROFILES,
+       LDAP_AUTZ_POST_PROFILES,
 } ldap_autz_status_t;
 
 /** User's access state
@@ -211,9 +224,9 @@ typedef struct {
        ldap_autz_call_env_t    *call_env;
        LDAPMessage             *entry;
        ldap_autz_status_t      status;
-       struct berval           **profile_values;
-       int                     value_idx;
-       char                    *profile_value;
+       char const              **profile_dn_list;              //!< List of profile DNs to apply, default profile first,
+                                                       ///< then profiles from the user object. NULL terminated.
+       int                     profiles_applied;       //!< Number of profile maps applied.
        char const              *dn;
        ldap_access_state_t     access_state;           //!< What state a user's account is in.
        rlm_rcode_t             rcode;                  //!< What rcode we'll finally respond with.
@@ -300,3 +313,8 @@ unlang_action_t rlm_ldap_check_cached(unlang_result_t *p_result,
 unlang_action_t rlm_ldap_map_profile(fr_ldap_result_code_t *ret, int *applied,
                                     rlm_ldap_t const *inst, request_t *request, fr_ldap_thread_trunk_t *ttrunk,
                                     char const *dn, int scope, char const *filter, fr_ldap_map_exp_t const *expanded);
+
+unlang_action_t rlm_ldap_map_profiles(fr_ldap_result_code_t *ret, int *applied,
+                                     rlm_ldap_t const *inst, request_t *request, fr_ldap_thread_trunk_t *ttrunk,
+                                     char const * const *dn_list, char const *filter,
+                                     fr_ldap_map_exp_t const *expanded);