dsdb: Remember the last ACL we read during a search and what it expanded to
[samba.git] / source4 / dsdb / samdb / ldb_modules / acl_read.c
index e2a2d4cb9ae01a2faef2dd90a3c7e3dad537d463..3c9cf7c0672c945dd5d7e5484b5d9196baa1cf62 100644 (file)
@@ -44,14 +44,24 @@ struct aclread_context {
        struct ldb_request *req;
        const char * const *attrs;
        const struct dsdb_schema *schema;
-       bool sd;
-       bool instance_type;
-       bool object_sid;
+       uint32_t sd_flags;
+       bool added_nTSecurityDescriptor;
+       bool added_instanceType;
+       bool added_objectSid;
+       bool added_objectClass;
        bool indirsync;
+
+       /* cache on the last parent we checked in this search */
+       struct ldb_dn *last_parent_dn;
+       int last_parent_check_ret;
 };
 
 struct aclread_private {
        bool enabled;
+
+       /* cache of the last SD we read during any search */
+       struct security_descriptor *sd_cached;
+       struct ldb_val sd_cached_blob;
 };
 
 static void aclread_mark_inaccesslible(struct ldb_message_element *el) {
@@ -62,6 +72,162 @@ static bool aclread_is_inaccessible(struct ldb_message_element *el) {
        return el->flags & LDB_FLAG_INTERNAL_INACCESSIBLE_ATTRIBUTE;
 }
 
+/*
+ * the object has a parent, so we have to check for visibility
+ *
+ * This helper function uses a per-search cache to avoid checking the
+ * parent object for each of many possible children.  This is likely
+ * to help on SCOPE_ONE searches and on typical tree structures for
+ * SCOPE_SUBTREE, where an OU has many users as children.
+ *
+ * We rely for safety on the DB being locked for reads during the full
+ * search.
+ */
+static int aclread_check_parent(struct aclread_context *ac,
+                               struct ldb_message *msg,
+                               struct ldb_request *req)
+{
+       int ret;
+       struct ldb_dn *parent_dn = NULL;
+
+       /* We may have a cached result from earlier in this search */
+       if (ac->last_parent_dn != NULL) {
+               /*
+                * We try the no-allocation ldb_dn_compare_base()
+                * first however it will not tell parents and
+                * grand-parents apart
+                */
+               int cmp_base = ldb_dn_compare_base(ac->last_parent_dn,
+                                                  msg->dn);
+               if (cmp_base == 0) {
+                       /* Now check if it is a direct parent */
+                       parent_dn = ldb_dn_get_parent(ac, msg->dn);
+                       if (parent_dn == NULL) {
+                               return ldb_oom(ldb_module_get_ctx(ac->module));
+                       }
+                       if (ldb_dn_compare(ac->last_parent_dn,
+                                          parent_dn) == 0) {
+                               TALLOC_FREE(parent_dn);
+
+                               /*
+                                * If we checked the same parent last
+                                * time, then return the cached
+                                * result.
+                                *
+                                * The cache is valid as long as the
+                                * search as the DB is read locked and
+                                * the session_info (connected user)
+                                * is constant.
+                                */
+                               return ac->last_parent_check_ret;
+                       }
+               }
+       }
+
+       {
+               TALLOC_CTX *frame = NULL;
+               frame = talloc_stackframe();
+
+               /*
+                * This may have been set in the block above, don't
+                * re-parse
+                */
+               if (parent_dn == NULL) {
+                       parent_dn = ldb_dn_get_parent(ac, msg->dn);
+                       if (parent_dn == NULL) {
+                               TALLOC_FREE(frame);
+                               return ldb_oom(ldb_module_get_ctx(ac->module));
+                       }
+               }
+               ret = dsdb_module_check_access_on_dn(ac->module,
+                                                    frame,
+                                                    parent_dn,
+                                                    SEC_ADS_LIST,
+                                                    NULL, req);
+               talloc_unlink(ac, ac->last_parent_dn);
+               ac->last_parent_dn = parent_dn;
+               ac->last_parent_check_ret = ret;
+
+               TALLOC_FREE(frame);
+       }
+       return ret;
+}
+
+/*
+ * The sd returned from this function is valid until the next call on
+ * this module context
+ *
+ * This helper function uses a cache on the module private data to
+ * speed up repeated use of the same SD.
+ */
+
+static int aclread_get_sd_from_ldb_message(struct aclread_context *ac,
+                                          struct ldb_message *acl_res,
+                                          struct security_descriptor **sd)
+{
+       struct ldb_message_element *sd_element;
+       struct ldb_context *ldb = ldb_module_get_ctx(ac->module);
+       struct aclread_private *private_data
+               = talloc_get_type(ldb_module_get_private(ac->module),
+                                 struct aclread_private);
+       enum ndr_err_code ndr_err;
+
+       sd_element = ldb_msg_find_element(acl_res, "nTSecurityDescriptor");
+       if (sd_element == NULL) {
+               return ldb_error(ldb, LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS,
+                                "nTSecurityDescriptor is missing");
+       }
+
+       if (sd_element->num_values != 1) {
+               return ldb_operr(ldb);
+       }
+
+       /*
+        * The time spent in ndr_pull_security_descriptor() is quite
+        * expensive, so we check if this is the same binary blob as last
+        * time, and if so return the memory tree from that previous parse.
+        */
+
+       if (private_data->sd_cached != NULL &&
+           private_data->sd_cached_blob.data != NULL &&
+           ldb_val_equal_exact(&sd_element->values[0],
+                               &private_data->sd_cached_blob)) {
+               *sd = private_data->sd_cached;
+               return LDB_SUCCESS;
+       }
+
+       *sd = talloc(private_data, struct security_descriptor);
+       if(!*sd) {
+               return ldb_oom(ldb);
+       }
+       ndr_err = ndr_pull_struct_blob(&sd_element->values[0], *sd, *sd,
+                            (ndr_pull_flags_fn_t)ndr_pull_security_descriptor);
+
+       if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+               TALLOC_FREE(*sd);
+               return ldb_operr(ldb);
+       }
+
+       talloc_unlink(private_data, private_data->sd_cached_blob.data);
+       if (ac->added_nTSecurityDescriptor) {
+               private_data->sd_cached_blob = sd_element->values[0];
+               talloc_steal(private_data, sd_element->values[0].data);
+       } else {
+               private_data->sd_cached_blob = ldb_val_dup(private_data,
+                                                          &sd_element->values[0]);
+               if (private_data->sd_cached_blob.data == NULL) {
+                       TALLOC_FREE(*sd);
+                       return ldb_operr(ldb);
+               }
+       }
+
+       talloc_unlink(private_data, private_data->sd_cached);
+       private_data->sd_cached = *sd;
+
+       return LDB_SUCCESS;
+}
+
+
 static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
 {
        struct ldb_context *ldb;
@@ -70,10 +236,11 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
        struct ldb_message *msg;
        int ret, num_of_attrs = 0;
        unsigned int i, k = 0;
-       struct security_descriptor *sd;
+       struct security_descriptor *sd = NULL;
        struct dom_sid *sid = NULL;
        TALLOC_CTX *tmp_ctx;
        uint32_t instanceType;
+       const struct dsdb_class *objectclass;
 
        ac = talloc_get_type(req->context, struct aclread_context);
        ldb = ldb_module_get_ctx(ac->module);
@@ -88,12 +255,31 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
        switch (ares->type) {
        case LDB_REPLY_ENTRY:
                msg = ares->message;
-               ret = dsdb_get_sd_from_ldb_message(ldb, tmp_ctx, msg, &sd);
-               if (ret != LDB_SUCCESS || sd == NULL ) {
-                       DEBUG(10, ("acl_read: cannot get descriptor\n"));
+               ret = aclread_get_sd_from_ldb_message(ac, msg, &sd);
+               if (ret != LDB_SUCCESS) {
+                       ldb_debug_set(ldb, LDB_DEBUG_FATAL,
+                                     "acl_read: cannot get descriptor of %s: %s\n",
+                                     ldb_dn_get_linearized(msg->dn), ldb_strerror(ret));
+                       ret = LDB_ERR_OPERATIONS_ERROR;
+                       goto fail;
+               } else if (sd == NULL) {
+                       ldb_debug_set(ldb, LDB_DEBUG_FATAL,
+                                     "acl_read: cannot get descriptor of %s (attribute not found)\n",
+                                     ldb_dn_get_linearized(msg->dn));
+                       ret = LDB_ERR_OPERATIONS_ERROR;
+                       goto fail;
+               }
+               /*
+                * Get the most specific structural object class for the ACL check
+                */
+               objectclass = dsdb_get_structural_oc_from_msg(ac->schema, msg);
+               if (objectclass == NULL) {
+                       ldb_asprintf_errstring(ldb, "acl_read: Failed to find a structural class for %s",
+                                              ldb_dn_get_linearized(msg->dn));
                        ret = LDB_ERR_OPERATIONS_ERROR;
                        goto fail;
                }
+
                sid = samdb_result_dom_sid(tmp_ctx, msg, "objectSid");
                /* get the object instance type */
                instanceType = ldb_msg_find_attr_as_uint(msg,
@@ -101,30 +287,33 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
                if (!ldb_dn_is_null(msg->dn) && !(instanceType & INSTANCE_TYPE_IS_NC_HEAD))
                {
                        /* the object has a parent, so we have to check for visibility */
-                       struct ldb_dn *parent_dn = ldb_dn_get_parent(tmp_ctx, msg->dn);
-
-                       ret = dsdb_module_check_access_on_dn(ac->module,
-                                                            tmp_ctx,
-                                                            parent_dn,
-                                                            SEC_ADS_LIST,
-                                                            NULL, req);
+                       ret = aclread_check_parent(ac, msg, req);
+                       
                        if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) {
                                talloc_free(tmp_ctx);
                                return LDB_SUCCESS;
                        } else if (ret != LDB_SUCCESS) {
+                               ldb_debug_set(ldb, LDB_DEBUG_FATAL,
+                                             "acl_read: %s check parent %s - %s\n",
+                                             ldb_dn_get_linearized(msg->dn),
+                                             ldb_strerror(ret),
+                                             ldb_errstring(ldb));
                                goto fail;
                        }
                }
+
                /* for every element in the message check RP */
                for (i=0; i < msg->num_elements; i++) {
                        const struct dsdb_attribute *attr;
-                       bool is_sd, is_objectsid, is_instancetype;
+                       bool is_sd, is_objectsid, is_instancetype, is_objectclass;
                        uint32_t access_mask;
                        attr = dsdb_attribute_by_lDAPDisplayName(ac->schema,
                                                                 msg->elements[i].name);
                        if (!attr) {
-                               DEBUG(2, ("acl_read: cannot find attribute %s in schema\n",
-                                          msg->elements[i].name));
+                               ldb_debug_set(ldb, LDB_DEBUG_FATAL,
+                                             "acl_read: %s cannot find attr[%s] in of schema\n",
+                                             ldb_dn_get_linearized(msg->dn),
+                                             msg->elements[i].name);
                                ret = LDB_ERR_OPERATIONS_ERROR;
                                goto fail;
                        }
@@ -134,22 +323,38 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
                                                    msg->elements[i].name) == 0;
                        is_instancetype = ldb_attr_cmp("instanceType",
                                                       msg->elements[i].name) == 0;
+                       is_objectclass = ldb_attr_cmp("objectClass",
+                                                     msg->elements[i].name) == 0;
                        /* these attributes were added to perform access checks and must be removed */
-                       if (is_objectsid && ac->object_sid) {
+                       if (is_objectsid && ac->added_objectSid) {
+                               aclread_mark_inaccesslible(&msg->elements[i]);
+                               continue;
+                       }
+                       if (is_instancetype && ac->added_instanceType) {
                                aclread_mark_inaccesslible(&msg->elements[i]);
                                continue;
                        }
-                       if (is_instancetype && ac->instance_type) {
+                       if (is_objectclass && ac->added_objectClass) {
                                aclread_mark_inaccesslible(&msg->elements[i]);
                                continue;
                        }
-                       if (is_sd && ac->sd) {
+                       if (is_sd && ac->added_nTSecurityDescriptor) {
                                aclread_mark_inaccesslible(&msg->elements[i]);
                                continue;
                        }
                        /* nTSecurityDescriptor is a special case */
                        if (is_sd) {
-                               access_mask = SEC_FLAG_SYSTEM_SECURITY|SEC_STD_READ_CONTROL;
+                               access_mask = 0;
+
+                               if (ac->sd_flags & (SECINFO_OWNER|SECINFO_GROUP)) {
+                                       access_mask |= SEC_STD_READ_CONTROL;
+                               }
+                               if (ac->sd_flags & SECINFO_DACL) {
+                                       access_mask |= SEC_STD_READ_CONTROL;
+                               }
+                               if (ac->sd_flags & SECINFO_SACL) {
+                                       access_mask |= SEC_FLAG_SYSTEM_SECURITY;
+                               }
                        } else {
                                access_mask = SEC_ADS_READ_PROP;
                        }
@@ -158,12 +363,18 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
                                access_mask |= SEC_ADS_CONTROL_ACCESS;
                        }
 
+                       if (access_mask == 0) {
+                               aclread_mark_inaccesslible(&msg->elements[i]);
+                               continue;
+                       }
+
                        ret = acl_check_access_on_attribute(ac->module,
                                                            tmp_ctx,
                                                            sd,
                                                            sid,
                                                            access_mask,
-                                                           attr);
+                                                           attr,
+                                                           objectclass);
 
                        /*
                         * Dirsync control needs the replpropertymetadata attribute
@@ -200,6 +411,12 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
                                        }
                                }
                        } else if (ret != LDB_SUCCESS) {
+                               ldb_debug_set(ldb, LDB_DEBUG_FATAL,
+                                             "acl_read: %s check attr[%s] gives %s - %s\n",
+                                             ldb_dn_get_linearized(msg->dn),
+                                             msg->elements[i].name,
+                                             ldb_strerror(ret),
+                                             ldb_errstring(ldb));
                                goto fail;
                        }
                }
@@ -229,6 +446,11 @@ static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares)
                                        k++;
                                }
                        }
+                       /*
+                        * This should not be needed, but some modules
+                        * may allocate values on the wrong context...
+                        */
+                       talloc_steal(ret_msg->elements, msg);
                } else {
                        ret_msg->elements = NULL;
                }
@@ -259,7 +481,11 @@ static int aclread_search(struct ldb_module *module, struct ldb_request *req)
        uint32_t flags = ldb_req_get_custom_flags(req);
        struct ldb_result *res;
        struct aclread_private *p;
+       bool need_sd = false;
+       bool explicit_sd_flags = false;
        bool is_untrusted = ldb_req_is_untrusted(req);
+       static const char * const _all_attrs[] = { "*", NULL };
+       bool all_attrs = false;
        const char * const *attrs = NULL;
        uint32_t instanceType;
        static const char *acl_attrs[] = {
@@ -287,7 +513,9 @@ static int aclread_search(struct ldb_module *module, struct ldb_request *req)
                ret = dsdb_module_search_dn(module, req, &res, req->op.search.base,
                                            acl_attrs,
                                            DSDB_FLAG_NEXT_MODULE |
-                                           DSDB_SEARCH_SHOW_DELETED, req);
+                                           DSDB_FLAG_AS_SYSTEM |
+                                           DSDB_SEARCH_SHOW_RECYCLED,
+                                           req);
                if (ret != LDB_SUCCESS) {
                        return ldb_error(ldb, ret,
                                        "acl_read: Error retrieving instanceType for base.");
@@ -325,33 +553,65 @@ static int aclread_search(struct ldb_module *module, struct ldb_request *req)
        if (!ac->schema) {
                return ldb_operr(ldb);
        }
+
+       attrs = req->op.search.attrs;
+       if (attrs == NULL) {
+               all_attrs = true;
+               attrs = _all_attrs;
+       } else if (attrs[0] == NULL) {
+               all_attrs = true;
+               attrs = _all_attrs;
+       } else if (ldb_attr_in_list(attrs, "*")) {
+               all_attrs = true;
+       }
+
        /*
         * In theory we should also check for the SD control but control verification is
         * expensive so we'd better had the ntsecuritydescriptor to the list of
         * searched attribute and then remove it !
         */
-       ac->sd = !(ldb_attr_in_list(req->op.search.attrs, "nTSecurityDescriptor"));
-       if (req->op.search.attrs && !ldb_attr_in_list(req->op.search.attrs, "*")) {
-               if (!ldb_attr_in_list(req->op.search.attrs, "instanceType")) {
-                       ac->instance_type = true;
-                       attrs = ldb_attr_list_copy_add(ac, req->op.search.attrs, "instanceType");
-               } else {
-                       attrs = req->op.search.attrs;
+       ac->sd_flags = dsdb_request_sd_flags(ac->req, &explicit_sd_flags);
+
+       if (ldb_attr_in_list(attrs, "nTSecurityDescriptor")) {
+               need_sd = false;
+       } else if (explicit_sd_flags && all_attrs) {
+               need_sd = false;
+       } else {
+               need_sd = true;
+       }
+
+       if (!all_attrs) {
+               if (!ldb_attr_in_list(attrs, "instanceType")) {
+                       attrs = ldb_attr_list_copy_add(ac, attrs, "instanceType");
+                       if (attrs == NULL) {
+                               return ldb_oom(ldb);
+                       }
+                       ac->added_instanceType = true;
                }
                if (!ldb_attr_in_list(req->op.search.attrs, "objectSid")) {
-                       ac->object_sid = true;
                        attrs = ldb_attr_list_copy_add(ac, attrs, "objectSid");
+                       if (attrs == NULL) {
+                               return ldb_oom(ldb);
+                       }
+                       ac->added_objectSid = true;
+               }
+               if (!ldb_attr_in_list(req->op.search.attrs, "objectClass")) {
+                       attrs = ldb_attr_list_copy_add(ac, attrs, "objectClass");
+                       if (attrs == NULL) {
+                               return ldb_oom(ldb);
+                       }
+                       ac->added_objectClass = true;
                }
        }
 
-       if (ac->sd) {
-               /* avoid replacing all attributes with nTSecurityDescriptor
-                * if attribute list is empty */
-               if (!attrs) {
-                       attrs = ldb_attr_list_copy_add(ac, req->op.search.attrs, "*");
-               }
+       if (need_sd) {
                attrs = ldb_attr_list_copy_add(ac, attrs, "nTSecurityDescriptor");
+               if (attrs == NULL) {
+                       return ldb_oom(ldb);
+               }
+               ac->added_nTSecurityDescriptor = true;
        }
+
        ac->attrs = req->op.search.attrs;
        ret = ldb_build_search_req_ex(&down_req,
                                      ldb, ac,
@@ -377,7 +637,7 @@ static int aclread_init(struct ldb_module *module)
        if (p == NULL) {
                return ldb_module_oom(module);
        }
-       p->enabled = lpcfg_parm_bool(ldb_get_opaque(ldb, "loadparm"), NULL, "acl", "search", false);
+       p->enabled = lpcfg_parm_bool(ldb_get_opaque(ldb, "loadparm"), NULL, "acl", "search", true);
        ldb_module_set_private(module, p);
        return ldb_next_init(module);
 }