]> git.ipfire.org Git - thirdparty/freeradius-server.git/commitdiff
Rework rlm_ldap_check_userobj_dynamic to be async
authorNick Porter <nick@portercomputing.co.uk>
Fri, 28 Apr 2023 13:48:22 +0000 (14:48 +0100)
committerArran Cudbard-Bell <a.cudbardb@freeradius.org>
Fri, 19 May 2023 19:35:55 +0000 (15:35 -0400)
src/modules/rlm_ldap/groups.c
src/modules/rlm_ldap/rlm_ldap.h

index 21aa550fb67bef57e81b5bb0be138bec38b96d96..c38ec52b5d351fe8bbbbf0dad3fb256eaf69f7c6 100644 (file)
@@ -67,6 +67,21 @@ typedef struct {
        void                    *uctx;                                  //!< Optional context for use in results parsing.
 } ldap_group_groupobj_ctx_t;
 
+/** Context to use when evaluating group membership from the user object in an xlat
+ *
+ */
+typedef struct {
+       ldap_memberof_xlat_ctx_t        *xlat_ctx;              //!< Xlat context being evaluated.
+       char const                      *attrs[2];              //!< For retrieving the group name.
+       struct berval                   **values;               //!< Values of the membership attribute to check.
+       int                             count;                  //!< How many entries there are in values.
+       int                             value_no;               //!< The current entry in values being processed.
+       char const                      *lookup_dn;             //!< The DN currently being looked up, when resolving DN to name.
+       char                            *group_name;            //!< Result of resolving the provided group DN as to a name.
+       fr_ldap_query_t                 *query;                 //!< Current query doing a DN to name resolution.
+       bool                            resolving_value;        //!< Is the current query resolving a DN from values.
+} ldap_group_userobj_dyn_ctx_t;
+
 /** Cancel a pending group lookup query
  *
  */
@@ -893,115 +908,198 @@ unlang_action_t rlm_ldap_check_groupobj_dynamic(rlm_rcode_t *p_result, request_t
        return UNLANG_ACTION_PUSHED_CHILD;
 }
 
-/** Query the LDAP directory to check if a user object is a member of a group
+/** Initiate resolving a group DN to its name
  *
- * @param[out] p_result                Result of calling the module.
- * @param[in] inst             rlm_ldap configuration.
- * @param[in] request          Current request.
- * @param[in] ttrunk           to use.
- * @param[in] dn               of user object.
- * @param[in] check            vp containing the group value (name or dn).
  */
-unlang_action_t rlm_ldap_check_userobj_dynamic(rlm_rcode_t *p_result, rlm_ldap_t const *inst, request_t *request,
-                                              fr_ldap_thread_trunk_t *ttrunk,
-                                              char const *dn, fr_pair_t const *check)
+static unlang_action_t ldap_dn2name_start (rlm_rcode_t *p_result, UNUSED int *priority, request_t *request, void *uctx)
 {
-       rlm_rcode_t     rcode = RLM_MODULE_NOTFOUND, ret;
-       bool            name_is_dn = false, value_is_dn = false;
-       fr_ldap_query_t *query;
+       ldap_group_userobj_dyn_ctx_t    *group_ctx = talloc_get_type_abort(uctx, ldap_group_userobj_dyn_ctx_t);
+       ldap_memberof_xlat_ctx_t        *xlat_ctx = group_ctx->xlat_ctx;
+       rlm_ldap_t const                *inst = xlat_ctx->inst;
 
-       LDAPMessage     *entry = NULL;
-       struct berval   **values = NULL;
+       if (!inst->groupobj_name_attr) {
+               REDEBUG("Told to resolve group DN to name but missing 'group.name_attribute' directive");
+               RETURN_MODULE_INVALID;
+       }
 
-       char const      *attrs[] = { inst->userobj_membership_attr, NULL };
-       int             i, count, ldap_errno;
+       RDEBUG2("Resolving group DN \"%pV\" to group name", fr_box_strvalue_buffer(group_ctx->lookup_dn));
 
-       RDEBUG2("Checking user object's %s attributes", inst->userobj_membership_attr);
-       RINDENT();
-       if (fr_ldap_trunk_search(&rcode,
-                                unlang_interpret_frame_talloc_ctx(request), &query, request, ttrunk, dn,
-                                LDAP_SCOPE_BASE, NULL, attrs, NULL, NULL, false) < 0) {
-               REXDENT();
-               goto finish;
-       }
-       REXDENT();
-       switch (rcode) {
-       case RLM_MODULE_OK:
-               break;
+       return fr_ldap_trunk_search(p_result, group_ctx, &group_ctx->query, request, xlat_ctx->ttrunk,
+                                   group_ctx->lookup_dn, LDAP_SCOPE_BASE, NULL, group_ctx->attrs,
+                                   NULL, NULL, true);
+}
 
-       case RLM_MODULE_NOTFOUND:
-               RDEBUG2("Can't check membership attributes, user object not found");
+/** Cancel an in-progress DN to name lookup.
+ *
+ */
+static void ldap_dn2name_cancel(UNUSED request_t *request, UNUSED fr_signal_t action, void *uctx)
+{
+       ldap_group_userobj_dyn_ctx_t    *group_ctx = talloc_get_type_abort(uctx, ldap_group_userobj_dyn_ctx_t);
 
-               FALL_THROUGH;
-       default:
-               goto finish;
-       }
+       if (!group_ctx->query || !group_ctx->query->treq) return;
 
-       entry = ldap_first_entry(query->ldap_conn->handle, query->result);
-       if (!entry) {
-               ldap_get_option(query->ldap_conn->handle, LDAP_OPT_RESULT_CODE, &ldap_errno);
-               REDEBUG("Failed retrieving entry: %s", ldap_err2string(ldap_errno));
+       fr_trunk_request_signal_cancel(group_ctx->query->treq);
+}
 
-               rcode = RLM_MODULE_FAIL;
+/** Initiate a user lookup to check membership.
+ *
+ * Used when the user's DN is already known but cached group membership has not been stored
+ *
+ */
+static unlang_action_t ldap_check_userobj_start(UNUSED rlm_rcode_t *p_result, UNUSED int *priority,
+                                               request_t *request, void *uctx)
+{
+       ldap_group_userobj_dyn_ctx_t    *group_ctx = talloc_get_type_abort(uctx, ldap_group_userobj_dyn_ctx_t);
+       ldap_memberof_xlat_ctx_t        *xlat_ctx = talloc_get_type_abort(group_ctx->xlat_ctx, ldap_memberof_xlat_ctx_t);
 
-               goto finish;
+       return fr_ldap_trunk_search(p_result, xlat_ctx, &xlat_ctx->query, request, xlat_ctx->ttrunk, xlat_ctx->dn,
+                                   LDAP_SCOPE_BASE, NULL, xlat_ctx->attrs, NULL, NULL, true);
+}
+
+/** Process the results of evaluating a user object when checking group membership
+ *
+ */
+static unlang_action_t ldap_check_userobj_resume(rlm_rcode_t *p_result, UNUSED int *priority,
+                                                request_t *request, void *uctx)
+{
+       ldap_group_userobj_dyn_ctx_t    *group_ctx = talloc_get_type_abort(uctx, ldap_group_userobj_dyn_ctx_t);
+       ldap_memberof_xlat_ctx_t        *xlat_ctx = talloc_get_type_abort(group_ctx->xlat_ctx, ldap_memberof_xlat_ctx_t);
+       rlm_ldap_t const                *inst = xlat_ctx->inst;
+       fr_ldap_query_t                 *query = xlat_ctx->query;
+       LDAPMessage                     *entry;
+       int                             ldap_errno;
+       bool                            value_is_dn = false;
+       fr_value_box_t                  *group = xlat_ctx->group;
+       char                            *value_name = NULL;
+
+       /*
+        *      If group_ctx->values is not populated, this is the first call
+        *      - extract the returned values if any.
+        */
+       if (!group_ctx->values) {
+               entry = ldap_first_entry(query->ldap_conn->handle, query->result);
+               if (!entry) {
+                       ldap_get_option(query->ldap_conn->handle, LDAP_OPT_RESULT_CODE, &ldap_errno);
+                       REDEBUG("Failed retrieving entry: %s", ldap_err2string(ldap_errno));
+                       RETURN_MODULE_FAIL;
+               }
+
+               group_ctx->values = ldap_get_values_len(query->ldap_conn->handle, entry, inst->userobj_membership_attr);
+               if (!group_ctx->values) {
+                       RDEBUG2("No group membership attribute(s) found in user object");
+                       RETURN_MODULE_FAIL;
+               }
+
+               /*
+                *      To avoid re-assessing after each call out to do a DN -> name
+                *      lookup, cache this.
+                */
+               group_ctx->count = ldap_count_values_len(group_ctx->values);
        }
 
-       values = ldap_get_values_len(query->ldap_conn->handle, entry, inst->userobj_membership_attr);
-       if (!values) {
-               RDEBUG2("No group membership attribute(s) found in user object");
+       /*
+        *      Following a call out to do a DN -> name lookup, group_ctx->query will be
+        *      populated - process the results.
+        */
+       if (group_ctx->query) {
+               char            *buff;
+               struct berval   **values = NULL;
+
+               switch (group_ctx->query->ret) {
+               case LDAP_RESULT_SUCCESS:
+                       break;
+
+               case LDAP_RESULT_NO_RESULT:
+               case LDAP_RESULT_BAD_DN:
+                       REDEBUG("Group DN \"%pV\" did not resolve to an object",
+                               fr_box_strvalue_buffer(group_ctx->lookup_dn));
+                       RETURN_MODULE_INVALID;
 
-               goto finish;
+               default:
+                       RETURN_MODULE_FAIL;
+               }
+
+               entry = ldap_first_entry(group_ctx->query->ldap_conn->handle, group_ctx->query->result);
+               if (!entry) {
+                       ldap_get_option(group_ctx->query->ldap_conn->handle, LDAP_OPT_RESULT_CODE, &ldap_errno);
+                       REDEBUG("Failed retrieving entry: %s", ldap_err2string(ldap_errno));
+                       RETURN_MODULE_INVALID;
+               }
+
+               values = ldap_get_values_len(group_ctx->query->ldap_conn->handle, entry, inst->groupobj_name_attr);
+               if (!values) {
+                       REDEBUG("No %s attributes found in object", inst->groupobj_name_attr);
+                       RETURN_MODULE_INVALID;
+               }
+
+               MEM(buff = talloc_bstrndup(group_ctx, values[0]->bv_val, values[0]->bv_len));
+               RDEBUG2("Group DN \"%pV\" resolves to name \"%pV\"", fr_box_strvalue_buffer(group_ctx->lookup_dn),
+                       fr_box_strvalue_len(values[0]->bv_val, values[0]->bv_len));
+               ldap_value_free_len(values);
+
+               if (group_ctx->resolving_value) {
+                       value_name = buff;
+               } else {
+                       group_ctx->group_name = buff;
+               }
        }
 
        /*
-        *      Loop over the list of groups the user is a member of,
-        *      looking for a match.
+        *      Loop over the list of groups the user is a member of, looking for a match.
         */
-       name_is_dn = fr_ldap_util_is_dn(check->vp_strvalue, check->vp_length);
-       count = ldap_count_values_len(values);
-       for (i = 0; i < count; i++) {
-               value_is_dn = fr_ldap_util_is_dn(values[i]->bv_val, values[i]->bv_len);
+       while (group_ctx->value_no < group_ctx->count) {
+               struct berval   *value = group_ctx->values[group_ctx->value_no];
+
+               /*
+                *      We have come back from resolving a membership DN to its name,
+                *      compare to the provided name.
+                */
+               if (value_name && group_ctx->resolving_value) {
+                       if (((talloc_array_length(value_name) - 1) == group->vb_length) &&
+                           (memcmp(group->vb_strvalue, value_name, group->vb_length) == 0)) {
+                               RDEBUG2("User found in group \"%pV\". Comparison between membership: name "
+                                      "(resolved from DN \"%pV\"), check: name", group,
+                                      fr_box_strvalue_buffer(group_ctx->lookup_dn));
+                               talloc_free(value_name);
+                               goto found;
+                       }
+                       talloc_const_free(group_ctx->lookup_dn);
+                       TALLOC_FREE(value_name);
+                       group_ctx->resolving_value = false;
+                       group_ctx->value_no++;
+                       continue;
+               }
+
+               value_is_dn = fr_ldap_util_is_dn(value->bv_val, value->bv_len);
 
                RDEBUG2("Processing %s value \"%pV\" as a %s", inst->userobj_membership_attr,
-                       fr_box_strvalue_len(values[i]->bv_val, values[i]->bv_len),
+                       fr_box_strvalue_len(value->bv_val, value->bv_len),
                        value_is_dn ? "DN" : "group name");
 
                /*
                 *      Both literal group names, do case sensitive comparison
                 */
-               if (!name_is_dn && !value_is_dn) {
-                       if ((check->vp_length == values[i]->bv_len) &&
-                           (memcmp(values[i]->bv_val, check->vp_strvalue, values[i]->bv_len) == 0)) {
-                               RDEBUG2("User found in group \"%s\". Comparison between membership: name, check: name",
-                                      check->vp_strvalue);
-                               rcode = RLM_MODULE_OK;
-
-                               goto finish;
+               if (!xlat_ctx->group_is_dn && !value_is_dn) {
+                       if ((group->vb_length == value->bv_len) &&
+                           (memcmp(value->bv_val, group->vb_strvalue, value->bv_len) == 0)) {
+                               RDEBUG2("User found in group \"%pV\". Comparison between membership: name, check: name",
+                                      group);
+                               goto found;
                        }
-
+                       group_ctx->value_no++;
                        continue;
                }
 
                /*
                 *      Both DNs, do case insensitive, binary safe comparison
                 */
-               if (name_is_dn && value_is_dn) {
-                       if (check->vp_length == values[i]->bv_len) {
-                               int j;
-
-                               for (j = 0; j < (int)values[i]->bv_len; j++) {
-                                       if (tolower(values[i]->bv_val[j]) != tolower(check->vp_strvalue[j])) break;
-                               }
-                               if (j == (int)values[i]->bv_len) {
-                                       RDEBUG2("User found in group DN \"%s\". "
-                                              "Comparison between membership: dn, check: dn", check->vp_strvalue);
-                                       rcode = RLM_MODULE_OK;
-
-                                       goto finish;
-                               }
+               if (xlat_ctx->group_is_dn && value_is_dn) {
+                       if (fr_ldap_berval_strncasecmp(value, group->vb_strvalue, group->vb_length) == 0) {
+                               RDEBUG2("User found in group DN \"%pV\". "
+                                      "Comparison between membership: dn, check: dn", group);
+                               goto found;
                        }
-
+                       group_ctx->value_no++;
                        continue;
                }
 
@@ -1009,33 +1107,28 @@ unlang_action_t rlm_ldap_check_userobj_dynamic(rlm_rcode_t *p_result, rlm_ldap_t
                 *      If the value is not a DN, and the name we were given is a dn
                 *      convert the value to a DN and do a comparison.
                 */
-               if (!value_is_dn && name_is_dn) {
-                       char *resolved;
-                       bool eq = false;
-
-                       RINDENT();
-                       rlm_ldap_group_dn2name(&ret, inst, request, ttrunk, check->vp_strvalue, &resolved);
-                       REXDENT();
+               if (!value_is_dn && xlat_ctx->group_is_dn) {
+                       /*
+                        *      So we only do the DN -> name lookup once, regardless of how many
+                        *      group values we have to check, the resolved name is put in group_ctx->group_name
+                        */
+                       if (!group_ctx->group_name) {
+                               group_ctx->lookup_dn = group->vb_strvalue;
 
-                       if (ret == RLM_MODULE_NOOP) continue;
+                               if (unlang_function_repeat_set(request, ldap_check_userobj_resume) < 0) RETURN_MODULE_FAIL;
 
-                       if (ret != RLM_MODULE_OK) {
-                               rcode = ret;
-                               goto finish;
+                               return unlang_function_push(request, ldap_dn2name_start, NULL, ldap_dn2name_cancel,
+                                                           ~FR_SIGNAL_CANCEL, UNLANG_SUB_FRAME, group_ctx);
                        }
 
-                       if (((talloc_array_length(resolved) - 1) == values[i]->bv_len) &&
-                           (memcmp(values[i]->bv_val, resolved, values[i]->bv_len) == 0)) eq = true;
-                       talloc_free(resolved);
-                       if (eq) {
-                               RDEBUG2("User found in group \"%pV\". Comparison between membership: name, check: name "
-                                      "(resolved from DN \"%s\")",
-                                      fr_box_strvalue_len(values[i]->bv_val, values[i]->bv_len), check->vp_strvalue);
-                               rcode = RLM_MODULE_OK;
-
-                               goto finish;
+                       if (((talloc_array_length(group_ctx->group_name) - 1) == value->bv_len) &&
+                           (memcmp(value->bv_val, group_ctx->group_name, value->bv_len) == 0)) {
+                               RDEBUG2("User found in group \"%pV\". Comparison between membership: "
+                                       "name, check: name (resolved from DN \"%pV\")",
+                                       fr_box_strvalue_len(value->bv_val, value->bv_len), group);
+                               goto found;
                        }
-
+                       group_ctx->value_no++;
                        continue;
                }
 
@@ -1043,44 +1136,69 @@ unlang_action_t rlm_ldap_check_userobj_dynamic(rlm_rcode_t *p_result, rlm_ldap_t
                 *      We have a value which is a DN, and a check item which specifies the name of a group,
                 *      convert the value to a name so we can do a comparison.
                 */
-               if (value_is_dn && !name_is_dn) {
-                       char *resolved;
-                       char *value;
-                       bool eq = false;
+               if (value_is_dn && !xlat_ctx->group_is_dn) {
+                       group_ctx->lookup_dn = fr_ldap_berval_to_string(group_ctx, value);
+                       group_ctx->resolving_value = true;
 
-                       value = fr_ldap_berval_to_string(request, values[i]);
-                       RINDENT();
-                       rlm_ldap_group_dn2name(&ret, inst, request, ttrunk, value, &resolved);
-                       REXDENT();
-                       talloc_free(value);
+                       if (unlang_function_repeat_set(request, ldap_check_userobj_resume) < 0) RETURN_MODULE_FAIL;
 
-                       if (ret == RLM_MODULE_NOOP) continue;
+                       return unlang_function_push(request, ldap_dn2name_start, NULL, ldap_dn2name_cancel,
+                                                   ~FR_SIGNAL_CANCEL, UNLANG_SUB_FRAME, group_ctx);
+               }
 
-                       if (ret != RLM_MODULE_OK) {
-                               rcode = ret;
-                               goto finish;
-                       }
+               fr_assert(0);
+       }
+       RETURN_MODULE_NOTFOUND;
 
-                       if (((talloc_array_length(resolved) - 1) == check->vp_length) &&
-                           (memcmp(check->vp_strvalue, resolved, check->vp_length) == 0)) eq = true;
-                       talloc_free(resolved);
-                       if (eq) {
-                               RDEBUG2("User found in group \"%pV\". Comparison between membership: name "
-                                      "(resolved from DN \"%s\"), check: name", &check->data, value);
-                               rcode = RLM_MODULE_OK;
+found:
+       xlat_ctx->found = true;
+       RETURN_MODULE_OK;
+}
 
-                               goto finish;
-                       }
+/** Ensure retrieved LDAP values are cleared up
+ *
+ */
+static int userobj_dyn_free(ldap_group_userobj_dyn_ctx_t *group_ctx)
+{
+       if (group_ctx->values) ldap_value_free_len(group_ctx->values);
+       return 0;
+}
 
-                       continue;
-               }
-               fr_assert(0);
-       }
+/** Query the LDAP directory to check if a user object is a member of a group
+ *
+ * @param[out] p_result                Result of calling the module.
+ * @param[in] request          Current request.
+ * @param[in] xlat_ctx         Context of the xlat being evaluated.
+ */
+unlang_action_t rlm_ldap_check_userobj_dynamic(rlm_rcode_t *p_result, request_t *request,
+                                              ldap_memberof_xlat_ctx_t *xlat_ctx)
+{
+       rlm_ldap_t const                *inst = xlat_ctx->inst;
+       ldap_group_userobj_dyn_ctx_t    *group_ctx;
 
-finish:
-       if (values) ldap_value_free_len(values);
+       MEM(group_ctx = talloc(unlang_interpret_frame_talloc_ctx(request), ldap_group_userobj_dyn_ctx_t));
+       talloc_set_destructor(group_ctx, userobj_dyn_free);
 
-       RETURN_MODULE_RCODE(rcode);
+       *group_ctx = (ldap_group_userobj_dyn_ctx_t) {
+               .xlat_ctx = xlat_ctx,
+               .attrs = { inst->groupobj_name_attr, NULL }
+       };
+
+       RDEBUG2("Checking user object's %s attributes", inst->userobj_membership_attr);
+
+       /*
+        *      If a previous query was required to find the user DN, that will have
+        *      retrieved the user object membership attribute and the resulting values
+        *      can be checked.
+        *      If not then a query is needed to retrieve the user object.
+        */
+       if (unlang_function_push(request, xlat_ctx->query ? NULL : ldap_check_userobj_start, ldap_check_userobj_resume,
+                                ldap_group_userobj_cancel, ~FR_SIGNAL_CANCEL, UNLANG_SUB_FRAME, group_ctx) < 0) {
+               talloc_free(group_ctx);
+               RETURN_MODULE_FAIL;
+       }
+
+       return UNLANG_ACTION_PUSHED_CHILD;
 }
 
 /** Check group membership attributes to see if a user is a member.
index dcdf375ada6c429a91e4545a1ff3d646b047234d..e54d67a1484f5c9993d2d00c28f715adcfcd2972 100644 (file)
@@ -253,9 +253,8 @@ unlang_action_t rlm_ldap_cacheable_groupobj(rlm_rcode_t *p_result, request_t *re
 unlang_action_t rlm_ldap_check_groupobj_dynamic(rlm_rcode_t *p_result, request_t *request,
                                                ldap_memberof_xlat_ctx_t *xlat_ctx);
 
-unlang_action_t rlm_ldap_check_userobj_dynamic(rlm_rcode_t *p_result,
-                                              rlm_ldap_t const *inst, request_t *request, fr_ldap_thread_trunk_t *ttrunk,
-                                              char const *dn, fr_pair_t const *check);
+unlang_action_t rlm_ldap_check_userobj_dynamic(rlm_rcode_t *p_result, request_t *request,
+                                              ldap_memberof_xlat_ctx_t *xlat_ctx);
 
 unlang_action_t rlm_ldap_check_cached(rlm_rcode_t *p_result,
                                      rlm_ldap_t const *inst, request_t *request, fr_value_box_t const *check);