diff options
Diffstat (limited to 'source4/dsdb/samdb/ldb_modules/extended_dn_store.c')
-rw-r--r-- | source4/dsdb/samdb/ldb_modules/extended_dn_store.c | 830 |
1 files changed, 830 insertions, 0 deletions
diff --git a/source4/dsdb/samdb/ldb_modules/extended_dn_store.c b/source4/dsdb/samdb/ldb_modules/extended_dn_store.c new file mode 100644 index 0000000..42970da --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/extended_dn_store.c @@ -0,0 +1,830 @@ +/* + ldb database library + + Copyright (C) Simo Sorce 2005-2008 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2007-2009 + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +/* + * Name: ldb + * + * Component: ldb extended dn control module + * + * Description: this module builds a special dn for returned search + * results nad creates the special DN in the backend store for new + * values. + * + * This also has the curious result that we convert <SID=S-1-2-345> + * in an attribute value into a normal DN for the rest of the stack + * to process + * + * Authors: Simo Sorce + * Andrew Bartlett + */ + +#include "includes.h" +#include <ldb.h> +#include <ldb_errors.h> +#include <ldb_module.h> +#include "librpc/gen_ndr/ndr_misc.h" +#include "dsdb/samdb/samdb.h" +#include "libcli/security/security.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include <time.h> + +struct extended_dn_replace_list { + struct extended_dn_replace_list *next; + struct dsdb_dn *dsdb_dn; + TALLOC_CTX *mem_ctx; + struct ldb_val *replace_dn; + struct extended_dn_context *ac; + struct ldb_request *search_req; + bool fpo_enabled; + bool require_object; + bool got_entry; +}; + + +struct extended_dn_context { + const struct dsdb_schema *schema; + struct ldb_module *module; + struct ldb_context *ldb; + struct ldb_request *req; + struct ldb_request *new_req; + + struct extended_dn_replace_list *ops; + struct extended_dn_replace_list *cur; + + /* + * Used by the FPO-enabled attribute validation. + */ + struct dsdb_trust_routing_table *routing_table; +}; + + +static struct extended_dn_context *extended_dn_context_init(struct ldb_module *module, + struct ldb_request *req) +{ + struct extended_dn_context *ac; + struct ldb_context *ldb = ldb_module_get_ctx(module); + ac = talloc_zero(req, struct extended_dn_context); + if (ac == NULL) { + ldb_oom(ldb); + return NULL; + } + + ac->schema = dsdb_get_schema(ldb, ac); + ac->module = module; + ac->ldb = ldb; + ac->req = req; + + return ac; +} + +static int extended_replace_dn(struct extended_dn_replace_list *os, + struct ldb_dn *dn) +{ + struct dsdb_dn *dsdb_dn = NULL; + const char *str = NULL; + + /* + * Rebuild with the string or binary 'extra part' the + * DN may have had as a prefix + */ + dsdb_dn = dsdb_dn_construct(os, dn, + os->dsdb_dn->extra_part, + os->dsdb_dn->oid); + if (dsdb_dn == NULL) { + return ldb_module_operr(os->ac->module); + } + + str = dsdb_dn_get_extended_linearized(os->mem_ctx, + dsdb_dn, 1); + if (str == NULL) { + return ldb_module_operr(os->ac->module); + } + + /* + * Replace the DN with the extended version of the DN + * (ie, add SID and GUID) + */ + *os->replace_dn = data_blob_string_const(str); + os->got_entry = true; + return LDB_SUCCESS; +} + +static int extended_dn_handle_fpo_attr(struct extended_dn_replace_list *os) +{ + struct dom_sid target_sid = { 0, }; + struct dom_sid target_domain = { 0, }; + struct ldb_message *fmsg = NULL; + char *fsid = NULL; + const struct dom_sid *domain_sid = NULL; + struct ldb_dn *domain_dn = NULL; + const struct lsa_TrustDomainInfoInfoEx *tdo = NULL; + uint32_t trust_attributes = 0; + const char *no_attrs[] = { NULL, }; + struct ldb_result *res = NULL; + NTSTATUS status; + bool match; + bool ok; + int ret; + + /* + * DN doesn't exist yet + * + * Check if a foreign SID is specified, + * which would trigger the creation + * of a foreignSecurityPrincipal. + */ + status = dsdb_get_extended_dn_sid(os->dsdb_dn->dn, + &target_sid, + "SID"); + if (NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) { + /* + * No SID specified + */ + return dsdb_module_werror(os->ac->module, + LDB_ERR_NO_SUCH_OBJECT, + WERR_NO_SUCH_USER, + "specified dn doesn't exist"); + } + if (!NT_STATUS_IS_OK(status)) { + return ldb_module_operr(os->ac->module); + } + if (ldb_dn_get_extended_comp_num(os->dsdb_dn->dn) != 1) { + return dsdb_module_werror(os->ac->module, + LDB_ERR_NO_SUCH_OBJECT, + WERR_NO_SUCH_USER, + "specified extended component other than SID"); + } + if (ldb_dn_get_comp_num(os->dsdb_dn->dn) != 0) { + return dsdb_module_werror(os->ac->module, + LDB_ERR_NO_SUCH_OBJECT, + WERR_NO_SUCH_USER, + "specified more the SID"); + } + + target_domain = target_sid; + sid_split_rid(&target_domain, NULL); + + match = dom_sid_equal(&global_sid_Builtin, &target_domain); + if (match) { + /* + * Non existing BUILTIN sid + */ + return dsdb_module_werror(os->ac->module, + LDB_ERR_NO_SUCH_OBJECT, + WERR_NO_SUCH_MEMBER, + "specified sid doesn't exist in BUILTIN"); + } + + domain_sid = samdb_domain_sid(os->ac->ldb); + if (domain_sid == NULL) { + return ldb_module_operr(os->ac->module); + } + match = dom_sid_equal(domain_sid, &target_domain); + if (match) { + /* + * Non existing SID in our domain. + */ + return dsdb_module_werror(os->ac->module, + LDB_ERR_UNWILLING_TO_PERFORM, + WERR_DS_INVALID_GROUP_TYPE, + "specified sid doesn't exist in domain"); + } + + if (os->ac->routing_table == NULL) { + status = dsdb_trust_routing_table_load(os->ac->ldb, os->ac, + &os->ac->routing_table); + if (!NT_STATUS_IS_OK(status)) { + return ldb_module_operr(os->ac->module); + } + } + + tdo = dsdb_trust_domain_by_sid(os->ac->routing_table, + &target_domain, NULL); + if (tdo != NULL) { + trust_attributes = tdo->trust_attributes; + } + + if (trust_attributes & LSA_TRUST_ATTRIBUTE_WITHIN_FOREST) { + return dsdb_module_werror(os->ac->module, + LDB_ERR_UNWILLING_TO_PERFORM, + WERR_DS_INVALID_GROUP_TYPE, + "specified sid doesn't exist in forest"); + } + + fmsg = ldb_msg_new(os); + if (fmsg == NULL) { + return ldb_module_oom(os->ac->module); + } + + fsid = dom_sid_string(fmsg, &target_sid); + if (fsid == NULL) { + return ldb_module_oom(os->ac->module); + } + + domain_dn = ldb_get_default_basedn(os->ac->ldb); + if (domain_dn == NULL) { + return ldb_module_operr(os->ac->module); + } + + fmsg->dn = ldb_dn_copy(fmsg, domain_dn); + if (fmsg->dn == NULL) { + return ldb_module_oom(os->ac->module); + } + + ok = ldb_dn_add_child_fmt(fmsg->dn, + "CN=%s,CN=ForeignSecurityPrincipals", + fsid); + if (!ok) { + return ldb_module_oom(os->ac->module); + } + + ret = ldb_msg_add_string(fmsg, "objectClass", "foreignSecurityPrincipal"); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = dsdb_module_add(os->ac->module, fmsg, + DSDB_FLAG_AS_SYSTEM | + DSDB_FLAG_NEXT_MODULE, + os->ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = dsdb_module_search_dn(os->ac->module, fmsg, &res, + fmsg->dn, no_attrs, + DSDB_FLAG_AS_SYSTEM | + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + os->ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + * dsdb_module_search_dn() guarantees exactly one result message + * on success. + */ + ret = extended_replace_dn(os, res->msgs[0]->dn); + TALLOC_FREE(fmsg); + if (ret != LDB_SUCCESS) { + return ret; + } + + return LDB_SUCCESS; +} + +/* An extra layer of indirection because LDB does not allow the original request to be altered */ + +static int extended_final_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + int ret = LDB_ERR_OPERATIONS_ERROR; + struct extended_dn_context *ac; + ac = talloc_get_type(req->context, struct extended_dn_context); + + if (ares->error != LDB_SUCCESS) { + ret = ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } else { + switch (ares->type) { + case LDB_REPLY_ENTRY: + + ret = ldb_module_send_entry(ac->req, ares->message, ares->controls); + break; + case LDB_REPLY_REFERRAL: + + ret = ldb_module_send_referral(ac->req, ares->referral); + break; + case LDB_REPLY_DONE: + + ret = ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + break; + } + } + return ret; +} + +static int extended_replace_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct extended_dn_replace_list *os = talloc_get_type(req->context, + struct extended_dn_replace_list); + + if (!ares) { + return ldb_module_done(os->ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error == LDB_ERR_NO_SUCH_OBJECT) { + if (os->got_entry) { + /* This is in internal error... */ + int ret = ldb_module_operr(os->ac->module); + return ldb_module_done(os->ac->req, NULL, NULL, ret); + } + + if (os->require_object && os->fpo_enabled) { + int ret; + + ret = extended_dn_handle_fpo_attr(os); + if (ret != LDB_SUCCESS) { + return ldb_module_done(os->ac->req, NULL, NULL, + ret); + } + /* os->got_entry is true at this point... */ + } + + if (!os->got_entry && os->require_object) { + /* + * It's an error if the target doesn't exist, + * unless it's a delete. + */ + int ret = dsdb_module_werror(os->ac->module, + LDB_ERR_CONSTRAINT_VIOLATION, + WERR_DS_NAME_REFERENCE_INVALID, + "Referenced object not found"); + return ldb_module_done(os->ac->req, NULL, NULL, ret); + } + + /* Don't worry too much about dangling references */ + + ldb_reset_err_string(os->ac->ldb); + if (os->next) { + struct extended_dn_replace_list *next; + + next = os->next; + + talloc_free(os); + + os = next; + return ldb_next_request(os->ac->module, next->search_req); + } else { + /* Otherwise, we are done - let's run the + * request now we have swapped the DNs for the + * full versions */ + return ldb_next_request(os->ac->module, os->ac->new_req); + } + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(os->ac->req, ares->controls, + ares->response, ares->error); + } + + /* Only entries are interesting, and we only want the olddn */ + switch (ares->type) { + case LDB_REPLY_ENTRY: + { + /* This *must* be the right DN, as this is a base + * search. We can't check, as it could be an extended + * DN, so a module below will resolve it */ + int ret; + + ret = extended_replace_dn(os, ares->message->dn); + if (ret != LDB_SUCCESS) { + return ldb_module_done(os->ac->req, NULL, NULL, ret); + } + /* os->got_entry is true at this point */ + break; + } + case LDB_REPLY_REFERRAL: + /* ignore */ + break; + + case LDB_REPLY_DONE: + + talloc_free(ares); + + if (!os->got_entry && os->require_object && os->fpo_enabled) { + int ret; + + ret = extended_dn_handle_fpo_attr(os); + if (ret != LDB_SUCCESS) { + return ldb_module_done(os->ac->req, NULL, NULL, + ret); + } + /* os->got_entry is true at this point... */ + } + + if (!os->got_entry && os->require_object) { + /* + * It's an error if the target doesn't exist, + * unless it's a delete. + */ + int ret = dsdb_module_werror(os->ac->module, + LDB_ERR_CONSTRAINT_VIOLATION, + WERR_DS_NAME_REFERENCE_INVALID, + "Referenced object not found"); + return ldb_module_done(os->ac->req, NULL, NULL, ret); + } + + /* Run the next search */ + + if (os->next) { + struct extended_dn_replace_list *next; + + next = os->next; + + talloc_free(os); + + os = next; + return ldb_next_request(os->ac->module, next->search_req); + } else { + /* Otherwise, we are done - let's run the + * request now we have swapped the DNs for the + * full versions */ + return ldb_next_request(os->ac->module, os->ac->new_req); + } + } + + talloc_free(ares); + return LDB_SUCCESS; +} + +/* We have a 'normal' DN in the inbound request. We need to find out + * what the GUID and SID are on the DN it points to, so we can + * construct an extended DN for storage. + * + * This creates a list of DNs to look up, and the plain DN to replace + */ + +static int extended_store_replace(struct extended_dn_context *ac, + TALLOC_CTX *callback_mem_ctx, + struct ldb_dn *self_dn, + struct ldb_val *plain_dn, + bool is_delete, + const struct dsdb_attribute *schema_attr) +{ + const char *oid = schema_attr->syntax->ldap_oid; + int ret; + struct extended_dn_replace_list *os; + static const char *attrs[] = { + "objectSid", + "objectGUID", + NULL + }; + uint32_t ctrl_flags = 0; + bool is_untrusted = ldb_req_is_untrusted(ac->req); + + os = talloc_zero(ac, struct extended_dn_replace_list); + if (!os) { + return ldb_oom(ac->ldb); + } + + os->ac = ac; + + os->mem_ctx = callback_mem_ctx; + + os->dsdb_dn = dsdb_dn_parse(os, ac->ldb, plain_dn, oid); + if (!os->dsdb_dn || !ldb_dn_validate(os->dsdb_dn->dn)) { + talloc_free(os); + ldb_asprintf_errstring(ac->ldb, + "could not parse %.*s as a %s DN", (int)plain_dn->length, plain_dn->data, + oid); + return LDB_ERR_INVALID_DN_SYNTAX; + } + + if (self_dn != NULL) { + ret = ldb_dn_compare(self_dn, os->dsdb_dn->dn); + if (ret == 0) { + /* + * If this is a reference to the object + * itself during an 'add', we won't + * be able to find the object. + */ + talloc_free(os); + return LDB_SUCCESS; + } + } + + if (is_delete && !ldb_dn_has_extended(os->dsdb_dn->dn)) { + /* NO need to figure this DN out, this element is + * going to be deleted anyway, and because it's not + * extended, we have enough information to do the + * delete */ + talloc_free(os); + return LDB_SUCCESS; + } + + + os->replace_dn = plain_dn; + + /* The search request here might happen to be for an + * 'extended' style DN, such as <GUID=abced...>. The next + * module in the stack will convert this into a normal DN for + * processing */ + ret = ldb_build_search_req(&os->search_req, + ac->ldb, os, os->dsdb_dn->dn, LDB_SCOPE_BASE, NULL, + attrs, NULL, os, extended_replace_callback, + ac->req); + LDB_REQ_SET_LOCATION(os->search_req); + if (ret != LDB_SUCCESS) { + talloc_free(os); + return ret; + } + + /* + * By default we require the presence of the target. + */ + os->require_object = true; + + /* + * Handle FPO-enabled attributes, see + * [MS-ADTS] 3.1.1.5.2.3 Special Classes and Attributes: + * + * FPO-enabled attributes: member, msDS-MembersForAzRole, + * msDS-NeverRevealGroup, msDS-NonMembers, msDS-RevealOnDemandGroup, + * msDS-ServiceAccount. + * + * Note there's no msDS-ServiceAccount in any schema (only + * msDS-HostServiceAccount and that's not an FPO-enabled attribute + * at least not in W2008R2) + * + * msDS-NonMembers always generates NOT_SUPPORTED against W2008R2. + * + * See also [MS-SAMR] 3.1.1.8.9 member. + */ + switch (schema_attr->attributeID_id) { + case DRSUAPI_ATTID_member: + case DRSUAPI_ATTID_msDS_MembersForAzRole: + case DRSUAPI_ATTID_msDS_NeverRevealGroup: + case DRSUAPI_ATTID_msDS_RevealOnDemandGroup: + os->fpo_enabled = true; + break; + + case DRSUAPI_ATTID_msDS_HostServiceAccount: + /* This is NOT a FPO-enabled attribute */ + break; + + case DRSUAPI_ATTID_msDS_NonMembers: + return dsdb_module_werror(os->ac->module, + LDB_ERR_UNWILLING_TO_PERFORM, + WERR_NOT_SUPPORTED, + "msDS-NonMembers is not supported"); + } + + if (schema_attr->linkID == 0) { + /* + * None linked attributes allow references + * to deleted objects. + */ + ctrl_flags |= DSDB_SEARCH_SHOW_RECYCLED; + } + + if (is_delete) { + /* + * On delete want to be able to + * find a deleted object, but + * it's not a problem if they doesn't + * exist. + */ + ctrl_flags |= DSDB_SEARCH_SHOW_RECYCLED; + os->require_object = false; + } + + if (!is_untrusted) { + struct ldb_control *ctrl = NULL; + + /* + * During provision or dbcheck we may not find + * an object. + */ + + ctrl = ldb_request_get_control(ac->req, LDB_CONTROL_RELAX_OID); + if (ctrl != NULL) { + os->require_object = false; + } + ctrl = ldb_request_get_control(ac->req, DSDB_CONTROL_DBCHECK); + if (ctrl != NULL) { + os->require_object = false; + } + } + + ret = dsdb_request_add_controls(os->search_req, + DSDB_FLAG_AS_SYSTEM | + ctrl_flags | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT); + if (ret != LDB_SUCCESS) { + talloc_free(os); + return ret; + } + + if (ac->ops) { + ac->cur->next = os; + } else { + ac->ops = os; + } + ac->cur = os; + + return LDB_SUCCESS; +} + + +/* add */ +static int extended_dn_add(struct ldb_module *module, struct ldb_request *req) +{ + struct extended_dn_context *ac; + int ret; + unsigned int i, j; + + if (ldb_dn_is_special(req->op.add.message->dn)) { + /* do not manipulate our control entries */ + return ldb_next_request(module, req); + } + + ac = extended_dn_context_init(module, req); + if (!ac) { + return ldb_operr(ldb_module_get_ctx(module)); + } + + if (!ac->schema) { + /* without schema, this doesn't make any sense */ + talloc_free(ac); + return ldb_next_request(module, req); + } + + for (i=0; i < req->op.add.message->num_elements; i++) { + const struct ldb_message_element *el = &req->op.add.message->elements[i]; + const struct dsdb_attribute *schema_attr + = dsdb_attribute_by_lDAPDisplayName(ac->schema, el->name); + if (!schema_attr) { + continue; + } + + /* We only setup an extended DN GUID on DN elements */ + if (schema_attr->dn_format == DSDB_INVALID_DN) { + continue; + } + + if (schema_attr->attributeID_id == DRSUAPI_ATTID_distinguishedName) { + /* distinguishedName values are ignored */ + continue; + } + + /* Before we setup a procedure to modify the incoming message, we must copy it */ + if (!ac->new_req) { + struct ldb_message *msg = ldb_msg_copy(ac, req->op.add.message); + if (!msg) { + return ldb_oom(ldb_module_get_ctx(module)); + } + + ret = ldb_build_add_req(&ac->new_req, ac->ldb, ac, msg, req->controls, ac, extended_final_callback, req); + LDB_REQ_SET_LOCATION(ac->new_req); + if (ret != LDB_SUCCESS) { + return ret; + } + } + /* Re-calculate el */ + el = &ac->new_req->op.add.message->elements[i]; + for (j = 0; j < el->num_values; j++) { + ret = extended_store_replace(ac, ac->new_req, + req->op.add.message->dn, + &el->values[j], + false, schema_attr); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } + + /* if no DNs were set continue */ + if (ac->ops == NULL) { + talloc_free(ac); + return ldb_next_request(module, req); + } + + /* start with the searches */ + return ldb_next_request(module, ac->ops->search_req); +} + +/* modify */ +static int extended_dn_modify(struct ldb_module *module, struct ldb_request *req) +{ + /* Look over list of modifications */ + /* Find if any are for linked attributes */ + /* Determine the effect of the modification */ + /* Apply the modify to the linked entry */ + + unsigned int i, j; + struct extended_dn_context *ac; + struct ldb_control *fix_links_control = NULL; + struct ldb_control *fix_link_sid_ctrl = NULL; + int ret; + + if (ldb_dn_is_special(req->op.mod.message->dn)) { + /* do not manipulate our control entries */ + return ldb_next_request(module, req); + } + + ac = extended_dn_context_init(module, req); + if (!ac) { + return ldb_operr(ldb_module_get_ctx(module)); + } + + if (!ac->schema) { + talloc_free(ac); + /* without schema, this doesn't make any sense */ + return ldb_next_request(module, req); + } + + fix_links_control = ldb_request_get_control(req, + DSDB_CONTROL_DBCHECK_FIX_DUPLICATE_LINKS); + if (fix_links_control != NULL) { + return ldb_next_request(module, req); + } + + fix_link_sid_ctrl = ldb_request_get_control(ac->req, + DSDB_CONTROL_DBCHECK_FIX_LINK_DN_SID); + if (fix_link_sid_ctrl != NULL) { + return ldb_next_request(module, req); + } + + for (i=0; i < req->op.mod.message->num_elements; i++) { + const struct ldb_message_element *el = &req->op.mod.message->elements[i]; + const struct dsdb_attribute *schema_attr + = dsdb_attribute_by_lDAPDisplayName(ac->schema, el->name); + if (!schema_attr) { + continue; + } + + /* We only setup an extended DN GUID on these particular DN objects */ + if (schema_attr->dn_format == DSDB_INVALID_DN) { + continue; + } + + if (schema_attr->attributeID_id == DRSUAPI_ATTID_distinguishedName) { + /* distinguishedName values are ignored */ + continue; + } + + /* Before we setup a procedure to modify the incoming message, we must copy it */ + if (!ac->new_req) { + struct ldb_message *msg = ldb_msg_copy(ac, req->op.mod.message); + if (!msg) { + talloc_free(ac); + return ldb_oom(ac->ldb); + } + + ret = ldb_build_mod_req(&ac->new_req, ac->ldb, ac, msg, req->controls, ac, extended_final_callback, req); + LDB_REQ_SET_LOCATION(ac->new_req); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + } + /* Re-calculate el */ + el = &ac->new_req->op.mod.message->elements[i]; + /* For each value being added, we need to setup the lookups to fill in the extended DN */ + for (j = 0; j < el->num_values; j++) { + /* If we are just going to delete this + * element, only do a lookup if + * extended_store_replace determines it's an + * input of an extended DN */ + bool is_delete = (LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_DELETE); + + ret = extended_store_replace(ac, ac->new_req, + NULL, /* self_dn to be ignored */ + &el->values[j], + is_delete, schema_attr); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + } + } + + /* if DNs were set continue */ + if (ac->ops == NULL) { + talloc_free(ac); + return ldb_next_request(module, req); + } + + /* start with the searches */ + return ldb_next_request(module, ac->ops->search_req); +} + +static const struct ldb_module_ops ldb_extended_dn_store_module_ops = { + .name = "extended_dn_store", + .add = extended_dn_add, + .modify = extended_dn_modify, +}; + +int ldb_extended_dn_store_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_extended_dn_store_module_ops); +} |