From: Arran Cudbard-Bell Date: Thu, 16 Jul 2026 22:22:31 +0000 (-0400) Subject: rlm_ldap: add profile search_mode option X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=6c90f31569acb9d460cb2bb5b97ade47c3dc4043;p=thirdparty%2Ffreeradius-server.git rlm_ldap: add profile search_mode option 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. --- diff --git a/raddb/mods-available/ldap b/raddb/mods-available/ldap index 07161ade426..224252bc885 100644 --- a/raddb/mods-available/ldap +++ b/raddb/mods-available/ldap @@ -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. diff --git a/src/modules/rlm_ldap/profile.c b/src/modules/rlm_ldap/profile.c index 103099f4a9c..a91e67ededd 100644 --- a/src/modules/rlm_ldap/profile.c +++ b/src/modules/rlm_ldap/profile.c @@ -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; } diff --git a/src/modules/rlm_ldap/rlm_ldap.c b/src/modules/rlm_ldap/rlm_ldap.c index 1d67c10bc56..f74b9b8b8ce 100644 --- a/src/modules/rlm_ldap/rlm_ldap.c +++ b/src/modules/rlm_ldap/rlm_ldap.c @@ -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; } } diff --git a/src/modules/rlm_ldap/rlm_ldap.h b/src/modules/rlm_ldap/rlm_ldap.h index e907a7e8e8e..49f630a657b 100644 --- a/src/modules/rlm_ldap/rlm_ldap.h +++ b/src/modules/rlm_ldap/rlm_ldap.h @@ -17,6 +17,18 @@ #include #include +/** 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);