From: Nick Porter Date: Wed, 17 Apr 2024 14:21:54 +0000 (+0100) Subject: Rework rlm_sql to use call_env for autz / group membership queries X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ac4e95798a4d5b4ae5bfcf21897191d5a8398673;p=thirdparty%2Ffreeradius-server.git Rework rlm_sql to use call_env for autz / group membership queries --- diff --git a/src/modules/rlm_sql/rlm_sql.c b/src/modules/rlm_sql/rlm_sql.c index 8144d56256c..f8d80c3c0f6 100644 --- a/src/modules/rlm_sql/rlm_sql.c +++ b/src/modules/rlm_sql/rlm_sql.c @@ -36,6 +36,7 @@ RCSID("$Id$") #include #include #include +#include #include #include @@ -96,12 +97,6 @@ static const conf_parser_t module_config[] = { { FR_CONF_OFFSET_FLAGS("logfile", CONF_FLAG_XLAT, rlm_sql_config_t, logfile) }, { FR_CONF_OFFSET("open_query", rlm_sql_config_t, connect_query) }, - { FR_CONF_OFFSET_FLAGS("authorize_check_query", CONF_FLAG_NOT_EMPTY | CONF_FLAG_XLAT, rlm_sql_config_t, authorize_check_query) }, - { FR_CONF_OFFSET_FLAGS("authorize_reply_query", CONF_FLAG_NOT_EMPTY | CONF_FLAG_XLAT, rlm_sql_config_t, authorize_reply_query) }, - - { FR_CONF_OFFSET_FLAGS("authorize_group_check_query", CONF_FLAG_NOT_EMPTY | CONF_FLAG_XLAT, rlm_sql_config_t, authorize_group_check_query) }, - { FR_CONF_OFFSET_FLAGS("authorize_group_reply_query", CONF_FLAG_NOT_EMPTY | CONF_FLAG_XLAT, rlm_sql_config_t, authorize_group_reply_query) }, - { FR_CONF_OFFSET_FLAGS("group_membership_query", CONF_FLAG_NOT_EMPTY | CONF_FLAG_XLAT, rlm_sql_config_t, groupmemb_query) }, { FR_CONF_OFFSET("safe_characters", rlm_sql_config_t, allowed_chars), .dflt = "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /" }, /* @@ -138,17 +133,64 @@ fr_dict_attr_autoload_t rlm_sql_dict_attr[] = { }; typedef struct { - fr_value_box_t user; + fr_value_box_t user; //!< Expansion of the sql_user_name + tmpl_t *check_query; //!< Tmpl to expand to form authorize_check_query + tmpl_t *reply_query; //!< Tmpl to expand to form authorize_reply_query + tmpl_t *membership_query; //!< Tmpl to expand to form group_membership_query + tmpl_t *group_check_query; //!< Tmpl to expand to form authorize_group_check_query + tmpl_t *group_reply_query; //!< Tmpl to expand to form authorize_group_reply_query } sql_autz_call_env_t; static const call_env_method_t authorize_method_env = { FR_CALL_ENV_METHOD_OUT(sql_autz_call_env_t), .env = (call_env_parser_t[]) { { FR_CALL_ENV_OFFSET("sql_user_name", FR_TYPE_STRING, CALL_ENV_FLAG_CONCAT, sql_autz_call_env_t, user) }, + { FR_CALL_ENV_PARSE_ONLY_OFFSET("authorize_check_query", FR_TYPE_STRING, CALL_ENV_FLAG_PARSE_ONLY, sql_autz_call_env_t, check_query) }, + { FR_CALL_ENV_PARSE_ONLY_OFFSET("authorize_reply_query", FR_TYPE_STRING, CALL_ENV_FLAG_PARSE_ONLY, sql_autz_call_env_t, reply_query) }, + { FR_CALL_ENV_PARSE_ONLY_OFFSET("group_membership_query", FR_TYPE_STRING, CALL_ENV_FLAG_PARSE_ONLY, sql_autz_call_env_t, membership_query) }, + { FR_CALL_ENV_PARSE_ONLY_OFFSET("authorize_group_check_query", FR_TYPE_STRING, CALL_ENV_FLAG_PARSE_ONLY, sql_autz_call_env_t, group_check_query) }, + { FR_CALL_ENV_PARSE_ONLY_OFFSET("authorize_group_reply_query", FR_TYPE_STRING, CALL_ENV_FLAG_PARSE_ONLY, sql_autz_call_env_t, group_reply_query) }, CALL_ENV_TERMINATOR } }; +typedef struct rlm_sql_grouplist_s rlm_sql_grouplist_t; + +/** Status of the authorization process + */ +typedef enum { + SQL_AUTZ_CHECK = 0x11, //!< Running user `check` query + SQL_AUTZ_REPLY = 0x12, //!< Running user `reply` query + SQL_AUTZ_GROUP_MEMB = 0x20, //!< Running group membership query + SQL_AUTZ_GROUP_CHECK = 0x21, //!< Running group `check` query + SQL_AUTZ_GROUP_REPLY = 0x22, //!< Running group `reply` query + SQL_AUTZ_PROFILE_START = 0x40, //!< Starting processing user profiles + SQL_AUTZ_PROFILE_CHECK = 0x41, //!< Running profile `check` query + SQL_AUTZ_PROFILE_REPLY = 0x42, //!< Running profile `reply` query +} sql_autz_status_t; + +#define SQL_AUTZ_STAGE_GROUP 0x20 +#define SQL_AUTZ_STAGE_PROFILE 0x40 + +/** Context for SQL authorization + */ +typedef struct { + rlm_sql_t const *inst; //!< Module instance. + request_t *request; //!< Request being processed. + rlm_rcode_t rcode; //!< Module return code. + rlm_sql_handle_t *handle; //!< Database connection handle in use for current authorization. + sql_autz_call_env_t *call_env; //!< Call environment data. + map_list_t check_tmp; //!< List to store check items before processing. + map_list_t reply_tmp; //!< List to store reply items before processing. + sql_autz_status_t status; //!< Current status of the authorization. + fr_value_box_list_t query; //!< Where expanded query tmpls will be written. + bool user_found; //!< Has the user been found anywhere? + rlm_sql_grouplist_t *groups; //!< List of groups returned by the group membership query. + rlm_sql_grouplist_t *group; //!< Current group being processed. + fr_pair_t *sql_group; //!< Pair to update with group being processed. + fr_pair_t *profile; //!< Current profile being processed. +} sql_autz_ctx_t; + typedef struct { fr_value_box_t user; } sql_redundant_call_env_t; @@ -171,16 +213,43 @@ static const call_env_method_t post_auth_method_env = { typedef struct { fr_value_box_t user; + tmpl_t *membership_query; } sql_group_xlat_call_env_t; static const call_env_method_t group_xlat_method_env = { FR_CALL_ENV_METHOD_OUT(sql_group_xlat_call_env_t), .env = (call_env_parser_t[]) { { FR_CALL_ENV_OFFSET("sql_user_name", FR_TYPE_STRING, CALL_ENV_FLAG_CONCAT, sql_group_xlat_call_env_t, user) }, + { FR_CALL_ENV_PARSE_ONLY_OFFSET("group_membership_query", FR_TYPE_STRING, CALL_ENV_FLAG_PARSE_ONLY, sql_group_xlat_call_env_t, membership_query) }, CALL_ENV_TERMINATOR } }; +static int _sql_escape_uxtx_free(void *uctx) +{ + return talloc_free(uctx); +} + +/* + * Create a thread local uctx which is used in SQL value box escaping + * so that an already reserved connection can be used. + */ +static void *sql_escape_uctx_alloc(request_t *request, void const *uctx) +{ + static _Thread_local rlm_sql_escape_uctx_t *t_ctx; + + if (unlikely(t_ctx == NULL)) { + rlm_sql_escape_uctx_t *ctx; + + MEM(ctx = talloc_zero(NULL, rlm_sql_escape_uctx_t)); + fr_atexit_thread_local(t_ctx, _sql_escape_uxtx_free, ctx); + } + t_ctx->sql = uctx; + t_ctx->handle = request_data_reference(request, (void *)sql_escape_uctx_alloc, 0); + + return t_ctx; +} + /* * Fall-Through checking function from rlm_files.c */ @@ -770,16 +839,14 @@ static void sql_set_user(rlm_sql_t const *inst, request_t *request, fr_value_box #define sql_unset_user(_i, _r) fr_pair_delete_by_da(&_r->request_pairs, _i->sql_user) -typedef struct rlm_sql_grouplist_s rlm_sql_grouplist_t; struct rlm_sql_grouplist_s { char *name; rlm_sql_grouplist_t *next; }; static int sql_get_grouplist(rlm_sql_t const *inst, rlm_sql_handle_t **handle, request_t *request, - rlm_sql_grouplist_t **phead) + char const *query, rlm_sql_grouplist_t **phead) { - char *expanded = NULL; int num_groups = 0; rlm_sql_row_t row; rlm_sql_grouplist_t *entry; @@ -789,12 +856,9 @@ static int sql_get_grouplist(rlm_sql_t const *inst, rlm_sql_handle_t **handle, r entry = *phead = NULL; - if (!inst->config.groupmemb_query || !*inst->config.groupmemb_query) return 0; - if (xlat_aeval(request, &expanded, request, inst->config.groupmemb_query, - inst->sql_escape_func, *handle) < 0) return -1; + if (!query || !*query) return 0; - ret = rlm_sql_select_query(inst, request, handle, expanded); - talloc_free(expanded); + ret = rlm_sql_select_query(inst, request, handle, query); if (ret != RLM_SQL_OK) return -1; while (rlm_sql_fetch_row(&row, inst, request, handle) == RLM_SQL_OK) { @@ -826,18 +890,12 @@ static int sql_get_grouplist(rlm_sql_t const *inst, rlm_sql_handle_t **handle, r /** Check if a given group is in the SQL group for this user. * */ -static bool CC_HINT(nonnull) sql_check_group(rlm_sql_t const *inst, request_t *request, char const *name, - fr_value_box_t *user) +static bool CC_HINT(nonnull) sql_check_group(rlm_sql_t const *inst, request_t *request, char const *query, char const *name) { bool rcode = false; rlm_sql_handle_t *handle; rlm_sql_grouplist_t *entry, *head = NULL; - /* - * Set the user attr here - */ - sql_set_user(inst, request, user); - /* * Get a socket for this lookup */ @@ -850,7 +908,7 @@ static bool CC_HINT(nonnull) sql_check_group(rlm_sql_t const *inst, request_t *r /* * Get the list of groups this user is a member of */ - if (sql_get_grouplist(inst, &handle, request, &head) < 0) { + if (sql_get_grouplist(inst, &handle, request, query, &head) < 0) { talloc_free(head); REDEBUG("Error getting group membership"); fr_pool_connection_release(inst->pool, request, handle); @@ -871,6 +929,31 @@ static bool CC_HINT(nonnull) sql_check_group(rlm_sql_t const *inst, request_t *r return rcode; } +typedef struct { + fr_value_box_list_t query; +} sql_group_xlat_ctx_t; + +static xlat_action_t sql_group_xlat_resume(TALLOC_CTX *ctx, fr_dcursor_t *out, xlat_ctx_t const *xctx, + request_t *request, fr_value_box_list_t *in) +{ + sql_group_xlat_ctx_t *xlat_ctx = talloc_get_type_abort(xctx->rctx, sql_group_xlat_ctx_t); + rlm_sql_t const *inst = talloc_get_type_abort(xctx->mctx->inst->data, rlm_sql_t); + fr_value_box_t *arg = fr_value_box_list_head(in); + char const *p = arg->vb_strvalue; + fr_value_box_t *query, *vb; + + query = fr_value_box_list_head(&xlat_ctx->query); + if (!query) return XLAT_ACTION_FAIL; + + fr_skip_whitespace(p); + + MEM(vb = fr_value_box_alloc(ctx, FR_TYPE_BOOL, attr_expr_bool_enum)); + vb->vb_bool = sql_check_group(inst, request, query->vb_strvalue, p); + fr_dcursor_append(out, vb); + + return XLAT_ACTION_DONE; +} + /** Check if the user is a member of a particular group * @@ -880,56 +963,179 @@ static bool CC_HINT(nonnull) sql_check_group(rlm_sql_t const *inst, request_t *r * * @ingroup xlat_functions */ -static xlat_action_t sql_group_xlat(TALLOC_CTX *ctx, fr_dcursor_t *out, - xlat_ctx_t const *xctx, - request_t *request, fr_value_box_list_t *in) +static xlat_action_t sql_group_xlat(UNUSED TALLOC_CTX *ctx, UNUSED fr_dcursor_t *out, xlat_ctx_t const *xctx, + request_t *request, UNUSED fr_value_box_list_t *in) { sql_group_xlat_call_env_t *call_env = talloc_get_type_abort(xctx->env_data, sql_group_xlat_call_env_t); - rlm_sql_t const *inst = talloc_get_type_abort(xctx->mctx->inst->data, rlm_sql_t); - fr_value_box_t *arg = fr_value_box_list_head(in); - char const *p = arg->vb_strvalue; - fr_value_box_t *vb; + sql_group_xlat_ctx_t *xlat_ctx; + rlm_sql_t const *inst = talloc_get_type_abort(xctx->mctx->inst->data, rlm_sql_t); - fr_skip_whitespace(p); + if (!call_env->membership_query) { + RWARN("Cannot check group membership - group_membership_query not set"); + return XLAT_ACTION_FAIL; + } - MEM(vb = fr_value_box_alloc(ctx, FR_TYPE_BOOL, attr_expr_bool_enum)); - vb->vb_bool = sql_check_group(inst, request, p, &call_env->user); - fr_dcursor_append(out, vb); + /* + * Set the user attr here + */ + sql_set_user(inst, request, &call_env->user); - return XLAT_ACTION_DONE; + MEM(xlat_ctx = talloc_zero(unlang_interpret_frame_talloc_ctx(request), sql_group_xlat_ctx_t)); + fr_value_box_list_init(&xlat_ctx->query); + + if (unlang_xlat_yield(request, sql_group_xlat_resume, NULL, 0, xlat_ctx) != XLAT_ACTION_YIELD) return XLAT_ACTION_FAIL; + if (unlang_tmpl_push(xlat_ctx, &xlat_ctx->query, request, call_env->membership_query, NULL) < 0) return XLAT_ACTION_FAIL; + return XLAT_ACTION_PUSH_UNLANG; } -static int sql_check_groupmemb(rlm_sql_t const *inst, request_t *request, rlm_sql_handle_t **handle, - fr_pair_t *sql_group, char const *group_name, - sql_fall_through_t *do_fall_through, rlm_rcode_t *rcode) +/** Process a "check" map + * + * Any entries using an assignment operator will be moved to the reply map + * for later merging into the request. + * + * @param request Current request. + * @param check_map to process. + * @param reply_map where any assignment entries will be moved. + * @return + * - 0 if all the check entries pass. + * - -1 if the checks fail. + */ +static int check_map_process(request_t *request, map_list_t *check_map, map_list_t *reply_map) { - bool added; - int rows; - fr_pair_t *vp; - char *expanded = NULL; - map_list_t check_tmp, reply_tmp; + map_t *map, *next; - added = false; - map_list_init(&check_tmp); - map_list_init(&reply_tmp); + for (map = map_list_head(check_map); + map != NULL; + map = next) { + next = map_list_next(check_map, map); - fr_pair_value_strdup(sql_group, group_name, true); + if (fr_assignment_op[map->op]) { + (void) map_list_remove(check_map, map); + map_list_insert_tail(reply_map, map); + continue; + } - if (inst->config.authorize_group_check_query) { - /* - * Expand the group query - */ - if (xlat_aeval(request, &expanded, request, inst->config.authorize_group_check_query, - inst->sql_escape_func, *handle) < 0) { - REDEBUG("Error generating query"); + if (!fr_comparison_op[map->op]) { + REDEBUG("Invalid operator '%s'", fr_tokens[map->op]); + goto fail; + } + + if (fr_type_is_structural(tmpl_attr_tail_da(map->lhs)->type) && + (map->op != T_OP_CMP_TRUE) && (map->op != T_OP_CMP_FALSE)) { + REDEBUG("Invalid comparison for structural type"); + goto fail; + } + + RDEBUG2(" &%s %s %s", map->lhs->name, fr_tokens[map->op], map->rhs->name); + if (radius_legacy_map_cmp(request, map) != 1) { + fail: + map_list_talloc_free(check_map); + map_list_talloc_free(reply_map); + RDEBUG2("failed match: skipping this entry"); return -1; } + } + return 0; +} + +static int sql_autz_ctx_free(sql_autz_ctx_t *to_free) +{ + (void) request_data_get(to_free->request, (void *)sql_escape_uctx_alloc, 0); + if (to_free->handle) fr_pool_connection_release(to_free->inst->pool, to_free->request, to_free->handle); + map_list_talloc_free(&to_free->check_tmp); + map_list_talloc_free(&to_free->reply_tmp); + sql_unset_user(to_free->inst, to_free->request); + + return 0; +} + +/** Resume function called after authorization group / profile expansion of check / reply query tmpl + * + * Groups and profiles are treated almost identically except: + * - groups are read from an SQL query + * - profiles are read from &control.User-Profile + * - if `cache_groups` is set, groups populate &control.SQL-Group + * + * Profiles are handled after groups, and will not happend if the last group resulted in `Fall-Through = no` + * + * Before each query is run, &request.SQL-Group is populated with the value of the group being evaluated. + * + * @param p_result Result of current authorization. + * @param priority Unused. + * @param request Current request. + * @param uctx Current authorization context. + * @return one of the RLM_MODULE_* values. + */ +static unlang_action_t mod_autz_group_resume(rlm_rcode_t *p_result, UNUSED int *priority, UNUSED request_t *request, void *uctx) +{ + sql_autz_ctx_t *autz_ctx = talloc_get_type_abort(uctx, sql_autz_ctx_t); + sql_autz_call_env_t *call_env = autz_ctx->call_env; + rlm_sql_t const *inst = autz_ctx->inst; + fr_value_box_t *query = fr_value_box_list_pop_head(&autz_ctx->query); + int rows; + sql_fall_through_t do_fall_through = FALL_THROUGH_DEFAULT; + fr_pair_t *vp; + + switch(autz_ctx->status) { + case SQL_AUTZ_GROUP_MEMB: + rows = sql_get_grouplist(inst, &autz_ctx->handle, request, query->vb_strvalue, &autz_ctx->groups); + talloc_free(query); - rows = sql_get_map_list(request->control_ctx, inst, request, handle, &check_tmp, expanded, request_attr_request); - TALLOC_FREE(expanded); if (rows < 0) { - REDEBUG("Error retrieving check pairs for group %s", group_name); - return -1; + talloc_free(autz_ctx->groups); + REDEBUG("Error retrieving group list"); + RETURN_MODULE_FAIL; + } + + if (rows == 0) { + RDEBUG2("User not found in any groups"); + break; + } + fr_assert(autz_ctx->groups); + + RDEBUG2("User found in the group table"); + autz_ctx->user_found = true; + autz_ctx->group = autz_ctx->groups; + MEM(pair_update_request(&autz_ctx->sql_group, inst->group_da) >= 0); + + next_group: + fr_pair_value_strdup(autz_ctx->sql_group, autz_ctx->group->name, true); + autz_ctx->status = SQL_AUTZ_GROUP_CHECK; + FALL_THROUGH; + + case SQL_AUTZ_PROFILE_START: + next_profile: + if (autz_ctx->status & SQL_AUTZ_STAGE_PROFILE) { + fr_pair_value_strdup(autz_ctx->sql_group, autz_ctx->profile->vp_strvalue, true); + autz_ctx->status = SQL_AUTZ_PROFILE_CHECK; + } + RDEBUG3("Processing %s %pV", + autz_ctx->status & SQL_AUTZ_STAGE_GROUP ? "group" : "profile", &autz_ctx->sql_group->data); + if (inst->config.cache_groups && autz_ctx->status & SQL_AUTZ_STAGE_GROUP) { + MEM(pair_append_control(&vp, inst->group_da) >= 0); + fr_pair_value_strdup(vp, autz_ctx->group->name, true); + } + + if (call_env->group_check_query) { + if (unlang_function_repeat_set(request, mod_autz_group_resume) < 0) RETURN_MODULE_FAIL; + if (unlang_tmpl_push(autz_ctx, &autz_ctx->query, request, + call_env->group_check_query, NULL) < 0) RETURN_MODULE_FAIL; + return UNLANG_ACTION_PUSHED_CHILD; + } + + goto group_reply_push; + + case SQL_AUTZ_GROUP_CHECK: + case SQL_AUTZ_PROFILE_CHECK: + rows = sql_get_map_list(autz_ctx, inst, request, &autz_ctx->handle, &autz_ctx->check_tmp, + query->vb_strvalue, request_attr_request); + talloc_free(query); + + if (rows < 0) { + REDEBUG("Error retrieving check pairs for %s %pV", + autz_ctx->status & SQL_AUTZ_STAGE_GROUP ? "group" : "profile", + &autz_ctx->sql_group->data); + RETURN_MODULE_FAIL; } /* @@ -937,402 +1143,313 @@ static int sql_check_groupmemb(rlm_sql_t const *inst, request_t *request, rlm_sq * process the reply rows */ if (rows > 0) { - map_t *map, *next; - - for (map = map_list_head(&check_tmp); - map != NULL; - map = next) { - next = map_list_next(&check_tmp, map); - - if (fr_assignment_op[map->op]) { - (void) map_list_remove(&check_tmp, map); - map_list_insert_tail(&reply_tmp, map); - continue; - } - - if (!fr_comparison_op[map->op]) { - REDEBUG("Invalid operator '%s'", fr_tokens[map->op]); - goto fail; - } - - if (fr_type_is_structural(tmpl_attr_tail_da(map->lhs)->type) && - (map->op != T_OP_CMP_TRUE) && (map->op != T_OP_CMP_FALSE)) { - REDEBUG("Invalid comparison for structural type"); - goto fail; - } - - RDEBUG2(" &%s %s %s", map->lhs->name, fr_tokens[map->op], - map->rhs ? map->rhs->name : "{ ... }"); - if (radius_legacy_map_cmp(request, map) != 1) { - fail: - map_list_talloc_free(&check_tmp); - map_list_talloc_free(&reply_tmp); - return 0; - } + if (check_map_process(request, &autz_ctx->check_tmp, &autz_ctx->reply_tmp) < 0) { + map_list_talloc_free(&autz_ctx->check_tmp); + goto next_group_find; } - - RDEBUG2("Group \"%s\": Conditional check items matched", group_name); + RDEBUG2("%s \"%pV\": Conditional check items matched", + autz_ctx->status & SQL_AUTZ_STAGE_GROUP ? "Group" : "Profile", &autz_ctx->sql_group->data); } else { - RDEBUG2("Group \"%s\": Conditional check items matched (empty)", group_name); + RDEBUG2("%s \"%pV\": Conditional check items matched (empty)", + autz_ctx->status & SQL_AUTZ_STAGE_GROUP ? "Group" : "Profile", &autz_ctx->sql_group->data); } - if (*rcode == RLM_MODULE_NOOP) *rcode = RLM_MODULE_OK; + if (autz_ctx->rcode == RLM_MODULE_NOOP) autz_ctx->rcode = RLM_MODULE_OK; - map_list_talloc_free(&check_tmp); + map_list_talloc_free(&autz_ctx->check_tmp); - if (inst->config.cache_groups) { - MEM(pair_append_control(&vp, inst->group_da) >= 0); - fr_pair_value_strdup(vp, group_name, true); - added = true; + if (call_env->group_reply_query) { + group_reply_push: + if (unlang_function_repeat_set(request, mod_autz_group_resume) < 0) RETURN_MODULE_FAIL; + if (unlang_tmpl_push(autz_ctx, &autz_ctx->query, request, + call_env->group_reply_query, NULL) < 0) RETURN_MODULE_FAIL; + autz_ctx->status = autz_ctx->status & SQL_AUTZ_STAGE_GROUP ? SQL_AUTZ_GROUP_REPLY : SQL_AUTZ_PROFILE_REPLY; + return UNLANG_ACTION_PUSHED_CHILD; } - } - if (inst->config.authorize_group_reply_query) { - /* - * Now get the reply pairs since the paircmp matched - */ - if (xlat_aeval(request, &expanded, request, inst->config.authorize_group_reply_query, - inst->sql_escape_func, *handle) < 0) { - REDEBUG("Error generating query"); - return -1; - } + if (map_list_num_elements(&autz_ctx->reply_tmp)) goto group_attr_cache; + + goto next_group_find; + + case SQL_AUTZ_GROUP_REPLY: + case SQL_AUTZ_PROFILE_REPLY: + rows = sql_get_map_list(autz_ctx, inst, request, &autz_ctx->handle, &autz_ctx->reply_tmp, + query->vb_strvalue, request_attr_reply); + talloc_free(query); - rows = sql_get_map_list(request->reply_ctx, inst, request, handle, &reply_tmp, expanded, request_attr_reply); - TALLOC_FREE(expanded); if (rows < 0) { - REDEBUG("Error retrieving reply pairs for group %s", group_name); - return -1; + REDEBUG("Error retrieving reply pairs for %s %pV", + autz_ctx->status & SQL_AUTZ_STAGE_GROUP ? "group" : "profile", &autz_ctx->sql_group->data); + RETURN_MODULE_FAIL; } if (rows == 0) { - *do_fall_through = FALL_THROUGH_DEFAULT; - return 0; + do_fall_through = FALL_THROUGH_DEFAULT; + goto group_attr_cache; } - fr_assert(!map_list_empty(&reply_tmp)); /* coverity, among others */ - *do_fall_through = fall_through(&reply_tmp); + fr_assert(!map_list_empty(&autz_ctx->reply_tmp)); /* coverity, among others */ + do_fall_through = fall_through(&autz_ctx->reply_tmp); + + group_attr_cache: + if (inst->config.cache_groups && autz_ctx->status & SQL_AUTZ_STAGE_GROUP) { + MEM(pair_append_control(&vp, inst->group_da) >= 0); + fr_pair_value_strdup(vp, autz_ctx->group->name, true); + } - RDEBUG2("Group \"%s\": Merging reply items", group_name); - *rcode = RLM_MODULE_UPDATED; + if (map_list_num_elements(&autz_ctx->reply_tmp) == 0) goto next_group_find; + RDEBUG2("%s \"%pV\": Merging control and reply items", + autz_ctx->status & SQL_AUTZ_STAGE_GROUP ? "Group" : "Profile", &autz_ctx->sql_group->data); + autz_ctx->rcode = RLM_MODULE_UPDATED; - if (radius_legacy_map_list_apply(request, &reply_tmp, NULL) < 0) { + RINDENT(); + if (radius_legacy_map_list_apply(request, &autz_ctx->reply_tmp, NULL) < 0) { RPEDEBUG("Failed applying reply item"); - map_list_talloc_free(&reply_tmp); - return -1; + REXDENT(); + RETURN_MODULE_FAIL; } + REXDENT(); + map_list_talloc_free(&autz_ctx->reply_tmp); + + next_group_find: + if (do_fall_through != FALL_THROUGH_YES) break; + if (autz_ctx->status & SQL_AUTZ_STAGE_PROFILE) { + autz_ctx->profile = fr_pair_find_by_da(&request->control_pairs, autz_ctx->profile, attr_user_profile); + if (autz_ctx->profile) goto next_profile; + break; + } + autz_ctx->group = autz_ctx->group->next; + if (autz_ctx->group) goto next_group; - map_list_talloc_free(&reply_tmp); + break; - /* - * If there's no reply query configured, then we assume - * FALL_THROUGH_NO, which is the same as the users file if you - * had no reply attributes. - */ - } else { - *do_fall_through = FALL_THROUGH_DEFAULT; + default: + fr_assert(0); } - if (inst->config.cache_groups && !added) { - MEM(pair_append_control(&vp, inst->group_da) >= 0); - fr_pair_value_strdup(vp, group_name, true); + /* + * If group processing has completed, check to see if profile processing should be done + */ + if ((autz_ctx->status & SQL_AUTZ_STAGE_GROUP) && + ((do_fall_through == FALL_THROUGH_YES) || + (inst->config.read_profiles && (do_fall_through == FALL_THROUGH_DEFAULT)))) { + RDEBUG3("... falling-through to profile processing"); + + autz_ctx->profile = fr_pair_find_by_da(&request->control_pairs, NULL, attr_user_profile); + if (autz_ctx->profile) { + MEM(pair_update_request(&autz_ctx->sql_group, inst->group_da) >= 0); + autz_ctx->status = SQL_AUTZ_PROFILE_START; + goto next_profile; + } } - return 0; -} + if (!autz_ctx->user_found) RETURN_MODULE_NOTFOUND; + RETURN_MODULE_RCODE(autz_ctx->rcode); +} -static unlang_action_t rlm_sql_process_groups(rlm_rcode_t *p_result, - rlm_sql_t const *inst, request_t *request, rlm_sql_handle_t **handle, - sql_fall_through_t *do_fall_through) +/** Resume function called after authorization check / reply tmpl expansion + * + * @param p_result Result of current authorization. + * @param priority Unused. + * @param request Current request. + * @param uctx Current authorization context. + * @return one of the RLM_MODULE_* values. + */ +static unlang_action_t mod_authorize_resume(rlm_rcode_t *p_result, int *priority, request_t *request, void *uctx) { - rlm_rcode_t rcode = RLM_MODULE_NOOP; - rlm_sql_grouplist_t *head = NULL, *entry = NULL; + sql_autz_ctx_t *autz_ctx = talloc_get_type_abort(uctx, sql_autz_ctx_t); + sql_autz_call_env_t *call_env = autz_ctx->call_env; + rlm_sql_t const *inst = autz_ctx->inst; + fr_value_box_t *query = fr_value_box_list_pop_head(&autz_ctx->query); int rows; - fr_pair_t *vp, *sql_group; + sql_fall_through_t do_fall_through = FALL_THROUGH_DEFAULT; - fr_assert(request->packet != NULL); + switch(autz_ctx->status) { + case SQL_AUTZ_CHECK: + rows = sql_get_map_list(autz_ctx, inst, request, &autz_ctx->handle, &autz_ctx->check_tmp, + query->vb_strvalue, request_attr_request); + talloc_free(query); - if (!inst->config.groupmemb_query) { - RWARN("Cannot do check groups when group_membership_query is not set"); + if (rows < 0) { + REDEBUG("Failed getting check attributes"); + RETURN_MODULE_FAIL; + } - do_nothing: - *do_fall_through = FALL_THROUGH_DEFAULT; + if (rows == 0) goto skip_reply; /* Don't need to handle map entries we don't have */ /* - * Didn't add group attributes or allocate - * memory, so don't do anything else. + * Only do this if *some* check pairs were returned */ - RETURN_MODULE_NOTFOUND; - } + RDEBUG2("User found in radcheck table"); + autz_ctx->user_found = true; - /* - * Get the list of groups this user is a member of - */ - rows = sql_get_grouplist(inst, handle, request, &head); - if (rows < 0) { - talloc_free(head); - REDEBUG("Error retrieving group list"); - RETURN_MODULE_FAIL; - } + if (check_map_process(request, &autz_ctx->check_tmp, &autz_ctx->reply_tmp) < 0) goto skip_reply; + RDEBUG2("Conditional check items matched"); - if (rows == 0) { - RDEBUG2("User not found in any groups"); - goto do_nothing; - } - fr_assert(head); + autz_ctx->rcode = RLM_MODULE_OK; + map_list_talloc_free(&autz_ctx->check_tmp); - RDEBUG2("User found in the group table"); + if (!call_env->reply_query) goto skip_reply; - MEM(pair_update_request(&sql_group, inst->group_da) >= 0); + if (unlang_function_repeat_set(request, mod_authorize_resume) < 0) RETURN_MODULE_FAIL; + if (unlang_tmpl_push(autz_ctx, &autz_ctx->query, request, call_env->reply_query, NULL) < 0) RETURN_MODULE_FAIL; + autz_ctx->status = SQL_AUTZ_REPLY; + return UNLANG_ACTION_PUSHED_CHILD; - for (entry = head; entry != NULL; entry = entry->next) { - if (sql_check_groupmemb(inst, request, handle, sql_group, entry->name, do_fall_through, &rcode) < 0) { - rcode = RLM_MODULE_FAIL; - goto finish; + case SQL_AUTZ_REPLY: + rows = sql_get_map_list(autz_ctx, inst, request, &autz_ctx->handle, &autz_ctx->reply_tmp, + query->vb_strvalue, request_attr_reply); + talloc_free(query); + + if (rows < 0) { + REDEBUG("SQL query error getting reply attributes"); + RETURN_MODULE_FAIL; } - if (*do_fall_through != FALL_THROUGH_YES) break; - } + if (rows == 0) goto skip_reply; - /* - * Apply user profiles - */ - for (vp = fr_pair_find_by_da(&request->control_pairs, NULL, attr_user_profile); - vp != NULL; - vp = fr_pair_find_by_da(&request->control_pairs, vp, attr_user_profile)) { - if (sql_check_groupmemb(inst, request, handle, sql_group, vp->vp_strvalue, do_fall_through, &rcode) < 0) { - rcode = RLM_MODULE_FAIL; - goto finish; + do_fall_through = fall_through(&autz_ctx->reply_tmp); + + RDEBUG2("User found in radreply table"); + autz_ctx->user_found = true; + + skip_reply: + if (map_list_num_elements(&autz_ctx->reply_tmp)) { + RDEBUG2("Merging control and reply items"); + RINDENT(); + if (radius_legacy_map_list_apply(request, &autz_ctx->reply_tmp, NULL) < 0) { + RPEDEBUG("Failed applying item"); + REXDENT(); + RETURN_MODULE_FAIL; + } + REXDENT(); + + autz_ctx->rcode = RLM_MODULE_UPDATED; + map_list_talloc_free(&autz_ctx->reply_tmp); } - if (*do_fall_through != FALL_THROUGH_YES) break; - } + if ((do_fall_through == FALL_THROUGH_YES) || + (inst->config.read_groups && (do_fall_through == FALL_THROUGH_DEFAULT))) { + RDEBUG3("... falling-through to group processing"); -finish: - fr_pair_delete(&request->request_pairs, sql_group); + if (!call_env->membership_query) { + RWARN("Cannot check groups when group_membership_query is not set"); + break; + } - talloc_free(head); - pair_delete_request(inst->group_da); + if (!call_env->group_check_query && !call_env->group_reply_query) { + RWARN("Cannot process groups when neither authorize_group_check_query nor authorize_group_check_query are set"); + break; + } - RETURN_MODULE_RCODE(rcode); -} + if (unlang_function_repeat_set(request, mod_autz_group_resume) < 0) RETURN_MODULE_FAIL; + if (unlang_tmpl_push(autz_ctx, &autz_ctx->query, request, + call_env->membership_query, NULL) < 0) RETURN_MODULE_FAIL; + autz_ctx->status = SQL_AUTZ_GROUP_MEMB; + return UNLANG_ACTION_PUSHED_CHILD; + } -static unlang_action_t CC_HINT(nonnull) mod_authorize(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request) -{ - rlm_rcode_t rcode = RLM_MODULE_NOOP; + if ((do_fall_through == FALL_THROUGH_YES) || + (inst->config.read_profiles && (do_fall_through == FALL_THROUGH_DEFAULT))) { + RDEBUG3("... falling-through to profile processing"); - rlm_sql_t const *inst = talloc_get_type_abort_const(mctx->inst->data, rlm_sql_t); - sql_autz_call_env_t *call_env = talloc_get_type_abort(mctx->env_data, sql_autz_call_env_t); - rlm_sql_handle_t *handle; + if (!call_env->group_check_query && !call_env->group_reply_query) { + RWARN("Cannot process profiles when neither authorize_group_check_query nor authorize_group_check_query are set"); + break; + } - map_list_t check_tmp; - map_list_t reply_tmp; + autz_ctx->profile = fr_pair_find_by_da(&request->control_pairs, NULL, attr_user_profile); + if (!autz_ctx->profile) break; - bool user_found = false; + MEM(pair_update_request(&autz_ctx->sql_group, inst->group_da) >= 0); + autz_ctx->status = SQL_AUTZ_PROFILE_START; + return mod_autz_group_resume(p_result, priority, request, autz_ctx); + } + break; - sql_fall_through_t do_fall_through = FALL_THROUGH_DEFAULT; + default: + fr_assert(0); + } - int rows; + if (!autz_ctx->user_found) RETURN_MODULE_NOTFOUND; + RETURN_MODULE_RCODE(autz_ctx->rcode); +} - char *expanded = NULL; +/** Start of module authorize method + * + * Pushes the tmpl relating to the first required query for evaluation + */ +static unlang_action_t CC_HINT(nonnull) mod_authorize(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request) +{ + rlm_sql_t const *inst = talloc_get_type_abort_const(mctx->inst->data, rlm_sql_t); + sql_autz_call_env_t *call_env = talloc_get_type_abort(mctx->env_data, sql_autz_call_env_t); + sql_autz_ctx_t *autz_ctx; - map_list_init(&check_tmp); - map_list_init(&reply_tmp); fr_assert(request->packet != NULL); fr_assert(request->reply != NULL); - if (!inst->config.authorize_check_query && !inst->config.authorize_reply_query && - !inst->config.read_groups) { + if (!call_env->check_query && !call_env->reply_query && !(inst->config.read_groups && call_env->membership_query)) { RWDEBUG("No authorization checks configured, returning noop"); - RETURN_MODULE_NOOP; } /* - * Set the user attr here + * Set and check the user attr here */ sql_set_user(inst, request, &call_env->user); + MEM(autz_ctx = talloc_zero(unlang_interpret_frame_talloc_ctx(request), sql_autz_ctx_t)); + *autz_ctx = (sql_autz_ctx_t) { + .inst = inst, + .call_env = call_env, + .request = request, + .rcode = RLM_MODULE_NOOP + }; + map_list_init(&autz_ctx->check_tmp); + map_list_init(&autz_ctx->reply_tmp); + talloc_set_destructor(autz_ctx, sql_autz_ctx_free); + /* * Reserve a socket * - * After this point use goto error or goto release to cleanup socket temporary pairlists and - * temporary attributes. - */ - handle = fr_pool_connection_get(inst->pool, request); - if (!handle) { - sql_unset_user(inst, request); - RETURN_MODULE_FAIL; - } - - /* - * Query the check table to find any conditions associated with this user/realm/whatever... + * This is freed by the talloc destructor for autz_ctx */ - if (inst->config.authorize_check_query) { - if (xlat_aeval(request, &expanded, request, inst->config.authorize_check_query, - inst->sql_escape_func, handle) < 0) { - REDEBUG("Failed generating query"); - rcode = RLM_MODULE_FAIL; - - error: - map_list_talloc_free(&check_tmp); - map_list_talloc_free(&reply_tmp); - sql_unset_user(inst, request); - - fr_pool_connection_release(inst->pool, request, handle); - - RETURN_MODULE_RCODE(rcode); - } - - rows = sql_get_map_list(request->control_ctx, inst, request, &handle, &check_tmp, expanded, request_attr_request); - TALLOC_FREE(expanded); - if (rows < 0) { - REDEBUG("Failed getting check attributes"); - rcode = RLM_MODULE_FAIL; - goto error; - } - - if (rows == 0) goto skip_reply; /* Don't need to free VPs we don't have */ + autz_ctx->handle = fr_pool_connection_get(inst->pool, request); + if (!autz_ctx->handle) RETURN_MODULE_FAIL; - /* - * Only do this if *some* check pairs were returned - */ - RDEBUG2("User found in radcheck table"); - user_found = true; + request_data_add(request, (void *)sql_escape_uctx_alloc, 0, autz_ctx->handle, false, false, false); - if (rows > 0) { - map_t *map, *next; - - for (map = map_list_head(&check_tmp); - map != NULL; - map = next) { - next = map_list_next(&check_tmp, map); - - if (fr_assignment_op[map->op]) { - (void) map_list_remove(&check_tmp, map); - map_list_insert_tail(&reply_tmp, map); - continue; - } - - if (!fr_comparison_op[map->op]) { - REDEBUG("Invalid operator '%s'", fr_tokens[map->op]); - goto fail; - } - - if (fr_type_is_structural(tmpl_attr_tail_da(map->lhs)->type) && - (map->op != T_OP_CMP_TRUE) && (map->op != T_OP_CMP_FALSE)) { - REDEBUG("Invalid comparison for structural type"); - goto fail; - } - - RDEBUG2(" &%s %s %s", map->lhs->name, fr_tokens[map->op], map->rhs->name); - if (radius_legacy_map_cmp(request, map) != 1) { - fail: - map_list_talloc_free(&check_tmp); - map_list_talloc_free(&reply_tmp); - RDEBUG2("failed match: skipping this entry"); - goto skip_reply; - } - } - - RDEBUG2("Conditional check items matched"); - } else { - RDEBUG2("Conditional check items matched (empty)"); - } - - rcode = RLM_MODULE_OK; - map_list_talloc_free(&check_tmp); - } - - if (inst->config.authorize_reply_query) { - /* - * Now get the reply pairs since the paircmp matched - */ - if (xlat_aeval(request, &expanded, request, inst->config.authorize_reply_query, - inst->sql_escape_func, handle) < 0) { - REDEBUG("Error generating query"); - rcode = RLM_MODULE_FAIL; - goto error; - } - - rows = sql_get_map_list(request->reply_ctx, inst, request, &handle, &reply_tmp, expanded, request_attr_reply); - TALLOC_FREE(expanded); - if (rows < 0) { - REDEBUG("SQL query error getting reply attributes"); - rcode = RLM_MODULE_FAIL; - goto error; - } - - if (rows == 0) goto skip_reply; - - do_fall_through = fall_through(&reply_tmp); - - RDEBUG2("User found in radreply table"); - user_found = true; + if (unlang_function_push(request, NULL, mod_authorize_resume, NULL, 0, + UNLANG_SUB_FRAME, autz_ctx) < 0) { + error: + talloc_free(autz_ctx); + RETURN_MODULE_FAIL; } -skip_reply: - if (map_list_num_elements(&reply_tmp)) { - RDEBUG2("Merging control and reply items"); - if (radius_legacy_map_list_apply(request, &reply_tmp, NULL) < 0) { - RPEDEBUG("Failed applying item"); - map_list_talloc_free(&reply_tmp); - rcode = RLM_MODULE_FAIL; - goto error; - } - - rcode = RLM_MODULE_OK; - map_list_talloc_free(&reply_tmp); - } + fr_value_box_list_init(&autz_ctx->query); /* - * group checks require a group membership query. + * Query the check table to find any conditions associated with this user/realm/whatever... */ - if (!inst->config.groupmemb_query) goto release; - - if ((do_fall_through == FALL_THROUGH_YES) || - (inst->config.read_groups && (do_fall_through == FALL_THROUGH_DEFAULT))) { - rlm_rcode_t ret; - - RDEBUG3("... falling-through to group processing"); - rlm_sql_process_groups(&ret, inst, request, &handle, &do_fall_through); - switch (ret) { - - /* - * Nothing bad happened, continue... - */ - case RLM_MODULE_UPDATED: - rcode = RLM_MODULE_UPDATED; - FALL_THROUGH; - - case RLM_MODULE_OK: - if (rcode != RLM_MODULE_UPDATED) rcode = RLM_MODULE_OK; - FALL_THROUGH; - - case RLM_MODULE_NOOP: - user_found = true; - break; - - case RLM_MODULE_NOTFOUND: - break; + if (call_env->check_query) { + if (unlang_tmpl_push(autz_ctx, &autz_ctx->query, request, call_env->check_query, NULL) < 0) goto error; + autz_ctx->status = SQL_AUTZ_CHECK; + return UNLANG_ACTION_PUSHED_CHILD; + } - default: - rcode = ret; - goto release; - } + if (call_env->reply_query) { + if (unlang_tmpl_push(autz_ctx, &autz_ctx->query, request, call_env->reply_query, NULL) < 0) goto error; + autz_ctx->status = SQL_AUTZ_REPLY; + return UNLANG_ACTION_PUSHED_CHILD; } /* - * At this point the key (user) hasn't be found in the check table, the reply table - * or the group mapping table. + * Neither check nor reply queries were set, so we must be doing group stuff */ -release: - if (!user_found) rcode = RLM_MODULE_NOTFOUND; - - fr_pool_connection_release(inst->pool, request, handle); - sql_unset_user(inst, request); - - RETURN_MODULE_RCODE(rcode); + if (unlang_tmpl_push(autz_ctx, &autz_ctx->query, request, call_env->membership_query, NULL) < 0) goto error; + autz_ctx->status = SQL_AUTZ_GROUP_MEMB; + return UNLANG_ACTION_PUSHED_CHILD; } /* @@ -1567,7 +1684,7 @@ static int mod_bootstrap(module_inst_ctx_t const *mctx) /* * Register the group comparison attribute */ - if (inst->config.groupmemb_query) { + if (cf_pair_find(conf, "group_membership_query")) { char const *group_attribute; fr_dict_attr_flags_t flags = {}; char buffer[256]; @@ -1667,12 +1784,12 @@ static int mod_instantiate(module_inst_ctx_t const *mctx) * Or we need group_membership_query if authorize_group_check_query or * authorize_group_reply_query is set. */ - if (!inst->config.groupmemb_query) { - if (inst->config.authorize_group_check_query) { + if (!cf_pair_find(conf, "group_membership_query")) { + if (cf_pair_find(conf, "authorize_group_check_query")) { WARN("Ignoring authorize_group_check_query as group_membership_query is not configured"); } - if (inst->config.authorize_group_reply_query) { + if (cf_pair_find(conf, "authorize_group_reply_query")) { WARN("Ignoring authorize_group_reply_query as group_membership_query is not configured"); } diff --git a/src/modules/rlm_sql/rlm_sql.h b/src/modules/rlm_sql/rlm_sql.h index dc5b7aacc85..783f4622dc3 100644 --- a/src/modules/rlm_sql/rlm_sql.h +++ b/src/modules/rlm_sql/rlm_sql.h @@ -94,12 +94,6 @@ typedef struct { char const *group_attribute; //!< Name of the group attribute. - char const *authorize_check_query; //!< Query used get check VPs for a user. - char const *authorize_reply_query; //!< Query used get reply VPs for a user. - char const *authorize_group_check_query; //!< Query used get check VPs for a group. - char const *authorize_group_reply_query; //!< Query used get reply VPs for a group. - char const *groupmemb_query; //!< Query to determine group membership. - bool cache_groups; //!< cache group names in &control.SQL-Group bool read_groups; //!< Read user groups by default. diff --git a/src/tests/modules/sql/groups_no_fall_through.unlang b/src/tests/modules/sql/groups_no_fall_through.unlang index 7be243e84ec..f49f9a327ab 100644 --- a/src/tests/modules/sql/groups_no_fall_through.unlang +++ b/src/tests/modules/sql/groups_no_fall_through.unlang @@ -25,7 +25,7 @@ string groupname # Run SQL # sql -if (!ok) { +if (!updated) { test_fail }