diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-05 17:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-05 17:47:29 +0000 |
commit | 4f5791ebd03eaec1c7da0865a383175b05102712 (patch) | |
tree | 8ce7b00f7a76baa386372422adebbe64510812d4 /source4/dsdb/samdb/ldb_modules | |
parent | Initial commit. (diff) | |
download | samba-upstream.tar.xz samba-upstream.zip |
Adding upstream version 2:4.17.12+dfsg.upstream/2%4.17.12+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'source4/dsdb/samdb/ldb_modules')
68 files changed, 69452 insertions, 0 deletions
diff --git a/source4/dsdb/samdb/ldb_modules/acl.c b/source4/dsdb/samdb/ldb_modules/acl.c new file mode 100644 index 0000000..78a5ddf --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/acl.c @@ -0,0 +1,2585 @@ +/* + ldb database library + + Copyright (C) Simo Sorce 2006-2008 + Copyright (C) Nadezhda Ivanova 2009 + Copyright (C) Anatoliy Atanasov 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 ACL module + * + * Description: Module that performs authorisation access checks based on the + * account's security context and the DACL of the object being polled. + * Only DACL checks implemented at this point + * + * Authors: Nadezhda Ivanova, Anatoliy Atanasov + */ + +#include "includes.h" +#include "ldb_module.h" +#include "auth/auth.h" +#include "libcli/security/security.h" +#include "dsdb/samdb/samdb.h" +#include "librpc/gen_ndr/ndr_security.h" +#include "param/param.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "lib/util/tsort.h" +#include "system/kerberos.h" +#include "auth/kerberos/kerberos.h" + +#undef strcasecmp +#undef strncasecmp + +struct acl_private { + bool acl_search; + const char **password_attrs; + void *cached_schema_ptr; + uint64_t cached_schema_metadata_usn; + uint64_t cached_schema_loaded_usn; + const char **confidential_attrs; +}; + +struct acl_context { + struct ldb_module *module; + struct ldb_request *req; + bool am_system; + bool am_administrator; + bool constructed_attrs; + bool allowedAttributes; + bool allowedAttributesEffective; + bool allowedChildClasses; + bool allowedChildClassesEffective; + bool sDRightsEffective; + struct dsdb_schema *schema; +}; + +static int acl_module_init(struct ldb_module *module) +{ + struct ldb_context *ldb; + struct acl_private *data; + int ret; + + ldb = ldb_module_get_ctx(module); + + data = talloc_zero(module, struct acl_private); + if (data == NULL) { + return ldb_oom(ldb); + } + + data->acl_search = lpcfg_parm_bool(ldb_get_opaque(ldb, "loadparm"), + NULL, "acl", "search", true); + ldb_module_set_private(module, data); + + ret = ldb_mod_register_control(module, LDB_CONTROL_SD_FLAGS_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "acl_module_init: Unable to register control with rootdse!\n"); + return ldb_operr(ldb); + } + + return ldb_next_init(module); +} + +static int acl_allowedAttributes(struct ldb_module *module, + const struct dsdb_schema *schema, + struct ldb_message *sd_msg, + struct ldb_message *msg, + struct acl_context *ac) +{ + struct ldb_message_element *oc_el; + struct ldb_context *ldb = ldb_module_get_ctx(module); + TALLOC_CTX *mem_ctx; + const char **attr_list; + int i, ret; + const struct dsdb_class *objectclass; + + /* If we don't have a schema yet, we can't do anything... */ + if (schema == NULL) { + ldb_asprintf_errstring(ldb, "cannot add allowedAttributes to %s because no schema is loaded", ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* Must remove any existing attribute */ + if (ac->allowedAttributes) { + ldb_msg_remove_attr(msg, "allowedAttributes"); + } + + mem_ctx = talloc_new(msg); + if (!mem_ctx) { + return ldb_oom(ldb); + } + + oc_el = ldb_msg_find_element(sd_msg, "objectClass"); + attr_list = dsdb_full_attribute_list(mem_ctx, schema, oc_el, DSDB_SCHEMA_ALL); + if (!attr_list) { + ldb_asprintf_errstring(ldb, "acl: Failed to get list of attributes"); + talloc_free(mem_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * Get the top-most structural object class for the ACL check + */ + objectclass = dsdb_get_last_structural_class(ac->schema, + oc_el); + if (objectclass == NULL) { + ldb_asprintf_errstring(ldb, "acl_read: Failed to find a structural class for %s", + ldb_dn_get_linearized(sd_msg->dn)); + talloc_free(mem_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + if (ac->allowedAttributes) { + for (i=0; attr_list && attr_list[i]; i++) { + ldb_msg_add_string(msg, "allowedAttributes", attr_list[i]); + } + } + if (ac->allowedAttributesEffective) { + struct security_descriptor *sd; + struct dom_sid *sid = NULL; + struct ldb_control *as_system = ldb_request_get_control(ac->req, + LDB_CONTROL_AS_SYSTEM_OID); + + if (as_system != NULL) { + as_system->critical = 0; + } + + ldb_msg_remove_attr(msg, "allowedAttributesEffective"); + if (ac->am_system || as_system) { + for (i=0; attr_list && attr_list[i]; i++) { + ldb_msg_add_string(msg, "allowedAttributesEffective", attr_list[i]); + } + return LDB_SUCCESS; + } + + ret = dsdb_get_sd_from_ldb_message(ldb_module_get_ctx(module), mem_ctx, sd_msg, &sd); + + if (ret != LDB_SUCCESS) { + return ret; + } + + sid = samdb_result_dom_sid(mem_ctx, sd_msg, "objectSid"); + for (i=0; attr_list && attr_list[i]; i++) { + const struct dsdb_attribute *attr = dsdb_attribute_by_lDAPDisplayName(schema, + attr_list[i]); + if (!attr) { + return ldb_operr(ldb); + } + /* remove constructed attributes */ + if (attr->systemFlags & DS_FLAG_ATTR_IS_CONSTRUCTED + || attr->systemOnly + || (attr->linkID != 0 && attr->linkID % 2 != 0 )) { + continue; + } + ret = acl_check_access_on_attribute(module, + msg, + sd, + sid, + SEC_ADS_WRITE_PROP, + attr, + objectclass); + if (ret == LDB_SUCCESS) { + ldb_msg_add_string(msg, "allowedAttributesEffective", attr_list[i]); + } + } + } + return LDB_SUCCESS; +} + +static int acl_childClasses(struct ldb_module *module, + const struct dsdb_schema *schema, + struct ldb_message *sd_msg, + struct ldb_message *msg, + const char *attrName) +{ + struct ldb_message_element *oc_el; + struct ldb_message_element *allowedClasses; + const struct dsdb_class *sclass; + unsigned int i, j; + int ret; + + /* If we don't have a schema yet, we can't do anything... */ + if (schema == NULL) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), "cannot add childClassesEffective to %s because no schema is loaded", ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* Must remove any existing attribute, or else confusion reins */ + ldb_msg_remove_attr(msg, attrName); + ret = ldb_msg_add_empty(msg, attrName, 0, &allowedClasses); + if (ret != LDB_SUCCESS) { + return ret; + } + + oc_el = ldb_msg_find_element(sd_msg, "objectClass"); + + for (i=0; oc_el && i < oc_el->num_values; i++) { + sclass = dsdb_class_by_lDAPDisplayName_ldb_val(schema, &oc_el->values[i]); + if (!sclass) { + /* We don't know this class? what is going on? */ + continue; + } + + for (j=0; sclass->possibleInferiors && sclass->possibleInferiors[j]; j++) { + ldb_msg_add_string(msg, attrName, sclass->possibleInferiors[j]); + } + } + if (allowedClasses->num_values > 1) { + TYPESAFE_QSORT(allowedClasses->values, allowedClasses->num_values, data_blob_cmp); + for (i=1 ; i < allowedClasses->num_values; i++) { + struct ldb_val *val1 = &allowedClasses->values[i-1]; + struct ldb_val *val2 = &allowedClasses->values[i]; + if (data_blob_cmp(val1, val2) == 0) { + memmove(val1, val2, (allowedClasses->num_values - i) * sizeof(struct ldb_val)); + allowedClasses->num_values--; + i--; + } + } + } + + return LDB_SUCCESS; +} + +static int acl_childClassesEffective(struct ldb_module *module, + const struct dsdb_schema *schema, + struct ldb_message *sd_msg, + struct ldb_message *msg, + struct acl_context *ac) +{ + struct ldb_message_element *oc_el; + struct ldb_message_element *allowedClasses = NULL; + const struct dsdb_class *sclass; + struct security_descriptor *sd; + struct ldb_control *as_system = ldb_request_get_control(ac->req, + LDB_CONTROL_AS_SYSTEM_OID); + struct dom_sid *sid = NULL; + unsigned int i, j; + int ret; + + if (as_system != NULL) { + as_system->critical = 0; + } + + if (ac->am_system || as_system) { + return acl_childClasses(module, schema, sd_msg, msg, "allowedChildClassesEffective"); + } + + /* If we don't have a schema yet, we can't do anything... */ + if (schema == NULL) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), "cannot add allowedChildClassesEffective to %s because no schema is loaded", ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* Must remove any existing attribute, or else confusion reins */ + ldb_msg_remove_attr(msg, "allowedChildClassesEffective"); + + oc_el = ldb_msg_find_element(sd_msg, "objectClass"); + ret = dsdb_get_sd_from_ldb_message(ldb_module_get_ctx(module), msg, sd_msg, &sd); + if (ret != LDB_SUCCESS) { + return ret; + } + + sid = samdb_result_dom_sid(msg, sd_msg, "objectSid"); + for (i=0; oc_el && i < oc_el->num_values; i++) { + sclass = dsdb_class_by_lDAPDisplayName_ldb_val(schema, &oc_el->values[i]); + if (!sclass) { + /* We don't know this class? what is going on? */ + continue; + } + + for (j=0; sclass->possibleInferiors && sclass->possibleInferiors[j]; j++) { + const struct dsdb_class *sc; + + sc = dsdb_class_by_lDAPDisplayName(schema, + sclass->possibleInferiors[j]); + if (!sc) { + /* We don't know this class? what is going on? */ + continue; + } + + ret = acl_check_access_on_objectclass(module, ac, + sd, sid, + SEC_ADS_CREATE_CHILD, + sc); + if (ret == LDB_SUCCESS) { + ldb_msg_add_string(msg, "allowedChildClassesEffective", + sclass->possibleInferiors[j]); + } + } + } + allowedClasses = ldb_msg_find_element(msg, "allowedChildClassesEffective"); + if (!allowedClasses) { + return LDB_SUCCESS; + } + + if (allowedClasses->num_values > 1) { + TYPESAFE_QSORT(allowedClasses->values, allowedClasses->num_values, data_blob_cmp); + for (i=1 ; i < allowedClasses->num_values; i++) { + struct ldb_val *val1 = &allowedClasses->values[i-1]; + struct ldb_val *val2 = &allowedClasses->values[i]; + if (data_blob_cmp(val1, val2) == 0) { + memmove(val1, val2, (allowedClasses->num_values - i) * sizeof( struct ldb_val)); + allowedClasses->num_values--; + i--; + } + } + } + return LDB_SUCCESS; +} + +static int acl_sDRightsEffective(struct ldb_module *module, + struct ldb_message *sd_msg, + struct ldb_message *msg, + struct acl_context *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_message_element *rightsEffective; + int ret; + struct security_descriptor *sd; + struct ldb_control *as_system = ldb_request_get_control(ac->req, + LDB_CONTROL_AS_SYSTEM_OID); + struct dom_sid *sid = NULL; + uint32_t flags = 0; + + if (as_system != NULL) { + as_system->critical = 0; + } + + /* Must remove any existing attribute, or else confusion reins */ + ldb_msg_remove_attr(msg, "sDRightsEffective"); + ret = ldb_msg_add_empty(msg, "sDRightsEffective", 0, &rightsEffective); + if (ret != LDB_SUCCESS) { + return ret; + } + if (ac->am_system || as_system) { + flags = SECINFO_OWNER | SECINFO_GROUP | SECINFO_SACL | SECINFO_DACL; + } else { + const struct dsdb_class *objectclass; + const struct dsdb_attribute *attr; + + objectclass = dsdb_get_structural_oc_from_msg(ac->schema, sd_msg); + if (objectclass == NULL) { + return ldb_operr(ldb); + } + + attr = dsdb_attribute_by_lDAPDisplayName(ac->schema, + "nTSecurityDescriptor"); + if (attr == NULL) { + return ldb_operr(ldb); + } + + /* Get the security descriptor from the message */ + ret = dsdb_get_sd_from_ldb_message(ldb, msg, sd_msg, &sd); + if (ret != LDB_SUCCESS) { + return ret; + } + sid = samdb_result_dom_sid(msg, sd_msg, "objectSid"); + ret = acl_check_access_on_attribute(module, + msg, + sd, + sid, + SEC_STD_WRITE_OWNER, + attr, + objectclass); + if (ret == LDB_SUCCESS) { + flags |= SECINFO_OWNER | SECINFO_GROUP; + } + ret = acl_check_access_on_attribute(module, + msg, + sd, + sid, + SEC_STD_WRITE_DAC, + attr, + objectclass); + if (ret == LDB_SUCCESS) { + flags |= SECINFO_DACL; + } + ret = acl_check_access_on_attribute(module, + msg, + sd, + sid, + SEC_FLAG_SYSTEM_SECURITY, + attr, + objectclass); + if (ret == LDB_SUCCESS) { + flags |= SECINFO_SACL; + } + } + return samdb_msg_add_uint(ldb_module_get_ctx(module), msg, msg, + "sDRightsEffective", flags); +} + +static int acl_validate_spn_value(TALLOC_CTX *mem_ctx, + struct ldb_context *ldb, + const struct ldb_val *spn_value, + uint32_t userAccountControl, + const struct ldb_val *samAccountName, + const struct ldb_val *dnsHostName, + const char *netbios_name, + const char *ntds_guid) +{ + int ret, princ_size; + krb5_context krb_ctx; + krb5_error_code kerr; + krb5_principal principal; + char *instanceName; + char *serviceType; + char *serviceName; + const char *spn_value_str = NULL; + size_t account_name_len; + const char *forest_name = samdb_forest_name(ldb, mem_ctx); + const char *base_domain = samdb_default_domain_name(ldb, mem_ctx); + struct loadparm_context *lp_ctx = talloc_get_type(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + bool is_dc = (userAccountControl & UF_SERVER_TRUST_ACCOUNT) || + (userAccountControl & UF_PARTIAL_SECRETS_ACCOUNT); + + spn_value_str = talloc_strndup(mem_ctx, + (const char *)spn_value->data, + spn_value->length); + if (spn_value_str == NULL) { + return ldb_oom(ldb); + } + + if (spn_value->length == samAccountName->length && + strncasecmp((const char *)spn_value->data, + (const char *)samAccountName->data, + spn_value->length) == 0) + { + /* MacOS X sets this value, and setting an SPN of your + * own samAccountName is both pointless and safe */ + return LDB_SUCCESS; + } + + kerr = smb_krb5_init_context_basic(mem_ctx, + lp_ctx, + &krb_ctx); + if (kerr != 0) { + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "Could not initialize kerberos context."); + } + + ret = krb5_parse_name(krb_ctx, spn_value_str, &principal); + if (ret) { + krb5_free_context(krb_ctx); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + princ_size = krb5_princ_size(krb_ctx, principal); + if (princ_size < 2) { + DBG_WARNING("princ_size=%d\n", princ_size); + goto fail; + } + + instanceName = smb_krb5_principal_get_comp_string(mem_ctx, krb_ctx, + principal, 1); + serviceType = smb_krb5_principal_get_comp_string(mem_ctx, krb_ctx, + principal, 0); + if (krb5_princ_size(krb_ctx, principal) == 3) { + serviceName = smb_krb5_principal_get_comp_string(mem_ctx, krb_ctx, + principal, 2); + } else { + serviceName = NULL; + } + + if (serviceName) { + if (!is_dc) { + DBG_WARNING("is_dc=false, serviceName=%s," + "serviceType=%s\n", serviceName, + serviceType); + goto fail; + } + if (strcasecmp(serviceType, "ldap") == 0) { + if (strcasecmp(serviceName, netbios_name) != 0 && + strcasecmp(serviceName, forest_name) != 0) { + DBG_WARNING("serviceName=%s\n", serviceName); + goto fail; + } + + } else if (strcasecmp(serviceType, "gc") == 0) { + if (strcasecmp(serviceName, forest_name) != 0) { + DBG_WARNING("serviceName=%s\n", serviceName); + goto fail; + } + } else { + if (strcasecmp(serviceName, base_domain) != 0 && + strcasecmp(serviceName, netbios_name) != 0) { + DBG_WARNING("serviceType=%s, " + "serviceName=%s\n", + serviceType, serviceName); + goto fail; + } + } + } + + account_name_len = samAccountName->length; + if (account_name_len && + samAccountName->data[account_name_len - 1] == '$') + { + /* Account for the '$' character. */ + --account_name_len; + } + + /* instanceName can be samAccountName without $ or dnsHostName + * or "ntds_guid._msdcs.forest_domain for DC objects */ + if (strlen(instanceName) == account_name_len + && strncasecmp(instanceName, + (const char *)samAccountName->data, + account_name_len) == 0) + { + goto success; + } + if ((dnsHostName != NULL) && + strlen(instanceName) == dnsHostName->length && + (strncasecmp(instanceName, + (const char *)dnsHostName->data, + dnsHostName->length) == 0)) + { + goto success; + } + if (is_dc) { + const char *guid_str; + guid_str = talloc_asprintf(mem_ctx,"%s._msdcs.%s", + ntds_guid, + forest_name); + if (strcasecmp(instanceName, guid_str) == 0) { + goto success; + } + } + +fail: + krb5_free_principal(krb_ctx, principal); + krb5_free_context(krb_ctx); + ldb_debug_set(ldb, LDB_DEBUG_WARNING, + "acl: spn validation failed for " + "spn[%.*s] uac[0x%x] account[%.*s] hostname[%.*s] " + "nbname[%s] ntds[%s] forest[%s] domain[%s]\n", + (int)spn_value->length, spn_value->data, + (unsigned)userAccountControl, + (int)samAccountName->length, samAccountName->data, + dnsHostName != NULL ? (int)dnsHostName->length : 0, + dnsHostName != NULL ? (const char *)dnsHostName->data : "", + netbios_name, ntds_guid, + forest_name, base_domain); + return LDB_ERR_CONSTRAINT_VIOLATION; + +success: + krb5_free_principal(krb_ctx, principal); + krb5_free_context(krb_ctx); + return LDB_SUCCESS; +} + +/* + * Passing in 'el' is critical, we want to check all the values. + * + */ +static int acl_check_spn(TALLOC_CTX *mem_ctx, + struct ldb_module *module, + struct ldb_request *req, + const struct ldb_message_element *el, + struct security_descriptor *sd, + struct dom_sid *sid, + const struct dsdb_attribute *attr, + const struct dsdb_class *objectclass, + const struct ldb_control *implicit_validated_write_control) +{ + int ret; + unsigned int i; + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_result *acl_res; + struct ldb_result *netbios_res; + struct ldb_dn *partitions_dn = samdb_partitions_dn(ldb, tmp_ctx); + uint32_t userAccountControl; + const char *netbios_name; + const struct ldb_val *dns_host_name_val = NULL; + const struct ldb_val *sam_account_name_val = NULL; + struct GUID ntds; + char *ntds_guid = NULL; + + static const char *acl_attrs[] = { + "samAccountName", + "dnsHostName", + "userAccountControl", + NULL + }; + static const char *netbios_attrs[] = { + "nETBIOSName", + NULL + }; + + if (implicit_validated_write_control != NULL) { + /* + * The validated write control dispenses with ACL + * checks. We act as if we have an implicit Self Write + * privilege, but, assuming we don't have Write + * Property, still proceed with further validation + * checks. + */ + } else { + /* if we have wp, we can do whatever we like */ + if (acl_check_access_on_attribute(module, + tmp_ctx, + sd, + sid, + SEC_ADS_WRITE_PROP, + attr, objectclass) == LDB_SUCCESS) { + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + + ret = acl_check_extended_right(tmp_ctx, + module, + req, + objectclass, + sd, + acl_user_token(module), + GUID_DRS_VALIDATE_SPN, + SEC_ADS_SELF_WRITE, + sid); + + if (ret != LDB_SUCCESS) { + dsdb_acl_debug(sd, acl_user_token(module), + req->op.mod.message->dn, + true, + 10); + talloc_free(tmp_ctx); + return ret; + } + } + + /* + * If we have "validated write spn", allow delete of any + * existing value (this keeps constrained delete to the same + * rules as unconstrained) + */ + if (req->operation == LDB_MODIFY) { + /* + * If not add or replace (eg delete), + * return success + */ + if (LDB_FLAG_MOD_TYPE(el->flags) != LDB_FLAG_MOD_ADD && + LDB_FLAG_MOD_TYPE(el->flags) != LDB_FLAG_MOD_REPLACE) + { + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + } + + ret = dsdb_module_search_dn(module, tmp_ctx, + &acl_res, req->op.mod.message->dn, + acl_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED, + req); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + dns_host_name_val = ldb_msg_find_ldb_val(acl_res->msgs[0], "dNSHostName"); + + ret = dsdb_msg_get_single_value(req->op.mod.message, + "dNSHostName", + dns_host_name_val, + &dns_host_name_val, + req->operation); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + userAccountControl = ldb_msg_find_attr_as_uint(acl_res->msgs[0], "userAccountControl", 0); + + sam_account_name_val = ldb_msg_find_ldb_val(acl_res->msgs[0], "sAMAccountName"); + + ret = dsdb_msg_get_single_value(req->op.mod.message, + "sAMAccountName", + sam_account_name_val, + &sam_account_name_val, + req->operation); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_module_search(module, tmp_ctx, + &netbios_res, partitions_dn, + LDB_SCOPE_ONELEVEL, + netbios_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM, + req, + "(ncName=%s)", + ldb_dn_get_linearized(ldb_get_default_basedn(ldb))); + + netbios_name = ldb_msg_find_attr_as_string(netbios_res->msgs[0], "nETBIOSName", NULL); + + /* NTDSDSA objectGuid of object we are checking SPN for */ + if (userAccountControl & (UF_SERVER_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT)) { + ret = dsdb_module_find_ntdsguid_for_computer(module, tmp_ctx, + req->op.mod.message->dn, &ntds, req); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to find NTDSDSA objectGuid for %s: %s", + ldb_dn_get_linearized(req->op.mod.message->dn), + ldb_strerror(ret)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + ntds_guid = GUID_string(tmp_ctx, &ntds); + } + + for (i=0; i < el->num_values; i++) { + ret = acl_validate_spn_value(tmp_ctx, + ldb, + &el->values[i], + userAccountControl, + sam_account_name_val, + dns_host_name_val, + netbios_name, + ntds_guid); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + +static int acl_check_dns_host_name(TALLOC_CTX *mem_ctx, + struct ldb_module *module, + struct ldb_request *req, + const struct ldb_message_element *el, + struct security_descriptor *sd, + struct dom_sid *sid, + const struct dsdb_attribute *attr, + const struct dsdb_class *objectclass, + const struct ldb_control *implicit_validated_write_control) +{ + int ret; + unsigned i; + TALLOC_CTX *tmp_ctx = NULL; + struct ldb_context *ldb = ldb_module_get_ctx(module); + const struct dsdb_schema *schema = NULL; + const struct ldb_message_element *allowed_suffixes = NULL; + struct ldb_result *nc_res = NULL; + struct ldb_dn *nc_root = NULL; + const char *nc_dns_name = NULL; + const char *dnsHostName_str = NULL; + size_t dns_host_name_len; + size_t account_name_len; + const struct ldb_message *msg = NULL; + const struct ldb_message *search_res = NULL; + const struct ldb_val *samAccountName = NULL; + const struct ldb_val *dnsHostName = NULL; + const struct dsdb_class *computer_objectclass = NULL; + bool is_subclass; + + static const char *nc_attrs[] = { + "msDS-AllowedDNSSuffixes", + NULL + }; + + tmp_ctx = talloc_new(mem_ctx); + if (tmp_ctx == NULL) { + return ldb_oom(ldb); + } + + if (implicit_validated_write_control != NULL) { + /* + * The validated write control dispenses with ACL + * checks. We act as if we have an implicit Self Write + * privilege, but, assuming we don't have Write + * Property, still proceed with further validation + * checks. + */ + } else { + /* if we have wp, we can do whatever we like */ + ret = acl_check_access_on_attribute(module, + tmp_ctx, + sd, + sid, + SEC_ADS_WRITE_PROP, + attr, objectclass); + if (ret == LDB_SUCCESS) { + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + + ret = acl_check_extended_right(tmp_ctx, + module, + req, + objectclass, + sd, + acl_user_token(module), + GUID_DRS_DNS_HOST_NAME, + SEC_ADS_SELF_WRITE, + sid); + + if (ret != LDB_SUCCESS) { + dsdb_acl_debug(sd, acl_user_token(module), + req->op.mod.message->dn, + true, + 10); + talloc_free(tmp_ctx); + return ret; + } + } + + /* + * If we have "validated write dnshostname", allow delete of + * any existing value (this keeps constrained delete to the + * same rules as unconstrained) + */ + if (req->operation == LDB_MODIFY) { + struct ldb_result *acl_res = NULL; + + static const char *acl_attrs[] = { + "sAMAccountName", + NULL + }; + + msg = req->op.mod.message; + + /* + * If not add or replace (eg delete), + * return success + */ + if ((el->flags + & (LDB_FLAG_MOD_ADD|LDB_FLAG_MOD_REPLACE)) == 0) + { + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + + ret = dsdb_module_search_dn(module, tmp_ctx, + &acl_res, msg->dn, + acl_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED, + req); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + search_res = acl_res->msgs[0]; + } else if (req->operation == LDB_ADD) { + msg = req->op.add.message; + search_res = msg; + } else { + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* Check if the account has objectclass 'computer' or 'server'. */ + + schema = dsdb_get_schema(ldb, req); + if (schema == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + computer_objectclass = dsdb_class_by_lDAPDisplayName(schema, "computer"); + if (computer_objectclass == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + is_subclass = dsdb_is_subclass_of(schema, objectclass, computer_objectclass); + if (!is_subclass) { + /* The account is not a computer -- check if it's a server. */ + + const struct dsdb_class *server_objectclass = NULL; + + server_objectclass = dsdb_class_by_lDAPDisplayName(schema, "server"); + if (server_objectclass == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + is_subclass = dsdb_is_subclass_of(schema, objectclass, server_objectclass); + if (!is_subclass) { + /* Not a computer or server, so no need to validate. */ + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + } + + samAccountName = ldb_msg_find_ldb_val(search_res, "sAMAccountName"); + + ret = dsdb_msg_get_single_value(msg, + "sAMAccountName", + samAccountName, + &samAccountName, + req->operation); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + account_name_len = samAccountName->length; + if (account_name_len && samAccountName->data[account_name_len - 1] == '$') { + /* Account for the '$' character. */ + --account_name_len; + } + + /* Check for add or replace requests with no value. */ + if (el->num_values == 0) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + dnsHostName = &el->values[0]; + + dnsHostName_str = (const char *)dnsHostName->data; + dns_host_name_len = dnsHostName->length; + + /* Check that sAMAccountName matches the new dNSHostName. */ + + if (dns_host_name_len < account_name_len) { + goto fail; + } + if (strncasecmp(dnsHostName_str, + (const char *)samAccountName->data, + account_name_len) != 0) + { + goto fail; + } + + dnsHostName_str += account_name_len; + dns_host_name_len -= account_name_len; + + /* Check the '.' character */ + + if (dns_host_name_len == 0 || *dnsHostName_str != '.') { + goto fail; + } + + ++dnsHostName_str; + --dns_host_name_len; + + /* Now we check the suffix. */ + + ret = dsdb_find_nc_root(ldb, + tmp_ctx, + search_res->dn, + &nc_root); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + nc_dns_name = samdb_dn_to_dns_domain(tmp_ctx, nc_root); + if (nc_dns_name == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + if (strlen(nc_dns_name) == dns_host_name_len && + strncasecmp(dnsHostName_str, + nc_dns_name, + dns_host_name_len) == 0) + { + /* It matches -- success. */ + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + + /* We didn't get a match, so now try msDS-AllowedDNSSuffixes. */ + + ret = dsdb_module_search_dn(module, tmp_ctx, + &nc_res, nc_root, + nc_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED, + req); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + allowed_suffixes = ldb_msg_find_element(nc_res->msgs[0], + "msDS-AllowedDNSSuffixes"); + if (allowed_suffixes == NULL) { + goto fail; + } + + for (i = 0; i < allowed_suffixes->num_values; ++i) { + const struct ldb_val *suffix = &allowed_suffixes->values[i]; + + if (suffix->length == dns_host_name_len && + strncasecmp(dnsHostName_str, + (const char *)suffix->data, + dns_host_name_len) == 0) + { + /* It matches -- success. */ + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + } + +fail: + ldb_debug_set(ldb, LDB_DEBUG_WARNING, + "acl: hostname validation failed for " + "hostname[%.*s] account[%.*s]\n", + (int)dnsHostName->length, dnsHostName->data, + (int)samAccountName->length, samAccountName->data); + talloc_free(tmp_ctx); + return LDB_ERR_CONSTRAINT_VIOLATION; +} + +static int acl_add(struct ldb_module *module, struct ldb_request *req) +{ + int ret; + struct ldb_dn *parent; + struct ldb_context *ldb; + const struct dsdb_schema *schema; + const struct dsdb_class *objectclass; + struct ldb_control *as_system; + struct ldb_message_element *el; + unsigned int instanceType = 0; + + if (ldb_dn_is_special(req->op.add.message->dn)) { + return ldb_next_request(module, req); + } + + as_system = ldb_request_get_control(req, LDB_CONTROL_AS_SYSTEM_OID); + if (as_system != NULL) { + as_system->critical = 0; + } + + if (dsdb_module_am_system(module) || as_system) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + parent = ldb_dn_get_parent(req, req->op.add.message->dn); + if (parent == NULL) { + return ldb_oom(ldb); + } + + schema = dsdb_get_schema(ldb, req); + if (!schema) { + return ldb_operr(ldb); + } + + objectclass = dsdb_get_structural_oc_from_msg(schema, req->op.add.message); + if (!objectclass) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "acl: unable to find or validate structural objectClass on %s\n", + ldb_dn_get_linearized(req->op.add.message->dn)); + return ldb_module_done(req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + + el = ldb_msg_find_element(req->op.add.message, "instanceType"); + if ((el != NULL) && (el->num_values != 1)) { + ldb_set_errstring(ldb, "acl: the 'instanceType' attribute is single-valued!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + instanceType = ldb_msg_find_attr_as_uint(req->op.add.message, + "instanceType", 0); + if (instanceType & INSTANCE_TYPE_IS_NC_HEAD) { + static const char *no_attrs[] = { NULL }; + struct ldb_result *partition_res; + struct ldb_dn *partitions_dn; + + partitions_dn = samdb_partitions_dn(ldb, req); + if (!partitions_dn) { + ldb_set_errstring(ldb, "acl: CN=partitions dn could not be generated!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + ret = dsdb_module_search(module, req, &partition_res, + partitions_dn, LDB_SCOPE_ONELEVEL, + no_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_ONE_ONLY | + DSDB_SEARCH_SHOW_RECYCLED, + req, + "(&(nCName=%s)(objectClass=crossRef))", + ldb_dn_get_linearized(req->op.add.message->dn)); + + if (ret == LDB_SUCCESS) { + /* Check that we can write to the crossRef object MS-ADTS 3.1.1.5.2.8.2 */ + ret = dsdb_module_check_access_on_dn(module, req, partition_res->msgs[0]->dn, + SEC_ADS_WRITE_PROP, + &objectclass->schemaIDGUID, req); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "acl: ACL check failed on crossRef object %s: %s\n", + ldb_dn_get_linearized(partition_res->msgs[0]->dn), + ldb_errstring(ldb)); + return ret; + } + + /* + * TODO: Remaining checks, like if we are + * the naming master etc need to be handled + * in the instanceType module + */ + return ldb_next_request(module, req); + } + + /* Check that we can create a crossRef object MS-ADTS 3.1.1.5.2.8.2 */ + ret = dsdb_module_check_access_on_dn(module, req, partitions_dn, + SEC_ADS_CREATE_CHILD, + &objectclass->schemaIDGUID, req); + if (ret == LDB_ERR_NO_SUCH_OBJECT && + ldb_request_get_control(req, LDB_CONTROL_RELAX_OID)) + { + /* Allow provision bootstrap */ + ret = LDB_SUCCESS; + } + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "acl: ACL check failed on CN=Partitions crossRef container %s: %s\n", + ldb_dn_get_linearized(partitions_dn), ldb_errstring(ldb)); + return ret; + } + + /* + * TODO: Remaining checks, like if we are the naming + * master and adding the crossRef object need to be + * handled in the instanceType module + */ + return ldb_next_request(module, req); + } + + ret = dsdb_module_check_access_on_dn(module, req, parent, + SEC_ADS_CREATE_CHILD, + &objectclass->schemaIDGUID, req); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "acl: unable to get access to %s\n", + ldb_dn_get_linearized(req->op.add.message->dn)); + return ret; + } + return ldb_next_request(module, req); +} + +/* checks if modifications are allowed on "Member" attribute */ +static int acl_check_self_membership(TALLOC_CTX *mem_ctx, + struct ldb_module *module, + struct ldb_request *req, + struct security_descriptor *sd, + struct dom_sid *sid, + const struct dsdb_attribute *attr, + const struct dsdb_class *objectclass) +{ + int ret; + unsigned int i; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_dn *user_dn; + struct ldb_message_element *member_el; + const struct ldb_message *msg = NULL; + + if (req->operation == LDB_MODIFY) { + msg = req->op.mod.message; + } else if (req->operation == LDB_ADD) { + msg = req->op.add.message; + } else { + return LDB_ERR_OPERATIONS_ERROR; + } + + /* if we have wp, we can do whatever we like */ + if (acl_check_access_on_attribute(module, + mem_ctx, + sd, + sid, + SEC_ADS_WRITE_PROP, + attr, objectclass) == LDB_SUCCESS) { + return LDB_SUCCESS; + } + /* if we are adding/deleting ourselves, check for self membership */ + ret = dsdb_find_dn_by_sid(ldb, mem_ctx, + &acl_user_token(module)->sids[PRIMARY_USER_SID_INDEX], + &user_dn); + if (ret != LDB_SUCCESS) { + return ret; + } + member_el = ldb_msg_find_element(msg, "member"); + if (!member_el) { + return ldb_operr(ldb); + } + /* user can only remove oneself */ + if (member_el->num_values == 0) { + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + for (i = 0; i < member_el->num_values; i++) { + if (strcasecmp((const char *)member_el->values[i].data, + ldb_dn_get_extended_linearized(mem_ctx, user_dn, 1)) != 0) { + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + } + ret = acl_check_extended_right(mem_ctx, + module, + req, + objectclass, + sd, + acl_user_token(module), + GUID_DRS_SELF_MEMBERSHIP, + SEC_ADS_SELF_WRITE, + sid); + if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) { + dsdb_acl_debug(sd, acl_user_token(module), + msg->dn, + true, + 10); + } + return ret; +} + +static int acl_check_password_rights( + TALLOC_CTX *mem_ctx, + struct ldb_module *module, + struct ldb_request *req, + struct security_descriptor *sd, + struct dom_sid *sid, + const struct dsdb_class *objectclass, + bool userPassword, + struct dsdb_control_password_acl_validation **control_for_response) +{ + int ret = LDB_SUCCESS; + unsigned int del_attr_cnt = 0, add_attr_cnt = 0, rep_attr_cnt = 0; + unsigned int del_val_cnt = 0, add_val_cnt = 0, rep_val_cnt = 0; + struct ldb_message_element *el; + struct ldb_message *msg; + struct ldb_control *c = NULL; + const char *passwordAttrs[] = { "userPassword", "clearTextPassword", + "unicodePwd", NULL }, **l; + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + struct dsdb_control_password_acl_validation *pav = NULL; + + if (tmp_ctx == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + pav = talloc_zero(req, struct dsdb_control_password_acl_validation); + if (pav == NULL) { + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + /* + * Set control_for_response to pav so it can be added to the response + * and be passed up to the audit_log module which uses it to identify + * password reset attempts. + */ + *control_for_response = pav; + + c = ldb_request_get_control(req, DSDB_CONTROL_PASSWORD_CHANGE_OLD_PW_CHECKED_OID); + if (c != NULL) { + pav->pwd_reset = false; + + /* + * The "DSDB_CONTROL_PASSWORD_CHANGE_OLD_PW_CHECKED_OID" control means that we + * have a user password change and not a set as the message + * looks like. In it's value blob it contains the NT and/or LM + * hash of the old password specified by the user. This control + * is used by the SAMR and "kpasswd" password change mechanisms. + * + * This control can't be used by real LDAP clients, + * the only caller is samdb_set_password_internal(), + * so we don't have to strict verification of the input. + */ + ret = acl_check_extended_right(tmp_ctx, + module, + req, + objectclass, + sd, + acl_user_token(module), + GUID_DRS_USER_CHANGE_PASSWORD, + SEC_ADS_CONTROL_ACCESS, + sid); + goto checked; + } + + c = ldb_request_get_control(req, DSDB_CONTROL_PASSWORD_HASH_VALUES_OID); + if (c != NULL) { + pav->pwd_reset = true; + + /* + * The "DSDB_CONTROL_PASSWORD_HASH_VALUES_OID" control, without + * "DSDB_CONTROL_PASSWORD_CHANGE_OLD_PW_CHECKED_OID" control means that we + * have a force password set. + * This control is used by the SAMR/NETLOGON/LSA password + * reset mechanisms. + * + * This control can't be used by real LDAP clients, + * the only caller is samdb_set_password_internal(), + * so we don't have to strict verification of the input. + */ + ret = acl_check_extended_right(tmp_ctx, + module, + req, + objectclass, + sd, + acl_user_token(module), + GUID_DRS_FORCE_CHANGE_PASSWORD, + SEC_ADS_CONTROL_ACCESS, + sid); + goto checked; + } + + el = ldb_msg_find_element(req->op.mod.message, "dBCSPwd"); + if (el != NULL) { + /* + * dBCSPwd is only allowed with a control. + */ + talloc_free(tmp_ctx); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + msg = ldb_msg_copy_shallow(tmp_ctx, req->op.mod.message); + if (msg == NULL) { + return ldb_module_oom(module); + } + for (l = passwordAttrs; *l != NULL; l++) { + if ((!userPassword) && (ldb_attr_cmp(*l, "userPassword") == 0)) { + continue; + } + + while ((el = ldb_msg_find_element(msg, *l)) != NULL) { + if (LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_DELETE) { + ++del_attr_cnt; + del_val_cnt += el->num_values; + } + if (LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_ADD) { + ++add_attr_cnt; + add_val_cnt += el->num_values; + } + if (LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_REPLACE) { + ++rep_attr_cnt; + rep_val_cnt += el->num_values; + } + ldb_msg_remove_element(msg, el); + } + } + + /* single deletes will be handled by the "password_hash" LDB module + * later in the stack, so we let it though here */ + if ((del_attr_cnt > 0) && (add_attr_cnt == 0) && (rep_attr_cnt == 0)) { + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + + + if (rep_attr_cnt > 0) { + pav->pwd_reset = true; + + ret = acl_check_extended_right(tmp_ctx, + module, + req, + objectclass, + sd, + acl_user_token(module), + GUID_DRS_FORCE_CHANGE_PASSWORD, + SEC_ADS_CONTROL_ACCESS, + sid); + goto checked; + } + + if (add_attr_cnt != del_attr_cnt) { + pav->pwd_reset = true; + + ret = acl_check_extended_right(tmp_ctx, + module, + req, + objectclass, + sd, + acl_user_token(module), + GUID_DRS_FORCE_CHANGE_PASSWORD, + SEC_ADS_CONTROL_ACCESS, + sid); + goto checked; + } + + if (add_val_cnt == 1 && del_val_cnt == 1) { + pav->pwd_reset = false; + + ret = acl_check_extended_right(tmp_ctx, + module, + req, + objectclass, + sd, + acl_user_token(module), + GUID_DRS_USER_CHANGE_PASSWORD, + SEC_ADS_CONTROL_ACCESS, + sid); + /* Very strange, but we get constraint violation in this case */ + if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) { + ret = LDB_ERR_CONSTRAINT_VIOLATION; + } + goto checked; + } + + if (add_val_cnt == 1 && del_val_cnt == 0) { + pav->pwd_reset = true; + + ret = acl_check_extended_right(tmp_ctx, + module, + req, + objectclass, + sd, + acl_user_token(module), + GUID_DRS_FORCE_CHANGE_PASSWORD, + SEC_ADS_CONTROL_ACCESS, + sid); + /* Very strange, but we get constraint violation in this case */ + if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) { + ret = LDB_ERR_CONSTRAINT_VIOLATION; + } + goto checked; + } + + /* + * Everything else is handled by the password_hash module where it will + * fail, but with the correct error code when the module is again + * checking the attributes. As the change request will lack the + * DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID control, we can be sure that + * any modification attempt that went this way will be rejected. + */ + + talloc_free(tmp_ctx); + return LDB_SUCCESS; + +checked: + if (ret != LDB_SUCCESS) { + dsdb_acl_debug(sd, acl_user_token(module), + req->op.mod.message->dn, + true, + 10); + talloc_free(tmp_ctx); + return ret; + } + + ret = ldb_request_add_control(req, + DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID, false, pav); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb_module_get_ctx(module), LDB_DEBUG_ERROR, + "Unable to register ACL validation control!\n"); + return ret; + } + return LDB_SUCCESS; +} + +/* + * Context needed by acl_callback + */ +struct acl_callback_context { + struct ldb_request *request; + struct ldb_module *module; +}; + +/* + * @brief Copy the password validation control to the reply. + * + * Copy the dsdb_control_password_acl_validation control from the request, + * to the reply. The control is used by the audit_log module to identify + * password rests. + * + * @param req the ldb request. + * @param ares the result, updated with the control. + */ +static void copy_password_acl_validation_control( + struct ldb_request *req, + struct ldb_reply *ares) +{ + struct ldb_control *pav_ctrl = NULL; + struct dsdb_control_password_acl_validation *pav = NULL; + + pav_ctrl = ldb_request_get_control( + discard_const(req), + DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID); + if (pav_ctrl == NULL) { + return; + } + + pav = talloc_get_type_abort( + pav_ctrl->data, + struct dsdb_control_password_acl_validation); + if (pav == NULL) { + return; + } + ldb_reply_add_control( + ares, + DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID, + false, + pav); +} +/* + * @brief call back function for acl_modify. + * + * Calls acl_copy to copy the dsdb_control_password_acl_validation from + * the request to the reply. + * + * @param req the ldb_request. + * @param ares the operation result. + * + * @return the LDB_STATUS + */ +static int acl_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct acl_callback_context *ac = NULL; + + ac = talloc_get_type(req->context, struct acl_callback_context); + + if (!ares) { + return ldb_module_done( + ac->request, + NULL, + NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + /* pass on to the callback */ + switch (ares->type) { + case LDB_REPLY_ENTRY: + return ldb_module_send_entry( + ac->request, + ares->message, + ares->controls); + + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral( + ac->request, + ares->referral); + + case LDB_REPLY_DONE: + /* + * Copy the ACL control from the request to the response + */ + copy_password_acl_validation_control(req, ares); + return ldb_module_done( + ac->request, + ares->controls, + ares->response, + ares->error); + + default: + /* Can't happen */ + return LDB_ERR_OPERATIONS_ERROR; + } +} + +static int acl_modify(struct ldb_module *module, struct ldb_request *req) +{ + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + const struct dsdb_schema *schema; + unsigned int i; + const struct dsdb_class *objectclass; + struct ldb_result *acl_res; + struct security_descriptor *sd; + struct dom_sid *sid = NULL; + struct ldb_control *as_system; + struct ldb_control *is_undelete; + struct ldb_control *implicit_validated_write_control = NULL; + bool userPassword; + bool password_rights_checked = false; + TALLOC_CTX *tmp_ctx; + const struct ldb_message *msg = req->op.mod.message; + static const char *acl_attrs[] = { + "nTSecurityDescriptor", + "objectClass", + "objectSid", + NULL + }; + struct acl_callback_context *context = NULL; + struct ldb_request *new_req = NULL; + struct dsdb_control_password_acl_validation *pav = NULL; + struct ldb_control **controls = NULL; + + if (ldb_dn_is_special(msg->dn)) { + return ldb_next_request(module, req); + } + + as_system = ldb_request_get_control(req, LDB_CONTROL_AS_SYSTEM_OID); + if (as_system != NULL) { + as_system->critical = 0; + } + + is_undelete = ldb_request_get_control(req, DSDB_CONTROL_RESTORE_TOMBSTONE_OID); + + implicit_validated_write_control = ldb_request_get_control( + req, DSDB_CONTROL_FORCE_ALLOW_VALIDATED_DNS_HOSTNAME_SPN_WRITE_OID); + if (implicit_validated_write_control != NULL) { + implicit_validated_write_control->critical = 0; + } + + /* Don't print this debug statement if elements[0].name is going to be NULL */ + if (msg->num_elements > 0) { + DEBUG(10, ("ldb:acl_modify: %s\n", msg->elements[0].name)); + } + if (dsdb_module_am_system(module) || as_system) { + return ldb_next_request(module, req); + } + + tmp_ctx = talloc_new(req); + if (tmp_ctx == NULL) { + return ldb_oom(ldb); + } + + ret = dsdb_module_search_dn(module, tmp_ctx, &acl_res, msg->dn, + acl_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED, + req); + + if (ret != LDB_SUCCESS) { + goto fail; + } + + userPassword = dsdb_user_password_support(module, req, req); + + schema = dsdb_get_schema(ldb, tmp_ctx); + if (!schema) { + talloc_free(tmp_ctx); + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "acl_modify: Error obtaining schema."); + } + + ret = dsdb_get_sd_from_ldb_message(ldb, tmp_ctx, acl_res->msgs[0], &sd); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "acl_modify: Error retrieving security descriptor."); + } + /* Theoretically we pass the check if the object has no sd */ + if (!sd) { + goto success; + } + + objectclass = dsdb_get_structural_oc_from_msg(schema, acl_res->msgs[0]); + if (!objectclass) { + talloc_free(tmp_ctx); + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "acl_modify: Error retrieving object class for GUID."); + } + sid = samdb_result_dom_sid(req, acl_res->msgs[0], "objectSid"); + for (i=0; i < msg->num_elements; i++) { + const struct ldb_message_element *el = &msg->elements[i]; + const struct dsdb_attribute *attr; + + /* + * This basic attribute existence check with the right errorcode + * is needed since this module is the first one which requests + * schema attribute information. + * The complete attribute checking is done in the + * "objectclass_attrs" module behind this one. + * + * NOTE: "clearTextPassword" is not defined in the schema. + */ + attr = dsdb_attribute_by_lDAPDisplayName(schema, el->name); + if (!attr && ldb_attr_cmp("clearTextPassword", el->name) != 0) { + ldb_asprintf_errstring(ldb, "acl_modify: attribute '%s' " + "on entry '%s' was not found in the schema!", + req->op.mod.message->elements[i].name, + ldb_dn_get_linearized(req->op.mod.message->dn)); + ret = LDB_ERR_NO_SUCH_ATTRIBUTE; + goto fail; + } + + if (ldb_attr_cmp("nTSecurityDescriptor", el->name) == 0) { + uint32_t sd_flags = dsdb_request_sd_flags(req, NULL); + uint32_t access_mask = 0; + + if (sd_flags & (SECINFO_OWNER|SECINFO_GROUP)) { + access_mask |= SEC_STD_WRITE_OWNER; + } + if (sd_flags & SECINFO_DACL) { + access_mask |= SEC_STD_WRITE_DAC; + } + if (sd_flags & SECINFO_SACL) { + access_mask |= SEC_FLAG_SYSTEM_SECURITY; + } + + ret = acl_check_access_on_attribute(module, + tmp_ctx, + sd, + sid, + access_mask, + attr, + objectclass); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "Object %s has no write dacl access\n", + ldb_dn_get_linearized(msg->dn)); + dsdb_acl_debug(sd, + acl_user_token(module), + msg->dn, + true, + 10); + ret = LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + goto fail; + } + } else if (ldb_attr_cmp("member", el->name) == 0) { + ret = acl_check_self_membership(tmp_ctx, + module, + req, + sd, + sid, + attr, + objectclass); + if (ret != LDB_SUCCESS) { + goto fail; + } + } else if (ldb_attr_cmp("dBCSPwd", el->name) == 0) { + /* this one is not affected by any rights, we should let it through + so that passwords_hash returns the correct error */ + continue; + } else if (ldb_attr_cmp("unicodePwd", el->name) == 0 || + (userPassword && ldb_attr_cmp("userPassword", el->name) == 0) || + ldb_attr_cmp("clearTextPassword", el->name) == 0) { + /* + * Ideally we would do the acl_check_password_rights + * before we checked the other attributes, i.e. in a + * loop before the current one. + * Have not done this as yet in order to limit the size + * of the change. To limit the possibility of breaking + * the ACL logic. + */ + if (password_rights_checked) { + continue; + } + ret = acl_check_password_rights(tmp_ctx, + module, + req, + sd, + sid, + objectclass, + userPassword, + &pav); + if (ret != LDB_SUCCESS) { + goto fail; + } + password_rights_checked = true; + } else if (ldb_attr_cmp("servicePrincipalName", el->name) == 0) { + ret = acl_check_spn(tmp_ctx, + module, + req, + el, + sd, + sid, + attr, + objectclass, + implicit_validated_write_control); + if (ret != LDB_SUCCESS) { + goto fail; + } + } else if (ldb_attr_cmp("dnsHostName", el->name) == 0) { + ret = acl_check_dns_host_name(tmp_ctx, + module, + req, + el, + sd, + sid, + attr, + objectclass, + implicit_validated_write_control); + if (ret != LDB_SUCCESS) { + goto fail; + } + } else if (is_undelete != NULL && (ldb_attr_cmp("isDeleted", el->name) == 0)) { + /* + * in case of undelete op permissions on + * isDeleted are irrelevant and + * distinguishedName is removed by the + * tombstone_reanimate module + */ + continue; + } else if (implicit_validated_write_control != NULL) { + /* Allow the update. */ + continue; + } else { + ret = acl_check_access_on_attribute(module, + tmp_ctx, + sd, + sid, + SEC_ADS_WRITE_PROP, + attr, + objectclass); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "Object %s has no write property access\n", + ldb_dn_get_linearized(msg->dn)); + dsdb_acl_debug(sd, + acl_user_token(module), + msg->dn, + true, + 10); + ret = LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + goto fail; + } + } + } + +success: + talloc_free(tmp_ctx); + context = talloc_zero(req, struct acl_callback_context); + + if (context == NULL) { + return ldb_oom(ldb); + } + context->request = req; + context->module = module; + ret = ldb_build_mod_req( + &new_req, + ldb, + req, + req->op.mod.message, + req->controls, + context, + acl_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(module, new_req); +fail: + talloc_free(tmp_ctx); + /* + * We copy the pav into the result, so that the password reset + * logging code in audit_log can log failed password reset attempts. + */ + if (pav) { + struct ldb_control *control = NULL; + + controls = talloc_zero_array(req, struct ldb_control *, 2); + if (controls == NULL) { + return ldb_oom(ldb); + } + + control = talloc(controls, struct ldb_control); + + if (control == NULL) { + return ldb_oom(ldb); + } + + control->oid= talloc_strdup( + control, + DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID); + if (control->oid == NULL) { + return ldb_oom(ldb); + } + control->critical = false; + control->data = pav; + *controls = control; + } + return ldb_module_done(req, controls, NULL, ret); +} + +/* similar to the modify for the time being. + * We need to consider the special delete tree case, though - TODO */ +static int acl_delete(struct ldb_module *module, struct ldb_request *req) +{ + int ret; + struct ldb_dn *parent; + struct ldb_context *ldb; + struct ldb_dn *nc_root; + struct ldb_control *as_system; + const struct dsdb_schema *schema; + const struct dsdb_class *objectclass; + struct security_descriptor *sd = NULL; + struct dom_sid *sid = NULL; + struct ldb_result *acl_res; + static const char *acl_attrs[] = { + "nTSecurityDescriptor", + "objectClass", + "objectSid", + NULL + }; + + if (ldb_dn_is_special(req->op.del.dn)) { + return ldb_next_request(module, req); + } + + as_system = ldb_request_get_control(req, LDB_CONTROL_AS_SYSTEM_OID); + if (as_system != NULL) { + as_system->critical = 0; + } + + if (dsdb_module_am_system(module) || as_system) { + return ldb_next_request(module, req); + } + + DEBUG(10, ("ldb:acl_delete: %s\n", ldb_dn_get_linearized(req->op.del.dn))); + + ldb = ldb_module_get_ctx(module); + + parent = ldb_dn_get_parent(req, req->op.del.dn); + if (parent == NULL) { + return ldb_oom(ldb); + } + + /* Make sure we aren't deleting a NC */ + + ret = dsdb_find_nc_root(ldb, req, req->op.del.dn, &nc_root); + if (ret != LDB_SUCCESS) { + return ret; + } + if (ldb_dn_compare(nc_root, req->op.del.dn) == 0) { + talloc_free(nc_root); + DEBUG(10,("acl:deleting a NC\n")); + /* Windows returns "ERR_UNWILLING_TO_PERFORM */ + return ldb_module_done(req, NULL, NULL, + LDB_ERR_UNWILLING_TO_PERFORM); + } + talloc_free(nc_root); + + ret = dsdb_module_search_dn(module, req, &acl_res, + req->op.del.dn, acl_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED, req); + /* we sould be able to find the parent */ + if (ret != LDB_SUCCESS) { + DEBUG(10,("acl: failed to find object %s\n", + ldb_dn_get_linearized(req->op.rename.olddn))); + return ret; + } + + ret = dsdb_get_sd_from_ldb_message(ldb, req, acl_res->msgs[0], &sd); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + if (!sd) { + return ldb_operr(ldb); + } + + schema = dsdb_get_schema(ldb, req); + if (!schema) { + return ldb_operr(ldb); + } + + sid = samdb_result_dom_sid(req, acl_res->msgs[0], "objectSid"); + + objectclass = dsdb_get_structural_oc_from_msg(schema, acl_res->msgs[0]); + if (!objectclass) { + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "acl_modify: Error retrieving object class for GUID."); + } + + if (ldb_request_get_control(req, LDB_CONTROL_TREE_DELETE_OID)) { + ret = acl_check_access_on_objectclass(module, req, sd, sid, + SEC_ADS_DELETE_TREE, + objectclass); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, req); + } + + /* First check if we have delete object right */ + ret = acl_check_access_on_objectclass(module, req, sd, sid, + SEC_STD_DELETE, + objectclass); + if (ret == LDB_SUCCESS) { + return ldb_next_request(module, req); + } + + /* Nope, we don't have delete object. Lets check if we have delete + * child on the parent */ + ret = dsdb_module_check_access_on_dn(module, req, parent, + SEC_ADS_DELETE_CHILD, + &objectclass->schemaIDGUID, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, req); +} +static int acl_check_reanimate_tombstone(TALLOC_CTX *mem_ctx, + struct ldb_module *module, + struct ldb_request *req, + struct ldb_dn *nc_root) +{ + int ret; + struct ldb_result *acl_res; + struct security_descriptor *sd = NULL; + struct dom_sid *sid = NULL; + const struct dsdb_schema *schema = NULL; + const struct dsdb_class *objectclass = NULL; + struct ldb_context *ldb = ldb_module_get_ctx(module); + static const char *acl_attrs[] = { + "nTSecurityDescriptor", + "objectClass", + "objectSid", + NULL + }; + + ret = dsdb_module_search_dn(module, mem_ctx, &acl_res, + nc_root, acl_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED, req); + if (ret != LDB_SUCCESS) { + DEBUG(10,("acl: failed to find object %s\n", + ldb_dn_get_linearized(nc_root))); + return ret; + } + + ret = dsdb_get_sd_from_ldb_message(mem_ctx, req, acl_res->msgs[0], &sd); + sid = samdb_result_dom_sid(mem_ctx, acl_res->msgs[0], "objectSid"); + schema = dsdb_get_schema(ldb, req); + if (!schema) { + return LDB_ERR_OPERATIONS_ERROR; + } + objectclass = dsdb_get_structural_oc_from_msg(schema, acl_res->msgs[0]); + if (ret != LDB_SUCCESS || !sd) { + return ldb_operr(ldb_module_get_ctx(module)); + } + return acl_check_extended_right(mem_ctx, + module, + req, + objectclass, + sd, + acl_user_token(module), + GUID_DRS_REANIMATE_TOMBSTONE, + SEC_ADS_CONTROL_ACCESS, sid); +} + +static int acl_rename(struct ldb_module *module, struct ldb_request *req) +{ + int ret; + struct ldb_dn *oldparent; + struct ldb_dn *newparent; + const struct dsdb_schema *schema; + const struct dsdb_class *objectclass; + const struct dsdb_attribute *attr = NULL; + struct ldb_context *ldb; + struct security_descriptor *sd = NULL; + struct dom_sid *sid = NULL; + struct ldb_result *acl_res; + struct ldb_dn *nc_root; + struct ldb_control *as_system; + struct ldb_control *is_undelete; + TALLOC_CTX *tmp_ctx; + const char *rdn_name; + static const char *acl_attrs[] = { + "nTSecurityDescriptor", + "objectClass", + "objectSid", + NULL + }; + + if (ldb_dn_is_special(req->op.rename.olddn)) { + return ldb_next_request(module, req); + } + + as_system = ldb_request_get_control(req, LDB_CONTROL_AS_SYSTEM_OID); + if (as_system != NULL) { + as_system->critical = 0; + } + + DEBUG(10, ("ldb:acl_rename: %s\n", ldb_dn_get_linearized(req->op.rename.olddn))); + if (dsdb_module_am_system(module) || as_system) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + tmp_ctx = talloc_new(req); + if (tmp_ctx == NULL) { + return ldb_oom(ldb); + } + + oldparent = ldb_dn_get_parent(tmp_ctx, req->op.rename.olddn); + if (oldparent == NULL) { + return ldb_oom(ldb); + } + newparent = ldb_dn_get_parent(tmp_ctx, req->op.rename.newdn); + if (newparent == NULL) { + return ldb_oom(ldb); + } + + /* Make sure we aren't renaming/moving a NC */ + + ret = dsdb_find_nc_root(ldb, req, req->op.rename.olddn, &nc_root); + if (ret != LDB_SUCCESS) { + return ret; + } + if (ldb_dn_compare(nc_root, req->op.rename.olddn) == 0) { + talloc_free(nc_root); + DEBUG(10,("acl:renaming/moving a NC\n")); + /* Windows returns "ERR_UNWILLING_TO_PERFORM */ + return ldb_module_done(req, NULL, NULL, + LDB_ERR_UNWILLING_TO_PERFORM); + } + + /* special check for undelete operation */ + is_undelete = ldb_request_get_control(req, DSDB_CONTROL_RESTORE_TOMBSTONE_OID); + if (is_undelete != NULL) { + is_undelete->critical = 0; + ret = acl_check_reanimate_tombstone(tmp_ctx, module, req, nc_root); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + talloc_free(nc_root); + + /* Look for the parent */ + + ret = dsdb_module_search_dn(module, tmp_ctx, &acl_res, + req->op.rename.olddn, acl_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED, req); + /* we sould be able to find the parent */ + if (ret != LDB_SUCCESS) { + DEBUG(10,("acl: failed to find object %s\n", + ldb_dn_get_linearized(req->op.rename.olddn))); + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_get_sd_from_ldb_message(ldb, req, acl_res->msgs[0], &sd); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + if (!sd) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + schema = dsdb_get_schema(ldb, acl_res); + if (!schema) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + sid = samdb_result_dom_sid(req, acl_res->msgs[0], "objectSid"); + + objectclass = dsdb_get_structural_oc_from_msg(schema, acl_res->msgs[0]); + if (!objectclass) { + talloc_free(tmp_ctx); + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "acl_modify: Error retrieving object class for GUID."); + } + + attr = dsdb_attribute_by_lDAPDisplayName(schema, "name"); + if (attr == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + ret = acl_check_access_on_attribute(module, tmp_ctx, sd, sid, + SEC_ADS_WRITE_PROP, + attr, objectclass); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "Object %s has no wp on %s\n", + ldb_dn_get_linearized(req->op.rename.olddn), + attr->lDAPDisplayName); + dsdb_acl_debug(sd, + acl_user_token(module), + req->op.rename.olddn, + true, + 10); + talloc_free(tmp_ctx); + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + + rdn_name = ldb_dn_get_rdn_name(req->op.rename.olddn); + if (rdn_name == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + attr = dsdb_attribute_by_lDAPDisplayName(schema, rdn_name); + if (attr == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + ret = acl_check_access_on_attribute(module, tmp_ctx, sd, sid, + SEC_ADS_WRITE_PROP, + attr, objectclass); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "Object %s has no wp on %s\n", + ldb_dn_get_linearized(req->op.rename.olddn), + attr->lDAPDisplayName); + dsdb_acl_debug(sd, + acl_user_token(module), + req->op.rename.olddn, + true, + 10); + talloc_free(tmp_ctx); + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + + if (ldb_dn_compare(oldparent, newparent) == 0) { + /* regular rename, not move, nothing more to do */ + talloc_free(tmp_ctx); + return ldb_next_request(module, req); + } + + /* new parent should have create child */ + ret = dsdb_module_check_access_on_dn(module, req, newparent, + SEC_ADS_CREATE_CHILD, + &objectclass->schemaIDGUID, req); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "acl:access_denied renaming %s", + ldb_dn_get_linearized(req->op.rename.olddn)); + talloc_free(tmp_ctx); + return ret; + } + + /* do we have delete object on the object? */ + /* this access is not necessary for undelete ops */ + if (is_undelete == NULL) { + ret = acl_check_access_on_objectclass(module, tmp_ctx, sd, sid, + SEC_STD_DELETE, + objectclass); + if (ret == LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ldb_next_request(module, req); + } + /* what about delete child on the current parent */ + ret = dsdb_module_check_access_on_dn(module, req, oldparent, + SEC_ADS_DELETE_CHILD, + &objectclass->schemaIDGUID, + req); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "acl:access_denied renaming %s", ldb_dn_get_linearized(req->op.rename.olddn)); + talloc_free(tmp_ctx); + return ldb_module_done(req, NULL, NULL, ret); + } + } + talloc_free(tmp_ctx); + + return ldb_next_request(module, req); +} + +static int acl_search_update_confidential_attrs(struct acl_context *ac, + struct acl_private *data) +{ + struct dsdb_attribute *a; + uint32_t n = 0; + + if (data->acl_search) { + /* + * If acl:search is activated, the acl_read module + * protects confidential attributes. + */ + return LDB_SUCCESS; + } + + if ((ac->schema == data->cached_schema_ptr) && + (ac->schema->metadata_usn == data->cached_schema_metadata_usn)) + { + return LDB_SUCCESS; + } + + data->cached_schema_ptr = NULL; + data->cached_schema_loaded_usn = 0; + data->cached_schema_metadata_usn = 0; + TALLOC_FREE(data->confidential_attrs); + + if (ac->schema == NULL) { + return LDB_SUCCESS; + } + + for (a = ac->schema->attributes; a; a = a->next) { + const char **attrs = data->confidential_attrs; + + if (!(a->searchFlags & SEARCH_FLAG_CONFIDENTIAL)) { + continue; + } + + attrs = talloc_realloc(data, attrs, const char *, n + 2); + if (attrs == NULL) { + TALLOC_FREE(data->confidential_attrs); + return ldb_module_oom(ac->module); + } + + attrs[n] = a->lDAPDisplayName; + attrs[n+1] = NULL; + n++; + + data->confidential_attrs = attrs; + } + + data->cached_schema_ptr = ac->schema; + data->cached_schema_metadata_usn = ac->schema->metadata_usn; + + return LDB_SUCCESS; +} + +static int acl_search_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct acl_context *ac; + struct acl_private *data; + struct ldb_result *acl_res; + static const char *acl_attrs[] = { + "objectClass", + "nTSecurityDescriptor", + "objectSid", + NULL + }; + int ret; + unsigned int i; + + ac = talloc_get_type(req->context, struct acl_context); + data = talloc_get_type(ldb_module_get_private(ac->module), struct acl_private); + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + if (ac->constructed_attrs) { + ret = dsdb_module_search_dn(ac->module, ac, &acl_res, ares->message->dn, + acl_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED, + req); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + } + + if (ac->allowedAttributes || ac->allowedAttributesEffective) { + ret = acl_allowedAttributes(ac->module, ac->schema, + acl_res->msgs[0], + ares->message, ac); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + } + + if (ac->allowedChildClasses) { + ret = acl_childClasses(ac->module, ac->schema, + acl_res->msgs[0], + ares->message, + "allowedChildClasses"); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + } + + if (ac->allowedChildClassesEffective) { + ret = acl_childClassesEffective(ac->module, ac->schema, + acl_res->msgs[0], + ares->message, ac); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + } + + if (ac->sDRightsEffective) { + ret = acl_sDRightsEffective(ac->module, + acl_res->msgs[0], + ares->message, ac); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + } + + if (data == NULL) { + return ldb_module_send_entry(ac->req, ares->message, + ares->controls); + } + + if (ac->am_system) { + return ldb_module_send_entry(ac->req, ares->message, + ares->controls); + } + + if (ac->am_administrator) { + return ldb_module_send_entry(ac->req, ares->message, + ares->controls); + } + + if (data->confidential_attrs != NULL) { + for (i = 0; data->confidential_attrs[i]; i++) { + ldb_msg_remove_attr(ares->message, + data->confidential_attrs[i]); + } + } + + return ldb_module_send_entry(ac->req, ares->message, ares->controls); + + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral(ac->req, ares->referral); + + case LDB_REPLY_DONE: + return ldb_module_done(ac->req, ares->controls, + ares->response, LDB_SUCCESS); + + } + return LDB_SUCCESS; +} + +static int acl_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct acl_context *ac; + struct ldb_parse_tree *down_tree = req->op.search.tree; + struct ldb_request *down_req; + struct acl_private *data; + int ret; + unsigned int i; + bool modify_search = true; + + if (ldb_dn_is_special(req->op.search.base)) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + ac = talloc_zero(req, struct acl_context); + if (ac == NULL) { + return ldb_oom(ldb); + } + data = talloc_get_type(ldb_module_get_private(module), struct acl_private); + + ac->module = module; + ac->req = req; + ac->am_system = dsdb_module_am_system(module); + ac->am_administrator = dsdb_module_am_administrator(module); + ac->constructed_attrs = false; + ac->allowedAttributes = ldb_attr_in_list(req->op.search.attrs, "allowedAttributes"); + ac->allowedAttributesEffective = ldb_attr_in_list(req->op.search.attrs, "allowedAttributesEffective"); + ac->allowedChildClasses = ldb_attr_in_list(req->op.search.attrs, "allowedChildClasses"); + ac->allowedChildClassesEffective = ldb_attr_in_list(req->op.search.attrs, "allowedChildClassesEffective"); + ac->sDRightsEffective = ldb_attr_in_list(req->op.search.attrs, "sDRightsEffective"); + ac->schema = dsdb_get_schema(ldb, ac); + + ac->constructed_attrs |= ac->allowedAttributes; + ac->constructed_attrs |= ac->allowedChildClasses; + ac->constructed_attrs |= ac->allowedChildClassesEffective; + ac->constructed_attrs |= ac->allowedAttributesEffective; + ac->constructed_attrs |= ac->sDRightsEffective; + + if (data == NULL) { + modify_search = false; + } + if (ac->am_system) { + modify_search = false; + } + + if (!ac->constructed_attrs && !modify_search) { + talloc_free(ac); + return ldb_next_request(module, req); + } + + data = talloc_get_type(ldb_module_get_private(ac->module), struct acl_private); + if (data == NULL) { + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "acl_private data is missing"); + } + + if (!ac->am_system && !ac->am_administrator) { + ret = acl_search_update_confidential_attrs(ac, data); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (data->confidential_attrs != NULL) { + down_tree = ldb_parse_tree_copy_shallow(ac, req->op.search.tree); + if (down_tree == NULL) { + return ldb_oom(ldb); + } + + for (i = 0; data->confidential_attrs[i]; i++) { + ldb_parse_tree_attr_replace(down_tree, + data->confidential_attrs[i], + "kludgeACLredactedattribute"); + } + } + } + + ret = ldb_build_search_req_ex(&down_req, + ldb, ac, + req->op.search.base, + req->op.search.scope, + down_tree, + req->op.search.attrs, + req->controls, + ac, acl_search_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + /* perform the search */ + return ldb_next_request(module, down_req); +} + +static int acl_extended(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_control *as_system = ldb_request_get_control(req, LDB_CONTROL_AS_SYSTEM_OID); + + /* allow everybody to read the sequence number */ + if (strcmp(req->op.extended.oid, + LDB_EXTENDED_SEQUENCE_NUMBER) == 0) { + return ldb_next_request(module, req); + } + + if (dsdb_module_am_system(module) || + dsdb_module_am_administrator(module) || as_system) { + return ldb_next_request(module, req); + } else { + ldb_asprintf_errstring(ldb, + "acl_extended: " + "attempted database modify not permitted. " + "User %s is not SYSTEM or an administrator", + acl_user_name(req, module)); + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } +} + +static const struct ldb_module_ops ldb_acl_module_ops = { + .name = "acl", + .search = acl_search, + .add = acl_add, + .modify = acl_modify, + .del = acl_delete, + .rename = acl_rename, + .extended = acl_extended, + .init_context = acl_module_init +}; + +int ldb_acl_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_acl_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/acl_read.c b/source4/dsdb/samdb/ldb_modules/acl_read.c new file mode 100644 index 0000000..21f72fb --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/acl_read.c @@ -0,0 +1,1293 @@ +/* + ldb database library + + Copyright (C) Simo Sorce 2006-2008 + Copyright (C) Nadezhda Ivanova 2010 + + 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 ACL Read module + * + * Description: Module that performs authorisation access checks on read requests + * Only DACL checks implemented at this point + * + * Author: Nadezhda Ivanova + */ + +#include "includes.h" +#include "ldb_module.h" +#include "auth/auth.h" +#include "libcli/security/security.h" +#include "dsdb/samdb/samdb.h" +#include "librpc/gen_ndr/ndr_security.h" +#include "param/param.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "lib/util/binsearch.h" + +#undef strcasecmp + +struct ldb_attr_vec { + const char** attrs; + size_t len; + size_t capacity; +}; + +struct aclread_context { + struct ldb_module *module; + struct ldb_request *req; + const struct dsdb_schema *schema; + uint32_t sd_flags; + bool added_nTSecurityDescriptor; + bool added_instanceType; + bool added_objectSid; + bool added_objectClass; + + bool do_list_object_initialized; + bool do_list_object; + bool base_invisible; + uint64_t num_entries; + + /* cache on the last parent we checked in this search */ + struct ldb_dn *last_parent_dn; + int last_parent_check_ret; + + bool am_administrator; + + bool got_tree_attrs; + struct ldb_attr_vec tree_attrs; +}; + +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; + const char **password_attrs; + size_t num_password_attrs; +}; + +struct access_check_context { + struct security_descriptor *sd; + struct dom_sid sid_buf; + const struct dom_sid *sid; + const struct dsdb_class *objectclass; +}; + +static void acl_element_mark_access_checked(struct ldb_message_element *el) +{ + el->flags |= LDB_FLAG_INTERNAL_ACCESS_CHECKED; +} + +static bool acl_element_is_access_checked(const struct ldb_message_element *el) +{ + return (el->flags & LDB_FLAG_INTERNAL_ACCESS_CHECKED) != 0; +} + +static bool attr_in_vec(const struct ldb_attr_vec *vec, const char *attr) +{ + const char **found = NULL; + + if (vec == NULL) { + return false; + } + + BINARY_ARRAY_SEARCH_V(vec->attrs, + vec->len, + attr, + ldb_attr_cmp, + found); + return found != NULL; +} + +static int acl_attr_cmp_fn(const char *a, const char **b) +{ + return ldb_attr_cmp(a, *b); +} + +static int attr_vec_add_unique(TALLOC_CTX *mem_ctx, + struct ldb_attr_vec *vec, + const char *attr) +{ + const char **exact = NULL; + const char **next = NULL; + size_t next_idx = 0; + + BINARY_ARRAY_SEARCH_GTE(vec->attrs, + vec->len, + attr, + acl_attr_cmp_fn, + exact, + next); + if (exact != NULL) { + return LDB_SUCCESS; + } + + if (vec->len == SIZE_MAX) { + return LDB_ERR_OPERATIONS_ERROR; + } + + if (next != NULL) { + next_idx = next - vec->attrs; + } + + if (vec->len >= vec->capacity) { + const char **attrs = NULL; + + if (vec->capacity == 0) { + vec->capacity = 4; + } else { + if (vec->capacity > SIZE_MAX / 2) { + return LDB_ERR_OPERATIONS_ERROR; + } + vec->capacity *= 2; + } + + attrs = talloc_realloc(mem_ctx, vec->attrs, const char *, vec->capacity); + if (attrs == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + vec->attrs = attrs; + } + SMB_ASSERT(vec->len < vec->capacity); + + if (next == NULL) { + vec->attrs[vec->len++] = attr; + } else { + size_t count = (vec->len - next_idx) * sizeof (vec->attrs[0]); + memmove(&vec->attrs[next_idx + 1], + &vec->attrs[next_idx], + count); + + vec->attrs[next_idx] = attr; + ++vec->len; + } + + return LDB_SUCCESS; +} + +static bool ldb_attr_always_present(const char *attr) +{ + static const char * const attrs_always_present[] = { + "objectClass", + "distinguishedName", + "name", + "objectGUID", + NULL + }; + + return ldb_attr_in_list(attrs_always_present, attr); +} + +static bool ldb_attr_always_visible(const char *attr) +{ + static const char * const attrs_always_visible[] = { + "isDeleted", + "isRecycled", + NULL + }; + + return ldb_attr_in_list(attrs_always_visible, attr); +} + +/* Collect a list of attributes required to match a given parse tree. */ +static int ldb_parse_tree_collect_acl_attrs(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_attr_vec *attrs, + const struct ldb_parse_tree *tree) +{ + const char *attr = NULL; + unsigned int i; + int ret; + + if (tree == NULL) { + return 0; + } + + switch (tree->operation) { + case LDB_OP_OR: + case LDB_OP_AND: /* attributes stored in list of subtrees */ + for (i = 0; i < tree->u.list.num_elements; i++) { + ret = ldb_parse_tree_collect_acl_attrs(module, mem_ctx, + attrs, tree->u.list.elements[i]); + if (ret) { + return ret; + } + } + return 0; + + case LDB_OP_NOT: /* attributes stored in single subtree */ + return ldb_parse_tree_collect_acl_attrs(module, mem_ctx, attrs, tree->u.isnot.child); + + case LDB_OP_PRESENT: + /* + * If the search filter is checking for an attribute's presence, + * and the attribute is always present, we can skip access + * rights checks. Every object has these attributes, and so + * there's no security reason to hide their presence. + * Note: the acl.py tests (e.g. test_search1()) rely on this + * exception. I.e. even if we lack Read Property (RP) rights + * for a child object, it should still appear as a visible + * object in 'objectClass=*' searches, so long as we have List + * Contents (LC) rights for the object. + */ + if (ldb_attr_always_present(tree->u.present.attr)) { + /* No need to check this attribute. */ + return 0; + } + + FALL_THROUGH; + case LDB_OP_EQUALITY: + if (ldb_attr_always_visible(tree->u.present.attr)) { + /* No need to check this attribute. */ + return 0; + } + + FALL_THROUGH; + default: /* single attribute in tree */ + attr = ldb_parse_tree_get_attr(tree); + return attr_vec_add_unique(mem_ctx, attrs, attr); + } +} + +/* + * 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; +} + +static int aclread_check_object_visible(struct aclread_context *ac, + struct ldb_message *msg, + struct ldb_request *req) +{ + uint32_t instanceType; + int ret; + + /* get the object instance type */ + instanceType = ldb_msg_find_attr_as_uint(msg, + "instanceType", 0); + if (instanceType & INSTANCE_TYPE_IS_NC_HEAD) { + /* + * NC_HEAD objects are always visible + */ + return LDB_SUCCESS; + } + + ret = aclread_check_parent(ac, msg, req); + if (ret == LDB_SUCCESS) { + /* + * SEC_ADS_LIST (List Children) alone + * on the parent is enough to make the + * object visible. + */ + return LDB_SUCCESS; + } + if (ret != LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) { + return ret; + } + + if (!ac->do_list_object_initialized) { + /* + * We only call dsdb_do_list_object() once + * and only when needed in order to + * check the dSHeuristics for fDoListObject. + */ + ac->do_list_object = dsdb_do_list_object(ac->module, ac, req); + ac->do_list_object_initialized = true; + } + + if (ac->do_list_object) { + TALLOC_CTX *frame = talloc_stackframe(); + struct ldb_dn *parent_dn = NULL; + + /* + * Here we're in "List Object" mode (fDoListObject=true). + * + * If SEC_ADS_LIST (List Children) is not + * granted on the parent, we need to check if + * SEC_ADS_LIST_OBJECT (List Object) is granted + * on the parent and also on the object itself. + * + * We could optimize this similar to aclread_check_parent(), + * but that would require quite a bit of restructuring, + * so that we cache the granted access bits instead + * of just the result for 'SEC_ADS_LIST (List Children)'. + * + * But as this is the uncommon case and + * 'SEC_ADS_LIST (List Children)' is most likely granted + * on most of the objects, we'll just implement what + * we have to. + */ + + parent_dn = ldb_dn_get_parent(frame, 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_OBJECT, + NULL, req); + if (ret != LDB_SUCCESS) { + TALLOC_FREE(frame); + return ret; + } + ret = dsdb_module_check_access_on_dn(ac->module, + frame, + msg->dn, + SEC_ADS_LIST_OBJECT, + NULL, req); + if (ret != LDB_SUCCESS) { + TALLOC_FREE(frame); + return ret; + } + + TALLOC_FREE(frame); + return LDB_SUCCESS; + } + + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; +} + +/* + * 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, + const 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_abort(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); + 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; +} + +/* Check whether the attribute is a password attribute. */ +static bool attr_is_secret(const char *attr, const struct aclread_private *private_data) +{ + const char **found = NULL; + + if (private_data->password_attrs == NULL) { + return false; + } + + BINARY_ARRAY_SEARCH_V(private_data->password_attrs, + private_data->num_password_attrs, + attr, + ldb_attr_cmp, + found); + return found != NULL; +} + +/* + * Returns the access mask required to read a given attribute + */ +static uint32_t get_attr_access_mask(const struct dsdb_attribute *attr, + uint32_t sd_flags) +{ + + uint32_t access_mask = 0; + bool is_sd; + + /* nTSecurityDescriptor is a special case */ + is_sd = (ldb_attr_cmp("nTSecurityDescriptor", + attr->lDAPDisplayName) == 0); + + if (is_sd) { + if (sd_flags & (SECINFO_OWNER|SECINFO_GROUP)) { + access_mask |= SEC_STD_READ_CONTROL; + } + if (sd_flags & SECINFO_DACL) { + access_mask |= SEC_STD_READ_CONTROL; + } + if (sd_flags & SECINFO_SACL) { + access_mask |= SEC_FLAG_SYSTEM_SECURITY; + } + } else { + access_mask = SEC_ADS_READ_PROP; + } + + if (attr->searchFlags & SEARCH_FLAG_CONFIDENTIAL) { + access_mask |= SEC_ADS_CONTROL_ACCESS; + } + + return access_mask; +} + +/* + * Checks that the user has sufficient access rights to view an attribute, else + * marks it as inaccessible. + */ +static int acl_redact_attr(TALLOC_CTX *mem_ctx, + struct ldb_message_element *el, + struct aclread_context *ac, + const struct aclread_private *private_data, + const struct ldb_message *msg, + const struct dsdb_schema *schema, + const struct security_descriptor *sd, + const struct dom_sid *sid, + const struct dsdb_class *objectclass) +{ + int ret; + const struct dsdb_attribute *attr = NULL; + uint32_t access_mask; + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + + if (attr_is_secret(el->name, private_data)) { + ldb_msg_element_mark_inaccessible(el); + return LDB_SUCCESS; + } + + /* Look up the attribute in the schema. */ + attr = dsdb_attribute_by_lDAPDisplayName(schema, el->name); + if (!attr) { + ldb_debug_set(ldb, + LDB_DEBUG_FATAL, + "acl_read: %s cannot find attr[%s] in schema\n", + ldb_dn_get_linearized(msg->dn), el->name); + return LDB_ERR_OPERATIONS_ERROR; + } + + access_mask = get_attr_access_mask(attr, ac->sd_flags); + if (access_mask == 0) { + DBG_ERR("Could not determine access mask for attribute %s\n", + el->name); + ldb_msg_element_mark_inaccessible(el); + return LDB_SUCCESS; + } + + /* We must check whether the user has rights to view the attribute. */ + + ret = acl_check_access_on_attribute(ac->module, mem_ctx, sd, sid, + access_mask, attr, objectclass); + + if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) { + ldb_msg_element_mark_inaccessible(el); + } 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), el->name, + ldb_strerror(ret), ldb_errstring(ldb)); + return ret; + } + + return LDB_SUCCESS; +} + +static int setup_access_check_context(struct aclread_context *ac, + const struct ldb_message *msg, + struct access_check_context *ctx) +{ + int ret; + + /* + * Fetch the schema so we can check which attributes are + * considered confidential. + */ + if (ac->schema == NULL) { + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + + /* Cache the schema for later use. */ + ac->schema = dsdb_get_schema(ldb, ac); + + if (ac->schema == NULL) { + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "aclread_callback: Error obtaining schema."); + } + } + + /* Fetch the object's security descriptor. */ + ret = aclread_get_sd_from_ldb_message(ac, msg, &ctx->sd); + if (ret != LDB_SUCCESS) { + ldb_debug_set(ldb_module_get_ctx(ac->module), LDB_DEBUG_FATAL, + "acl_read: cannot get descriptor of %s: %s\n", + ldb_dn_get_linearized(msg->dn), ldb_strerror(ret)); + return LDB_ERR_OPERATIONS_ERROR; + } else if (ctx->sd == NULL) { + ldb_debug_set(ldb_module_get_ctx(ac->module), LDB_DEBUG_FATAL, + "acl_read: cannot get descriptor of %s (attribute not found)\n", + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_OPERATIONS_ERROR; + } + /* + * Get the most specific structural object class for the ACL check + */ + ctx->objectclass = dsdb_get_structural_oc_from_msg(ac->schema, msg); + if (ctx->objectclass == NULL) { + ldb_asprintf_errstring(ldb_module_get_ctx(ac->module), + "acl_read: Failed to find a structural class for %s", + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* Fetch the object's SID. */ + ret = samdb_result_dom_sid_buf(msg, "objectSid", &ctx->sid_buf); + if (ret == LDB_SUCCESS) { + ctx->sid = &ctx->sid_buf; + } else if (ret == LDB_ERR_NO_SUCH_ATTRIBUTE) { + /* This is expected. */ + ctx->sid = NULL; + } else { + ldb_asprintf_errstring(ldb_module_get_ctx(ac->module), + "acl_read: Failed to parse objectSid as dom_sid for %s", + ldb_dn_get_linearized(msg->dn)); + return ret; + } + + return LDB_SUCCESS; +} + +/* + * Whether this attribute was added to perform access checks and must be + * removed. + */ +static bool should_remove_attr(const char *attr, const struct aclread_context *ac) +{ + if (ac->added_nTSecurityDescriptor && + ldb_attr_cmp("nTSecurityDescriptor", attr) == 0) + { + return true; + } + + if (ac->added_objectSid && + ldb_attr_cmp("objectSid", attr) == 0) + { + return true; + } + + if (ac->added_instanceType && + ldb_attr_cmp("instanceType", attr) == 0) + { + return true; + } + + if (ac->added_objectClass && + ldb_attr_cmp("objectClass", attr) == 0) + { + return true; + } + + return false; +} + +static int aclread_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct aclread_context *ac; + struct aclread_private *private_data = NULL; + struct ldb_message *msg; + int ret; + unsigned int i; + struct access_check_context acl_ctx; + + ac = talloc_get_type_abort(req->context, struct aclread_context); + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR ); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + switch (ares->type) { + case LDB_REPLY_ENTRY: + msg = ares->message; + + if (!ldb_dn_is_null(msg->dn)) { + /* + * this is a real object, so we have + * to check for visibility + */ + ret = aclread_check_object_visible(ac, msg, req); + if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) { + return LDB_SUCCESS; + } else if (ret != LDB_SUCCESS) { + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + 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)); + return ldb_module_done(ac->req, NULL, NULL, ret); + } + } + + /* for every element in the message check RP */ + for (i = 0; i < msg->num_elements; ++i) { + struct ldb_message_element *el = &msg->elements[i]; + + /* Remove attributes added to perform access checks. */ + if (should_remove_attr(el->name, ac)) { + ldb_msg_element_mark_inaccessible(el); + continue; + } + + if (acl_element_is_access_checked(el)) { + /* We will have already checked this attribute. */ + continue; + } + + /* + * We need to fetch the security descriptor to check + * this attribute. + */ + break; + } + + if (i == msg->num_elements) { + /* All elements have been checked. */ + goto reply_entry_done; + } + + ret = setup_access_check_context(ac, msg, &acl_ctx); + if (ret != LDB_SUCCESS) { + return ret; + } + + private_data = talloc_get_type_abort(ldb_module_get_private(ac->module), + struct aclread_private); + + for (/* begin where we left off */; i < msg->num_elements; ++i) { + struct ldb_message_element *el = &msg->elements[i]; + + /* Remove attributes added to perform access checks. */ + if (should_remove_attr(el->name, ac)) { + ldb_msg_element_mark_inaccessible(el); + continue; + } + + if (acl_element_is_access_checked(el)) { + /* We will have already checked this attribute. */ + continue; + } + + /* + * We need to check whether the attribute is secret, + * confidential, or access-controlled. + */ + ret = acl_redact_attr(ac, + el, + ac, + private_data, + msg, + ac->schema, + acl_ctx.sd, + acl_ctx.sid, + acl_ctx.objectclass); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + } + + reply_entry_done: + ldb_msg_remove_inaccessible(msg); + + ac->num_entries++; + return ldb_module_send_entry(ac->req, msg, ares->controls); + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral(ac->req, ares->referral); + case LDB_REPLY_DONE: + if (ac->base_invisible && ac->num_entries == 0) { + /* + * If the base is invisible and we didn't + * returned any object, we need to return + * NO_SUCH_OBJECT. + */ + return ldb_module_done(ac->req, + NULL, NULL, + LDB_ERR_NO_SUCH_OBJECT); + } + return ldb_module_done(ac->req, ares->controls, + ares->response, LDB_SUCCESS); + + } + return LDB_SUCCESS; +} + + +static int aclread_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + int ret; + struct aclread_context *ac; + struct ldb_request *down_req; + bool am_system; + 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; + static const char *acl_attrs[] = { + "instanceType", + NULL + }; + + ldb = ldb_module_get_ctx(module); + p = talloc_get_type(ldb_module_get_private(module), struct aclread_private); + + am_system = ldb_request_get_control(req, LDB_CONTROL_AS_SYSTEM_OID) != NULL; + if (!am_system) { + am_system = dsdb_module_am_system(module); + } + + /* skip access checks if we are system or system control is supplied + * or this is not LDAP server request */ + if (!p || !p->enabled || + am_system || + !is_untrusted) { + return ldb_next_request(module, req); + } + /* no checks on special dn */ + if (ldb_dn_is_special(req->op.search.base)) { + return ldb_next_request(module, req); + } + + ac = talloc_zero(req, struct aclread_context); + if (ac == NULL) { + return ldb_oom(ldb); + } + ac->module = module; + ac->req = req; + + attrs = req->op.search.attrs; + if (attrs == 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_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")) { + 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 (need_sd) { + attrs = ldb_attr_list_copy_add(ac, attrs, "nTSecurityDescriptor"); + if (attrs == NULL) { + return ldb_oom(ldb); + } + ac->added_nTSecurityDescriptor = true; + } + + ac->am_administrator = dsdb_module_am_administrator(module); + + /* check accessibility of base */ + if (!ldb_dn_is_null(req->op.search.base)) { + ret = dsdb_module_search_dn(module, req, &res, req->op.search.base, + acl_attrs, + DSDB_FLAG_NEXT_MODULE | + 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."); + } + ret = aclread_check_object_visible(ac, res->msgs[0], req); + if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) { + if (req->op.search.scope == LDB_SCOPE_BASE) { + return ldb_module_done(req, NULL, NULL, + LDB_ERR_NO_SUCH_OBJECT); + } + /* + * Defer LDB_ERR_NO_SUCH_OBJECT, + * we may return sub objects + */ + ac->base_invisible = true; + } else if (ret != LDB_SUCCESS) { + return ldb_module_done(req, NULL, NULL, ret); + } + } + + ret = ldb_build_search_req_ex(&down_req, + ldb, ac, + req->op.search.base, + req->op.search.scope, + req->op.search.tree, + attrs, + req->controls, + ac, aclread_callback, + req); + + if (ret != LDB_SUCCESS) { + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * We provide 'ac' as the control value, which is then used by the + * callback to avoid double-work. + */ + ret = ldb_request_add_control(down_req, DSDB_CONTROL_ACL_READ_OID, false, ac); + if (ret != LDB_SUCCESS) { + return ldb_error(ldb, ret, + "acl_read: Error adding acl_read control."); + } + + return ldb_next_request(module, down_req); +} + +/* + * Here we mark inaccessible attributes known to be looked for in the + * filter. This only redacts attributes found in the search expression. If any + * extended attribute match rules examine different attributes without their own + * access control checks, a security bypass is possible. + */ +static int acl_redact_msg_for_filter(struct ldb_module *module, struct ldb_request *req, struct ldb_message *msg) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + const struct aclread_private *private_data = NULL; + struct ldb_control *control = NULL; + struct aclread_context *ac = NULL; + struct access_check_context acl_ctx; + int ret; + unsigned i; + + /* + * The private data contains a list of attributes which are to be + * considered secret. + */ + private_data = talloc_get_type(ldb_module_get_private(module), struct aclread_private); + if (private_data == NULL) { + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "aclread_private data is missing"); + } + if (!private_data->enabled) { + return LDB_SUCCESS; + } + + control = ldb_request_get_control(req, DSDB_CONTROL_ACL_READ_OID); + if (control == NULL) { + /* + * We've bypassed the acl_read module for this request, and + * should skip redaction in this case. + */ + return LDB_SUCCESS; + } + + ac = talloc_get_type_abort(control->data, struct aclread_context); + + if (!ac->got_tree_attrs) { + ret = ldb_parse_tree_collect_acl_attrs(module, ac, &ac->tree_attrs, req->op.search.tree); + if (ret != LDB_SUCCESS) { + return ret; + } + ac->got_tree_attrs = true; + } + + for (i = 0; i < msg->num_elements; ++i) { + struct ldb_message_element *el = &msg->elements[i]; + + /* Is the attribute mentioned in the search expression? */ + if (attr_in_vec(&ac->tree_attrs, el->name)) { + /* + * We need to fetch the security descriptor to check + * this element. + */ + break; + } + + /* + * This attribute is not in the search filter, so we can leave + * handling it till aclread_callback(), by which time we know + * this object is a match. This saves work checking ACLs if the + * search is unindexed and most objects don't match the filter. + */ + } + + if (i == msg->num_elements) { + /* All elements have been checked. */ + return LDB_SUCCESS; + } + + ret = setup_access_check_context(ac, msg, &acl_ctx); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* For every element in the message and the parse tree, check RP. */ + + for (/* begin where we left off */; i < msg->num_elements; ++i) { + struct ldb_message_element *el = &msg->elements[i]; + + /* Is the attribute mentioned in the search expression? */ + if (!attr_in_vec(&ac->tree_attrs, el->name)) { + /* + * If not, leave it for later and check the next + * attribute. + */ + continue; + } + + /* + * We need to check whether the attribute is secret, + * confidential, or access-controlled. + */ + ret = acl_redact_attr(ac, + el, + ac, + private_data, + msg, + ac->schema, + acl_ctx.sd, + acl_ctx.sid, + acl_ctx.objectclass); + if (ret != LDB_SUCCESS) { + return ret; + } + + acl_element_mark_access_checked(el); + } + + return LDB_SUCCESS; +} + +static int ldb_attr_cmp_fn(const void *_a, const void *_b) +{ + const char * const *a = _a; + const char * const *b = _b; + + return ldb_attr_cmp(*a, *b); +} + +static int aclread_init(struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + unsigned int i, n, j; + TALLOC_CTX *mem_ctx = NULL; + int ret; + bool userPassword_support; + static const char * const attrs[] = { "passwordAttribute", NULL }; + static const char * const secret_attrs[] = { + DSDB_SECRET_ATTRIBUTES + }; + struct ldb_result *res; + struct ldb_message *msg; + struct ldb_message_element *password_attributes; + struct aclread_private *p = talloc_zero(module, struct aclread_private); + if (p == NULL) { + return ldb_module_oom(module); + } + p->enabled = lpcfg_parm_bool(ldb_get_opaque(ldb, "loadparm"), NULL, "acl", "search", true); + + ret = ldb_mod_register_control(module, LDB_CONTROL_SD_FLAGS_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "acl_module_init: Unable to register sd_flags control with rootdse!\n"); + return ldb_operr(ldb); + } + + ldb_module_set_private(module, p); + + mem_ctx = talloc_new(module); + if (!mem_ctx) { + return ldb_oom(ldb); + } + + ret = dsdb_module_search_dn(module, mem_ctx, &res, + ldb_dn_new(mem_ctx, ldb, "@KLUDGEACL"), + attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM, + NULL); + if (ret != LDB_SUCCESS) { + goto done; + } + if (res->count == 0) { + goto done; + } + + if (res->count > 1) { + talloc_free(mem_ctx); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + msg = res->msgs[0]; + + password_attributes = ldb_msg_find_element(msg, "passwordAttribute"); + if (!password_attributes) { + goto done; + } + p->password_attrs = talloc_array(p, const char *, + password_attributes->num_values + + ARRAY_SIZE(secret_attrs)); + if (!p->password_attrs) { + talloc_free(mem_ctx); + return ldb_oom(ldb); + } + + n = 0; + for (i=0; i < password_attributes->num_values; i++) { + p->password_attrs[n] = (const char *)password_attributes->values[i].data; + talloc_steal(p->password_attrs, password_attributes->values[i].data); + n++; + } + + for (i=0; i < ARRAY_SIZE(secret_attrs); i++) { + bool found = false; + + for (j=0; j < n; j++) { + if (strcasecmp(p->password_attrs[j], secret_attrs[i]) == 0) { + found = true; + break; + } + } + + if (found) { + continue; + } + + p->password_attrs[n] = talloc_strdup(p->password_attrs, + secret_attrs[i]); + if (p->password_attrs[n] == NULL) { + talloc_free(mem_ctx); + return ldb_oom(ldb); + } + n++; + } + p->num_password_attrs = n; + + /* Sort the password attributes so we can use binary search. */ + TYPESAFE_QSORT(p->password_attrs, p->num_password_attrs, ldb_attr_cmp_fn); + + ret = ldb_register_redact_callback(ldb, acl_redact_msg_for_filter, module); + if (ret != LDB_SUCCESS) { + return ret; + } + +done: + talloc_free(mem_ctx); + ret = ldb_next_init(module); + + if (ret != LDB_SUCCESS) { + return ret; + } + + if (p->password_attrs != NULL) { + /* + * Check this after the modules have be initialised so we can + * actually read the backend DB. + */ + userPassword_support = dsdb_user_password_support(module, + module, + NULL); + if (!userPassword_support) { + const char **found = NULL; + + /* + * Remove the userPassword attribute, as it is not + * considered secret. + */ + BINARY_ARRAY_SEARCH_V(p->password_attrs, + p->num_password_attrs, + "userPassword", + ldb_attr_cmp, + found); + if (found != NULL) { + size_t found_idx = found - p->password_attrs; + + /* Shift following elements backwards by one. */ + for (i = found_idx; i < p->num_password_attrs - 1; ++i) { + p->password_attrs[i] = p->password_attrs[i + 1]; + } + --p->num_password_attrs; + } + } + } + return ret; +} + +static const struct ldb_module_ops ldb_aclread_module_ops = { + .name = "aclread", + .search = aclread_search, + .init_context = aclread_init +}; + +int ldb_aclread_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_aclread_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/acl_util.c b/source4/dsdb/samdb/ldb_modules/acl_util.c new file mode 100644 index 0000000..56aa4bd --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/acl_util.c @@ -0,0 +1,356 @@ +/* + ACL utility functions + + Copyright (C) Nadezhda Ivanova 2010 + + 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: acl_util + * + * Component: ldb ACL modules + * + * Description: Some auxiliary functions used for access checking + * + * Author: Nadezhda Ivanova + */ +#include "includes.h" +#include "ldb_module.h" +#include "auth/auth.h" +#include "libcli/security/security.h" +#include "dsdb/samdb/samdb.h" +#include "librpc/gen_ndr/ndr_security.h" +#include "param/param.h" +#include "dsdb/samdb/ldb_modules/util.h" + +struct security_token *acl_user_token(struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct auth_session_info *session_info + = (struct auth_session_info *)ldb_get_opaque( + ldb, + DSDB_SESSION_INFO); + if(!session_info) { + return NULL; + } + return session_info->security_token; +} + +/* performs an access check from inside the module stack + * given the dn of the object to be checked, the required access + * guid is either the guid of the extended right, or NULL + */ + +int dsdb_module_check_access_on_dn(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_dn *dn, + uint32_t access_mask, + const struct GUID *guid, + struct ldb_request *parent) +{ + int ret; + struct ldb_result *acl_res; + static const char *acl_attrs[] = { + "nTSecurityDescriptor", + "objectSid", + NULL + }; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct auth_session_info *session_info + = (struct auth_session_info *)ldb_get_opaque( + ldb, + DSDB_SESSION_INFO); + if(!session_info) { + return ldb_operr(ldb); + } + ret = dsdb_module_search_dn(module, mem_ctx, &acl_res, dn, + acl_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED, + parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "access_check: failed to find object %s\n", + ldb_dn_get_linearized(dn)); + return ret; + } + return dsdb_check_access_on_dn_internal(ldb, acl_res, + mem_ctx, + session_info->security_token, + dn, + access_mask, + guid); +} + +int acl_check_access_on_attribute(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + const struct security_descriptor *sd, + const struct dom_sid *rp_sid, + uint32_t access_mask, + const struct dsdb_attribute *attr, + const struct dsdb_class *objectclass) +{ + int ret; + NTSTATUS status; + uint32_t access_granted; + struct object_tree *root = NULL; + struct object_tree *new_node = NULL; + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + struct security_token *token = acl_user_token(module); + + if (!insert_in_object_tree(tmp_ctx, + &objectclass->schemaIDGUID, + access_mask, NULL, + &root)) { + DEBUG(10, ("acl_search: cannot add to object tree class schemaIDGUID\n")); + goto fail; + } + new_node = root; + + if (!GUID_all_zero(&attr->attributeSecurityGUID)) { + if (!insert_in_object_tree(tmp_ctx, + &attr->attributeSecurityGUID, + access_mask, new_node, + &new_node)) { + DEBUG(10, ("acl_search: cannot add to object tree securityGUID\n")); + goto fail; + } + } + + if (!insert_in_object_tree(tmp_ctx, + &attr->schemaIDGUID, + access_mask, new_node, + &new_node)) { + DEBUG(10, ("acl_search: cannot add to object tree attributeGUID\n")); + goto fail; + } + + status = sec_access_check_ds(sd, token, + access_mask, + &access_granted, + root, + rp_sid); + if (!NT_STATUS_IS_OK(status)) { + ret = LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + else { + ret = LDB_SUCCESS; + } + talloc_free(tmp_ctx); + return ret; +fail: + talloc_free(tmp_ctx); + return ldb_operr(ldb_module_get_ctx(module)); +} + +int acl_check_access_on_objectclass(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct security_descriptor *sd, + struct dom_sid *rp_sid, + uint32_t access_mask, + const struct dsdb_class *objectclass) +{ + int ret; + NTSTATUS status; + uint32_t access_granted; + struct object_tree *root = NULL; + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + struct security_token *token = acl_user_token(module); + + if (!insert_in_object_tree(tmp_ctx, + &objectclass->schemaIDGUID, + access_mask, NULL, + &root)) { + DEBUG(10, ("acl_search: cannot add to object tree class schemaIDGUID\n")); + goto fail; + } + + status = sec_access_check_ds(sd, token, + access_mask, + &access_granted, + root, + rp_sid); + if (!NT_STATUS_IS_OK(status)) { + ret = LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } else { + ret = LDB_SUCCESS; + } + talloc_free(tmp_ctx); + return ret; +fail: + talloc_free(tmp_ctx); + return ldb_operr(ldb_module_get_ctx(module)); +} + +/* checks for validated writes */ +int acl_check_extended_right(TALLOC_CTX *mem_ctx, + struct ldb_module *module, + struct ldb_request *req, + const struct dsdb_class *objectclass, + struct security_descriptor *sd, + struct security_token *token, + const char *ext_right, + uint32_t right_type, + struct dom_sid *sid) +{ + struct GUID right; + NTSTATUS status; + uint32_t access_granted; + struct object_tree *root = NULL; + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + static const char *no_attrs[] = { NULL }; + struct ldb_result *extended_rights_res = NULL; + struct ldb_dn *extended_rights_dn = NULL; + struct ldb_context *ldb = ldb_module_get_ctx(module); + int ret = 0; + + /* + * Find the extended right and check if applies to + * the objectclass of the object + */ + extended_rights_dn = samdb_extended_rights_dn(ldb, req); + if (!extended_rights_dn) { + ldb_set_errstring(ldb, + "access_check: CN=Extended-Rights dn could not be generated!"); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* Note: we are checking only the structural object class. */ + ret = dsdb_module_search(module, req, &extended_rights_res, + extended_rights_dn, LDB_SCOPE_ONELEVEL, + no_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM, + req, + "(&(rightsGuid=%s)(appliesTo=%s))", + ext_right, + GUID_string(tmp_ctx, + &(objectclass->schemaIDGUID))); + + if (ret != LDB_SUCCESS) { + return ret; + } else if (extended_rights_res->count == 0 ) { + ldb_debug(ldb, LDB_DEBUG_TRACE, + "acl_check_extended_right: Could not find appliesTo for %s\n", + ext_right); + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + + GUID_from_string(ext_right, &right); + + if (!insert_in_object_tree(tmp_ctx, &right, right_type, + NULL, &root)) { + DEBUG(10, ("acl_ext_right: cannot add to object tree\n")); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + status = sec_access_check_ds(sd, token, + right_type, + &access_granted, + root, + sid); + + if (!NT_STATUS_IS_OK(status)) { + talloc_free(tmp_ctx); + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + +const char *acl_user_name(TALLOC_CTX *mem_ctx, struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct auth_session_info *session_info + = (struct auth_session_info *)ldb_get_opaque( + ldb, + DSDB_SESSION_INFO); + if (!session_info) { + return "UNKNOWN (NULL)"; + } + + return talloc_asprintf(mem_ctx, "%s\\%s", + session_info->info->domain_name, + session_info->info->account_name); +} + +uint32_t dsdb_request_sd_flags(struct ldb_request *req, bool *explicit) +{ + struct ldb_control *sd_control; + uint32_t sd_flags = 0; + + if (explicit) { + *explicit = false; + } + + sd_control = ldb_request_get_control(req, LDB_CONTROL_SD_FLAGS_OID); + if (sd_control != NULL && sd_control->data != NULL) { + struct ldb_sd_flags_control *sdctr = talloc_get_type_abort(sd_control->data, struct ldb_sd_flags_control); + + sd_flags = sdctr->secinfo_flags; + + if (explicit) { + *explicit = true; + } + + /* mark it as handled */ + sd_control->critical = 0; + } + + /* we only care for the last 4 bits */ + sd_flags &= 0x0000000F; + + /* + * MS-ADTS 3.1.1.3.4.1.11 says that no bits + * equals all 4 bits + */ + if (sd_flags == 0) { + sd_flags = SECINFO_OWNER | SECINFO_GROUP | SECINFO_DACL | SECINFO_SACL; + } + + return sd_flags; +} + +int dsdb_module_schedule_sd_propagation(struct ldb_module *module, + struct ldb_dn *nc_root, + struct GUID guid, + struct GUID parent_guid, + bool include_self) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct dsdb_extended_sec_desc_propagation_op *op; + int ret; + + op = talloc_zero(module, struct dsdb_extended_sec_desc_propagation_op); + if (op == NULL) { + return ldb_oom(ldb); + } + + op->nc_root = nc_root; + op->guid = guid; + op->include_self = include_self; + op->parent_guid = parent_guid; + + ret = dsdb_module_extended(module, op, NULL, + DSDB_EXTENDED_SEC_DESC_PROPAGATION_OID, + op, + DSDB_FLAG_TOP_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_FLAG_TRUSTED, + NULL); + TALLOC_FREE(op); + return ret; +} diff --git a/source4/dsdb/samdb/ldb_modules/anr.c b/source4/dsdb/samdb/ldb_modules/anr.c new file mode 100644 index 0000000..e083f5d --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/anr.c @@ -0,0 +1,436 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2007 + Copyright (C) Simo Sorce <idra@samba.org> 2008 + Copyright (C) Andrew Tridgell 2004 + + 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 anr module + * + * Description: module to implement 'ambiguous name resolution' + * + * Author: Andrew Bartlett + */ + +#include "includes.h" +#include "ldb_module.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" + +#undef strcasecmp + +/** + * Make a and 'and' or 'or' tree from the two supplied elements + */ +static struct ldb_parse_tree *make_parse_list(struct ldb_module *module, + TALLOC_CTX *mem_ctx, enum ldb_parse_op op, + struct ldb_parse_tree *first_arm, struct ldb_parse_tree *second_arm) +{ + struct ldb_context *ldb; + struct ldb_parse_tree *list; + + ldb = ldb_module_get_ctx(module); + + list = talloc(mem_ctx, struct ldb_parse_tree); + if (list == NULL){ + ldb_oom(ldb); + return NULL; + } + list->operation = op; + + list->u.list.num_elements = 2; + list->u.list.elements = talloc_array(list, struct ldb_parse_tree *, 2); + if (!list->u.list.elements) { + ldb_oom(ldb); + return NULL; + } + list->u.list.elements[0] = talloc_steal(list, first_arm); + list->u.list.elements[1] = talloc_steal(list, second_arm); + return list; +} + +/** + * Make an equality or prefix match tree, from the attribute, operation and matching value supplied + */ +static struct ldb_parse_tree *make_match_tree(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + enum ldb_parse_op op, + const char *attr, + struct ldb_val *match) +{ + struct ldb_context *ldb; + struct ldb_parse_tree *match_tree; + + ldb = ldb_module_get_ctx(module); + + match_tree = talloc(mem_ctx, struct ldb_parse_tree); + + /* Depending on what type of match was selected, fill in the right part of the union */ + + match_tree->operation = op; + switch (op) { + case LDB_OP_SUBSTRING: + match_tree->u.substring.attr = attr; + + match_tree->u.substring.start_with_wildcard = 0; + match_tree->u.substring.end_with_wildcard = 1; + match_tree->u.substring.chunks = talloc_array(match_tree, struct ldb_val *, 2); + + if (match_tree->u.substring.chunks == NULL){ + talloc_free(match_tree); + ldb_oom(ldb); + return NULL; + } + match_tree->u.substring.chunks[0] = match; + match_tree->u.substring.chunks[1] = NULL; + break; + case LDB_OP_EQUALITY: + match_tree->u.equality.attr = attr; + match_tree->u.equality.value = *match; + break; + default: + talloc_free(match_tree); + return NULL; + } + return match_tree; +} + +struct anr_context { + bool found_anr; + struct ldb_module *module; + struct ldb_request *req; +}; + +/** + * Given the match for an 'ambigious name resolution' query, create a + * parse tree with an 'or' of all the anr attributes in the schema. + */ + +/** + * Callback function to do the heavy lifting for the parse tree walker + */ +static int anr_replace_value(struct anr_context *ac, + TALLOC_CTX *mem_ctx, + struct ldb_val *match, + struct ldb_parse_tree **ntree) +{ + struct ldb_parse_tree *tree = NULL; + struct ldb_module *module = ac->module; + struct ldb_parse_tree *match_tree; + struct dsdb_attribute *cur; + const struct dsdb_schema *schema; + struct ldb_context *ldb; + uint8_t *p; + enum ldb_parse_op op; + + ldb = ldb_module_get_ctx(module); + + schema = dsdb_get_schema(ldb, ac); + if (!schema) { + ldb_asprintf_errstring(ldb, "no schema with which to construct anr filter"); + return LDB_ERR_OPERATIONS_ERROR; + } + + if (match->length > 1 && match->data[0] == '=') { + struct ldb_val *match2 = talloc(mem_ctx, struct ldb_val); + if (match2 == NULL){ + return ldb_oom(ldb); + } + *match2 = data_blob_const(match->data+1, match->length - 1); + match = match2; + op = LDB_OP_EQUALITY; + } else { + op = LDB_OP_SUBSTRING; + } + for (cur = schema->attributes; cur; cur = cur->next) { + if (!(cur->searchFlags & SEARCH_FLAG_ANR)) continue; + match_tree = make_match_tree(module, mem_ctx, op, cur->lDAPDisplayName, match); + + if (tree) { + /* Inject an 'or' with the current tree */ + tree = make_parse_list(module, mem_ctx, LDB_OP_OR, tree, match_tree); + if (tree == NULL) { + return ldb_oom(ldb); + } + } else { + tree = match_tree; + } + } + + + /* If the search term has a space in it, + split it up at the first space. */ + + p = memchr(match->data, ' ', match->length); + + if (p) { + struct ldb_parse_tree *first_split_filter, *second_split_filter, *split_filters, *match_tree_1, *match_tree_2; + struct ldb_val *first_match = talloc(tree, struct ldb_val); + struct ldb_val *second_match = talloc(tree, struct ldb_val); + if (!first_match || !second_match) { + return ldb_oom(ldb); + } + *first_match = data_blob_const(match->data, p-match->data); + *second_match = data_blob_const(p+1, match->length - (p-match->data) - 1); + + /* Add (|(&(givenname=first)(sn=second))(&(givenname=second)(sn=first))) */ + + match_tree_1 = make_match_tree(module, mem_ctx, op, "givenName", first_match); + match_tree_2 = make_match_tree(module, mem_ctx, op, "sn", second_match); + + first_split_filter = make_parse_list(module, ac, LDB_OP_AND, match_tree_1, match_tree_2); + if (first_split_filter == NULL){ + return ldb_oom(ldb); + } + + match_tree_1 = make_match_tree(module, mem_ctx, op, "sn", first_match); + match_tree_2 = make_match_tree(module, mem_ctx, op, "givenName", second_match); + + second_split_filter = make_parse_list(module, ac, LDB_OP_AND, match_tree_1, match_tree_2); + if (second_split_filter == NULL){ + return ldb_oom(ldb); + } + + split_filters = make_parse_list(module, mem_ctx, LDB_OP_OR, + first_split_filter, second_split_filter); + if (split_filters == NULL) { + return ldb_oom(ldb); + } + + if (tree) { + /* Inject an 'or' with the current tree */ + tree = make_parse_list(module, mem_ctx, LDB_OP_OR, tree, split_filters); + } else { + tree = split_filters; + } + } + *ntree = tree; + return LDB_SUCCESS; +} + +/* + replace any occurances of an attribute with a new, generated attribute tree +*/ +static int anr_replace_subtrees(struct anr_context *ac, + struct ldb_parse_tree *tree, + const char *attr, + struct ldb_parse_tree **ntree) +{ + int ret; + unsigned int i; + + switch (tree->operation) { + case LDB_OP_AND: + case LDB_OP_OR: + for (i=0;i<tree->u.list.num_elements;i++) { + ret = anr_replace_subtrees(ac, tree->u.list.elements[i], + attr, &tree->u.list.elements[i]); + if (ret != LDB_SUCCESS) { + return ret; + } + *ntree = tree; + } + break; + case LDB_OP_NOT: + ret = anr_replace_subtrees(ac, tree->u.isnot.child, attr, &tree->u.isnot.child); + if (ret != LDB_SUCCESS) { + return ret; + } + *ntree = tree; + break; + case LDB_OP_EQUALITY: + if (ldb_attr_cmp(tree->u.equality.attr, attr) == 0) { + ret = anr_replace_value(ac, tree, &tree->u.equality.value, ntree); + if (ret != LDB_SUCCESS) { + return ret; + } + } + break; + case LDB_OP_SUBSTRING: + if (ldb_attr_cmp(tree->u.substring.attr, attr) == 0) { + if (tree->u.substring.start_with_wildcard == 0 && + tree->u.substring.end_with_wildcard == 1 && + tree->u.substring.chunks[0] != NULL && + tree->u.substring.chunks[1] == NULL) { + ret = anr_replace_value(ac, tree, tree->u.substring.chunks[0], ntree); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } + break; + default: + break; + } + + return LDB_SUCCESS; +} + +struct anr_present_ctx { + bool found_anr; + const char *attr; +}; + +/* + callback to determine if ANR is in use at all + */ +static int parse_tree_anr_present(struct ldb_parse_tree *tree, void *private_context) +{ + struct anr_present_ctx *ctx = private_context; + switch (tree->operation) { + case LDB_OP_EQUALITY: + case LDB_OP_GREATER: + case LDB_OP_LESS: + case LDB_OP_APPROX: + if (ldb_attr_cmp(tree->u.equality.attr, ctx->attr) == 0) { + ctx->found_anr = true; + } + break; + case LDB_OP_SUBSTRING: + if (ldb_attr_cmp(tree->u.substring.attr, ctx->attr) == 0) { + ctx->found_anr = true; + } + break; + case LDB_OP_PRESENT: + if (ldb_attr_cmp(tree->u.present.attr, ctx->attr) == 0) { + ctx->found_anr = true; + } + break; + case LDB_OP_EXTENDED: + if (tree->u.extended.attr && + ldb_attr_cmp(tree->u.extended.attr, ctx->attr) == 0) { + ctx->found_anr = true; + } + break; + default: + break; + } + return LDB_SUCCESS; +} + + +static int anr_search_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct anr_context *ac; + + ac = talloc_get_type(req->context, struct anr_context); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + return ldb_module_send_entry(ac->req, ares->message, ares->controls); + + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral(ac->req, ares->referral); + + case LDB_REPLY_DONE: + return ldb_module_done(ac->req, ares->controls, + ares->response, LDB_SUCCESS); + + } + return LDB_SUCCESS; +} + +/* search */ +static int anr_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct ldb_parse_tree *anr_tree; + struct ldb_request *down_req; + struct anr_context *ac; + struct anr_present_ctx ctx; + const char *attr = "anr"; + int ret; + + ctx.found_anr = false; + ctx.attr = attr; + + ldb_parse_tree_walk(req->op.search.tree, + parse_tree_anr_present, + &ctx); + + if (!ctx.found_anr) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + ac = talloc(req, struct anr_context); + if (!ac) { + return ldb_oom(ldb); + } + + ac->module = module; + ac->req = req; + +#if 0 + printf("oldanr : %s\n", ldb_filter_from_tree (0, req->op.search.tree)); +#endif + + /* First make a copy, so we don't overwrite caller memory */ + + anr_tree = ldb_parse_tree_copy_shallow(ac, req->op.search.tree); + + if (anr_tree == NULL) { + return ldb_operr(ldb); + } + + /* Now expand 'anr' out */ + ret = anr_replace_subtrees(ac, anr_tree, attr, &anr_tree); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + ret = ldb_build_search_req_ex(&down_req, + ldb, ac, + req->op.search.base, + req->op.search.scope, + anr_tree, + req->op.search.attrs, + req->controls, + ac, anr_search_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + talloc_steal(down_req, anr_tree); + + return ldb_next_request(module, down_req); +} + +static const struct ldb_module_ops ldb_anr_module_ops = { + .name = "anr", + .search = anr_search +}; + +int ldb_anr_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_anr_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/audit_log.c b/source4/dsdb/samdb/ldb_modules/audit_log.c new file mode 100644 index 0000000..7cc3ff6 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/audit_log.c @@ -0,0 +1,1913 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018 + + 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/>. +*/ + +/* + * Provide an audit log of changes made to the database and at a + * higher level details of any password changes and resets. + * + */ + +#include "includes.h" +#include "ldb_module.h" +#include "lib/audit_logging/audit_logging.h" + +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "dsdb/samdb/ldb_modules/audit_util_proto.h" +#include "libcli/security/dom_sid.h" +#include "auth/common_auth.h" +#include "param/param.h" +#include "librpc/gen_ndr/windows_event_ids.h" + +#define OPERATION_JSON_TYPE "dsdbChange" +#define OPERATION_HR_TAG "DSDB Change" +#define OPERATION_MAJOR 1 +#define OPERATION_MINOR 0 +#define OPERATION_LOG_LVL 5 + +#define PASSWORD_JSON_TYPE "passwordChange" +#define PASSWORD_HR_TAG "Password Change" +#define PASSWORD_MAJOR 1 +#define PASSWORD_MINOR 1 +#define PASSWORD_LOG_LVL 5 + +#define TRANSACTION_JSON_TYPE "dsdbTransaction" +#define TRANSACTION_HR_TAG "DSDB Transaction" +#define TRANSACTION_MAJOR 1 +#define TRANSACTION_MINOR 0 +#define TRANSACTION_LOG_FAILURE_LVL 5 +#define TRANSACTION_LOG_COMPLETION_LVL 10 + +#define REPLICATION_JSON_TYPE "replicatedUpdate" +#define REPLICATION_HR_TAG "Replicated Update" +#define REPLICATION_MAJOR 1 +#define REPLICATION_MINOR 0 +#define REPLICATION_LOG_LVL 5 +/* + * Attribute values are truncated in the logs if they are longer than + * MAX_LENGTH + */ +#define MAX_LENGTH 1024 + +#define min(a, b) (((a)>(b))?(b):(a)) + +/* + * Private data for the module, stored in the ldb_module private data + */ +struct audit_private { + /* + * Should details of database operations be sent over the + * messaging bus. + */ + bool send_samdb_events; + /* + * Should details of password changes and resets be sent over + * the messaging bus. + */ + bool send_password_events; + /* + * The messaging context to send the messages over. Will only + * be set if send_samdb_events or send_password_events are + * true. + */ + struct imessaging_context *msg_ctx; + /* + * Unique transaction id for the current transaction + */ + struct GUID transaction_guid; + /* + * Transaction start time, used to calculate the transaction + * duration. + */ + struct timeval transaction_start; +}; + +/* + * @brief Has the password changed. + * + * Does the message contain a change to one of the password attributes? The + * password attributes are defined in DSDB_PASSWORD_ATTRIBUTES + * + * @return true if the message contains a password attribute + * + */ +static bool has_password_changed(const struct ldb_message *message) +{ + unsigned int i; + if (message == NULL) { + return false; + } + for (i=0;i<message->num_elements;i++) { + if (dsdb_audit_is_password_attribute( + message->elements[i].name)) { + return true; + } + } + return false; +} + +/* + * @brief get the password change windows event id + * + * Get the Windows Event Id for the action being performed on the user password. + * + * This routine assumes that the request contains password attributes and that the + * password ACL checks have been performed by acl.c + * + * @param request the ldb_request to inspect + * @param reply the ldb_reply, will contain the password controls + * + * @return The windows event code. + */ +static enum event_id_type get_password_windows_event_id( + const struct ldb_request *request, + const struct ldb_reply *reply) +{ + if(request->operation == LDB_ADD) { + return EVT_ID_PASSWORD_RESET; + } else { + struct ldb_control *pav_ctrl = NULL; + struct dsdb_control_password_acl_validation *pav = NULL; + + pav_ctrl = ldb_reply_get_control( + discard_const(reply), + DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID); + if (pav_ctrl == NULL) { + return EVT_ID_PASSWORD_RESET; + } + + pav = talloc_get_type_abort( + pav_ctrl->data, + struct dsdb_control_password_acl_validation); + + if (pav->pwd_reset) { + return EVT_ID_PASSWORD_RESET; + } else { + return EVT_ID_PASSWORD_CHANGE; + } + } +} +/* + * @brief Is the request a password "Change" or a "Reset" + * + * Get a description of the action being performed on the user password. This + * routine assumes that the request contains password attributes and that the + * password ACL checks have been performed by acl.c + * + * @param request the ldb_request to inspect + * @param reply the ldb_reply, will contain the password controls + * + * @return "Change" if the password is being changed. + * "Reset" if the password is being reset. + */ +static const char *get_password_action( + const struct ldb_request *request, + const struct ldb_reply *reply) +{ + if(request->operation == LDB_ADD) { + return "Reset"; + } else { + struct ldb_control *pav_ctrl = NULL; + struct dsdb_control_password_acl_validation *pav = NULL; + + pav_ctrl = ldb_reply_get_control( + discard_const(reply), + DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID); + if (pav_ctrl == NULL) { + return "Reset"; + } + + pav = talloc_get_type_abort( + pav_ctrl->data, + struct dsdb_control_password_acl_validation); + + if (pav->pwd_reset) { + return "Reset"; + } else { + return "Change"; + } + } +} + +/* + * @brief generate a JSON object detailing an ldb operation. + * + * Generate a JSON object detailing an ldb operation. + * + * @param module the ldb module + * @param request the request + * @param reply the result of the operation. + * + * @return the generated JSON object, should be freed with json_free. + * + * + */ +static struct json_object operation_json( + struct ldb_module *module, + const struct ldb_request *request, + const struct ldb_reply *reply) +{ + struct ldb_context *ldb = NULL; + const struct dom_sid *sid = NULL; + bool as_system = false; + struct json_object wrapper = json_empty_object; + struct json_object audit = json_empty_object; + const struct tsocket_address *remote = NULL; + const char *dn = NULL; + const char* operation = NULL; + const struct GUID *unique_session_token = NULL; + const struct ldb_message *message = NULL; + struct audit_private *audit_private + = talloc_get_type_abort(ldb_module_get_private(module), + struct audit_private); + int rc = 0; + + ldb = ldb_module_get_ctx(module); + + remote = dsdb_audit_get_remote_address(ldb); + if (remote != NULL && dsdb_audit_is_system_session(module)) { + as_system = true; + sid = dsdb_audit_get_actual_sid(ldb); + unique_session_token = + dsdb_audit_get_actual_unique_session_token(ldb); + } else { + sid = dsdb_audit_get_user_sid(module); + unique_session_token = + dsdb_audit_get_unique_session_token(module); + } + dn = dsdb_audit_get_primary_dn(request); + operation = dsdb_audit_get_operation_name(request); + + audit = json_new_object(); + if (json_is_invalid(&audit)) { + goto failure; + } + rc = json_add_version(&audit, OPERATION_MAJOR, OPERATION_MINOR); + if (rc != 0) { + goto failure; + } + rc = json_add_int(&audit, "statusCode", reply->error); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "status", ldb_strerror(reply->error)); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "operation", operation); + if (rc != 0) { + goto failure; + } + rc = json_add_address(&audit, "remoteAddress", remote); + if (rc != 0) { + goto failure; + } + rc = json_add_bool(&audit, "performedAsSystem", as_system); + if (rc != 0) { + goto failure; + } + rc = json_add_sid(&audit, "userSid", sid); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "dn", dn); + if (rc != 0) { + goto failure; + } + rc = json_add_guid( + &audit, "transactionId", &audit_private->transaction_guid); + if (rc != 0) { + goto failure; + } + rc = json_add_guid(&audit, "sessionId", unique_session_token); + if (rc != 0) { + goto failure; + } + + message = dsdb_audit_get_message(request); + if (message != NULL) { + struct json_object attributes = + dsdb_audit_attributes_json( + request->operation, + message); + if (json_is_invalid(&attributes)) { + goto failure; + } + rc = json_add_object(&audit, "attributes", &attributes); + if (rc != 0) { + goto failure; + } + } + + wrapper = json_new_object(); + if (json_is_invalid(&wrapper)) { + goto failure; + } + rc = json_add_timestamp(&wrapper); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&wrapper, "type", OPERATION_JSON_TYPE); + if (rc != 0) { + goto failure; + } + rc = json_add_object(&wrapper, OPERATION_JSON_TYPE, &audit); + if (rc != 0) { + goto failure; + } + return wrapper; + +failure: + /* + * On a failure audit will not have been added to wrapper so it + * needs to free it to avoid a leak. + * + * wrapper is freed to invalidate it as it will have only been + * partially constructed and may be inconsistent. + * + * All the json manipulation routines handle a freed object correctly + */ + json_free(&audit); + json_free(&wrapper); + DBG_ERR("Unable to create ldb operation JSON audit message\n"); + return wrapper; +} + +/* + * @brief generate a JSON object detailing a replicated update. + * + * Generate a JSON object detailing a replicated update + * + * @param module the ldb module + * @param request the request + * @paran reply the result of the operation + * + * @return the generated JSON object, should be freed with json_free. + * NULL if there was an error generating the message. + * + */ +static struct json_object replicated_update_json( + struct ldb_module *module, + const struct ldb_request *request, + const struct ldb_reply *reply) +{ + struct json_object wrapper = json_empty_object; + struct json_object audit = json_empty_object; + struct audit_private *audit_private + = talloc_get_type_abort(ldb_module_get_private(module), + struct audit_private); + struct dsdb_extended_replicated_objects *ro = talloc_get_type( + request->op.extended.data, + struct dsdb_extended_replicated_objects); + const char *partition_dn = NULL; + const char *error = NULL; + int rc = 0; + + partition_dn = ldb_dn_get_linearized(ro->partition_dn); + error = get_friendly_werror_msg(ro->error); + + audit = json_new_object(); + if (json_is_invalid(&audit)) { + goto failure; + } + rc = json_add_version(&audit, REPLICATION_MAJOR, REPLICATION_MINOR); + if (rc != 0) { + goto failure; + } + rc = json_add_int(&audit, "statusCode", reply->error); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "status", ldb_strerror(reply->error)); + if (rc != 0) { + goto failure; + } + rc = json_add_guid( + &audit, "transactionId", &audit_private->transaction_guid); + if (rc != 0) { + goto failure; + } + rc = json_add_int(&audit, "objectCount", ro->num_objects); + if (rc != 0) { + goto failure; + } + rc = json_add_int(&audit, "linkCount", ro->linked_attributes_count); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "partitionDN", partition_dn); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "error", error); + if (rc != 0) { + goto failure; + } + rc = json_add_int(&audit, "errorCode", W_ERROR_V(ro->error)); + if (rc != 0) { + goto failure; + } + rc = json_add_guid( + &audit, "sourceDsa", &ro->source_dsa->source_dsa_obj_guid); + if (rc != 0) { + goto failure; + } + rc = json_add_guid( + &audit, "invocationId", &ro->source_dsa->source_dsa_invocation_id); + if (rc != 0) { + goto failure; + } + + wrapper = json_new_object(); + if (json_is_invalid(&wrapper)) { + goto failure; + } + rc = json_add_timestamp(&wrapper); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&wrapper, "type", REPLICATION_JSON_TYPE); + if (rc != 0) { + goto failure; + } + rc = json_add_object(&wrapper, REPLICATION_JSON_TYPE, &audit); + if (rc != 0) { + goto failure; + } + return wrapper; +failure: + /* + * On a failure audit will not have been added to wrapper so it + * needs to be freed it to avoid a leak. + * + * wrapper is freed to invalidate it as it will have only been + * partially constructed and may be inconsistent. + * + * All the json manipulation routines handle a freed object correctly + */ + json_free(&audit); + json_free(&wrapper); + DBG_ERR("Unable to create replicated update JSON audit message\n"); + return wrapper; +} + +/* + * @brief generate a JSON object detailing a password change. + * + * Generate a JSON object detailing a password change. + * + * @param module the ldb module + * @param request the request + * @param reply the result/response + * @param status the status code returned for the underlying ldb operation. + * + * @return the generated JSON object. + * + */ +static struct json_object password_change_json( + struct ldb_module *module, + const struct ldb_request *request, + const struct ldb_reply *reply) +{ + struct ldb_context *ldb = NULL; + const struct dom_sid *sid = NULL; + const char *dn = NULL; + struct json_object wrapper = json_empty_object; + struct json_object audit = json_empty_object; + const struct tsocket_address *remote = NULL; + const char* action = NULL; + const struct GUID *unique_session_token = NULL; + struct audit_private *audit_private + = talloc_get_type_abort(ldb_module_get_private(module), + struct audit_private); + int rc = 0; + enum event_id_type event_id; + + ldb = ldb_module_get_ctx(module); + + remote = dsdb_audit_get_remote_address(ldb); + sid = dsdb_audit_get_user_sid(module); + dn = dsdb_audit_get_primary_dn(request); + action = get_password_action(request, reply); + unique_session_token = dsdb_audit_get_unique_session_token(module); + event_id = get_password_windows_event_id(request, reply); + + audit = json_new_object(); + if (json_is_invalid(&audit)) { + goto failure; + } + rc = json_add_version(&audit, PASSWORD_MAJOR, PASSWORD_MINOR); + if (rc != 0) { + goto failure; + } + rc = json_add_int(&audit, "eventId", event_id); + if (rc != 0) { + goto failure; + } + rc = json_add_int(&audit, "statusCode", reply->error); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "status", ldb_strerror(reply->error)); + if (rc != 0) { + goto failure; + } + rc = json_add_address(&audit, "remoteAddress", remote); + if (rc != 0) { + goto failure; + } + rc = json_add_sid(&audit, "userSid", sid); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "dn", dn); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "action", action); + if (rc != 0) { + goto failure; + } + rc = json_add_guid( + &audit, "transactionId", &audit_private->transaction_guid); + if (rc != 0) { + goto failure; + } + rc = json_add_guid(&audit, "sessionId", unique_session_token); + if (rc != 0) { + goto failure; + } + + wrapper = json_new_object(); + if (json_is_invalid(&wrapper)) { + goto failure; + } + rc = json_add_timestamp(&wrapper); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&wrapper, "type", PASSWORD_JSON_TYPE); + if (rc != 0) { + goto failure; + } + rc = json_add_object(&wrapper, PASSWORD_JSON_TYPE, &audit); + if (rc != 0) { + goto failure; + } + + return wrapper; +failure: + /* + * On a failure audit will not have been added to wrapper so it + * needs to free it to avoid a leak. + * + * wrapper is freed to invalidate it as it will have only been + * partially constructed and may be inconsistent. + * + * All the json manipulation routines handle a freed object correctly + */ + json_free(&wrapper); + json_free(&audit); + DBG_ERR("Unable to create password change JSON audit message\n"); + return wrapper; +} + + +/* + * @brief create a JSON object containing details of a transaction event. + * + * Create a JSON object detailing a transaction transaction life cycle events, + * i.e. begin, commit, roll back + * + * @param action a one word description of the event/action + * @param transaction_id the GUID identifying the current transaction. + * @param status the status code returned by the operation + * @param duration the duration of the operation. + * + * @return a JSON object detailing the event + */ +static struct json_object transaction_json( + const char *action, + struct GUID *transaction_id, + const int64_t duration) +{ + struct json_object wrapper = json_empty_object; + struct json_object audit = json_empty_object; + int rc = 0; + + audit = json_new_object(); + if (json_is_invalid(&audit)) { + goto failure; + } + + rc = json_add_version(&audit, TRANSACTION_MAJOR, TRANSACTION_MINOR); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "action", action); + if (rc != 0) { + goto failure; + } + rc = json_add_guid(&audit, "transactionId", transaction_id); + if (rc != 0) { + goto failure; + } + rc = json_add_int(&audit, "duration", duration); + if (rc != 0) { + goto failure; + } + + wrapper = json_new_object(); + if (json_is_invalid(&wrapper)) { + goto failure; + } + rc = json_add_timestamp(&wrapper); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&wrapper, "type", TRANSACTION_JSON_TYPE); + if (rc != 0) { + goto failure; + } + rc = json_add_object(&wrapper, TRANSACTION_JSON_TYPE, &audit); + if (rc != 0) { + goto failure; + } + + return wrapper; +failure: + /* + * On a failure audit will not have been added to wrapper so it + * needs to free it to avoid a leak. + * + * wrapper is freed to invalidate it as it will have only been + * partially constructed and may be inconsistent. + * + * All the json manipulation routines handle a freed object correctly + */ + json_free(&wrapper); + json_free(&audit); + DBG_ERR("Unable to create transaction JSON audit message\n"); + return wrapper; +} + + +/* + * @brief generate a JSON object detailing a commit failure. + * + * Generate a JSON object containing details of a commit failure. + * + * @param action the commit action, "commit" or "prepare" + * @param status the status code returned by commit + * @param reason any extra failure information/reason available + * @param transaction_id the GUID identifying the current transaction. + */ +static struct json_object commit_failure_json( + const char *action, + const int64_t duration, + int status, + const char *reason, + struct GUID *transaction_id) +{ + struct json_object wrapper = json_empty_object; + struct json_object audit = json_empty_object; + int rc = 0; + + audit = json_new_object(); + if (json_is_invalid(&audit)) { + goto failure; + } + rc = json_add_version(&audit, TRANSACTION_MAJOR, TRANSACTION_MINOR); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "action", action); + if (rc != 0) { + goto failure; + } + rc = json_add_guid(&audit, "transactionId", transaction_id); + if (rc != 0) { + goto failure; + } + rc = json_add_int(&audit, "duration", duration); + if (rc != 0) { + goto failure; + } + rc = json_add_int(&audit, "statusCode", status); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "status", ldb_strerror(status)); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "reason", reason); + if (rc != 0) { + goto failure; + } + + wrapper = json_new_object(); + if (json_is_invalid(&wrapper)) { + goto failure; + } + rc = json_add_timestamp(&wrapper); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&wrapper, "type", TRANSACTION_JSON_TYPE); + if (rc != 0) { + goto failure; + } + rc = json_add_object(&wrapper, TRANSACTION_JSON_TYPE, &audit); + if (rc != 0) { + goto failure; + } + + return wrapper; +failure: + /* + * On a failure audit will not have been added to wrapper so it + * needs to free it to avoid a leak. + * + * wrapper is freed to invalidate it as it will have only been + * partially constructed and may be inconsistent. + * + * All the json manipulation routines handle a freed object correctly + */ + json_free(&audit); + json_free(&wrapper); + DBG_ERR("Unable to create commit failure JSON audit message\n"); + return wrapper; +} + +/* + * @brief Print a human readable log line for a password change event. + * + * Generate a human readable log line detailing a password change. + * + * @param mem_ctx The talloc context that will own the generated log line. + * @param module the ldb module + * @param request the request + * @param reply the result/response + * @param status the status code returned for the underlying ldb operation. + * + * @return the generated log line. + */ +static char *password_change_human_readable( + TALLOC_CTX *mem_ctx, + struct ldb_module *module, + const struct ldb_request *request, + const struct ldb_reply *reply) +{ + struct ldb_context *ldb = NULL; + const char *remote_host = NULL; + const struct dom_sid *sid = NULL; + struct dom_sid_buf user_sid; + const char *timestamp = NULL; + char *log_entry = NULL; + const char *action = NULL; + const char *dn = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_module_get_ctx(module); + + remote_host = dsdb_audit_get_remote_host(ldb, ctx); + sid = dsdb_audit_get_user_sid(module); + timestamp = audit_get_timestamp(ctx); + action = get_password_action(request, reply); + dn = dsdb_audit_get_primary_dn(request); + + log_entry = talloc_asprintf( + mem_ctx, + "[%s] at [%s] status [%s] " + "remote host [%s] SID [%s] DN [%s]", + action, + timestamp, + ldb_strerror(reply->error), + remote_host, + dom_sid_str_buf(sid, &user_sid), + dn); + TALLOC_FREE(ctx); + return log_entry; +} +/* + * @brief Generate a human readable string, detailing attributes in a message + * + * For modify operations each attribute is prefixed with the action. + * Normal values are enclosed in [] + * Base64 values are enclosed in {} + * Truncated values are indicated by three trailing dots "..." + * + * @param ldb The ldb_context + * @param buffer The attributes will be appended to the buffer. + * assumed to have been allocated via talloc. + * @param operation The operation type + * @param message the message to process + * + */ +static char *log_attributes( + struct ldb_context *ldb, + char *buffer, + enum ldb_request_type operation, + const struct ldb_message *message) +{ + size_t i, j; + for (i=0;i<message->num_elements;i++) { + if (i > 0) { + buffer = talloc_asprintf_append_buffer(buffer, " "); + } + + if (message->elements[i].name == NULL) { + ldb_debug( + ldb, + LDB_DEBUG_ERROR, + "Error: Invalid element name (NULL) at " + "position %zu", i); + return NULL; + } + + if (operation == LDB_MODIFY) { + const char *action =NULL; + action = dsdb_audit_get_modification_action( + message->elements[i].flags); + buffer = talloc_asprintf_append_buffer( + buffer, + "%s: %s ", + action, + message->elements[i].name); + } else { + buffer = talloc_asprintf_append_buffer( + buffer, + "%s ", + message->elements[i].name); + } + + if (dsdb_audit_redact_attribute(message->elements[i].name)) { + /* + * Do not log the value of any secret or password + * attributes + */ + buffer = talloc_asprintf_append_buffer( + buffer, + "[REDACTED SECRET ATTRIBUTE]"); + continue; + } + + for (j=0;j<message->elements[i].num_values;j++) { + struct ldb_val v; + bool use_b64_encode = false; + size_t length; + if (j > 0) { + buffer = talloc_asprintf_append_buffer( + buffer, + " "); + } + + v = message->elements[i].values[j]; + length = min(MAX_LENGTH, v.length); + use_b64_encode = ldb_should_b64_encode(ldb, &v); + if (use_b64_encode) { + const char *encoded = ldb_base64_encode( + buffer, + (char *)v.data, + length); + buffer = talloc_asprintf_append_buffer( + buffer, + "{%s%s}", + encoded, + (v.length > MAX_LENGTH ? "..." : "")); + } else { + buffer = talloc_asprintf_append_buffer( + buffer, + "[%*.*s%s]", + (int)length, + (int)length, + (char *)v.data, + (v.length > MAX_LENGTH ? "..." : "")); + } + } + } + return buffer; +} + +/* + * @brief generate a human readable log entry detailing an ldb operation. + * + * Generate a human readable log entry detailing an ldb operation. + * + * @param mem_ctx The talloc context owning the returned string. + * @param module the ldb module + * @param request the request + * @param reply the result of the operation + * + * @return the log entry. + * + */ +static char *operation_human_readable( + TALLOC_CTX *mem_ctx, + struct ldb_module *module, + const struct ldb_request *request, + const struct ldb_reply *reply) +{ + struct ldb_context *ldb = NULL; + const char *remote_host = NULL; + const struct tsocket_address *remote = NULL; + const struct dom_sid *sid = NULL; + struct dom_sid_buf user_sid; + const char *timestamp = NULL; + const char *op_name = NULL; + char *log_entry = NULL; + const char *dn = NULL; + const char *new_dn = NULL; + const struct ldb_message *message = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_module_get_ctx(module); + + remote_host = dsdb_audit_get_remote_host(ldb, ctx); + remote = dsdb_audit_get_remote_address(ldb); + if (remote != NULL && dsdb_audit_is_system_session(module)) { + sid = dsdb_audit_get_actual_sid(ldb); + } else { + sid = dsdb_audit_get_user_sid(module); + } + timestamp = audit_get_timestamp(ctx); + op_name = dsdb_audit_get_operation_name(request); + dn = dsdb_audit_get_primary_dn(request); + new_dn = dsdb_audit_get_secondary_dn(request); + + message = dsdb_audit_get_message(request); + + log_entry = talloc_asprintf( + mem_ctx, + "[%s] at [%s] status [%s] " + "remote host [%s] SID [%s] DN [%s]", + op_name, + timestamp, + ldb_strerror(reply->error), + remote_host, + dom_sid_str_buf(sid, &user_sid), + dn); + if (new_dn != NULL) { + log_entry = talloc_asprintf_append_buffer( + log_entry, + " New DN [%s]", + new_dn); + } + if (message != NULL) { + log_entry = talloc_asprintf_append_buffer(log_entry, + " attributes ["); + log_entry = log_attributes(ldb, + log_entry, + request->operation, + message); + log_entry = talloc_asprintf_append_buffer(log_entry, "]"); + } + TALLOC_FREE(ctx); + return log_entry; +} + +/* + * @brief generate a human readable log entry detailing a replicated update + * operation. + * + * Generate a human readable log entry detailing a replicated update operation + * + * @param mem_ctx The talloc context owning the returned string. + * @param module the ldb module + * @param request the request + * @param reply the result of the operation. + * + * @return the log entry. + * + */ +static char *replicated_update_human_readable( + TALLOC_CTX *mem_ctx, + struct ldb_module *module, + const struct ldb_request *request, + const struct ldb_reply *reply) +{ + struct dsdb_extended_replicated_objects *ro = talloc_get_type( + request->op.extended.data, + struct dsdb_extended_replicated_objects); + const char *partition_dn = NULL; + const char *error = NULL; + char *log_entry = NULL; + char *timestamp = NULL; + struct GUID_txt_buf object_buf; + const char *object = NULL; + struct GUID_txt_buf invocation_buf; + const char *invocation = NULL; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + timestamp = audit_get_timestamp(ctx); + error = get_friendly_werror_msg(ro->error); + partition_dn = ldb_dn_get_linearized(ro->partition_dn); + object = GUID_buf_string( + &ro->source_dsa->source_dsa_obj_guid, + &object_buf); + invocation = GUID_buf_string( + &ro->source_dsa->source_dsa_invocation_id, + &invocation_buf); + + + log_entry = talloc_asprintf( + mem_ctx, + "at [%s] status [%s] error [%s] partition [%s] objects [%d] " + "links [%d] object [%s] invocation [%s]", + timestamp, + ldb_strerror(reply->error), + error, + partition_dn, + ro->num_objects, + ro->linked_attributes_count, + object, + invocation); + + TALLOC_FREE(ctx); + return log_entry; +} +/* + * @brief create a human readable log entry detailing a transaction event. + * + * Create a human readable log entry detailing a transaction event. + * i.e. begin, commit, roll back + * + * @param mem_ctx The talloc context owning the returned string. + * @param action a one word description of the event/action + * @param duration the duration of the transaction. + * + * @return the log entry + */ +static char *transaction_human_readable( + TALLOC_CTX *mem_ctx, + const char* action, + const int64_t duration) +{ + const char *timestamp = NULL; + char *log_entry = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + timestamp = audit_get_timestamp(ctx); + + log_entry = talloc_asprintf( + mem_ctx, + "[%s] at [%s] duration [%"PRIi64"]", + action, + timestamp, + duration); + + TALLOC_FREE(ctx); + return log_entry; +} + +/* + * @brief generate a human readable log entry detailing a commit failure. + * + * Generate generate a human readable log entry detailing a commit failure. + * + * @param mem_ctx The talloc context owning the returned string. + * @param action the commit action, "prepare" or "commit" + * @param status the status code returned by commit + * @param reason any extra failure information/reason available + * + * @return the log entry + */ +static char *commit_failure_human_readable( + TALLOC_CTX *mem_ctx, + const char *action, + const int64_t duration, + int status, + const char *reason) +{ + const char *timestamp = NULL; + char *log_entry = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + timestamp = audit_get_timestamp(ctx); + + log_entry = talloc_asprintf( + mem_ctx, + "[%s] at [%s] duration [%"PRIi64"] status [%d] reason [%s]", + action, + timestamp, + duration, + status, + reason); + + TALLOC_FREE(ctx); + return log_entry; +} + +/* + * @brief log details of a standard ldb operation. + * + * Log the details of an ldb operation in JSON and or human readable format + * and send over the message bus. + * + * @param module the ldb_module + * @param request the operation request. + * @param reply the operation result. + * @param the status code returned for the operation. + * + */ +static void log_standard_operation( + struct ldb_module *module, + const struct ldb_request *request, + const struct ldb_reply *reply) +{ + + const struct ldb_message *message = dsdb_audit_get_message(request); + bool password_changed = has_password_changed(message); + struct audit_private *audit_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct audit_private); + + TALLOC_CTX *ctx = talloc_new(NULL); + + if (CHECK_DEBUGLVLC(DBGC_DSDB_AUDIT, OPERATION_LOG_LVL)) { + char *entry = NULL; + entry = operation_human_readable( + ctx, + module, + request, + reply); + audit_log_human_text( + OPERATION_HR_TAG, + entry, + DBGC_DSDB_AUDIT, + OPERATION_LOG_LVL); + TALLOC_FREE(entry); + } + if (CHECK_DEBUGLVLC(DBGC_DSDB_PWD_AUDIT, PASSWORD_LOG_LVL)) { + if (password_changed) { + char *entry = NULL; + entry = password_change_human_readable( + ctx, + module, + request, + reply); + audit_log_human_text( + PASSWORD_HR_TAG, + entry, + DBGC_DSDB_PWD_AUDIT, + PASSWORD_LOG_LVL); + TALLOC_FREE(entry); + } + } + if (CHECK_DEBUGLVLC(DBGC_DSDB_AUDIT_JSON, OPERATION_LOG_LVL) || + (audit_private->msg_ctx + && audit_private->send_samdb_events)) { + struct json_object json; + json = operation_json(module, request, reply); + audit_log_json( + &json, + DBGC_DSDB_AUDIT_JSON, + OPERATION_LOG_LVL); + if (audit_private->msg_ctx + && audit_private->send_samdb_events) { + audit_message_send( + audit_private->msg_ctx, + DSDB_EVENT_NAME, + MSG_DSDB_LOG, + &json); + } + json_free(&json); + } + if (CHECK_DEBUGLVLC(DBGC_DSDB_PWD_AUDIT_JSON, PASSWORD_LOG_LVL) || + (audit_private->msg_ctx + && audit_private->send_password_events)) { + if (password_changed) { + struct json_object json; + json = password_change_json(module, request, reply); + audit_log_json( + &json, + DBGC_DSDB_PWD_AUDIT_JSON, + PASSWORD_LOG_LVL); + if (audit_private->send_password_events) { + audit_message_send( + audit_private->msg_ctx, + DSDB_PWD_EVENT_NAME, + MSG_DSDB_PWD_LOG, + &json); + } + json_free(&json); + } + } + TALLOC_FREE(ctx); +} + +/* + * @brief log details of a replicated update. + * + * Log the details of a replicated update in JSON and or human readable + * format and send over the message bus. + * + * @param module the ldb_module + * @param request the operation request + * @param reply the result of the operation. + * + */ +static void log_replicated_operation( + struct ldb_module *module, + const struct ldb_request *request, + const struct ldb_reply *reply) +{ + + struct audit_private *audit_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct audit_private); + + TALLOC_CTX *ctx = talloc_new(NULL); + + if (CHECK_DEBUGLVLC(DBGC_DSDB_AUDIT, REPLICATION_LOG_LVL)) { + char *entry = NULL; + entry = replicated_update_human_readable( + ctx, + module, + request, + reply); + audit_log_human_text( + REPLICATION_HR_TAG, + entry, + DBGC_DSDB_AUDIT, + REPLICATION_LOG_LVL); + TALLOC_FREE(entry); + } + if (CHECK_DEBUGLVLC(DBGC_DSDB_AUDIT_JSON, REPLICATION_LOG_LVL) || + (audit_private->msg_ctx && audit_private->send_samdb_events)) { + struct json_object json; + json = replicated_update_json(module, request, reply); + audit_log_json( + &json, + DBGC_DSDB_AUDIT_JSON, + REPLICATION_LOG_LVL); + if (audit_private->send_samdb_events) { + audit_message_send( + audit_private->msg_ctx, + DSDB_EVENT_NAME, + MSG_DSDB_LOG, + &json); + } + json_free(&json); + } + TALLOC_FREE(ctx); +} + +/* + * @brief log details of an ldb operation. + * + * Log the details of an ldb operation in JSON and or human readable format + * and send over the message bus. + * + * @param module the ldb_module + * @param request the operation request + * @part reply the result of the operation + * + */ +static void log_operation( + struct ldb_module *module, + const struct ldb_request *request, + const struct ldb_reply *reply) +{ + + if (request->operation == LDB_EXTENDED) { + if (strcmp( + request->op.extended.oid, + DSDB_EXTENDED_REPLICATED_OBJECTS_OID) != 0) { + + log_replicated_operation(module, request, reply); + } + } else { + log_standard_operation(module, request, reply); + } +} + +/* + * @brief log details of a transaction event. + * + * Log the details of a transaction event in JSON and or human readable format + * and send over the message bus. + * + * @param module the ldb_module + * @param action the transaction event i.e. begin, commit, roll back. + * @param log_level the logging level + * + */ +static void log_transaction( + struct ldb_module *module, + const char *action, + int log_level) +{ + + struct audit_private *audit_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct audit_private); + const struct timeval now = timeval_current(); + const int64_t duration = usec_time_diff(&now, &audit_private->transaction_start); + + TALLOC_CTX *ctx = talloc_new(NULL); + + if (CHECK_DEBUGLVLC(DBGC_DSDB_TXN_AUDIT, log_level)) { + char* entry = NULL; + entry = transaction_human_readable(ctx, action, duration); + audit_log_human_text( + TRANSACTION_HR_TAG, + entry, + DBGC_DSDB_TXN_AUDIT, + log_level); + TALLOC_FREE(entry); + } + if (CHECK_DEBUGLVLC(DBGC_DSDB_TXN_AUDIT_JSON, log_level) || + (audit_private->msg_ctx && audit_private->send_samdb_events)) { + struct json_object json; + json = transaction_json( + action, + &audit_private->transaction_guid, + duration); + audit_log_json( + &json, + DBGC_DSDB_TXN_AUDIT_JSON, + log_level); + if (audit_private->send_samdb_events) { + audit_message_send( + audit_private->msg_ctx, + DSDB_EVENT_NAME, + MSG_DSDB_LOG, + &json); + } + json_free(&json); + } + TALLOC_FREE(ctx); +} + +/* + * @brief log details of a commit failure. + * + * Log the details of a commit failure in JSON and or human readable + * format and send over the message bus. + * + * @param module the ldb_module + * @param action the commit action "prepare" or "commit" + * @param status the ldb status code returned by prepare commit. + * + */ +static void log_commit_failure( + struct ldb_module *module, + const char *action, + int status) +{ + + struct audit_private *audit_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct audit_private); + const char* reason = dsdb_audit_get_ldb_error_string(module, status); + const int log_level = TRANSACTION_LOG_FAILURE_LVL; + const struct timeval now = timeval_current(); + const int64_t duration = usec_time_diff(&now, + &audit_private->transaction_start); + + TALLOC_CTX *ctx = talloc_new(NULL); + + if (CHECK_DEBUGLVLC(DBGC_DSDB_TXN_AUDIT, log_level)) { + + char* entry = NULL; + entry = commit_failure_human_readable( + ctx, + action, + duration, + status, + reason); + audit_log_human_text( + TRANSACTION_HR_TAG, + entry, + DBGC_DSDB_TXN_AUDIT, + TRANSACTION_LOG_FAILURE_LVL); + TALLOC_FREE(entry); + } + if (CHECK_DEBUGLVLC(DBGC_DSDB_TXN_AUDIT_JSON, log_level) || + (audit_private->msg_ctx + && audit_private->send_samdb_events)) { + struct json_object json; + json = commit_failure_json( + action, + duration, + status, + reason, + &audit_private->transaction_guid); + audit_log_json( + &json, + DBGC_DSDB_TXN_AUDIT_JSON, + log_level); + if (audit_private->send_samdb_events) { + audit_message_send(audit_private->msg_ctx, + DSDB_EVENT_NAME, + MSG_DSDB_LOG, + &json); + } + json_free(&json); + } + TALLOC_FREE(ctx); +} + +/* + * Context needed by audit_callback + */ +struct audit_callback_context { + struct ldb_request *request; + struct ldb_module *module; +}; + +/* + * @brief call back function for the ldb_operations. + * + * As the LDB operations are async, and we wish to examine the results of + * the operations, a callback needs to be registered to process the results + * of the LDB operations. + * + * @param req the ldb request + * @param res the result of the operation + * + * @return the LDB_STATUS + */ +static int audit_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct audit_callback_context *ac = NULL; + + ac = talloc_get_type( + req->context, + struct audit_callback_context); + + if (!ares) { + return ldb_module_done( + ac->request, + NULL, + NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + /* pass on to the callback */ + switch (ares->type) { + case LDB_REPLY_ENTRY: + return ldb_module_send_entry( + ac->request, + ares->message, + ares->controls); + + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral( + ac->request, + ares->referral); + + case LDB_REPLY_DONE: + /* + * Log the operation once DONE + */ + log_operation(ac->module, ac->request, ares); + return ldb_module_done( + ac->request, + ares->controls, + ares->response, + ares->error); + + default: + /* Can't happen */ + return LDB_ERR_OPERATIONS_ERROR; + } +} + +/* + * @brief Add the current transaction identifier to the request. + * + * Add the current transaction identifier in the module private data, + * to the request as a control. + * + * @param module + * @param req the request. + * + * @return an LDB_STATUS code, LDB_SUCCESS if successful. + */ +static int add_transaction_id( + struct ldb_module *module, + struct ldb_request *req) +{ + struct audit_private *audit_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct audit_private); + struct dsdb_control_transaction_identifier *transaction_id; + int ret; + + transaction_id = talloc_zero( + req, + struct dsdb_control_transaction_identifier); + if (transaction_id == NULL) { + struct ldb_context *ldb = ldb_module_get_ctx(module); + return ldb_oom(ldb); + } + transaction_id->transaction_guid = audit_private->transaction_guid; + ret = ldb_request_add_control(req, + DSDB_CONTROL_TRANSACTION_IDENTIFIER_OID, + false, + transaction_id); + return ret; + +} + +/* + * @brief log details of an add operation. + * + * Log the details of an add operation. + * + * @param module the ldb_module + * @param req the ldb_request + * + * @return ldb status code + */ +static int log_add( + struct ldb_module *module, + struct ldb_request *req) +{ + struct audit_callback_context *context = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + int ret; + + ldb = ldb_module_get_ctx(module); + context = talloc_zero(req, struct audit_callback_context); + + if (context == NULL) { + return ldb_oom(ldb); + } + context->request = req; + context->module = module; + /* + * We want to log the return code status, so we need to register + * a callback function to get the actual result. + * We need to take a new copy so that we don't alter the callers copy + */ + ret = ldb_build_add_req( + &new_req, + ldb, + req, + req->op.add.message, + req->controls, + context, + audit_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + ret = add_transaction_id(module, new_req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(module, new_req); +} + +/* + * @brief log details of an delete operation. + * + * Log the details of an delete operation. + * + * @param module the ldb_module + * @param req the ldb_request + * + * @return ldb status code + */ +static int log_delete( + struct ldb_module *module, + struct ldb_request *req) +{ + struct audit_callback_context *context = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + int ret; + + ldb = ldb_module_get_ctx(module); + context = talloc_zero(req, struct audit_callback_context); + + if (context == NULL) { + return ldb_oom(ldb); + } + context->request = req; + context->module = module; + /* + * We want to log the return code status, so we need to register + * a callback function to get the actual result. + * We need to take a new copy so that we don't alter the callers copy + */ + ret = ldb_build_del_req(&new_req, + ldb, + req, + req->op.del.dn, + req->controls, + context, + audit_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + ret = add_transaction_id(module, new_req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(module, new_req); +} + +/* + * @brief log details of a modify operation. + * + * Log the details of a modify operation. + * + * @param module the ldb_module + * @param req the ldb_request + * + * @return ldb status code + */ +static int log_modify( + struct ldb_module *module, + struct ldb_request *req) +{ + struct audit_callback_context *context = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + int ret; + + ldb = ldb_module_get_ctx(module); + context = talloc_zero(req, struct audit_callback_context); + + if (context == NULL) { + return ldb_oom(ldb); + } + context->request = req; + context->module = module; + /* + * We want to log the return code status, so we need to register + * a callback function to get the actual result. + * We need to take a new copy so that we don't alter the callers copy + */ + ret = ldb_build_mod_req( + & new_req, + ldb, + req, + req->op.mod.message, + req->controls, + context, + audit_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + ret = add_transaction_id(module, new_req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(module, new_req); +} + +/* + * @brief process a transaction start. + * + * process a transaction start, as we don't currently log transaction starts + * just generate the new transaction_id. + * + * @param module the ldb_module + * @param req the ldb_request + * + * @return ldb status code + */ +static int log_start_transaction(struct ldb_module *module) +{ + struct audit_private *audit_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct audit_private); + + /* + * We do not log transaction begins + * however we do generate a new transaction_id and record the start + * time so that we can log the transaction duration. + * + */ + audit_private->transaction_guid = GUID_random(); + audit_private->transaction_start = timeval_current(); + return ldb_next_start_trans(module); +} + +/* + * @brief log details of a prepare commit. + * + * Log the details of a prepare commit, currently only details of + * failures are logged. + * + * @param module the ldb_module + * @param req the ldb_request + * + * @return ldb status code + */ +static int log_prepare_commit(struct ldb_module *module) +{ + + int ret = ldb_next_prepare_commit(module); + if (ret != LDB_SUCCESS) { + /* + * We currently only log prepare commit failures + */ + log_commit_failure(module, "prepare", ret); + } + return ret; +} + +/* + * @brief process a transaction end aka commit. + * + * process a transaction end, as we don't currently log transaction ends + * just clear transaction_id. + * + * @param module the ldb_module + * @param req the ldb_request + * + * @return ldb status code + */ +static int log_end_transaction(struct ldb_module *module) +{ + struct audit_private *audit_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct audit_private); + int ret = 0; + + + ret = ldb_next_end_trans(module); + if (ret == LDB_SUCCESS) { + log_transaction( + module, + "commit", + TRANSACTION_LOG_COMPLETION_LVL); + } else { + log_commit_failure(module, "commit", ret); + } + /* + * Clear the transaction id inserted by log_start_transaction + */ + audit_private->transaction_guid = GUID_zero(); + return ret; +} + +/* + * @brief log details of a transaction delete aka roll back. + * + * Log details of a transaction roll back. + * + * @param module the ldb_module + * @param req the ldb_request + * + * @return ldb status code + */ +static int log_del_transaction(struct ldb_module *module) +{ + struct audit_private *audit_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct audit_private); + + log_transaction(module, "rollback", TRANSACTION_LOG_FAILURE_LVL); + audit_private->transaction_guid = GUID_zero(); + return ldb_next_del_trans(module); +} + +/* + * @brief log details of an extended operation. + * + * Log the details of an extended operation. + * + * @param module the ldb_module + * @param req the ldb_request + * + * @return ldb status code + */ +static int log_extended( + struct ldb_module *module, + struct ldb_request *req) +{ + struct audit_callback_context *context = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + int ret; + + /* + * Currently we only log replication extended operations + */ + if (strcmp( + req->op.extended.oid, + DSDB_EXTENDED_REPLICATED_OBJECTS_OID) != 0) { + + return ldb_next_request(module, req); + } + ldb = ldb_module_get_ctx(module); + context = talloc_zero(req, struct audit_callback_context); + + if (context == NULL) { + return ldb_oom(ldb); + } + context->request = req; + context->module = module; + /* + * We want to log the return code status, so we need to register + * a callback function to get the actual result. + * We need to take a new copy so that we don't alter the callers copy + */ + ret = ldb_build_extended_req( + &new_req, + ldb, + req, + req->op.extended.oid, + req->op.extended.data, + req->controls, + context, + audit_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + ret = add_transaction_id(module, new_req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(module, new_req); +} + +/* + * @brief module initialisation + */ +static int log_init(struct ldb_module *module) +{ + + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct audit_private *audit_private = NULL; + struct loadparm_context *lp_ctx + = talloc_get_type_abort(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + struct tevent_context *ev = ldb_get_event_context(ldb); + bool sdb_events = false; + bool pwd_events = false; + + audit_private = talloc_zero(module, struct audit_private); + if (audit_private == NULL) { + return ldb_module_oom(module); + } + + if (lp_ctx != NULL) { + sdb_events = lpcfg_dsdb_event_notification(lp_ctx); + pwd_events = lpcfg_dsdb_password_event_notification(lp_ctx); + } + if (sdb_events || pwd_events) { + audit_private->send_samdb_events = sdb_events; + audit_private->send_password_events = pwd_events; + audit_private->msg_ctx + = imessaging_client_init(audit_private, + lp_ctx, + ev); + } + + ldb_module_set_private(module, audit_private); + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_audit_log_module_ops = { + .name = "audit_log", + .init_context = log_init, + .add = log_add, + .modify = log_modify, + .del = log_delete, + .start_transaction = log_start_transaction, + .prepare_commit = log_prepare_commit, + .end_transaction = log_end_transaction, + .del_transaction = log_del_transaction, + .extended = log_extended, +}; + +int ldb_audit_log_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_audit_log_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/audit_util.c b/source4/dsdb/samdb/ldb_modules/audit_util.c new file mode 100644 index 0000000..f251025 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/audit_util.c @@ -0,0 +1,697 @@ +/* + ldb database module utility library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018 + + 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/>. +*/ + +/* + * Common utility functions for SamDb audit logging. + * + */ + +#include "includes.h" +#include "ldb_module.h" +#include "lib/audit_logging/audit_logging.h" + +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "libcli/security/dom_sid.h" +#include "libcli/security/security_token.h" +#include "auth/common_auth.h" +#include "param/param.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "dsdb/samdb/ldb_modules/audit_util_proto.h" + +#define MAX_LENGTH 1024 + +#define min(a, b) (((a)>(b))?(b):(a)) + +/* + * List of attributes considered secret or confidential the values of these + * attributes should not be displayed in log messages. + */ +static const char * const secret_attributes[] = { + DSDB_SECRET_ATTRIBUTES, + NULL}; +/* + * List of attributes that contain a password, used to detect password changes + */ +static const char * const password_attributes[] = { + DSDB_PASSWORD_ATTRIBUTES, + NULL}; + +/* + * @brief Should the value of the specified value be redacted. + * + * The values of secret or password attributes should not be displayed. + * + * @param name The attributes name. + * + * @return True if the attribute should be redacted + */ +bool dsdb_audit_redact_attribute(const char * name) +{ + + if (ldb_attr_in_list(secret_attributes, name)) { + return true; + } + + if (ldb_attr_in_list(password_attributes, name)) { + return true; + } + + return false; +} + +/* + * @brief is the attribute a password attribute? + * + * Is the attribute a password attribute. + * + * @return True if the attribute is a "Password" attribute. + */ +bool dsdb_audit_is_password_attribute(const char * name) +{ + + bool is_password = ldb_attr_in_list(password_attributes, name); + return is_password; +} + +/* + * @brief Get the remote address from the ldb context. + * + * The remote address is stored in the ldb opaque value "remoteAddress" + * it is the responsibility of the higher level code to ensure that this + * value is set. + * + * @param ldb the ldb_context. + * + * @return the remote address if known, otherwise NULL. + */ +const struct tsocket_address *dsdb_audit_get_remote_address( + struct ldb_context *ldb) +{ + void *opaque_remote_address = NULL; + struct tsocket_address *remote_address; + + opaque_remote_address = ldb_get_opaque(ldb, + "remoteAddress"); + if (opaque_remote_address == NULL) { + return NULL; + } + + remote_address = talloc_get_type(opaque_remote_address, + struct tsocket_address); + return remote_address; +} + +/* + * @brief Get the actual user SID from ldb context. + * + * The actual user SID is stored in the ldb opaque value "networkSessionInfo" + * it is the responsibility of the higher level code to ensure that this + * value is set. + * + * @param ldb the ldb_context. + * + * @return the users actual sid. + */ +const struct dom_sid *dsdb_audit_get_actual_sid(struct ldb_context *ldb) +{ + void *opaque_session = NULL; + struct auth_session_info *session = NULL; + struct security_token *user_token = NULL; + + opaque_session = ldb_get_opaque(ldb, DSDB_NETWORK_SESSION_INFO); + if (opaque_session == NULL) { + return NULL; + } + + session = talloc_get_type(opaque_session, struct auth_session_info); + if (session == NULL) { + return NULL; + } + + user_token = session->security_token; + if (user_token == NULL) { + return NULL; + } + return &user_token->sids[0]; +} +/* + * @brief get the ldb error string. + * + * Get the ldb error string if set, otherwise get the generic error code + * for the status code. + * + * @param ldb the ldb_context. + * @param status the ldb_status code. + * + * @return a string describing the error. + */ +const char *dsdb_audit_get_ldb_error_string( + struct ldb_module *module, + int status) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + const char *err_string = ldb_errstring(ldb); + + if (err_string == NULL) { + return ldb_strerror(status); + } + return err_string; +} + +/* + * @brief get the SID of the user performing the operation. + * + * Get the SID of the user performing the operation. + * + * @param module the ldb_module. + * + * @return the SID of the currently logged on user. + */ +const struct dom_sid *dsdb_audit_get_user_sid(const struct ldb_module *module) +{ + struct security_token *user_token = NULL; + + /* + * acl_user_token does not alter module so it's safe + * to discard the const. + */ + user_token = acl_user_token(discard_const(module)); + if (user_token == NULL) { + return NULL; + } + return &user_token->sids[0]; + +} + +/* + * @brief is operation being performed using the system session. + * + * Is the operation being performed using the system session. + * + * @param module the ldb_module. + * + * @return true if the operation is being performed using the system session. + */ +bool dsdb_audit_is_system_session(const struct ldb_module *module) +{ + struct security_token *user_token = NULL; + + /* + * acl_user_token does not alter module and security_token_is_system + * does not alter the security token so it's safe to discard the const. + */ + user_token = acl_user_token(discard_const(module)); + if (user_token == NULL) { + return false; + } + return security_token_is_system(user_token);; + +} + +/* + * @brief get the session identifier GUID + * + * Get the GUID that uniquely identifies the current authenticated session. + * + * @param module the ldb_module. + * + * @return the unique session GUID + */ +const struct GUID *dsdb_audit_get_unique_session_token( + const struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(discard_const(module)); + struct auth_session_info *session_info + = (struct auth_session_info *)ldb_get_opaque( + ldb, + DSDB_SESSION_INFO); + if(!session_info) { + return NULL; + } + return &session_info->unique_session_token; +} + +/* + * @brief get the actual user session identifier + * + * Get the GUID that uniquely identifies the current authenticated session. + * This is the session of the connected user, as it may differ from the + * session the operation is being performed as, i.e. for operations performed + * under the system session. + * + * @param context the ldb_context. + * + * @return the unique session GUID + */ +const struct GUID *dsdb_audit_get_actual_unique_session_token( + struct ldb_context *ldb) +{ + struct auth_session_info *session_info + = (struct auth_session_info *)ldb_get_opaque( + ldb, + DSDB_NETWORK_SESSION_INFO); + if(!session_info) { + return NULL; + } + return &session_info->unique_session_token; +} + +/* + * @brief Get a printable string value for the remote host address. + * + * Get a printable string representation of the remote host, for display in the + * the audit logs. + * + * @param ldb the ldb context. + * @param mem_ctx the talloc memory context that will own the returned string. + * + * @return A string representation of the remote host address or "Unknown" + * + */ +char *dsdb_audit_get_remote_host(struct ldb_context *ldb, TALLOC_CTX *mem_ctx) +{ + const struct tsocket_address *remote_address; + char* remote_host = NULL; + + remote_address = dsdb_audit_get_remote_address(ldb); + if (remote_address == NULL) { + remote_host = talloc_asprintf(mem_ctx, "Unknown"); + return remote_host; + } + + remote_host = tsocket_address_string(remote_address, mem_ctx); + return remote_host; +} + +/* + * @brief get a printable representation of the primary DN. + * + * Get a printable representation of the primary DN. The primary DN is the + * DN of the object being added, deleted, modified or renamed. + * + * @param the ldb_request. + * + * @return a printable and linearized DN + */ +const char* dsdb_audit_get_primary_dn(const struct ldb_request *request) +{ + struct ldb_dn *dn = NULL; + switch (request->operation) { + case LDB_ADD: + if (request->op.add.message != NULL) { + dn = request->op.add.message->dn; + } + break; + case LDB_MODIFY: + if (request->op.mod.message != NULL) { + dn = request->op.mod.message->dn; + } + break; + case LDB_DELETE: + dn = request->op.del.dn; + break; + case LDB_RENAME: + dn = request->op.rename.olddn; + break; + default: + dn = NULL; + break; + } + if (dn == NULL) { + return NULL; + } + return ldb_dn_get_linearized(dn); +} + +/* + * @brief Get the ldb_message from a request. + * + * Get the ldb_message for the request, returns NULL is there is no + * associated ldb_message + * + * @param The request + * + * @return the message associated with this request, or NULL + */ +const struct ldb_message *dsdb_audit_get_message( + const struct ldb_request *request) +{ + switch (request->operation) { + case LDB_ADD: + return request->op.add.message; + case LDB_MODIFY: + return request->op.mod.message; + default: + return NULL; + } +} + +/* + * @brief get the secondary dn, i.e. the target dn for a rename. + * + * Get the secondary dn, i.e. the target for a rename. This is only applicable + * got a rename operation, for the non rename operations this function returns + * NULL. + * + * @param request the ldb_request. + * + * @return the secondary dn in a printable and linearized form. + */ +const char *dsdb_audit_get_secondary_dn(const struct ldb_request *request) +{ + switch (request->operation) { + case LDB_RENAME: + return ldb_dn_get_linearized(request->op.rename.newdn); + default: + return NULL; + } +} + +/* + * @brief Map the request operation to a description. + * + * Get a description of the operation for logging + * + * @param request the ldb_request + * + * @return a string describing the operation, or "Unknown" if the operation + * is not known. + */ +const char *dsdb_audit_get_operation_name(const struct ldb_request *request) +{ + switch (request->operation) { + case LDB_SEARCH: + return "Search"; + case LDB_ADD: + return "Add"; + case LDB_MODIFY: + return "Modify"; + case LDB_DELETE: + return "Delete"; + case LDB_RENAME: + return "Rename"; + case LDB_EXTENDED: + return "Extended"; + case LDB_REQ_REGISTER_CONTROL: + return "Register Control"; + case LDB_REQ_REGISTER_PARTITION: + return "Register Partition"; + default: + return "Unknown"; + } +} + +/* + * @brief get a description of a modify action for logging. + * + * Get a brief description of the modification action suitable for logging. + * + * @param flags the ldb_attributes flags. + * + * @return a brief description, or "unknown". + */ +const char *dsdb_audit_get_modification_action(unsigned int flags) +{ + switch (LDB_FLAG_MOD_TYPE(flags)) { + case LDB_FLAG_MOD_ADD: + return "add"; + case LDB_FLAG_MOD_DELETE: + return "delete"; + case LDB_FLAG_MOD_REPLACE: + return "replace"; + default: + return "unknown"; + } +} + +/* + * @brief Add an ldb_value to a json object array + * + * Convert the current ldb_value to a JSON object and append it to array. + * { + * "value":"xxxxxxxx", + * "base64":true + * "truncated":true + * } + * + * value is the JSON string representation of the ldb_val, + * will be null if the value is zero length. The value will be + * truncated if it is more than MAX_LENGTH bytes long. It will also + * be base64 encoded if it contains any non printable characters. + * + * base64 Indicates that the value is base64 encoded, will be absent if the + * value is not encoded. + * + * truncated Indicates that the length of the value exceeded MAX_LENGTH and was + * truncated. Note that vales are truncated and then base64 encoded. + * so an encoded value can be longer than MAX_LENGTH. + * + * @param array the JSON array to append the value to. + * @param lv the ldb_val to convert and append to the array. + * + */ +static int dsdb_audit_add_ldb_value(struct json_object *array, + const struct ldb_val lv) +{ + bool base64; + int len; + struct json_object value = json_empty_object; + int rc = 0; + + json_assert_is_array(array); + if (json_is_invalid(array)) { + return -1; + } + + if (lv.length == 0 || lv.data == NULL) { + rc = json_add_object(array, NULL, NULL); + if (rc != 0) { + goto failure; + } + return 0; + } + + base64 = ldb_should_b64_encode(NULL, &lv); + len = min(lv.length, MAX_LENGTH); + value = json_new_object(); + if (json_is_invalid(&value)) { + goto failure; + } + + if (lv.length > MAX_LENGTH) { + rc = json_add_bool(&value, "truncated", true); + if (rc != 0) { + goto failure; + } + } + if (base64) { + TALLOC_CTX *ctx = talloc_new(NULL); + char *encoded = ldb_base64_encode( + ctx, + (char*) lv.data, + len); + + if (ctx == NULL) { + goto failure; + } + + rc = json_add_bool(&value, "base64", true); + if (rc != 0) { + TALLOC_FREE(ctx); + goto failure; + } + rc = json_add_string(&value, "value", encoded); + if (rc != 0) { + TALLOC_FREE(ctx); + goto failure; + } + TALLOC_FREE(ctx); + } else { + rc = json_add_stringn(&value, "value", (char *)lv.data, len); + if (rc != 0) { + goto failure; + } + } + /* + * As array is a JSON array the element name is NULL + */ + rc = json_add_object(array, NULL, &value); + if (rc != 0) { + goto failure; + } + return 0; +failure: + /* + * In the event of a failure value will not have been added to array + * so it needs to be freed to prevent a leak. + */ + json_free(&value); + DBG_ERR("unable to add ldb value to JSON audit message"); + return -1; +} + +/* + * @brief Build a JSON object containing the attributes in an ldb_message. + * + * Build a JSON object containing all the attributes in an ldb_message. + * The attributes are keyed by attribute name, the values of "secret attributes" + * are supressed. + * + * { + * "password":{ + * "redacted":true, + * "action":"delete" + * }, + * "name":{ + * "values": [ + * { + * "value":"xxxxxxxx", + * "base64":true + * "truncated":true + * }, + * ], + * "action":"add", + * } + * } + * + * values is an array of json objects generated by add_ldb_value. + * redacted indicates that the attribute is secret. + * action is only set for modification operations. + * + * @param operation the ldb operation being performed + * @param message the ldb_message to process. + * + * @return A populated json object. + * + */ +struct json_object dsdb_audit_attributes_json( + enum ldb_request_type operation, + const struct ldb_message* message) +{ + + unsigned int i, j; + struct json_object attributes = json_new_object(); + + if (json_is_invalid(&attributes)) { + goto failure; + } + for (i=0;i<message->num_elements;i++) { + struct json_object actions = json_empty_object; + struct json_object attribute = json_empty_object; + struct json_object action = json_empty_object; + const char *name = message->elements[i].name; + int rc = 0; + + action = json_new_object(); + if (json_is_invalid(&action)) { + goto failure; + } + + /* + * If this is a modify operation tag the attribute with + * the modification action. + */ + if (operation == LDB_MODIFY) { + const char *act = NULL; + const int flags = message->elements[i].flags; + act = dsdb_audit_get_modification_action(flags); + rc = json_add_string(&action, "action", act); + if (rc != 0) { + json_free(&action); + goto failure; + } + } + if (operation == LDB_ADD) { + rc = json_add_string(&action, "action", "add"); + if (rc != 0) { + json_free(&action); + goto failure; + } + } + + /* + * If the attribute is a secret attribute, tag it as redacted + * and don't include the values + */ + if (dsdb_audit_redact_attribute(name)) { + rc = json_add_bool(&action, "redacted", true); + if (rc != 0) { + json_free(&action); + goto failure; + } + } else { + struct json_object values; + /* + * Add the values for the action + */ + values = json_new_array(); + if (json_is_invalid(&values)) { + json_free(&action); + goto failure; + } + + for (j=0;j<message->elements[i].num_values;j++) { + rc = dsdb_audit_add_ldb_value( + &values, message->elements[i].values[j]); + if (rc != 0) { + json_free(&values); + json_free(&action); + goto failure; + } + } + rc = json_add_object(&action, "values", &values); + if (rc != 0) { + json_free(&values); + json_free(&action); + goto failure; + } + } + attribute = json_get_object(&attributes, name); + if (json_is_invalid(&attribute)) { + json_free(&action); + goto failure; + } + actions = json_get_array(&attribute, "actions"); + if (json_is_invalid(&actions)) { + json_free(&action); + goto failure; + } + rc = json_add_object(&actions, NULL, &action); + if (rc != 0) { + json_free(&action); + goto failure; + } + rc = json_add_object(&attribute, "actions", &actions); + if (rc != 0) { + json_free(&actions); + goto failure; + } + rc = json_add_object(&attributes, name, &attribute); + if (rc != 0) { + json_free(&attribute); + goto failure; + } + } + return attributes; +failure: + json_free(&attributes); + DBG_ERR("Unable to create ldb attributes JSON audit message\n"); + return attributes; +} diff --git a/source4/dsdb/samdb/ldb_modules/count_attrs.c b/source4/dsdb/samdb/ldb_modules/count_attrs.c new file mode 100644 index 0000000..c72898a --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/count_attrs.c @@ -0,0 +1,644 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2019 + + 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/>. +*/ + +/* + * Count how often different attributes are searched for, for performance + * analysis. The counts are stored in tdb files in the 'debug' subdirectory of + * Samba installation's private directory, and can be read using + * script/attr_count_read. + */ + +#include "includes.h" +#include "ldb_module.h" +#include "param/param.h" +#include "lib/tdb_wrap/tdb_wrap.h" +#include "system/filesys.h" + +#define NULL_ATTRS "__null_attrs__" +#define EMPTY_ATTRS "__empty_attrs__" +#define UNKNOWN_ATTR "__unknown_attribute__" +#define STAR_ATTR "*" + +#define NULL_REQ_PSEUDO_N -2LL; +#define STAR_REQ_PSEUDO_N -4LL; + +#undef strcasecmp + +struct count_attrs_private { + struct tdb_wrap *requested; + struct tdb_wrap *duplicates; + struct tdb_wrap *found; + struct tdb_wrap *not_found; + struct tdb_wrap *unwanted; + struct tdb_wrap *star_match; + struct tdb_wrap *null_req; + struct tdb_wrap *empty_req; + struct tdb_wrap *req_vs_found; +}; + + +struct count_attrs_context { + struct ldb_module *module; + struct ldb_request *req; + bool has_star; + bool is_null; + const char **requested_attrs; + size_t n_attrs; +}; + + +static int add_key(struct tdb_context *tdb, + struct TDB_DATA key) +{ + int ret; + uint32_t one = 1; + struct TDB_DATA value = { + .dptr = (uint8_t *)&one, + .dsize = sizeof(one) + }; + ret = tdb_store(tdb, + key, + value, + 0); + return ret; +} + +static int increment_attr_count(struct tdb_context *tdb, + const char *attr) +{ + /* + * Note that as we don't lock the database, there is a small window + * between the fetch and store in which identical updates from + * separate processes can race to clobber each other. If this happens + * the stored count will be one less than it should be. + * + * We don't worry about that because it should be quite rare and + * agnostic as to which counts are affected, meaning the overall + * statistical truth is preserved. + */ + int ret; + uint32_t *val; + TDB_DATA key = { + .dptr = discard_const(attr), + .dsize = strlen(attr) + }; + + TDB_DATA data = tdb_fetch(tdb, key); + if (data.dptr == NULL) { + ret = tdb_error(tdb); + if (ret != TDB_ERR_NOEXIST) { + const char *errstr = tdb_errorstr(tdb); + DBG_ERR("tdb fetch error: %s\n", errstr); + return LDB_ERR_OPERATIONS_ERROR; + } + /* this key is unknown. We'll add it and get out of here. */ + ret = add_key(tdb, key); + if (ret != 0) { + DBG_ERR("could not add %s: %d\n", attr, ret); + } + return ret; + } + + val = (uint32_t *)data.dptr; + (*val)++; + + ret = tdb_store(tdb, + key, + data, + 0); + + if (ret != 0) { + const char *errstr = tdb_errorstr(tdb); + DBG_ERR("tdb store error: %s\n", errstr); + free(data.dptr); + return LDB_ERR_OPERATIONS_ERROR; + } + free(data.dptr); + return LDB_SUCCESS; +} + + +static int increment_req_vs_found(struct tdb_context *tdb, + struct count_attrs_context *ac, + size_t n_found) +{ + /* + * Here we record the number of elements in each reply along with the + * number of attributes in the corresponding request. Requests for + * NULL and "*" are arbitrarily given the attribute counts -2 and -4 + * respectively. This leads them to be plotted as two stacks on the + * left hand side of the scatter plot. + */ + int ret; + ssize_t k[2]; + uint32_t *val = NULL; + TDB_DATA key = { + .dptr = (unsigned char *)k, + .dsize = sizeof(k) + }; + TDB_DATA data = {0}; + ssize_t n_req = ac->n_attrs; + if (ac->is_null) { + n_req = NULL_REQ_PSEUDO_N; + } else if (ac->has_star) { + n_req = STAR_REQ_PSEUDO_N; + } + k[0] = n_req; + k[1] = n_found; + + data = tdb_fetch(tdb, key); + if (data.dptr == NULL) { + ret = tdb_error(tdb); + if (ret != TDB_ERR_NOEXIST) { + const char *errstr = tdb_errorstr(tdb); + DBG_ERR("req vs found fetch error: %s\n", errstr); + return LDB_ERR_OPERATIONS_ERROR; + } + /* unknown key */ + ret = add_key(tdb, key); + if (ret != 0) { + DBG_ERR("could not add req vs found %zu:%zu: %d\n", + n_req, n_found, ret); + } + return ret; + } + + val = (uint32_t *)data.dptr; + (*val)++; + + ret = tdb_store(tdb, key, data, 0); + if (ret != 0) { + const char *errstr = tdb_errorstr(tdb); + DBG_ERR("req vs found store error: %s\n", errstr); + free(data.dptr); + return LDB_ERR_OPERATIONS_ERROR; + } + free(data.dptr); + return LDB_SUCCESS; +} + + +static int strcasecmp_ptr(const char **a, const char **b) +{ + return strcasecmp(*a, *b); +} + + +static const char **get_sorted_attrs(TALLOC_CTX *mem_ctx, + const char * const *unsorted_attrs, + size_t n_attrs) +{ + size_t i; + const char **attrs = talloc_array(mem_ctx, + const char *, + n_attrs); + + if (attrs == NULL) { + return NULL; + } + for (i = 0; i < n_attrs; i++) { + const char *a = unsorted_attrs[i]; + if (a == NULL) { + DBG_ERR("attrs have disappeared! " + "wanted %zu; got %zu\n", + n_attrs, i); + talloc_free(attrs); + return NULL; + } + attrs[i] = a; + } + + qsort(attrs, n_attrs, sizeof(char *), QSORT_CAST strcasecmp_ptr); + return attrs; +} + + + +static int count_attrs_search_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct count_attrs_private *priv = NULL; + struct ldb_message *msg = NULL; + size_t i, j; + int ret; + + struct count_attrs_context *ac = \ + talloc_get_type(req->context, + struct count_attrs_context); + + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + + priv = talloc_get_type_abort(ldb_module_get_private(ac->module), + struct count_attrs_private); + + if (ares == NULL) { + DBG_ERR("ares is NULL\n"); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + DBG_INFO("ares error %d\n", ares->error); + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral(ac->req, ares->referral); + + case LDB_REPLY_DONE: + return ldb_module_done(ac->req, ares->controls, + ares->response, LDB_SUCCESS); + + case LDB_REPLY_ENTRY: + msg = ares->message; + if (ac->is_null || ac->n_attrs == 0) { + struct tdb_context *tdb = NULL; + /* + * Note when attributes are found when the requested + * list was empty or NULL + */ + if (ac->is_null) { + tdb = priv->null_req->tdb; + } else { + tdb = priv->empty_req->tdb; + } + for (i = 0; i < msg->num_elements; i++) { + const char *name = msg->elements[i].name; + ret = increment_attr_count(tdb, name); + if (ret != LDB_SUCCESS) { + talloc_free(ares); + DBG_ERR("inc failed\n"); + return ret; + } + } + } else { + /* + * We make sorted lists of the requested and found + * elements, which makes it easy to find missing or + * intruding values. + */ + struct tdb_context *found_tdb = priv->found->tdb; + struct tdb_context *unwanted_tdb = \ + priv->unwanted->tdb; + struct tdb_context *star_match_tdb = \ + priv->star_match->tdb; + struct tdb_context *not_found_tdb = \ + priv->not_found->tdb; + + const char **requested_attrs = ac->requested_attrs; + const char **found_attrs = \ + talloc_array(ac, const char *, + msg->num_elements); + if (found_attrs == NULL) { + return ldb_oom(ldb); + } + + for (i = 0; i < msg->num_elements; i++) { + found_attrs[i] = msg->elements[i].name; + } + + qsort(found_attrs, msg->num_elements, sizeof(char *), + QSORT_CAST strcasecmp_ptr); + + + /* find and report duplicates */ + for (i = 1; i < msg->num_elements; i++) { + if (strcasecmp(found_attrs[i], + found_attrs[i - 1]) == 0) { + DBG_ERR("duplicate element: %s!\n", + found_attrs[i]); + /* + * If this happens it will muck up our + * counts, but probably have worse + * effects on the rest of the module + * stack. */ + } + } + + /* + * This next bit is like the merge stage of a + * mergesort, but instead of merging we only detect + * absense or presence. + */ + i = 0; + j = 0; + while (i < ac->n_attrs || + j < msg->num_elements) { + int cmp; + if (i >= ac->n_attrs) { + cmp = 1; + } else if (j >= msg->num_elements) { + cmp = -1; + } else { + cmp = strcasecmp(requested_attrs[i], + found_attrs[j] + ); + } + + if (cmp < 0) { + /* We did not find the element */ + ret = increment_attr_count( + not_found_tdb, + requested_attrs[i]); + i++; + } else if (cmp > 0) { + /* + * We found the element, but didn't + * specifically ask for it. + */ + if (ac->has_star) { + ret = increment_attr_count( + star_match_tdb, + found_attrs[j]); + } else { + ret = increment_attr_count( + unwanted_tdb, + found_attrs[j]); + } + j++; + } else { + /* We got what we asked for. */ + ret = increment_attr_count( + found_tdb, + found_attrs[j]); + i++; + j++; + } + if (ret != LDB_SUCCESS) { + talloc_free(ares); + DBG_ERR("inc failed\n"); + return ret; + } + } + } + ret = increment_req_vs_found(priv->req_vs_found->tdb, + ac, + msg->num_elements); + + if (ret != LDB_SUCCESS) { + talloc_free(ares); + DBG_ERR("inc of req vs found failed\n"); + return ret; + } + + return ldb_module_send_entry( + ac->req, + ares->message, + ares->controls); + } + + talloc_free(ares); + return LDB_SUCCESS; +} + + +static int count_attrs_search(struct ldb_module *module, + struct ldb_request *req) +{ + int ret; + const char * const *attrs = req->op.search.attrs; + struct count_attrs_private *count_attrs_private = NULL; + struct tdb_context *tdb = NULL; + struct ldb_request *down_req = NULL; + struct count_attrs_context *ac = NULL; + bool has_star = false; + bool is_null = false; + size_t n_attrs = 0; + const char **sorted_attrs = NULL; + struct ldb_context *ldb = ldb_module_get_ctx(module); + + + void *untyped_private = ldb_module_get_private(module); + if (untyped_private == NULL) { + /* + * There are some cases (in early start up, and during a + * backup restore) in which we get a NULL private object, in + * which case all we can do is ignore it and pass the request + * on unexamined. + */ + return ldb_next_request(module, req); + } + + count_attrs_private = talloc_get_type_abort(untyped_private, + struct count_attrs_private); + tdb = count_attrs_private->requested->tdb; + + ac = talloc_zero(req, struct count_attrs_context); + if (ac == NULL) { + return ldb_oom(ldb); + } + + if (attrs == NULL) { + ret = increment_attr_count(tdb, NULL_ATTRS); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + is_null = true; + } else if (attrs[0] == NULL) { + ret = increment_attr_count(tdb, EMPTY_ATTRS); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + } else { + size_t i, j; + for (i = 0; attrs[i] != NULL; i++) { + ret = increment_attr_count(tdb, attrs[i]); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + if (strcmp("*", attrs[i]) == 0) { + has_star = true; + } + } + n_attrs = i; + sorted_attrs = get_sorted_attrs(req, + attrs, + n_attrs); + /* + * Find, report, and remove duplicates. Duplicate attrs in + * requests are allowed, but don't work well with our + * merge-count algorithm. + */ + j = 0; + for (i = 1; i < n_attrs; i++) { + if (strcasecmp(sorted_attrs[i], + sorted_attrs[j]) == 0) { + ret = increment_attr_count( + count_attrs_private->duplicates->tdb, + sorted_attrs[i]); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + } else { + j++; + if (j != i) { + sorted_attrs[j] = sorted_attrs[i]; + } + } + } + n_attrs = j; + } + + ac->module = module; + ac->req = req; + ac->has_star = has_star; + ac->is_null = is_null; + ac->n_attrs = n_attrs; + ac->requested_attrs = sorted_attrs; + + ret = ldb_build_search_req_ex(&down_req, + ldb, + ac, + req->op.search.base, + req->op.search.scope, + req->op.search.tree, + req->op.search.attrs, + req->controls, + ac, + count_attrs_search_callback, + req); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + return ldb_next_request(module, down_req); +} + + +static struct tdb_wrap * open_private_tdb(TALLOC_CTX *mem_ctx, + struct loadparm_context *lp_ctx, + const char *name) +{ + struct tdb_wrap *store = NULL; + char *filename = lpcfg_private_path(mem_ctx, lp_ctx, name); + + if (filename == NULL) { + return NULL; + } + + store = tdb_wrap_open(mem_ctx, filename, 1000, + TDB_CLEAR_IF_FIRST, + O_RDWR | O_CREAT, + 0660); + if (store == NULL) { + DBG_ERR("failed to open tdb at %s\n", filename); + } + TALLOC_FREE(filename); + return store; +} + +static int make_private_dir(TALLOC_CTX *mem_ctx, + struct loadparm_context *lp_ctx, + const char *name) +{ + int ret; + char *dirname = lpcfg_private_path(mem_ctx, lp_ctx, name); + if (dirname == NULL) { + return -1; + } + ret = mkdir(dirname, 0755); + TALLOC_FREE(dirname); + return ret; +} + + +static int count_attrs_init(struct ldb_module *module) +{ + struct ldb_context *ldb = NULL; + struct count_attrs_private *data = NULL; + struct loadparm_context *lp_ctx = NULL; + int ret; + + ldb = ldb_module_get_ctx(module); + + data = talloc_zero(module, struct count_attrs_private); + if (data == NULL) { + return ldb_oom(ldb); + } + + lp_ctx = talloc_get_type(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + + ret = make_private_dir(data, lp_ctx, "debug"); + if (ret != 0) { + goto no_private_dir; + } + data->requested = open_private_tdb(data, lp_ctx, + "debug/attr_counts_requested.tdb"); + data->duplicates = \ + open_private_tdb(data, lp_ctx, + "debug/attr_counts_duplicates.tdb"); + data->found = open_private_tdb(data, lp_ctx, + "debug/attr_counts_found.tdb"); + data->not_found = open_private_tdb(data, lp_ctx, + "debug/attr_counts_not_found.tdb"); + data->unwanted = open_private_tdb(data, lp_ctx, + "debug/attr_counts_unwanted.tdb"); + data->star_match = open_private_tdb(data, lp_ctx, + "debug/attr_counts_star_match.tdb"); + data->null_req = open_private_tdb(data, lp_ctx, + "debug/attr_counts_null_req.tdb"); + data->empty_req = open_private_tdb(data, lp_ctx, + "debug/attr_counts_empty_req.tdb"); + data->req_vs_found = \ + open_private_tdb(data, lp_ctx, + "debug/attr_counts_req_vs_found.tdb"); + if (data->requested == NULL || + data->duplicates == NULL || + data->found == NULL || + data->not_found == NULL || + data->unwanted == NULL || + data->star_match == NULL || + data->null_req == NULL || + data->empty_req == NULL || + data->req_vs_found == NULL) { + goto no_private_dir; + } + + ldb_module_set_private(module, data); + return ldb_next_init(module); + + no_private_dir: + /* + * If we leave the private data NULL, the search function knows not to + * do anything. + */ + DBG_WARNING("the count_attrs module could not open its databases\n"); + DBG_WARNING("attributes will not be counted.\n"); + TALLOC_FREE(data); + ldb_module_set_private(module, NULL); + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_count_attrs_module_ops = { + .name = "count_attrs", + .search = count_attrs_search, + .init_context = count_attrs_init +}; + +int ldb_count_attrs_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_count_attrs_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/descriptor.c b/source4/dsdb/samdb/ldb_modules/descriptor.c new file mode 100644 index 0000000..bf0f8ec --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/descriptor.c @@ -0,0 +1,1901 @@ +/* + ldb database library + + Copyright (C) Simo Sorce 2006-2008 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2005-2007 + Copyright (C) Nadezhda Ivanova 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: DS Security descriptor module + * + * Description: + * - Calculate the security descriptor of a newly created object + * - Perform sd recalculation on a move operation + * - Handle sd modification invariants + * + * Author: Nadezhda Ivanova + */ + +#include "includes.h" +#include <ldb_module.h> +#include "util/dlinklist.h" +#include "dsdb/samdb/samdb.h" +#include "librpc/ndr/libndr.h" +#include "librpc/gen_ndr/ndr_security.h" +#include "libcli/security/security.h" +#include "auth/auth.h" +#include "param/param.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "lib/util/util_tdb.h" +#include "lib/dbwrap/dbwrap.h" +#include "lib/dbwrap/dbwrap_rbt.h" + +struct descriptor_changes { + struct descriptor_changes *prev, *next; + struct ldb_dn *nc_root; + struct GUID guid; + struct GUID parent_guid; + bool force_self; + bool force_children; + struct ldb_dn *stopped_dn; + size_t ref_count; + size_t sort_count; +}; + +struct descriptor_transaction { + TALLOC_CTX *mem; + struct { + /* + * We used to have a list of changes, appended with each + * DSDB_EXTENDED_SEC_DESC_PROPAGATION_OID operation. + * + * But the main problem was that a replication + * cycle (mainly the initial replication) calls + * DSDB_EXTENDED_SEC_DESC_PROPAGATION_OID for the + * same object[GUID] more than once. With + * DRSUAPI_DRS_GET_TGT we'll get the naming + * context head object and other top level + * containers, every often. + * + * It means we'll process objects more + * than once and waste a lot of time + * doing the same work again and again. + * + * We use an objectGUID based map in order to + * avoid registering objects more than once. + * In an domain with 22000 object it can + * reduce the work from 4 hours down to ~ 3.5 minutes. + */ + struct descriptor_changes *list; + struct db_context *map; + size_t num_registrations; + size_t num_registered; + size_t num_toplevel; + size_t num_processed; + } changes; + struct { + struct db_context *map; + size_t num_processed; + size_t num_skipped; + } objects; +}; + +struct descriptor_data { + struct descriptor_transaction transaction; +}; + +struct descriptor_context { + struct ldb_module *module; + struct ldb_request *req; + struct ldb_message *msg; + struct ldb_reply *search_res; + struct ldb_reply *search_oc_res; + struct ldb_val *parentsd_val; + struct ldb_message_element *sd_element; + struct ldb_val *sd_val; + uint32_t sd_flags; + int (*step_fn)(struct descriptor_context *); +}; + +static struct dom_sid *get_default_ag(TALLOC_CTX *mem_ctx, + struct ldb_dn *dn, + struct security_token *token, + struct ldb_context *ldb) +{ + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + const struct dom_sid *domain_sid = samdb_domain_sid(ldb); + struct dom_sid *da_sid = dom_sid_add_rid(tmp_ctx, domain_sid, DOMAIN_RID_ADMINS); + struct dom_sid *ea_sid = dom_sid_add_rid(tmp_ctx, domain_sid, DOMAIN_RID_ENTERPRISE_ADMINS); + struct dom_sid *sa_sid = dom_sid_add_rid(tmp_ctx, domain_sid, DOMAIN_RID_SCHEMA_ADMINS); + struct dom_sid *dag_sid; + struct ldb_dn *nc_root; + int ret; + + ret = dsdb_find_nc_root(ldb, tmp_ctx, dn, &nc_root); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return NULL; + } + + if (ldb_dn_compare(nc_root, ldb_get_schema_basedn(ldb)) == 0) { + if (security_token_has_sid(token, sa_sid)) { + dag_sid = dom_sid_dup(mem_ctx, sa_sid); + } else if (security_token_has_sid(token, ea_sid)) { + dag_sid = dom_sid_dup(mem_ctx, ea_sid); + } else if (security_token_has_sid(token, da_sid)) { + dag_sid = dom_sid_dup(mem_ctx, da_sid); + } else if (security_token_is_system(token)) { + dag_sid = dom_sid_dup(mem_ctx, sa_sid); + } else { + dag_sid = NULL; + } + } else if (ldb_dn_compare(nc_root, ldb_get_config_basedn(ldb)) == 0) { + if (security_token_has_sid(token, ea_sid)) { + dag_sid = dom_sid_dup(mem_ctx, ea_sid); + } else if (security_token_has_sid(token, da_sid)) { + dag_sid = dom_sid_dup(mem_ctx, da_sid); + } else if (security_token_is_system(token)) { + dag_sid = dom_sid_dup(mem_ctx, ea_sid); + } else { + dag_sid = NULL; + } + } else if (ldb_dn_compare(nc_root, ldb_get_default_basedn(ldb)) == 0) { + if (security_token_has_sid(token, da_sid)) { + dag_sid = dom_sid_dup(mem_ctx, da_sid); + } else if (security_token_has_sid(token, ea_sid)) { + dag_sid = dom_sid_dup(mem_ctx, ea_sid); + } else if (security_token_is_system(token)) { + dag_sid = dom_sid_dup(mem_ctx, da_sid); + } else { + dag_sid = NULL; + } + } else { + dag_sid = NULL; + } + + talloc_free(tmp_ctx); + return dag_sid; +} + +static struct security_descriptor *get_sd_unpacked(struct ldb_module *module, TALLOC_CTX *mem_ctx, + const struct dsdb_class *objectclass) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct security_descriptor *sd; + const struct dom_sid *domain_sid = samdb_domain_sid(ldb); + + if (!objectclass->defaultSecurityDescriptor || !domain_sid) { + return NULL; + } + + sd = sddl_decode(mem_ctx, + objectclass->defaultSecurityDescriptor, + domain_sid); + return sd; +} + +static struct dom_sid *get_default_group(TALLOC_CTX *mem_ctx, + struct ldb_context *ldb, + struct dom_sid *dag) +{ + /* + * This depends on the function level of the DC + * which is 2008R2 in our case. Which means it is + * higher than 2003 and we should use the + * "default administrator group" also as owning group. + * + * This matches dcpromo for a 2003 domain + * on a Windows 2008R2 DC. + */ + return dag; +} + +static struct security_descriptor *descr_handle_sd_flags(TALLOC_CTX *mem_ctx, + struct security_descriptor *new_sd, + struct security_descriptor *old_sd, + uint32_t sd_flags) +{ + struct security_descriptor *final_sd; + /* if there is no control or control == 0 modify everything */ + if (!sd_flags) { + return new_sd; + } + + final_sd = talloc_zero(mem_ctx, struct security_descriptor); + final_sd->revision = SECURITY_DESCRIPTOR_REVISION_1; + final_sd->type = SEC_DESC_SELF_RELATIVE; + + if (sd_flags & (SECINFO_OWNER)) { + if (new_sd->owner_sid) { + final_sd->owner_sid = talloc_memdup(mem_ctx, new_sd->owner_sid, sizeof(struct dom_sid)); + } + final_sd->type |= new_sd->type & SEC_DESC_OWNER_DEFAULTED; + } + else if (old_sd) { + if (old_sd->owner_sid) { + final_sd->owner_sid = talloc_memdup(mem_ctx, old_sd->owner_sid, sizeof(struct dom_sid)); + } + final_sd->type |= old_sd->type & SEC_DESC_OWNER_DEFAULTED; + } + + if (sd_flags & (SECINFO_GROUP)) { + if (new_sd->group_sid) { + final_sd->group_sid = talloc_memdup(mem_ctx, new_sd->group_sid, sizeof(struct dom_sid)); + } + final_sd->type |= new_sd->type & SEC_DESC_GROUP_DEFAULTED; + } + else if (old_sd) { + if (old_sd->group_sid) { + final_sd->group_sid = talloc_memdup(mem_ctx, old_sd->group_sid, sizeof(struct dom_sid)); + } + final_sd->type |= old_sd->type & SEC_DESC_GROUP_DEFAULTED; + } + + if (sd_flags & (SECINFO_SACL)) { + final_sd->sacl = security_acl_dup(mem_ctx,new_sd->sacl); + final_sd->type |= new_sd->type & (SEC_DESC_SACL_PRESENT | + SEC_DESC_SACL_DEFAULTED|SEC_DESC_SACL_AUTO_INHERIT_REQ | + SEC_DESC_SACL_AUTO_INHERITED|SEC_DESC_SACL_PROTECTED | + SEC_DESC_SERVER_SECURITY); + } + else if (old_sd && old_sd->sacl) { + final_sd->sacl = security_acl_dup(mem_ctx,old_sd->sacl); + final_sd->type |= old_sd->type & (SEC_DESC_SACL_PRESENT | + SEC_DESC_SACL_DEFAULTED|SEC_DESC_SACL_AUTO_INHERIT_REQ | + SEC_DESC_SACL_AUTO_INHERITED|SEC_DESC_SACL_PROTECTED | + SEC_DESC_SERVER_SECURITY); + } + + if (sd_flags & (SECINFO_DACL)) { + final_sd->dacl = security_acl_dup(mem_ctx,new_sd->dacl); + final_sd->type |= new_sd->type & (SEC_DESC_DACL_PRESENT | + SEC_DESC_DACL_DEFAULTED|SEC_DESC_DACL_AUTO_INHERIT_REQ | + SEC_DESC_DACL_AUTO_INHERITED|SEC_DESC_DACL_PROTECTED | + SEC_DESC_DACL_TRUSTED); + } + else if (old_sd && old_sd->dacl) { + final_sd->dacl = security_acl_dup(mem_ctx,old_sd->dacl); + final_sd->type |= old_sd->type & (SEC_DESC_DACL_PRESENT | + SEC_DESC_DACL_DEFAULTED|SEC_DESC_DACL_AUTO_INHERIT_REQ | + SEC_DESC_DACL_AUTO_INHERITED|SEC_DESC_DACL_PROTECTED | + SEC_DESC_DACL_TRUSTED); + } + /* not so sure about this */ + final_sd->type |= new_sd->type & SEC_DESC_RM_CONTROL_VALID; + return final_sd; +} + +static DATA_BLOB *get_new_descriptor(struct ldb_module *module, + struct ldb_dn *dn, + TALLOC_CTX *mem_ctx, + const struct dsdb_class *objectclass, + const struct ldb_val *parent, + const struct ldb_val *object, + const struct ldb_val *old_sd, + uint32_t sd_flags) +{ + struct security_descriptor *user_descriptor = NULL, *parent_descriptor = NULL; + struct security_descriptor *old_descriptor = NULL; + struct security_descriptor *new_sd, *final_sd; + DATA_BLOB *linear_sd; + enum ndr_err_code ndr_err; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct auth_session_info *session_info + = ldb_get_opaque(ldb, DSDB_SESSION_INFO); + const struct dom_sid *domain_sid = samdb_domain_sid(ldb); + struct dom_sid *default_owner; + struct dom_sid *default_group; + struct security_descriptor *default_descriptor = NULL; + struct GUID *object_list = NULL; + + if (objectclass != NULL) { + default_descriptor = get_sd_unpacked(module, mem_ctx, objectclass); + object_list = talloc_zero_array(mem_ctx, struct GUID, 2); + if (object_list == NULL) { + return NULL; + } + object_list[0] = objectclass->schemaIDGUID; + } + + if (object) { + user_descriptor = talloc(mem_ctx, struct security_descriptor); + if (!user_descriptor) { + return NULL; + } + ndr_err = ndr_pull_struct_blob(object, user_descriptor, + user_descriptor, + (ndr_pull_flags_fn_t)ndr_pull_security_descriptor); + + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + talloc_free(user_descriptor); + return NULL; + } + } else { + user_descriptor = default_descriptor; + } + + if (old_sd) { + old_descriptor = talloc(mem_ctx, struct security_descriptor); + if (!old_descriptor) { + return NULL; + } + ndr_err = ndr_pull_struct_blob(old_sd, old_descriptor, + old_descriptor, + (ndr_pull_flags_fn_t)ndr_pull_security_descriptor); + + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + talloc_free(old_descriptor); + return NULL; + } + } + + if (parent) { + parent_descriptor = talloc(mem_ctx, struct security_descriptor); + if (!parent_descriptor) { + return NULL; + } + ndr_err = ndr_pull_struct_blob(parent, parent_descriptor, + parent_descriptor, + (ndr_pull_flags_fn_t)ndr_pull_security_descriptor); + + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + talloc_free(parent_descriptor); + return NULL; + } + } + + if (user_descriptor && default_descriptor && + (user_descriptor->dacl == NULL)) + { + user_descriptor->dacl = default_descriptor->dacl; + user_descriptor->type |= default_descriptor->type & ( + SEC_DESC_DACL_PRESENT | + SEC_DESC_DACL_DEFAULTED|SEC_DESC_DACL_AUTO_INHERIT_REQ | + SEC_DESC_DACL_AUTO_INHERITED|SEC_DESC_DACL_PROTECTED | + SEC_DESC_DACL_TRUSTED); + } + + if (user_descriptor && default_descriptor && + (user_descriptor->sacl == NULL)) + { + user_descriptor->sacl = default_descriptor->sacl; + user_descriptor->type |= default_descriptor->type & ( + SEC_DESC_SACL_PRESENT | + SEC_DESC_SACL_DEFAULTED|SEC_DESC_SACL_AUTO_INHERIT_REQ | + SEC_DESC_SACL_AUTO_INHERITED|SEC_DESC_SACL_PROTECTED | + SEC_DESC_SERVER_SECURITY); + } + + + if (!(sd_flags & SECINFO_OWNER) && user_descriptor) { + user_descriptor->owner_sid = NULL; + + /* + * We need the correct owner sid + * when calculating the DACL or SACL + */ + if (old_descriptor) { + user_descriptor->owner_sid = old_descriptor->owner_sid; + } + } + if (!(sd_flags & SECINFO_GROUP) && user_descriptor) { + user_descriptor->group_sid = NULL; + + /* + * We need the correct group sid + * when calculating the DACL or SACL + */ + if (old_descriptor) { + user_descriptor->group_sid = old_descriptor->group_sid; + } + } + if (!(sd_flags & SECINFO_DACL) && user_descriptor) { + user_descriptor->dacl = NULL; + + /* + * We add SEC_DESC_DACL_PROTECTED so that + * create_security_descriptor() skips + * the unused inheritance calculation + */ + user_descriptor->type |= SEC_DESC_DACL_PROTECTED; + } + if (!(sd_flags & SECINFO_SACL) && user_descriptor) { + user_descriptor->sacl = NULL; + + /* + * We add SEC_DESC_SACL_PROTECTED so that + * create_security_descriptor() skips + * the unused inheritance calculation + */ + user_descriptor->type |= SEC_DESC_SACL_PROTECTED; + } + + default_owner = get_default_ag(mem_ctx, dn, + session_info->security_token, ldb); + default_group = get_default_group(mem_ctx, ldb, default_owner); + new_sd = create_security_descriptor(mem_ctx, + parent_descriptor, + user_descriptor, + true, + object_list, + SEC_DACL_AUTO_INHERIT | + SEC_SACL_AUTO_INHERIT, + session_info->security_token, + default_owner, default_group, + map_generic_rights_ds); + if (!new_sd) { + return NULL; + } + final_sd = descr_handle_sd_flags(mem_ctx, new_sd, old_descriptor, sd_flags); + + if (!final_sd) { + return NULL; + } + + if (final_sd->dacl) { + final_sd->dacl->revision = SECURITY_ACL_REVISION_ADS; + } + if (final_sd->sacl) { + final_sd->sacl->revision = SECURITY_ACL_REVISION_ADS; + } + + { + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + DBG_DEBUG("Object %s created with descriptor %s\n\n", + ldb_dn_get_linearized(dn), + sddl_encode(tmp_ctx, final_sd, domain_sid)); + TALLOC_FREE(tmp_ctx); + } + + linear_sd = talloc(mem_ctx, DATA_BLOB); + if (!linear_sd) { + return NULL; + } + + ndr_err = ndr_push_struct_blob(linear_sd, mem_ctx, + final_sd, + (ndr_push_flags_fn_t)ndr_push_security_descriptor); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + return NULL; + } + + return linear_sd; +} + +static DATA_BLOB *descr_get_descriptor_to_show(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_val *sd, + uint32_t sd_flags) +{ + struct security_descriptor *old_sd, *final_sd; + DATA_BLOB *linear_sd; + enum ndr_err_code ndr_err; + + old_sd = talloc(mem_ctx, struct security_descriptor); + if (!old_sd) { + return NULL; + } + ndr_err = ndr_pull_struct_blob(sd, old_sd, + old_sd, + (ndr_pull_flags_fn_t)ndr_pull_security_descriptor); + + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + talloc_free(old_sd); + return NULL; + } + + final_sd = descr_handle_sd_flags(mem_ctx, old_sd, NULL, sd_flags); + + if (!final_sd) { + return NULL; + } + + linear_sd = talloc(mem_ctx, DATA_BLOB); + if (!linear_sd) { + return NULL; + } + + ndr_err = ndr_push_struct_blob(linear_sd, mem_ctx, + final_sd, + (ndr_push_flags_fn_t)ndr_push_security_descriptor); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + return NULL; + } + + return linear_sd; +} + +static struct descriptor_context *descriptor_init_context(struct ldb_module *module, + struct ldb_request *req) +{ + struct ldb_context *ldb; + struct descriptor_context *ac; + + ldb = ldb_module_get_ctx(module); + + ac = talloc_zero(req, struct descriptor_context); + if (ac == NULL) { + ldb_set_errstring(ldb, "Out of Memory"); + return NULL; + } + + ac->module = module; + ac->req = req; + return ac; +} + +static int descriptor_search_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct descriptor_context *ac; + struct ldb_val *sd_val = NULL; + struct ldb_message_element *sd_el; + DATA_BLOB *show_sd; + int ret = LDB_SUCCESS; + + ac = talloc_get_type(req->context, struct descriptor_context); + + if (!ares) { + ret = LDB_ERR_OPERATIONS_ERROR; + goto fail; + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + sd_el = ldb_msg_find_element(ares->message, "nTSecurityDescriptor"); + if (sd_el) { + sd_val = sd_el->values; + } + + if (sd_val) { + show_sd = descr_get_descriptor_to_show(ac->module, ac->req, + sd_val, ac->sd_flags); + if (!show_sd) { + ret = LDB_ERR_OPERATIONS_ERROR; + goto fail; + } + ldb_msg_remove_attr(ares->message, "nTSecurityDescriptor"); + ret = ldb_msg_add_steal_value(ares->message, "nTSecurityDescriptor", show_sd); + if (ret != LDB_SUCCESS) { + goto fail; + } + } + return ldb_module_send_entry(ac->req, ares->message, ares->controls); + + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral(ac->req, ares->referral); + + case LDB_REPLY_DONE: + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + +fail: + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, ret); +} + +static int descriptor_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_request *add_req; + struct ldb_message *msg; + struct ldb_result *parent_res; + const struct ldb_val *parent_sd = NULL; + const struct ldb_val *user_sd; + struct ldb_dn *dn = req->op.add.message->dn; + struct ldb_dn *parent_dn, *nc_root; + struct ldb_message_element *objectclass_element, *sd_element; + int ret; + const struct dsdb_schema *schema; + DATA_BLOB *sd; + const struct dsdb_class *objectclass; + static const char * const parent_attrs[] = { "nTSecurityDescriptor", NULL }; + uint32_t instanceType; + bool isNC = false; + uint32_t sd_flags = dsdb_request_sd_flags(req, NULL); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(dn)) { + return ldb_next_request(module, req); + } + + user_sd = ldb_msg_find_ldb_val(req->op.add.message, "nTSecurityDescriptor"); + sd_element = ldb_msg_find_element(req->op.add.message, "nTSecurityDescriptor"); + /* nTSecurityDescriptor without a value is an error, letting through so it is handled */ + if (user_sd == NULL && sd_element) { + return ldb_next_request(module, req); + } + + ldb_debug(ldb, LDB_DEBUG_TRACE,"descriptor_add: %s\n", ldb_dn_get_linearized(dn)); + + instanceType = ldb_msg_find_attr_as_uint(req->op.add.message, "instanceType", 0); + + if (instanceType & INSTANCE_TYPE_IS_NC_HEAD) { + isNC = true; + } + + if (!isNC) { + ret = dsdb_find_nc_root(ldb, req, dn, &nc_root); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_TRACE,"descriptor_add: Could not find NC root for %s\n", + ldb_dn_get_linearized(dn)); + return ret; + } + + if (ldb_dn_compare(dn, nc_root) == 0) { + DEBUG(0, ("Found DN %s being a NC by the old method\n", ldb_dn_get_linearized(dn))); + isNC = true; + } + } + + if (isNC) { + DEBUG(2, ("DN: %s is a NC\n", ldb_dn_get_linearized(dn))); + } + if (!isNC) { + /* if the object has a parent, retrieve its SD to + * use for calculation. Unfortunately we do not yet have + * instanceType, so we use dsdb_find_nc_root. */ + + parent_dn = ldb_dn_get_parent(req, dn); + if (parent_dn == NULL) { + return ldb_oom(ldb); + } + + /* we aren't any NC */ + ret = dsdb_module_search_dn(module, req, &parent_res, parent_dn, + parent_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED, + req); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_TRACE,"descriptor_add: Could not find SD for %s\n", + ldb_dn_get_linearized(parent_dn)); + return ret; + } + if (parent_res->count != 1) { + return ldb_operr(ldb); + } + parent_sd = ldb_msg_find_ldb_val(parent_res->msgs[0], "nTSecurityDescriptor"); + } + + schema = dsdb_get_schema(ldb, req); + + objectclass_element = ldb_msg_find_element(req->op.add.message, "objectClass"); + if (objectclass_element == NULL) { + return ldb_operr(ldb); + } + + objectclass = dsdb_get_last_structural_class(schema, + objectclass_element); + if (objectclass == NULL) { + return ldb_operr(ldb); + } + + /* + * The SD_FLAG control is ignored on add + * and we default to all bits set. + */ + sd_flags = SECINFO_OWNER|SECINFO_GROUP|SECINFO_SACL|SECINFO_DACL; + + sd = get_new_descriptor(module, dn, req, + objectclass, parent_sd, + user_sd, NULL, sd_flags); + if (sd == NULL) { + return ldb_operr(ldb); + } + msg = ldb_msg_copy_shallow(req, req->op.add.message); + if (msg == NULL) { + return ldb_oom(ldb); + } + if (sd_element != NULL) { + sd_element->values[0] = *sd; + } else { + ret = ldb_msg_add_steal_value(msg, + "nTSecurityDescriptor", + sd); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + ret = ldb_build_add_req(&add_req, ldb, req, + msg, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(add_req); + if (ret != LDB_SUCCESS) { + return ldb_error(ldb, ret, + "descriptor_add: Error creating new add request."); + } + + return ldb_next_request(module, add_req); +} + +static int descriptor_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_request *mod_req; + struct ldb_message *msg; + struct ldb_result *current_res, *parent_res; + const struct ldb_val *old_sd = NULL; + const struct ldb_val *parent_sd = NULL; + const struct ldb_val *user_sd; + struct ldb_dn *dn = req->op.mod.message->dn; + struct ldb_dn *parent_dn; + struct ldb_message_element *objectclass_element, *sd_element; + int ret; + uint32_t instanceType; + bool explicit_sd_flags = false; + uint32_t sd_flags = dsdb_request_sd_flags(req, &explicit_sd_flags); + const struct dsdb_schema *schema; + DATA_BLOB *sd; + const struct dsdb_class *objectclass; + static const char * const parent_attrs[] = { "nTSecurityDescriptor", NULL }; + static const char * const current_attrs[] = { "nTSecurityDescriptor", + "instanceType", + "objectClass", NULL }; + struct GUID parent_guid = { .time_low = 0 }; + struct ldb_control *sd_propagation_control; + int cmp_ret = -1; + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(dn)) { + return ldb_next_request(module, req); + } + + sd_propagation_control = ldb_request_get_control(req, + DSDB_CONTROL_SEC_DESC_PROPAGATION_OID); + if (sd_propagation_control != NULL) { + if (sd_propagation_control->data != module) { + return ldb_operr(ldb); + } + if (req->op.mod.message->num_elements != 0) { + return ldb_operr(ldb); + } + if (explicit_sd_flags) { + return ldb_operr(ldb); + } + if (sd_flags != 0xF) { + return ldb_operr(ldb); + } + if (sd_propagation_control->critical == 0) { + return ldb_operr(ldb); + } + + sd_propagation_control->critical = 0; + } + + sd_element = ldb_msg_find_element(req->op.mod.message, "nTSecurityDescriptor"); + if (sd_propagation_control == NULL && sd_element == NULL) { + return ldb_next_request(module, req); + } + + /* + * nTSecurityDescriptor with DELETE is not supported yet. + * TODO: handle this correctly. + */ + if (sd_propagation_control == NULL && + LDB_FLAG_MOD_TYPE(sd_element->flags) == LDB_FLAG_MOD_DELETE) + { + return ldb_module_error(module, + LDB_ERR_UNWILLING_TO_PERFORM, + "MOD_DELETE for nTSecurityDescriptor " + "not supported yet"); + } + + user_sd = ldb_msg_find_ldb_val(req->op.mod.message, "nTSecurityDescriptor"); + /* nTSecurityDescriptor without a value is an error, letting through so it is handled */ + if (sd_propagation_control == NULL && user_sd == NULL) { + return ldb_next_request(module, req); + } + + ldb_debug(ldb, LDB_DEBUG_TRACE,"descriptor_modify: %s\n", ldb_dn_get_linearized(dn)); + + ret = dsdb_module_search_dn(module, req, ¤t_res, dn, + current_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_SHOW_EXTENDED_DN, + req); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR,"descriptor_modify: Could not find %s\n", + ldb_dn_get_linearized(dn)); + return ret; + } + + instanceType = ldb_msg_find_attr_as_uint(current_res->msgs[0], + "instanceType", 0); + /* if the object has a parent, retrieve its SD to + * use for calculation */ + if (!ldb_dn_is_null(current_res->msgs[0]->dn) && + !(instanceType & INSTANCE_TYPE_IS_NC_HEAD)) { + NTSTATUS status; + + parent_dn = ldb_dn_get_parent(req, dn); + if (parent_dn == NULL) { + return ldb_oom(ldb); + } + ret = dsdb_module_search_dn(module, req, &parent_res, parent_dn, + parent_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_SHOW_EXTENDED_DN, + req); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, "descriptor_modify: Could not find SD for %s\n", + ldb_dn_get_linearized(parent_dn)); + return ret; + } + if (parent_res->count != 1) { + return ldb_operr(ldb); + } + parent_sd = ldb_msg_find_ldb_val(parent_res->msgs[0], "nTSecurityDescriptor"); + + status = dsdb_get_extended_dn_guid(parent_res->msgs[0]->dn, + &parent_guid, + "GUID"); + if (!NT_STATUS_IS_OK(status)) { + return ldb_operr(ldb); + } + } + + schema = dsdb_get_schema(ldb, req); + + objectclass_element = ldb_msg_find_element(current_res->msgs[0], "objectClass"); + if (objectclass_element == NULL) { + return ldb_operr(ldb); + } + + objectclass = dsdb_get_last_structural_class(schema, + objectclass_element); + if (objectclass == NULL) { + return ldb_operr(ldb); + } + + old_sd = ldb_msg_find_ldb_val(current_res->msgs[0], "nTSecurityDescriptor"); + if (old_sd == NULL) { + return ldb_operr(ldb); + } + + if (sd_propagation_control != NULL) { + /* + * This just triggers a recalculation of the + * inherited aces. + */ + user_sd = old_sd; + } + + sd = get_new_descriptor(module, current_res->msgs[0]->dn, req, + objectclass, parent_sd, + user_sd, old_sd, sd_flags); + if (sd == NULL) { + return ldb_operr(ldb); + } + msg = ldb_msg_copy_shallow(req, req->op.mod.message); + if (msg == NULL) { + return ldb_oom(ldb); + } + cmp_ret = data_blob_cmp(old_sd, sd); + if (sd_propagation_control != NULL) { + if (cmp_ret == 0) { + /* + * The nTSecurityDescriptor is unchanged, + * which means we can stop the processing. + * + * We mark the control as critical again, + * as we have not processed it, so the caller + * can tell that the descriptor was unchanged. + */ + sd_propagation_control->critical = 1; + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS); + } + + ret = ldb_msg_append_value(msg, "nTSecurityDescriptor", + sd, LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ldb_oom(ldb); + } + } else if (cmp_ret != 0) { + struct GUID guid; + struct ldb_dn *nc_root; + NTSTATUS status; + + ret = dsdb_find_nc_root(ldb, + msg, + current_res->msgs[0]->dn, + &nc_root); + if (ret != LDB_SUCCESS) { + return ldb_oom(ldb); + } + + status = dsdb_get_extended_dn_guid(current_res->msgs[0]->dn, + &guid, + "GUID"); + if (!NT_STATUS_IS_OK(status)) { + return ldb_operr(ldb); + } + + /* + * Force SD propagation on children of this record + */ + ret = dsdb_module_schedule_sd_propagation(module, + nc_root, + guid, + parent_guid, + false); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + sd_element->values[0] = *sd; + } else { + sd_element->values[0] = *sd; + } + + ret = ldb_build_mod_req(&mod_req, ldb, req, + msg, + req->controls, + req, + dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(mod_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, mod_req); +} + +static int descriptor_search(struct ldb_module *module, struct ldb_request *req) +{ + int ret; + struct ldb_context *ldb; + struct ldb_request *down_req; + struct descriptor_context *ac; + bool explicit_sd_flags = false; + uint32_t sd_flags = dsdb_request_sd_flags(req, &explicit_sd_flags); + bool show_sd = explicit_sd_flags; + + if (!show_sd && + ldb_attr_in_list(req->op.search.attrs, "nTSecurityDescriptor")) + { + show_sd = true; + } + + if (!show_sd) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + ac = descriptor_init_context(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + ac->sd_flags = sd_flags; + + ret = ldb_build_search_req_ex(&down_req, ldb, ac, + req->op.search.base, + req->op.search.scope, + req->op.search.tree, + req->op.search.attrs, + req->controls, + ac, descriptor_search_callback, + ac->req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(ac->module, down_req); +} + +static int descriptor_rename_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct descriptor_context *ac = NULL; + struct ldb_context *ldb = NULL; + struct ldb_dn *newdn = req->op.rename.newdn; + struct GUID guid; + struct ldb_dn *nc_root; + struct GUID parent_guid = { .time_low = 0 }; + int ret; + + ac = talloc_get_type_abort(req->context, struct descriptor_context); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + if (ares->type != LDB_REPLY_DONE) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + ret = dsdb_module_guid_by_dn(ac->module, + newdn, + &guid, + req); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, + ret); + } + ret = dsdb_find_nc_root(ldb, req, newdn, &nc_root); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, + ret); + } + + /* + * After a successful rename, force SD propagation on this + * record (get a new inherited SD from the potentially new + * parent + * + * We don't know the parent guid here (it is filled in as + * all-zero in the initialiser above), but we're not in a hot + * code path here, as the "descriptor" module is located above + * the "repl_meta_data", only originating changes are handled + * here. + * + * If it turns out to be a problem we may search for the new + * parent guid. + */ + + ret = dsdb_module_schedule_sd_propagation(ac->module, + nc_root, + guid, + parent_guid, + true); + if (ret != LDB_SUCCESS) { + ret = ldb_operr(ldb); + return ldb_module_done(ac->req, NULL, NULL, + ret); + } + + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); +} + + + + +static int descriptor_rename(struct ldb_module *module, struct ldb_request *req) +{ + struct descriptor_context *ac = NULL; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_dn *olddn = req->op.rename.olddn; + struct ldb_dn *newdn = req->op.rename.newdn; + struct ldb_request *down_req; + int ret; + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.rename.olddn)) { + return ldb_next_request(module, req); + } + + ldb_debug(ldb, LDB_DEBUG_TRACE,"descriptor_rename: %s\n", + ldb_dn_get_linearized(olddn)); + + if (ldb_dn_compare(olddn, newdn) == 0) { + /* No special work required for a case-only rename */ + return ldb_next_request(module, req); + } + + ac = descriptor_init_context(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + ret = ldb_build_rename_req(&down_req, ldb, ac, + req->op.rename.olddn, + req->op.rename.newdn, + req->controls, + ac, descriptor_rename_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, down_req); +} + +static void descriptor_changes_parser(TDB_DATA key, TDB_DATA data, void *private_data) +{ + struct descriptor_changes **c_ptr = (struct descriptor_changes **)private_data; + uintptr_t ptr = 0; + + SMB_ASSERT(data.dsize == sizeof(ptr)); + + memcpy(&ptr, data.dptr, data.dsize); + + *c_ptr = talloc_get_type_abort((void *)ptr, struct descriptor_changes); +} + +static void descriptor_object_parser(TDB_DATA key, TDB_DATA data, void *private_data) +{ + SMB_ASSERT(data.dsize == 0); +} + +static int descriptor_extended_sec_desc_propagation(struct ldb_module *module, + struct ldb_request *req) +{ + struct descriptor_data *descriptor_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct descriptor_data); + struct descriptor_transaction *t = &descriptor_private->transaction; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct dsdb_extended_sec_desc_propagation_op *op; + struct descriptor_changes *c = NULL; + TDB_DATA key; + NTSTATUS status; + + op = talloc_get_type(req->op.extended.data, + struct dsdb_extended_sec_desc_propagation_op); + if (op == NULL) { + ldb_debug(ldb, LDB_DEBUG_FATAL, + "descriptor_extended_sec_desc_propagation: " + "invalid extended data\n"); + return LDB_ERR_PROTOCOL_ERROR; + } + + if (t->mem == NULL) { + return ldb_module_operr(module); + } + + if (GUID_equal(&op->parent_guid, &op->guid)) { + /* + * This is an unexpected situation, + * it should never happen! + */ + DBG_ERR("ERROR: Object %s is its own parent (nc_root=%s)\n", + GUID_string(t->mem, &op->guid), + ldb_dn_get_extended_linearized(t->mem, op->nc_root, 1)); + return ldb_module_operr(module); + } + + /* + * First we check if we already have an registration + * for the given object. + */ + + key = make_tdb_data((const void*)&op->guid, sizeof(op->guid)); + status = dbwrap_parse_record(t->changes.map, key, + descriptor_changes_parser, &c); + if (NT_STATUS_EQUAL(status, NT_STATUS_NOT_FOUND)) { + c = NULL; + status = NT_STATUS_OK; + } + if (!NT_STATUS_IS_OK(status)) { + ldb_debug(ldb, LDB_DEBUG_FATAL, + "dbwrap_parse_record() - %s\n", + nt_errstr(status)); + return ldb_module_operr(module); + } + + if (c == NULL) { + /* + * Create a new structure if we + * don't know about the object yet. + */ + + c = talloc_zero(t->mem, struct descriptor_changes); + if (c == NULL) { + return ldb_module_oom(module); + } + c->nc_root = ldb_dn_copy(c, op->nc_root); + if (c->nc_root == NULL) { + return ldb_module_oom(module); + } + c->guid = op->guid; + } + + if (ldb_dn_compare(c->nc_root, op->nc_root) != 0) { + /* + * This is an unexpected situation, + * we don't expect the nc root to change + * during a replication cycle. + */ + DBG_ERR("ERROR: Object %s nc_root changed %s => %s\n", + GUID_string(c, &c->guid), + ldb_dn_get_extended_linearized(c, c->nc_root, 1), + ldb_dn_get_extended_linearized(c, op->nc_root, 1)); + return ldb_module_operr(module); + } + + c->ref_count += 1; + + /* + * always use the last known parent_guid. + */ + c->parent_guid = op->parent_guid; + + /* + * Note that we only set, but don't clear values here, + * it means c->force_self and c->force_children can + * both be true in the end. + */ + if (op->include_self) { + c->force_self = true; + } else { + c->force_children = true; + } + + if (c->ref_count == 1) { + struct TDB_DATA val = make_tdb_data((const void*)&c, sizeof(c)); + + /* + * Remember the change by objectGUID in order + * to avoid processing it more than once. + */ + + status = dbwrap_store(t->changes.map, key, val, TDB_INSERT); + if (!NT_STATUS_IS_OK(status)) { + ldb_debug(ldb, LDB_DEBUG_FATAL, + "dbwrap_parse_record() - %s\n", + nt_errstr(status)); + return ldb_module_operr(module); + } + + DLIST_ADD_END(t->changes.list, c); + t->changes.num_registered += 1; + } + t->changes.num_registrations += 1; + + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS); +} + +static int descriptor_extended(struct ldb_module *module, struct ldb_request *req) +{ + if (strcmp(req->op.extended.oid, DSDB_EXTENDED_SEC_DESC_PROPAGATION_OID) == 0) { + return descriptor_extended_sec_desc_propagation(module, req); + } + + return ldb_next_request(module, req); +} + +static int descriptor_init(struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + int ret; + struct descriptor_data *descriptor_private; + + ret = ldb_mod_register_control(module, LDB_CONTROL_SD_FLAGS_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "descriptor: Unable to register control with rootdse!\n"); + return ldb_operr(ldb); + } + + descriptor_private = talloc_zero(module, struct descriptor_data); + if (descriptor_private == NULL) { + ldb_oom(ldb); + return LDB_ERR_OPERATIONS_ERROR; + } + ldb_module_set_private(module, descriptor_private); + + return ldb_next_init(module); +} + +static int descriptor_sd_propagation_object(struct ldb_module *module, + struct ldb_message *msg, + bool *stop) +{ + struct descriptor_data *descriptor_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct descriptor_data); + struct descriptor_transaction *t = &descriptor_private->transaction; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_request *sub_req; + struct ldb_result *mod_res; + struct ldb_control *sd_propagation_control; + struct GUID guid; + int ret; + TDB_DATA key; + TDB_DATA empty_val = { .dsize = 0, }; + NTSTATUS status; + struct descriptor_changes *c = NULL; + + *stop = false; + + /* + * We get the GUID of the object + * in order to have the cache key + * for the object. + */ + + status = dsdb_get_extended_dn_guid(msg->dn, &guid, "GUID"); + if (!NT_STATUS_IS_OK(status)) { + return ldb_operr(ldb); + } + key = make_tdb_data((const void*)&guid, sizeof(guid)); + + /* + * Check if we already processed this object. + */ + status = dbwrap_parse_record(t->objects.map, key, + descriptor_object_parser, NULL); + if (NT_STATUS_IS_OK(status)) { + /* + * All work is already one + */ + t->objects.num_skipped += 1; + *stop = true; + return LDB_SUCCESS; + } + if (!NT_STATUS_EQUAL(status, NT_STATUS_NOT_FOUND)) { + ldb_debug(ldb, LDB_DEBUG_FATAL, + "dbwrap_parse_record() - %s\n", + nt_errstr(status)); + return ldb_module_operr(module); + } + + t->objects.num_processed += 1; + + /* + * Remember that we're processing this object. + */ + status = dbwrap_store(t->objects.map, key, empty_val, TDB_INSERT); + if (!NT_STATUS_IS_OK(status)) { + ldb_debug(ldb, LDB_DEBUG_FATAL, + "dbwrap_parse_record() - %s\n", + nt_errstr(status)); + return ldb_module_operr(module); + } + + /* + * Check that if there's a descriptor_change in our list, + * which we may be able to remove from the pending list + * when we processed the object. + */ + + status = dbwrap_parse_record(t->changes.map, key, descriptor_changes_parser, &c); + if (NT_STATUS_EQUAL(status, NT_STATUS_NOT_FOUND)) { + c = NULL; + status = NT_STATUS_OK; + } + if (!NT_STATUS_IS_OK(status)) { + ldb_debug(ldb, LDB_DEBUG_FATAL, + "dbwrap_parse_record() - %s\n", + nt_errstr(status)); + return ldb_module_operr(module); + } + + mod_res = talloc_zero(msg, struct ldb_result); + if (mod_res == NULL) { + return ldb_module_oom(module); + } + + ret = ldb_build_mod_req(&sub_req, ldb, mod_res, + msg, + NULL, + mod_res, + ldb_modify_default_callback, + NULL); + LDB_REQ_SET_LOCATION(sub_req); + if (ret != LDB_SUCCESS) { + return ldb_module_operr(module); + } + + ldb_req_mark_trusted(sub_req); + + ret = ldb_request_add_control(sub_req, + DSDB_CONTROL_SEC_DESC_PROPAGATION_OID, + true, module); + if (ret != LDB_SUCCESS) { + return ldb_module_operr(module); + } + + sd_propagation_control = ldb_request_get_control(sub_req, + DSDB_CONTROL_SEC_DESC_PROPAGATION_OID); + if (sd_propagation_control == NULL) { + return ldb_module_operr(module); + } + + ret = dsdb_request_add_controls(sub_req, + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED); + if (ret != LDB_SUCCESS) { + return ldb_module_operr(module); + } + + ret = descriptor_modify(module, sub_req); + if (ret == LDB_SUCCESS) { + ret = ldb_wait(sub_req->handle, LDB_WAIT_ALL); + } + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "descriptor_modify on %s failed: %s", + ldb_dn_get_linearized(msg->dn), + ldb_errstring(ldb_module_get_ctx(module))); + return LDB_ERR_OPERATIONS_ERROR; + } + + if (sd_propagation_control->critical != 0) { + if (c == NULL) { + /* + * If we don't have a + * descriptor_changes structure + * we're done. + */ + *stop = true; + } else if (!c->force_children) { + /* + * If we don't need to + * propagate to children, + * we're done. + */ + *stop = true; + } + } + + if (c != NULL && !c->force_children) { + /* + * Remove the pending change, + * we already done all required work, + * there's no need to do it again. + * + * Note DLIST_REMOVE() is a noop + * if the element is not part of + * the list. + */ + DLIST_REMOVE(t->changes.list, c); + } + + talloc_free(mod_res); + + return LDB_SUCCESS; +} + +static int descriptor_sd_propagation_msg_sort(struct ldb_message **m1, + struct ldb_message **m2) +{ + struct ldb_dn *dn1 = (*m1)->dn; + struct ldb_dn *dn2 = (*m2)->dn; + + /* + * This sorts in tree order, parents first + */ + return ldb_dn_compare(dn2, dn1); +} + +static int descriptor_sd_propagation_recursive(struct ldb_module *module, + struct descriptor_changes *change) +{ + struct descriptor_data *descriptor_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct descriptor_data); + struct descriptor_transaction *t = &descriptor_private->transaction; + struct ldb_result *guid_res = NULL; + struct ldb_result *res = NULL; + unsigned int i; + const char * const no_attrs[] = { "@__NONE__", NULL }; + struct ldb_dn *stopped_dn = NULL; + struct GUID_txt_buf guid_buf; + int ret; + bool stop = false; + + t->changes.num_processed += 1; + + /* + * First confirm this object has children, or exists + * (depending on change->force_self) + * + * LDB_SCOPE_SUBTREE searches are expensive. + * + * We know this is safe against a rename race as we are in the + * prepare_commit(), so must be in a transaction. + */ + + /* Find the DN by GUID, as this is stable under rename */ + ret = dsdb_module_search(module, + change, + &guid_res, + change->nc_root, + LDB_SCOPE_SUBTREE, + no_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_DELETED | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_SHOW_EXTENDED_DN, + NULL, /* parent_req */ + "(objectGUID=%s)", + GUID_buf_string(&change->guid, + &guid_buf)); + + if (ret != LDB_SUCCESS) { + return ret; + } + + if (guid_res->count != 1) { + /* + * We were just given this GUID during the same + * transaction, if it is missing this is a big + * problem. + * + * Cleanup of tombstones does not trigger this module + * as it just does a delete. + */ + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "failed to find GUID %s under %s " + "for transaction-end SD inheritance: %d results", + GUID_buf_string(&change->guid, + &guid_buf), + ldb_dn_get_linearized(change->nc_root), + guid_res->count); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * OK, so there was a parent, are there children? Note: that + * this time we do not search for deleted/recycled objects + */ + ret = dsdb_module_search(module, + change, + &res, + guid_res->msgs[0]->dn, + LDB_SCOPE_ONELEVEL, + no_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM, + NULL, /* parent_req */ + "(objectClass=*)"); + if (ret != LDB_SUCCESS) { + /* + * LDB_ERR_NO_SUCH_OBJECT, say if the DN was a deleted + * object, is ignored by the caller + */ + return ret; + } + + if (res->count == 0 && !change->force_self) { + /* All done, no children */ + TALLOC_FREE(res); + return LDB_SUCCESS; + } + + /* + * First, if we are in force_self mode (eg renamed under new + * parent) then apply the SD to the top object + */ + if (change->force_self) { + ret = descriptor_sd_propagation_object(module, + guid_res->msgs[0], + &stop); + if (ret != LDB_SUCCESS) { + TALLOC_FREE(guid_res); + return ret; + } + + if (stop == true && !change->force_children) { + /* There was no change, nothing more to do */ + TALLOC_FREE(guid_res); + return LDB_SUCCESS; + } + + if (res->count == 0) { + /* All done! */ + TALLOC_FREE(guid_res); + return LDB_SUCCESS; + } + } + + /* + * Look for children + * + * Note: that we do not search for deleted/recycled objects + */ + ret = dsdb_module_search(module, + change, + &res, + guid_res->msgs[0]->dn, + LDB_SCOPE_SUBTREE, + no_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_EXTENDED_DN, + NULL, /* parent_req */ + "(objectClass=*)"); + if (ret != LDB_SUCCESS) { + return ret; + } + + TYPESAFE_QSORT(res->msgs, res->count, + descriptor_sd_propagation_msg_sort); + + /* We start from 1, the top object has been done */ + for (i = 1; i < res->count; i++) { + /* + * ldb_dn_compare_base() does not match for NULL but + * this is clearer + */ + if (stopped_dn != NULL) { + ret = ldb_dn_compare_base(stopped_dn, + res->msgs[i]->dn); + /* + * Skip further processing of this + * sub-subtree + */ + if (ret == 0) { + continue; + } + } + ret = descriptor_sd_propagation_object(module, + res->msgs[i], + &stop); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (stop) { + /* + * If this child didn't change, then nothing + * under it needs to change + * + * res has been sorted into tree order so the + * next few entries can be skipped + */ + stopped_dn = res->msgs[i]->dn; + } + } + + TALLOC_FREE(res); + return LDB_SUCCESS; +} + +static int descriptor_start_transaction(struct ldb_module *module) +{ + struct descriptor_data *descriptor_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct descriptor_data); + struct descriptor_transaction *t = &descriptor_private->transaction; + + if (t->mem != NULL) { + return ldb_module_operr(module); + } + + *t = (struct descriptor_transaction) { .mem = NULL, }; + t->mem = talloc_new(descriptor_private); + if (t->mem == NULL) { + return ldb_module_oom(module); + } + t->changes.map = db_open_rbt(t->mem); + if (t->changes.map == NULL) { + TALLOC_FREE(t->mem); + *t = (struct descriptor_transaction) { .mem = NULL, }; + return ldb_module_oom(module); + } + t->objects.map = db_open_rbt(t->mem); + if (t->objects.map == NULL) { + TALLOC_FREE(t->mem); + *t = (struct descriptor_transaction) { .mem = NULL, }; + return ldb_module_oom(module); + } + + return ldb_next_start_trans(module); +} + +static int descriptor_prepare_commit(struct ldb_module *module) +{ + struct descriptor_data *descriptor_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct descriptor_data); + struct descriptor_transaction *t = &descriptor_private->transaction; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct descriptor_changes *c, *n; + int ret; + + DBG_NOTICE("changes: num_registrations=%zu\n", + t->changes.num_registrations); + DBG_NOTICE("changes: num_registered=%zu\n", + t->changes.num_registered); + + /* + * The security descriptor propagation + * needs to apply the inheritance from + * an object to itself and/or all it's + * children. + * + * In the initial replication during + * a join, we have every object in our + * list. + * + * In order to avoid useless work it's + * better to start with toplevel objects and + * move down to the leaf object from there. + * + * So if the parent_guid is also in our list, + * we better move the object behind its parent. + * + * It allows that the recursive processing of + * the parent already does the work needed + * for the child. + * + * If we have a list for this directory tree: + * + * A + * -> B + * -> C + * -> D + * -> E + * + * The initial list would have the order D, E, B, A, C + * + * By still processing from the front, we ensure that, + * when D is found to be below C, that E follows because + * we keep peeling items off the front for checking and + * move them behind their parent. + * + * So we would go: + * + * E B A C D + * + * B A C D E + * + * A B C D E + */ + for (c = t->changes.list; c; c = n) { + struct descriptor_changes *pc = NULL; + n = c->next; + + if (c->sort_count >= t->changes.num_registered) { + /* + * This should never happen, but it's + * a sanity check in order to avoid + * endless loops. Just stop sorting. + */ + break; + } + + /* + * Check if we have the parent also in the list. + */ + if (!GUID_all_zero((const void*)&c->parent_guid)) { + TDB_DATA pkey; + NTSTATUS status; + + pkey = make_tdb_data((const void*)&c->parent_guid, + sizeof(c->parent_guid)); + + status = dbwrap_parse_record(t->changes.map, pkey, + descriptor_changes_parser, &pc); + if (NT_STATUS_EQUAL(status, NT_STATUS_NOT_FOUND)) { + pc = NULL; + status = NT_STATUS_OK; + } + if (!NT_STATUS_IS_OK(status)) { + ldb_debug(ldb, LDB_DEBUG_FATAL, + "dbwrap_parse_record() - %s\n", + nt_errstr(status)); + return ldb_module_operr(module); + } + } + + if (pc == NULL) { + /* + * There is no parent in the list + */ + t->changes.num_toplevel += 1; + continue; + } + + /* + * Move the child after the parent + * + * Note that we do that multiple times + * in case the parent already moved itself. + * + * See the comment above the loop. + */ + DLIST_REMOVE(t->changes.list, c); + DLIST_ADD_AFTER(t->changes.list, c, pc); + + /* + * Remember how often we moved the object + * in order to avoid endless loops. + */ + c->sort_count += 1; + } + + DBG_NOTICE("changes: num_toplevel=%zu\n", t->changes.num_toplevel); + + while (t->changes.list != NULL) { + c = t->changes.list; + + DLIST_REMOVE(t->changes.list, c); + + /* + * Note that descriptor_sd_propagation_recursive() + * may also remove other elements of the list, + * so we can't use a next pointer + */ + ret = descriptor_sd_propagation_recursive(module, c); + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + continue; + } + if (ret != LDB_SUCCESS) { + return ret; + } + } + + DBG_NOTICE("changes: num_processed=%zu\n", t->changes.num_processed); + DBG_NOTICE("objects: num_processed=%zu\n", t->objects.num_processed); + DBG_NOTICE("objects: num_skipped=%zu\n", t->objects.num_skipped); + + return ldb_next_prepare_commit(module); +} + +static int descriptor_end_transaction(struct ldb_module *module) +{ + struct descriptor_data *descriptor_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct descriptor_data); + struct descriptor_transaction *t = &descriptor_private->transaction; + + TALLOC_FREE(t->mem); + *t = (struct descriptor_transaction) { .mem = NULL, }; + + return ldb_next_end_trans(module); +} + +static int descriptor_del_transaction(struct ldb_module *module) +{ + struct descriptor_data *descriptor_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct descriptor_data); + struct descriptor_transaction *t = &descriptor_private->transaction; + + TALLOC_FREE(t->mem); + *t = (struct descriptor_transaction) { .mem = NULL, }; + + return ldb_next_del_trans(module); +} + +static const struct ldb_module_ops ldb_descriptor_module_ops = { + .name = "descriptor", + .search = descriptor_search, + .add = descriptor_add, + .modify = descriptor_modify, + .rename = descriptor_rename, + .init_context = descriptor_init, + .extended = descriptor_extended, + .start_transaction = descriptor_start_transaction, + .prepare_commit = descriptor_prepare_commit, + .end_transaction = descriptor_end_transaction, + .del_transaction = descriptor_del_transaction, +}; + +int ldb_descriptor_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_descriptor_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/dirsync.c b/source4/dsdb/samdb/ldb_modules/dirsync.c new file mode 100644 index 0000000..fbb7579 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/dirsync.c @@ -0,0 +1,1428 @@ +/* + SAMDB control module + + Copyright (C) Matthieu Patou <mat@matws.net> 2011 + + 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/>. +*/ + + +#include "includes.h" +#include "ldb/include/ldb.h" +#include "ldb/include/ldb_errors.h" +#include "ldb/include/ldb_module.h" +#include "libcli/security/security.h" +#include "librpc/gen_ndr/drsblobs.h" +#include "librpc/gen_ndr/ndr_drsblobs.h" +#include "librpc/ndr/libndr.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "lib/util/smb_strtox.h" + +#define LDAP_DIRSYNC_OBJECT_SECURITY 0x01 +#define LDAP_DIRSYNC_ANCESTORS_FIRST_ORDER 0x800 +#define LDAP_DIRSYNC_PUBLIC_DATA_ONLY 0x2000 +#define LDAP_DIRSYNC_INCREMENTAL_VALUES 0x80000000 + + +struct dirsync_context { + struct ldb_module *module; + struct ldb_request *req; + + /* + * We keep a track of the number of attributes that we + * add just for the need of the implementation + * it will be usefull to track then entries that needs not to + * be returned because there is no real change + */ + + unsigned int nbDefaultAttrs; + uint64_t highestUSN; + uint64_t fromreqUSN; + uint32_t cursor_size; + bool noextended; + int extended_type; + bool linkIncrVal; + bool localonly; + bool partial; + int functional_level; + const struct GUID *our_invocation_id; + const struct dsdb_schema *schema; + struct ldb_dn *nc_root; + struct drsuapi_DsReplicaCursor *cursors; +}; + + +static int dirsync_filter_entry(struct ldb_request *req, + struct ldb_message *msg, + struct ldb_control **controls, + struct dirsync_context *dsc, + bool referral) +{ + struct ldb_context *ldb; + uint64_t val = 0; + enum ndr_err_code ndr_err; + uint32_t n; + int i; + unsigned int size, j; + struct ldb_val *replMetaData = NULL; + struct replPropertyMetaDataBlob rmd; + const struct dsdb_attribute *attr; + const char **listAttr = NULL; + bool namereturned = false; + bool nameasked = false; + NTSTATUS status; + /* Ajustment for the added attributes, it will reduce the number of + * expected to be here attributes*/ + unsigned int delta = 0; + const char **myaccept = NULL; + const char *emptyaccept[] = { NULL }; + const char *extendedaccept[] = { "GUID", "SID", "WKGUID", NULL }; + const char *rdn = NULL; + struct ldb_message_element *el; + struct ldb_message *newmsg; + bool keep = false; + /* + * Where we asked to do extended dn ? + * if so filter out everything bug GUID, SID, WKGUID, + * if not filter out everything (just keep the dn). + */ + if ( dsc->noextended == true ) { + myaccept = emptyaccept; + } else { + myaccept = extendedaccept; + } + ldb = ldb_module_get_ctx(dsc->module); + + if (msg->num_elements == 0) { + /* + * Entry that we don't really have access to + */ + return LDB_SUCCESS; + } + ldb_dn_extended_filter(msg->dn, myaccept); + + /* + * If the RDN starts with CN then the CN attribute is never returned + */ + rdn = ldb_dn_get_rdn_name(msg->dn); + + /* + * if objectGUID is asked and we are dealing for the referrals entries and + * the usn searched is 0 then we didn't count the objectGUID as an automatically + * returned attribute, do to so we increament delta. + */ + if (referral == true && + ldb_attr_in_list(req->op.search.attrs, "objectGUID") && + dsc->fromreqUSN == 0) { + delta++; + } + + + /* + * In terms of big O notation this is not the best algorithm, + * but we try our best not to make the worse one. + * We are obliged to run through the n message's elements + * and through the p elements of the replPropertyMetaData. + * + * It turns out that we are crawling twice the message's elements + * the first crawl is to remove the non replicated and generated + * attributes. The second one is to remove attributes that haven't + * a USN > as the requested one. + * + * In the second crawl we are reading the list of elements in the + * replPropertyMetaData for each remaining replicated attribute. + * In order to keep the list small + * + * We have a O(n'*p') complexity, in worse case n' = n and p' = p + * but in most case n' = n/2 (at least half of returned attributes + * are not replicated or generated) and p' is small as we + * list only the attribute that have been modified since last interogation + * + */ + newmsg = ldb_msg_new(dsc->req); + if (newmsg == NULL) { + return ldb_oom(ldb); + } + for (i = msg->num_elements - 1; i >= 0; i--) { + if (ldb_attr_cmp(msg->elements[i].name, "uSNChanged") == 0) { + int error = 0; + /* Read the USN it will used at the end of the filtering + * to update the max USN in the cookie if we + * decide to keep this entry + */ + val = smb_strtoull( + (const char*)msg->elements[i].values[0].data, + NULL, + 0, + &error, + SMB_STR_STANDARD); + if (error != 0) { + ldb_set_errstring(ldb, + "Failed to convert USN"); + return ldb_module_done(dsc->req, + NULL, + NULL, + LDB_ERR_OPERATIONS_ERROR); + } + continue; + } + + if (ldb_attr_cmp(msg->elements[i].name, + "replPropertyMetaData") == 0) { + replMetaData = (talloc_steal(dsc, &msg->elements[i].values[0])); + continue; + } + } + + if (replMetaData == NULL) { + bool guidfound = false; + + /* + * We are in the case of deleted object where we don't have the + * right to read it. + */ + if (!ldb_msg_find_attr_as_uint(msg, "isDeleted", 0)) { + /* + * This is not a deleted item and we don't + * have the replPropertyMetaData. + * Do not return it + */ + return LDB_SUCCESS; + } + newmsg->dn = ldb_dn_new(newmsg, ldb, ""); + if (newmsg->dn == NULL) { + return ldb_oom(ldb); + } + + el = ldb_msg_find_element(msg, "objectGUID"); + if ( el != NULL) { + guidfound = true; + } + /* + * We expect to find the GUID in the object, + * if it turns out not to be the case sometime + * well will uncomment the code bellow + */ + SMB_ASSERT(guidfound == true); + /* + if (guidfound == false) { + struct GUID guid; + struct ldb_val *new_val; + DATA_BLOB guid_blob; + + tmp[0] = '\0'; + txt = strrchr(txt, ':'); + if (txt == NULL) { + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + txt++; + + status = GUID_from_string(txt, &guid); + if (!NT_STATUS_IS_OK(status)) { + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + + status = GUID_to_ndr_blob(&guid, msg, &guid_blob); + if (!NT_STATUS_IS_OK(status)) { + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + + new_val = talloc(msg, struct ldb_val); + if (new_val == NULL) { + return ldb_oom(ldb); + } + new_val->data = talloc_steal(new_val, guid_blob.data); + new_val->length = guid_blob.length; + if (ldb_msg_add_value(msg, "objectGUID", new_val, NULL) != 0) { + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + } + */ + ldb_msg_add(newmsg, el, LDB_FLAG_MOD_ADD); + talloc_steal(newmsg->elements, el->name); + talloc_steal(newmsg->elements, el->values); + + talloc_steal(newmsg->elements, msg); + return ldb_module_send_entry(dsc->req, msg, controls); + } + + ndr_err = ndr_pull_struct_blob(replMetaData, dsc, &rmd, + (ndr_pull_flags_fn_t)ndr_pull_replPropertyMetaDataBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + ldb_set_errstring(ldb, "Unable to unmarshall replPropertyMetaData"); + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + if (ldb_attr_in_list(req->op.search.attrs, "name") || + ldb_attr_in_list(req->op.search.attrs, "*")) { + nameasked = true; + } + + /* + * If we don't have an USN and no updateness array then we skip the + * test phase this is an optimisation for the case when you + * first query the DC without a cookie. + * As this query is most probably the one + * that will return the biggest answer, skipping this part + * will really save time. + */ + if (ldb_dn_compare(dsc->nc_root, msg->dn) == 0) { + /* If we have name then we expect to have parentGUID, + * it will not be the case for the root of the NC + */ + delta++; + } + + if (dsc->fromreqUSN > 0 || dsc->cursors != NULL) { + j = 0; + /* + * Allocate an array of size(replMetaData) of char* + * we know that it will be oversized but it's a short lived element + */ + listAttr = talloc_array(msg, const char*, rmd.ctr.ctr1.count + 1); + if (listAttr == NULL) { + return ldb_oom(ldb); + } + for (n=0; n < rmd.ctr.ctr1.count; n++) { + struct replPropertyMetaData1 *omd = &rmd.ctr.ctr1.array[n]; + if (omd->local_usn > dsc->fromreqUSN) { + const struct dsdb_attribute *a = dsdb_attribute_by_attributeID_id(dsc->schema, + omd->attid); + if (!dsc->localonly) { + struct drsuapi_DsReplicaCursor *tab = dsc->cursors; + uint32_t l; + for (l=0; l < dsc->cursor_size; l++) { + if (GUID_equal(&tab[l].source_dsa_invocation_id, &omd->originating_invocation_id) && + tab[l].highest_usn >= omd->originating_usn) { + /* + * If we have in the uptodateness vector an entry + * with the same invocation id as the originating invocation + * and if the usn in the vector is greater or equal to + * the one in originating_usn, then it means that this entry + * has already been sent (from another DC) to the client + * no need to resend it one more time. + */ + goto skip; + } + } + /* If we are here it's because we have a usn > (max(usn of vectors))*/ + } + if (namereturned == false && + nameasked == true && + ldb_attr_cmp(a->lDAPDisplayName, "name") == 0) { + namereturned = true; + if (ldb_dn_compare(dsc->nc_root, msg->dn) == 0) { + delta++; + } + } + listAttr[j] = a->lDAPDisplayName; + j++; +skip: + continue; + } + } + size = j; + } else { + size = 0; + if (ldb_attr_in_list(req->op.search.attrs, "*") || + ldb_attr_in_list(req->op.search.attrs, "name")) { + namereturned = true; + } + } + + + /* + * Let's loop around the remaining elements + * to see which one are in the listAttr. + * If they are in this array it means that + * their localusn > usn from the request (in the cookie) + * if not we remove the attribute. + */ + for (i = msg->num_elements - 1; i >= 0; i--) { + const char *ldapattrname; + + el = &(msg->elements[i]); + ldapattrname = el->name; + + attr = dsdb_attribute_by_lDAPDisplayName(dsc->schema, + el->name); + if (attr == NULL) { + continue; + } + + keep = false; + + if (attr->linkID & 1) { + /* + * Attribute is a backlink so let's remove it + */ + continue; + } + + if (ldb_attr_cmp(msg->elements[i].name, + "replPropertyMetaData") == 0) { + continue; + } + + if ((attr->systemFlags & (DS_FLAG_ATTR_NOT_REPLICATED | DS_FLAG_ATTR_IS_CONSTRUCTED))) { + if (ldb_attr_cmp(attr->lDAPDisplayName, "objectGUID") != 0 && + ldb_attr_cmp(attr->lDAPDisplayName, "parentGUID") != 0) { + /* + * Attribute is constructed or not replicated, let's get rid of it + */ + continue; + } else { + /* Let's keep the attribute that we forced to be added + * even if they are not in the replicationMetaData + * or are just generated + */ + if (namereturned == false && + (ldb_attr_cmp(attr->lDAPDisplayName, "parentGUID") == 0)) { + delta++; + continue; + } + if (ldb_msg_add(newmsg, el, LDB_FLAG_MOD_ADD) != LDB_SUCCESS) { + return ldb_error(ldb, + LDB_ERR_OPERATIONS_ERROR, + "Unable to add attribute"); + } + talloc_steal(newmsg->elements, el->name); + talloc_steal(newmsg->elements, el->values); + continue; + } + } + + if (ldb_attr_cmp(msg->elements[i].name, rdn) == 0) { + /* + * We have an attribute that is the same as the start of the RDN + * (ie. attribute CN with rdn CN=). + */ + continue; + } + + if (ldb_attr_cmp(attr->lDAPDisplayName, "instanceType") == 0) { + if (ldb_msg_add(newmsg, el, LDB_FLAG_MOD_ADD) != LDB_SUCCESS) { + return ldb_error(ldb, + LDB_ERR_OPERATIONS_ERROR, + "Unable to add attribute"); + } + talloc_steal(newmsg->elements, el->name); + talloc_steal(newmsg->elements, el->values); + continue; + } + /* For links, when our functional level > windows 2000 + * we use the RMD_LOCAL_USN information to decide whether + * we return the attribute or not. + * For windows 2000 this information is in the replPropertyMetaData + * so it will be handled like any other replicated attribute + */ + + if (dsc->functional_level > DS_DOMAIN_FUNCTION_2000 && + attr->linkID != 0 ) { + int k; + /* + * Elements for incremental changes on linked attributes + */ + struct ldb_message_element *el_incr_add = NULL; + struct ldb_message_element *el_incr_del = NULL; + /* + * Attribute is a forwardlink so let's remove it + */ + + for (k = el->num_values -1; k >= 0; k--) { + char *dn_ln; + uint32_t flags = 0; + uint32_t tmp_usn = 0; + uint32_t tmp_usn2 = 0; + struct GUID invocation_id = GUID_zero(); + struct dsdb_dn *dn = dsdb_dn_parse(msg, ldb, &el->values[k], attr->syntax->ldap_oid); + struct ldb_dn *copydn; + if (dn == NULL) { + ldb_set_errstring(ldb, "Cannot parse DN"); + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + + copydn = ldb_dn_copy(msg, dn->dn); + if (copydn == NULL) { + ldb_oom(ldb); + } + + status = dsdb_get_extended_dn_uint32(dn->dn, &tmp_usn, "RMD_LOCAL_USN"); + if (!NT_STATUS_IS_OK(status)) { + talloc_free(dn); + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + status = dsdb_get_extended_dn_guid(dn->dn, &invocation_id, "RMD_INVOCID"); + if (!NT_STATUS_IS_OK(status)) { + talloc_free(dn); + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + + status = dsdb_get_extended_dn_uint32(dn->dn, &flags, "RMD_FLAGS"); + if (!NT_STATUS_IS_OK(status)) { + talloc_free(dn); + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + + status = dsdb_get_extended_dn_uint32(dn->dn, &tmp_usn2, "RMD_ORIGINATING_USN"); + if (!NT_STATUS_IS_OK(status)) { + talloc_free(dn); + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + + ldb_dn_extended_filter(dn->dn, myaccept); + dn_ln = dsdb_dn_get_extended_linearized(dn, dn, + dsc->extended_type); + if (dn_ln == NULL) + { + talloc_free(dn); + ldb_set_errstring(ldb, "Cannot linearize dn"); + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + + talloc_free(el->values[k].data); + el->values[k].data = (uint8_t*)talloc_steal(el->values, dn_ln); + if (el->values[k].data == NULL) { + talloc_free(dn); + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + el->values[k].length = strlen(dn_ln); + + + if (tmp_usn > dsc->fromreqUSN) { + if (!dsc->localonly) { + struct drsuapi_DsReplicaCursor *tab = dsc->cursors; + uint32_t l; + + for (l=0; l < dsc->cursor_size; l++) { + if (GUID_equal(&tab[l].source_dsa_invocation_id, &invocation_id) && + tab[l].highest_usn >= tmp_usn2) { + /* + * If we have in the uptodateness vector an entry + * with the same invocation id as the originating invocation + * and if the usn in the vector is greater or equal to + * the one in originating_usn, then it means that this entry + * has already been sent (from another DC) to the client + * no need to resend it one more time. + */ + goto skip_link; + } + } + /* If we are here it's because we have a usn > (max(usn of vectors))*/ + keep = true; + } else { + keep = true; + } + /* If we are here it's because the link is more recent than either any + * originating usn or local usn + */ + + if (dsc->linkIncrVal == true) { + struct ldb_message_element *tmpel; + if (flags & DSDB_RMD_FLAG_DELETED) { + /* We have to check that the inactive link still point to an existing object */ + struct GUID guid; + struct ldb_dn *tdn; + int ret; + + status = dsdb_get_extended_dn_guid(copydn, &guid, "GUID"); + if (!NT_STATUS_IS_OK(status)) { + DEBUG(0,(__location__ " Unable to extract GUID in linked attribute '%s' in '%s'\n", + el->name, ldb_dn_get_linearized(copydn))); + return ldb_operr(ldb); + } + ret = dsdb_module_dn_by_guid(dsc->module, newmsg, &guid, &tdn, req); + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + DEBUG(2, (" Search of guid %s returned 0 objects, skipping it !\n", + GUID_string(newmsg, &guid))); + continue; + } else if (ret != LDB_SUCCESS) { + DEBUG(0, (__location__ " Search of guid %s failed with error code %d\n", + GUID_string(newmsg, &guid), + ret)); + continue; + } + tmpel = el_incr_del; + } else { + tmpel = el_incr_add; + } + + if (tmpel == NULL) { + tmpel = talloc_zero(newmsg, struct ldb_message_element); + if (tmpel == NULL) { + return ldb_oom(ldb); + } + tmpel->values = talloc_array(tmpel, struct ldb_val, 1); + if (tmpel->values == NULL) { + return ldb_oom(ldb); + } + if (flags & DSDB_RMD_FLAG_DELETED) { + tmpel->name = talloc_asprintf(tmpel, + "%s;range=0-0", + el->name); + } + else { + tmpel->name = talloc_asprintf(tmpel, + "%s;range=1-1", + el->name); + } + if (tmpel->name == NULL) { + return ldb_oom(ldb); + } + tmpel->num_values = 1; + } else { + tmpel->num_values += 1; + tmpel->values = talloc_realloc(tmpel, + tmpel->values, + struct ldb_val, + tmpel->num_values); + if (tmpel->values == NULL) { + return ldb_oom(ldb); + } + } + tmpel->values[tmpel->num_values -1].data =talloc_steal(tmpel->values, el->values[k].data); + tmpel->values[tmpel->num_values -1].length = el->values[k].length; + + if (flags & DSDB_RMD_FLAG_DELETED) { + el_incr_del = tmpel; + } else { + el_incr_add = tmpel; + } + } + } + + if (dsc->linkIncrVal == false) { + if (flags & DSDB_RMD_FLAG_DELETED) { + ARRAY_DEL_ELEMENT( + el->values, + k, + el->num_values); + el->num_values--; + } + } +skip_link: + talloc_free(dn); + + } + if (keep == true) { + if (dsc->linkIncrVal == false) { + if (ldb_msg_add(newmsg, el, LDB_FLAG_MOD_ADD) != LDB_SUCCESS) { + return ldb_error(ldb, + LDB_ERR_OPERATIONS_ERROR, + "Unable to add attribute"); + } + talloc_steal(newmsg->elements, el->name); + talloc_steal(newmsg->elements, el->values); + } else { + if (el_incr_del) { + if (ldb_msg_add(newmsg, el_incr_del, LDB_FLAG_MOD_ADD)) + return ldb_error(ldb, + LDB_ERR_OPERATIONS_ERROR, + "Unable to add attribute"); + } + if (el_incr_add) { + if (ldb_msg_add(newmsg, el_incr_add, LDB_FLAG_MOD_ADD)) + return ldb_error(ldb, + LDB_ERR_OPERATIONS_ERROR, + "Unable to add attribute"); + } + } + } + continue; + } + + if (listAttr) { + for (j=0; j<size; j++) { + /* + * We mark attribute that has already been seen well + * as seen. So that after attribute that are still in + * listAttr are attributes that has been modified after + * the requested USN but not present in the attributes + * returned by the ldb search. + * That is to say attributes that have been removed + */ + if (listAttr[j] && ldb_attr_cmp(listAttr[j], ldapattrname) == 0) { + listAttr[j] = NULL; + keep = true; + continue; + } + } + } else { + keep = true; + } + + if (keep == true) { + if (ldb_msg_add(newmsg, el, LDB_FLAG_MOD_ADD) != LDB_SUCCESS) { + return ldb_error(ldb, + LDB_ERR_OPERATIONS_ERROR, + "Unable to add attribute"); + } + talloc_steal(newmsg->elements, el->name); + talloc_steal(newmsg->elements, el->values); + continue; + } + } + talloc_steal(newmsg->elements, msg); + + /* + * Here we run through the list of attributes returned + * in the propertyMetaData. + * Entries of this list have usn > requested_usn, + * entries that are also present in the message have been + * replaced by NULL, so at this moment the list contains + * only elements that have a usn > requested_usn and that + * haven't been seen. It's attributes that were removed. + * We add them to the message like empty elements. + */ + for (j=0; j<size; j++) { + if (listAttr[j] && ( + ldb_attr_in_list(req->op.search.attrs, "*") || + ldb_attr_in_list(req->op.search.attrs, listAttr[j])) && + (ldb_attr_cmp(listAttr[j], rdn) != 0) && + (ldb_attr_cmp(listAttr[j], "instanceType") != 0)) { + ldb_msg_add_empty(newmsg, listAttr[j], LDB_FLAG_MOD_DELETE, NULL); + } + } + talloc_free(listAttr); + + if ((newmsg->num_elements - ( dsc->nbDefaultAttrs - delta)) > 0) { + /* + * After cleaning attributes there is still some attributes that were not added just + * for the purpose of the control (objectGUID, instanceType, ...) + */ + + newmsg->dn = talloc_steal(newmsg, msg->dn); + if (val > dsc->highestUSN) { + dsc->highestUSN = val; + } + return ldb_module_send_entry(dsc->req, newmsg, controls); + } else { + talloc_free(newmsg); + return LDB_SUCCESS; + } +} + + +static int dirsync_create_vector(struct ldb_request *req, + struct ldb_reply *ares, + struct dirsync_context *dsc, + struct ldapControlDirSyncCookie *cookie, + struct ldb_context *ldb) +{ + struct ldb_result *resVector; + const char* attrVector[] = {"replUpToDateVector", NULL }; + uint64_t highest_usn; + uint32_t count = 1; + int ret; + struct drsuapi_DsReplicaCursor *tab; + + ret = ldb_sequence_number(ldb, LDB_SEQ_HIGHEST_SEQ, &highest_usn); + if (ret != LDB_SUCCESS) { + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, "Unable to get highest USN from current NC"); + } + + /* If we have a full answer then the highest USN + * is not the highest USN from the result set but the + * highest of the naming context, unless the sequence is not updated yet. + */ + if (highest_usn > dsc->highestUSN) { + dsc->highestUSN = highest_usn; + } + + + ret = dsdb_module_search_dn(dsc->module, dsc, &resVector, + dsc->nc_root, + attrVector, + DSDB_FLAG_NEXT_MODULE, req); + if (ret != LDB_SUCCESS) { + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "Unable to get replUpToDateVector for current NC"); + } + + if (resVector->count != 0) { + DATA_BLOB blob; + uint32_t i; + struct ldb_message_element *el = ldb_msg_find_element(resVector->msgs[0], "replUpToDateVector"); + if (el) { + enum ndr_err_code ndr_err; + struct replUpToDateVectorBlob utd; + blob.data = el->values[0].data; + blob.length = el->values[0].length; + ndr_err = ndr_pull_struct_blob(&blob, dsc, &utd, + (ndr_pull_flags_fn_t)ndr_pull_replUpToDateVectorBlob); + + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "Unable to pull replUpToDateVectorBlob structure"); + } + + + count += utd.ctr.ctr2.count; + tab = talloc_array(cookie, struct drsuapi_DsReplicaCursor, count); + if (tab == NULL) { + return ldb_oom(ldb); + } + for (i=1; i < count; i++) { + memset(&tab[i], 0, sizeof(struct drsuapi_DsReplicaCursor)); + tab[i].highest_usn = utd.ctr.ctr2.cursors[i-1].highest_usn; + tab[i].source_dsa_invocation_id = utd.ctr.ctr2.cursors[i-1].source_dsa_invocation_id; + } + } else { + tab = talloc_array(cookie, struct drsuapi_DsReplicaCursor, count); + if (tab == NULL) { + return ldb_oom(ldb); + } + } + } else { + /* + * No replUpToDateVector ? it happens quite often (1 DC, + * other DCs didn't update ... + */ + tab = talloc_array(cookie, struct drsuapi_DsReplicaCursor, count); + if (tab == NULL) { + return ldb_oom(ldb); + } + } + /* Our vector is always the first */ + tab[0].highest_usn = dsc->highestUSN; + tab[0].source_dsa_invocation_id = *(dsc->our_invocation_id); + + + /* We have to add the updateness vector that we have*/ + /* Version is always 1 in dirsync cookies */ + cookie->blob.extra.uptodateness_vector.version = 1; + cookie->blob.extra.uptodateness_vector.reserved = 0; + cookie->blob.extra.uptodateness_vector.ctr.ctr1.count = count; + cookie->blob.extra.uptodateness_vector.ctr.ctr1.reserved = 0; + cookie->blob.extra.uptodateness_vector.ctr.ctr1.cursors = tab; + + return LDB_SUCCESS; +} + +static int dirsync_search_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + int ret; + struct dirsync_context *dsc; + struct ldb_result *res, *res2; + struct ldb_dirsync_control *control; + struct ldapControlDirSyncCookie *cookie; + struct ldb_context *ldb; + struct ldb_dn *dn; + struct ldb_val *val; + DATA_BLOB *blob; + NTTIME now; + const char *attrs[] = { "objectGUID", NULL }; + enum ndr_err_code ndr_err; + char *tmp; + uint32_t flags; + + dsc = talloc_get_type_abort(req->context, struct dirsync_context); + ldb = ldb_module_get_ctx(dsc->module); + if (!ares) { + return ldb_module_done(dsc->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(dsc->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + return dirsync_filter_entry(req, ares->message, ares->controls, dsc, false); + + case LDB_REPLY_REFERRAL: + /* Skip the ldap(s):// so up to 8 chars, + * we don't care to be precise as the goal is to be in + * the name of DC, then we search the next '/' + * as it will be the last char before the DN of the referal + */ + if (strncmp(ares->referral, "ldap://", 7) == 0) { + tmp = ares->referral + 7; + } else if (strncmp(ares->referral, "ldaps://", 8) == 0) { + tmp = ares->referral + 8; + } else { + return ldb_operr(ldb); + } + + tmp = strchr(tmp, '/'); + if (tmp == NULL) { + return ldb_operr(ldb); + } + tmp++; + + dn = ldb_dn_new(dsc, ldb, tmp); + if (dn == NULL) { + return ldb_oom(ldb); + } + + flags = DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_DELETED | + DSDB_SEARCH_SHOW_EXTENDED_DN; + + ret = dsdb_module_search_tree(dsc->module, dsc, &res, + dn, LDB_SCOPE_BASE, + req->op.search.tree, + req->op.search.attrs, + flags, req); + + if (ret != LDB_SUCCESS) { + talloc_free(dn); + return ret; + } + + if (res->count > 1) { + char *ldbmsg = talloc_asprintf(dn, "LDB returned more than result for dn: %s", tmp); + if (ldbmsg) { + ldb_set_errstring(ldb, ldbmsg); + } + talloc_free(dn); + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } else if (res->count == 0) { + /* if nothing is returned then it means that we don't + * have access to it. + */ + return LDB_SUCCESS; + } + + talloc_free(dn); + /* + * Fetch the objectGUID of the root of current NC + */ + ret = dsdb_module_search_dn(dsc->module, dsc, &res2, + req->op.search.base, + attrs, + DSDB_FLAG_NEXT_MODULE, req); + + if (ret != LDB_SUCCESS) { + return ret; + } + if (res2->msgs[0]->num_elements != 1) { + ldb_set_errstring(ldb, + "More than 1 attribute returned while looking for objectGUID"); + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + + val = res2->msgs[0]->elements[0].values; + ret = ldb_msg_add_value(res->msgs[0], "parentGUID", val, NULL); + /* + * It *very* important to steal otherwise as val is in a subcontext + * related to res2, when the value will be one more time stolen + * it's elements[x].values that will be stolen, so it's important to + * recreate the context hierrachy as if it was done from a ldb_request + */ + talloc_steal(res->msgs[0]->elements[0].values, val); + if (ret != LDB_SUCCESS) { + return ret; + } + return dirsync_filter_entry(req, res->msgs[0], res->controls, dsc, true); + + case LDB_REPLY_DONE: + /* + * Let's add our own control + */ + + control = talloc_zero(ares->controls, struct ldb_dirsync_control); + if (control == NULL) { + return ldb_oom(ldb); + } + + /* + * When outputing flags is used to say more results. + * For the moment we didn't honnor the size info */ + + control->flags = 0; + + /* + * max_attribute is unused cf. 3.1.1.3.4.1.3 LDAP_SERVER_DIRSYNC_OID in MS-ADTS + */ + + control->max_attributes = 0; + cookie = talloc_zero(control, struct ldapControlDirSyncCookie); + if (cookie == NULL) { + return ldb_oom(ldb); + } + + if (!dsc->partial) { + ret = dirsync_create_vector(req, ares, dsc, cookie, ldb); + if (ret != LDB_SUCCESS) { + return ldb_module_done(dsc->req, NULL, NULL, ret); + } + } + + unix_to_nt_time(&now, time(NULL)); + cookie->blob.time = now; + cookie->blob.highwatermark.highest_usn = dsc->highestUSN; + cookie->blob.highwatermark.tmp_highest_usn = dsc->highestUSN; + cookie->blob.guid1 = *(dsc->our_invocation_id); + + blob = talloc_zero(control, DATA_BLOB); + if (blob == NULL) { + return ldb_oom(ldb); + } + + ndr_err = ndr_push_struct_blob(blob, blob, cookie, + (ndr_push_flags_fn_t)ndr_push_ldapControlDirSyncCookie); + + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + ldb_set_errstring(ldb, "Can't marshall ldapControlDirSyncCookie struct"); + return ldb_module_done(dsc->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + control->cookie = (char *)blob->data; + control->cookie_len = blob->length; + ldb_reply_add_control(ares, LDB_CONTROL_DIRSYNC_OID, true, control); + + return ldb_module_done(dsc->req, ares->controls, + ares->response, LDB_SUCCESS); + + } + return LDB_SUCCESS; +} + +static int dirsync_ldb_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_control *control; + struct ldb_result *acl_res; + struct ldb_dirsync_control *dirsync_ctl; + struct ldb_control *extended = NULL; + struct ldb_request *down_req; + struct dirsync_context *dsc; + struct ldb_context *ldb; + struct ldb_parse_tree *new_tree = req->op.search.tree; + enum ndr_err_code ndr_err; + DATA_BLOB blob; + const char **attrs; + int ret; + + + if (ldb_dn_is_special(req->op.search.base)) { + return ldb_next_request(module, req); + } + + /* + * check if there's a dirsync control + */ + control = ldb_request_get_control(req, LDB_CONTROL_DIRSYNC_OID); + if (control == NULL) { + /* not found go on */ + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + /* + * This control must always be critical otherwise we return PROTOCOL error + */ + if (!control->critical) { + return ldb_operr(ldb); + } + + dsc = talloc_zero(req, struct dirsync_context); + if (dsc == NULL) { + return ldb_oom(ldb); + } + dsc->module = module; + dsc->req = req; + dsc->nbDefaultAttrs = 0; + + + dirsync_ctl = talloc_get_type(control->data, struct ldb_dirsync_control); + if (dirsync_ctl == NULL) { + return ldb_error(ldb, LDB_ERR_PROTOCOL_ERROR, "No data in dirsync control"); + } + + ret = dsdb_find_nc_root(ldb, dsc, req->op.search.base, &dsc->nc_root); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (ldb_dn_compare(dsc->nc_root, req->op.search.base) != 0) { + if (dirsync_ctl->flags & LDAP_DIRSYNC_OBJECT_SECURITY) { + return ldb_error(ldb, LDB_ERR_UNWILLING_TO_PERFORM, + "DN is not one of the naming context"); + } + else { + return ldb_error(ldb, LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS, + "dN is not one of the naming context"); + } + } + + if (!(dirsync_ctl->flags & LDAP_DIRSYNC_OBJECT_SECURITY)) { + struct dom_sid *sid; + struct security_descriptor *sd = NULL; + const char *acl_attrs[] = { "nTSecurityDescriptor", "objectSid", "objectClass", NULL }; + const struct dsdb_schema *schema = NULL; + const struct dsdb_class *objectclass = NULL; + /* + * If we don't have the flag and if we have the "replicate directory change" granted + * then we upgrade ourself to system to not be blocked by the acl + */ + /* FIXME we won't check the replicate directory change filtered attribute set + * it should be done so that if attr is not empty then we check that the user + * has also this right + */ + + /* + * First change to system to get the SD of the root of current NC + * if we don't the acl_read will forbid us the right to read it ... + */ + ret = dsdb_module_search_dn(module, dsc, &acl_res, + req->op.search.base, + acl_attrs, + DSDB_FLAG_NEXT_MODULE|DSDB_FLAG_AS_SYSTEM, req); + + if (ret != LDB_SUCCESS) { + return ret; + } + + sid = samdb_result_dom_sid(dsc, acl_res->msgs[0], "objectSid"); + /* sid can be null ... */ + ret = dsdb_get_sd_from_ldb_message(ldb_module_get_ctx(module), acl_res, acl_res->msgs[0], &sd); + + if (ret != LDB_SUCCESS) { + return ret; + } + schema = dsdb_get_schema(ldb, req); + if (!schema) { + return LDB_ERR_OPERATIONS_ERROR; + } + objectclass = dsdb_get_structural_oc_from_msg(schema, acl_res->msgs[0]); + + /* + * While we never use the answer to this for access + * control (after CVE-2023-4154), we return a + * different error message depending on if the user + * was granted GUID_DRS_GET_CHANGES to provide a closer + * emulation and keep some tests passing. + * + * (Samba's ACL logic is not well suited to redacting + * only the secret and RODC filtered attributes). + */ + ret = acl_check_extended_right(dsc, module, req, objectclass, + sd, acl_user_token(module), + GUID_DRS_GET_CHANGES, SEC_ADS_CONTROL_ACCESS, sid); + + if (ret != LDB_SUCCESS) { + return ret; + } + talloc_free(acl_res); + } else if (ret != LDB_SUCCESS) { + return ret; + } + + dsc->functional_level = dsdb_functional_level(ldb); + + if (req->op.search.attrs) { + attrs = ldb_attr_list_copy(dsc, req->op.search.attrs); + if (attrs == NULL) { + return ldb_oom(ldb); + } + /* + * Check if we have only "dn" as attribute, if so then + * treat as if "*" was requested + */ + if (attrs && attrs[0]) { + if (ldb_attr_cmp(attrs[0], "dn") == 0 && !attrs[1]) { + attrs = talloc_array(dsc, const char*, 2); + if (attrs == NULL) { + return ldb_oom(ldb); + } + attrs[0] = "*"; + attrs[1] = NULL; + } + } + /* + * When returning all the attributes return also the SD as + * Windws do so. + */ + if (ldb_attr_in_list(attrs, "*")) { + struct ldb_sd_flags_control *sdctr = talloc_zero(dsc, struct ldb_sd_flags_control); + sdctr->secinfo_flags = 0xF; + ret = ldb_request_add_control(req, LDB_CONTROL_SD_FLAGS_OID, false, sdctr); + if (ret != LDB_SUCCESS) { + return ret; + } + attrs = ldb_attr_list_copy_add(dsc, attrs, "parentGUID"); + if (attrs == NULL) { + return ldb_oom(ldb); + } + attrs = ldb_attr_list_copy_add(dsc, attrs, "replPropertyMetaData"); + if (attrs == NULL) { + return ldb_oom(ldb); + } + /* + * When no attributes are asked we in anycase expect at least 3 attributes: + * * instanceType + * * objectGUID + * * parentGUID + */ + + dsc->nbDefaultAttrs = 3; + } else { + /* + * We will need this two attributes in the callback + */ + attrs = ldb_attr_list_copy_add(dsc, attrs, "usnChanged"); + if (attrs == NULL) { + return ldb_operr(ldb); + } + attrs = ldb_attr_list_copy_add(dsc, attrs, "replPropertyMetaData"); + if (attrs == NULL) { + return ldb_operr(ldb); + } + + if (!ldb_attr_in_list(attrs, "instanceType")) { + attrs = ldb_attr_list_copy_add(dsc, attrs, "instanceType"); + if (attrs == NULL) { + return ldb_operr(ldb); + } + dsc->nbDefaultAttrs++; + } + + if (!ldb_attr_in_list(attrs, "objectGUID")) { + attrs = ldb_attr_list_copy_add(dsc, attrs, "objectGUID"); + if (attrs == NULL) { + return ldb_operr(ldb); + } + } + /* + * Always increment the number of asked attributes as we don't care if objectGUID was asked + * or not for counting the number of "real" attributes returned. + */ + dsc->nbDefaultAttrs++; + + if (!ldb_attr_in_list(attrs, "parentGUID")) { + attrs = ldb_attr_list_copy_add(dsc, attrs, "parentGUID"); + if (attrs == NULL) { + return ldb_operr(ldb); + } + } + dsc->nbDefaultAttrs++; + + } + } else { + struct ldb_sd_flags_control *sdctr = talloc_zero(dsc, struct ldb_sd_flags_control); + sdctr->secinfo_flags = 0xF; + ret = ldb_request_add_control(req, LDB_CONTROL_SD_FLAGS_OID, false, sdctr); + attrs = talloc_array(dsc, const char*, 4); + if (attrs == NULL) { + return ldb_operr(ldb); + } + attrs[0] = "*"; + attrs[1] = "parentGUID"; + attrs[2] = "replPropertyMetaData"; + attrs[3] = NULL; + if (ret != LDB_SUCCESS) { + return ret; + } + /* + * When no attributes are asked we in anycase expect at least 3 attributes: + * * instanceType + * * objectGUID + * * parentGUID + */ + + dsc->nbDefaultAttrs = 3; + } + + /* check if there's an extended dn control */ + extended = ldb_request_get_control(req, LDB_CONTROL_EXTENDED_DN_OID); + if (extended != NULL) { + struct ldb_extended_dn_control *extended_ctrl = NULL; + + if (extended->data != NULL) { + extended_ctrl = talloc_get_type(extended->data, + struct ldb_extended_dn_control); + } + if (extended_ctrl != NULL) { + dsc->extended_type = extended_ctrl->type; + } + } else { + ret = ldb_request_add_control(req, LDB_CONTROL_EXTENDED_DN_OID, false, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + dsc->noextended = true; + } + + if (ldb_request_get_control(req, LDB_CONTROL_REVEAL_INTERNALS) == NULL) { + ret = ldb_request_add_control(req, LDB_CONTROL_REVEAL_INTERNALS, false, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + if (ldb_request_get_control(req, LDB_CONTROL_SHOW_RECYCLED_OID) == NULL) { + ret = ldb_request_add_control(req, LDB_CONTROL_SHOW_RECYCLED_OID, false, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + if (ldb_request_get_control(req, LDB_CONTROL_SHOW_DELETED_OID) == NULL) { + ret = ldb_request_add_control(req, LDB_CONTROL_SHOW_DELETED_OID, false, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + if (dirsync_ctl->flags & LDAP_DIRSYNC_INCREMENTAL_VALUES) { + dsc->linkIncrVal = true; + } else { + dsc->linkIncrVal = false; + } + + dsc->our_invocation_id = samdb_ntds_invocation_id(ldb); + if (dsc->our_invocation_id == NULL) { + return ldb_operr(ldb); + } + + if (dirsync_ctl->cookie_len > 0) { + struct ldapControlDirSyncCookie cookie; + + blob.data = (uint8_t *)dirsync_ctl->cookie; + blob.length = dirsync_ctl->cookie_len; + ndr_err = ndr_pull_struct_blob(&blob, dsc, &cookie, + (ndr_pull_flags_fn_t)ndr_pull_ldapControlDirSyncCookie); + + /* If we can't unmarshall the cookie into the correct structure we return + * unsupported critical extension + */ + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + return ldb_error(ldb, LDB_ERR_UNSUPPORTED_CRITICAL_EXTENSION, + "Unable to unmarshall cookie as a ldapControlDirSyncCookie structure"); + } + + /* + * Let's search for the max usn within the cookie + */ + if (GUID_equal(&(cookie.blob.guid1), dsc->our_invocation_id)) { + /* + * Ok, it's our invocation ID so we can treat the demand + * Let's take the highest usn from (tmp)highest_usn + */ + dsc->fromreqUSN = cookie.blob.highwatermark.tmp_highest_usn; + dsc->localonly = true; + + if (cookie.blob.highwatermark.highest_usn > cookie.blob.highwatermark.tmp_highest_usn) { + dsc->fromreqUSN = cookie.blob.highwatermark.highest_usn; + } + } else { + dsc->localonly = false; + } + if (cookie.blob.extra_length > 0 && + cookie.blob.extra.uptodateness_vector.ctr.ctr1.count > 0) { + struct drsuapi_DsReplicaCursor cursor; + uint32_t p; + for (p=0; p < cookie.blob.extra.uptodateness_vector.ctr.ctr1.count; p++) { + cursor = cookie.blob.extra.uptodateness_vector.ctr.ctr1.cursors[p]; + if (GUID_equal( &(cursor.source_dsa_invocation_id), dsc->our_invocation_id)) { + if (cursor.highest_usn > dsc->fromreqUSN) { + dsc->fromreqUSN = cursor.highest_usn; + } + } + } + dsc->cursors = talloc_steal(dsc, + cookie.blob.extra.uptodateness_vector.ctr.ctr1.cursors); + if (dsc->cursors == NULL) { + return ldb_oom(ldb); + } + dsc->cursor_size = p; + } + } + + DEBUG(4, ("Dirsync: searching with min usn > %llu\n", + (long long unsigned int)dsc->fromreqUSN)); + if (dsc->fromreqUSN > 0) { + /* FIXME it would be better to use PRId64 */ + char *expression = talloc_asprintf(dsc, "(&%s(uSNChanged>=%llu))", + ldb_filter_from_tree(dsc, + req->op.search.tree), + (long long unsigned int)(dsc->fromreqUSN + 1)); + + if (expression == NULL) { + return ldb_oom(ldb); + } + new_tree = ldb_parse_tree(req, expression); + if (new_tree == NULL) { + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "Problem while parsing tree"); + } + + } + /* + * Mark dirsync control as uncritical (done) + * + * We need this so ranged_results knows how to behave with + * dirsync + */ + control->critical = false; + dsc->schema = dsdb_get_schema(ldb, dsc); + /* + * At the beginning we make the hypothesis that we will return a + * complete result set. + */ + + dsc->partial = false; + + /* + * 3.1.1.3.4.1.3 of MS-ADTS.pdf specify that if the scope is not subtree + * we treat the search as if subtree was specified + */ + + ret = ldb_build_search_req_ex(&down_req, ldb, dsc, + req->op.search.base, + LDB_SCOPE_SUBTREE, + new_tree, + attrs, + req->controls, + dsc, dirsync_search_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + /* perform the search */ + return ldb_next_request(module, down_req); +} + +static int dirsync_ldb_init(struct ldb_module *module) +{ + int ret; + + ret = ldb_mod_register_control(module, LDB_CONTROL_DIRSYNC_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb_module_get_ctx(module), LDB_DEBUG_ERROR, + "dirsync: Unable to register control with rootdse!\n"); + return ldb_operr(ldb_module_get_ctx(module)); + } + + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_dirsync_ldb_module_ops = { + .name = "dirsync", + .search = dirsync_ldb_search, + .init_context = dirsync_ldb_init, +}; + +/* + initialise the module + */ +_PUBLIC_ int ldb_dirsync_module_init(const char *version) +{ + int ret; + LDB_MODULE_CHECK_VERSION(version); + ret = ldb_register_module(&ldb_dirsync_ldb_module_ops); + return ret; +} diff --git a/source4/dsdb/samdb/ldb_modules/dns_notify.c b/source4/dsdb/samdb/ldb_modules/dns_notify.c new file mode 100644 index 0000000..41973ef --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/dns_notify.c @@ -0,0 +1,450 @@ +/* + ldb database library + + Copyright (C) Samuel Cabrero <samuelcabrero@kernevil.me> 2014 + + 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 dns_notify module + * + * Description: Notify the DNS server when zones are changed, either by direct + * RPC management calls or DRS inbound replication. + * + * Author: Samuel Cabrero <samuelcabrero@kernevil.me> + */ + +#include "includes.h" +#include "ldb_module.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/common/proto.h" +#include "librpc/gen_ndr/ndr_irpc.h" +#include "lib/messaging/irpc.h" +#include "librpc/gen_ndr/ndr_irpc_c.h" +#include "param/param.h" +#include "util/dlinklist.h" + +#undef strcasecmp + +struct dns_notify_watched_dn { + struct dns_notify_watched_dn *next, *prev; + struct ldb_dn *dn; +}; + +struct dns_notify_private { + struct dns_notify_watched_dn *watched; + bool reload_zones; +}; + +struct dns_notify_dnssrv_state { + struct imessaging_context *msg_ctx; + struct dnssrv_reload_dns_zones r; +}; + +static void dns_notify_dnssrv_done(struct tevent_req *req) +{ + NTSTATUS status; + struct dns_notify_dnssrv_state *state; + + state = tevent_req_callback_data(req, struct dns_notify_dnssrv_state); + + status = dcerpc_dnssrv_reload_dns_zones_r_recv(req, state); + if (!NT_STATUS_IS_OK(status)) { + DEBUG(1, ("%s: Error notifying dns server: %s\n", + __func__, nt_errstr(status))); + } + imessaging_cleanup(state->msg_ctx); + + talloc_free(req); + talloc_free(state); +} + +static void dns_notify_dnssrv_send(struct ldb_module *module) +{ + struct ldb_context *ldb; + struct loadparm_context *lp_ctx; + struct dns_notify_dnssrv_state *state; + struct dcerpc_binding_handle *handle; + struct tevent_req *req; + + ldb = ldb_module_get_ctx(module); + + lp_ctx = ldb_get_opaque(ldb, "loadparm"); + if (lp_ctx == NULL) { + return; + } + + state = talloc_zero(module, struct dns_notify_dnssrv_state); + if (state == NULL) { + return; + } + + /* Initialize messaging client */ + state->msg_ctx = imessaging_client_init(state, lp_ctx, + ldb_get_event_context(ldb)); + if (state->msg_ctx == NULL) { + ldb_asprintf_errstring(ldb, "Failed to generate client messaging context in %s", + lpcfg_imessaging_path(state, lp_ctx)); + talloc_free(state); + return; + } + + /* Get a handle to notify the DNS server */ + handle = irpc_binding_handle_by_name(state, state->msg_ctx, + "dnssrv", + &ndr_table_irpc); + if (handle == NULL) { + imessaging_cleanup(state->msg_ctx); + talloc_free(state); + return; + } + + /* Send the notifications */ + req = dcerpc_dnssrv_reload_dns_zones_r_send(state, + ldb_get_event_context(ldb), + handle, + &state->r); + if (req == NULL) { + imessaging_cleanup(state->msg_ctx); + talloc_free(state); + return; + } + tevent_req_set_callback(req, dns_notify_dnssrv_done, state); +} + +static int dns_notify_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct dns_notify_private *data; + struct dns_notify_watched_dn *w; + struct dsdb_schema *schema; + const struct dsdb_class *objectclass; + + if (ldb_dn_is_special(req->op.add.message->dn)) { + return ldb_next_request(module, req); + } + + if (ldb_request_get_control(req, LDB_CONTROL_RELAX_OID)) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + data = talloc_get_type(ldb_module_get_private(module), + struct dns_notify_private); + if (data == NULL) { + return ldb_operr(ldb); + } + + for (w = data->watched; w; w = w->next) { + if (ldb_dn_compare_base(w->dn, req->op.add.message->dn) == 0) { + schema = dsdb_get_schema(ldb, req); + if (schema == NULL) { + return ldb_operr(ldb); + } + + objectclass = dsdb_get_structural_oc_from_msg(schema, req->op.add.message); + if (objectclass == NULL) { + return ldb_operr(ldb); + } + + if (ldb_attr_cmp(objectclass->lDAPDisplayName, "dnsZone") == 0) { + data->reload_zones = true; + break; + } + } + } + + return ldb_next_request(module, req); +} + +static int dns_notify_modify(struct ldb_module *module, struct ldb_request *req) +{ + TALLOC_CTX *tmp_ctx; + struct ldb_context *ldb; + struct dns_notify_private *data; + struct dns_notify_watched_dn *w; + struct ldb_dn *dn; + struct ldb_result *res; + struct dsdb_schema *schema; + const struct dsdb_class *objectclass; + const char * const attrs[] = { "objectClass", NULL }; + int ret; + + if (ldb_dn_is_special(req->op.mod.message->dn)) { + return ldb_next_request(module, req); + } + + if (ldb_request_get_control(req, LDB_CONTROL_RELAX_OID)) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + data = talloc_get_type(ldb_module_get_private(module), + struct dns_notify_private); + if (data == NULL) { + return ldb_operr(ldb); + } + + tmp_ctx = talloc_new(module); + if (tmp_ctx == NULL) { + return ldb_oom(ldb); + } + + for (w = data->watched; w; w = w->next) { + if (ldb_dn_compare_base(w->dn, req->op.add.message->dn) == 0) { + dn = ldb_dn_copy(tmp_ctx, req->op.mod.message->dn); + + ret = dsdb_module_search_dn(module, tmp_ctx, &res, dn, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, req); + if (ret != LDB_SUCCESS) { + /* + * We want the give the caller the + * error from trying the actual + * request, below + */ + break; + } + + schema = dsdb_get_schema(ldb, req); + if (schema == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + objectclass = dsdb_get_structural_oc_from_msg(schema, res->msgs[0]); + if (objectclass == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + if (ldb_attr_cmp(objectclass->lDAPDisplayName, "dnsZone") == 0) { + data->reload_zones = true; + break; + } + } + } + + talloc_free(tmp_ctx); + return ldb_next_request(module, req); +} + +static int dns_notify_delete(struct ldb_module *module, struct ldb_request *req) +{ + TALLOC_CTX *tmp_ctx; + struct ldb_context *ldb; + struct dns_notify_private *data; + struct dns_notify_watched_dn *w; + struct ldb_dn *old_dn; + struct ldb_result *res; + struct dsdb_schema *schema; + const struct dsdb_class *objectclass; + const char * const attrs[] = { "objectClass", NULL }; + int ret; + + if (ldb_dn_is_special(req->op.del.dn)) { + return ldb_next_request(module, req); + } + + if (ldb_request_get_control(req, LDB_CONTROL_RELAX_OID)) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + data = talloc_get_type(ldb_module_get_private(module), + struct dns_notify_private); + if (data == NULL) { + return ldb_operr(ldb); + } + + tmp_ctx = talloc_new(module); + if (tmp_ctx == NULL) { + return ldb_oom(ldb); + } + + for (w = data->watched; w; w = w->next) { + if (ldb_dn_compare_base(w->dn, req->op.add.message->dn) == 0) { + old_dn = ldb_dn_copy(tmp_ctx, req->op.del.dn); + ret = dsdb_module_search_dn(module, tmp_ctx, &res, old_dn, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, req); + if (ret != LDB_SUCCESS) { + /* + * We want the give the caller the + * error from trying the actual + * request, below + */ + break; + } + + schema = dsdb_get_schema(ldb, req); + if (schema == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + objectclass = dsdb_get_structural_oc_from_msg(schema, res->msgs[0]); + if (objectclass == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + if (ldb_attr_cmp(objectclass->lDAPDisplayName, "dnsZone") == 0) { + data->reload_zones = true; + break; + } + } + } + + talloc_free(tmp_ctx); + return ldb_next_request(module, req); +} + +static int dns_notify_start_trans(struct ldb_module *module) +{ + struct ldb_context *ldb; + struct dns_notify_private *data; + + ldb = ldb_module_get_ctx(module); + data = talloc_get_type(ldb_module_get_private(module), + struct dns_notify_private); + if (data == NULL) { + return ldb_operr(ldb); + } + + data->reload_zones = false; + + return ldb_next_start_trans(module); +} + +static int dns_notify_end_trans(struct ldb_module *module) +{ + struct ldb_context *ldb; + struct dns_notify_private *data; + int ret; + + ldb = ldb_module_get_ctx(module); + data = talloc_get_type(ldb_module_get_private(module), + struct dns_notify_private); + if (data == NULL) { + return ldb_operr(ldb); + } + + ret = ldb_next_end_trans(module); + if (ret == LDB_SUCCESS) { + if (data->reload_zones) { + dns_notify_dnssrv_send(module); + } + } + + return ret; +} + +static int dns_notify_del_trans(struct ldb_module *module) +{ + struct ldb_context *ldb; + struct dns_notify_private *data; + + ldb = ldb_module_get_ctx(module); + data = talloc_get_type(ldb_module_get_private(module), + struct dns_notify_private); + if (data == NULL) { + return ldb_operr(ldb); + } + + data->reload_zones = false; + + return ldb_next_del_trans(module); +} + +static int dns_notify_init(struct ldb_module *module) +{ + struct ldb_context *ldb; + struct dns_notify_private *data; + struct dns_notify_watched_dn *watched; + struct ldb_dn *domain_dn; + struct ldb_dn *forest_dn; + + ldb = ldb_module_get_ctx(module); + + data = talloc_zero(module, struct dns_notify_private); + if (data == NULL) { + return ldb_oom(ldb); + } + + domain_dn = ldb_get_default_basedn(ldb); + forest_dn = ldb_get_root_basedn(ldb); + + /* Register hook on domain partition */ + watched = talloc_zero(data, struct dns_notify_watched_dn); + if (watched == NULL) { + talloc_free(data); + return ldb_oom(ldb); + } + watched->dn = ldb_dn_new_fmt(watched, ldb, + "CN=MicrosoftDNS,CN=System,%s", + ldb_dn_get_linearized(domain_dn)); + if (watched->dn == NULL) { + talloc_free(data); + return ldb_oom(ldb); + } + DLIST_ADD(data->watched, watched); + + /* Check for DomainDnsZones partition and register hook */ + watched = talloc_zero(data, struct dns_notify_watched_dn); + if (watched == NULL) { + talloc_free(data); + return ldb_oom(ldb); + } + watched->dn = ldb_dn_new_fmt(watched, ldb, "CN=MicrosoftDNS,DC=DomainDnsZones,%s", ldb_dn_get_linearized(forest_dn)); + DLIST_ADD(data->watched, watched); + + /* Check for ForestDnsZones partition and register hook */ + watched = talloc_zero(data, struct dns_notify_watched_dn); + if (watched == NULL) { + talloc_free(data); + return ldb_oom(ldb); + } + watched->dn = ldb_dn_new_fmt(watched, ldb, "CN=MicrosoftDNS,DC=ForestDnsZones,%s", ldb_dn_get_linearized(forest_dn)); + DLIST_ADD(data->watched, watched); + + ldb_module_set_private(module, data); + + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_dns_notify_module_ops = { + .name = "dns_notify", + .init_context = dns_notify_init, + .add = dns_notify_add, + .modify = dns_notify_modify, + .del = dns_notify_delete, + .start_transaction = dns_notify_start_trans, + .end_transaction = dns_notify_end_trans, + .del_transaction = dns_notify_del_trans, +}; + +int ldb_dns_notify_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_dns_notify_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/dsdb_notification.c b/source4/dsdb/samdb/ldb_modules/dsdb_notification.c new file mode 100644 index 0000000..ef92eac --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/dsdb_notification.c @@ -0,0 +1,262 @@ +/* + notification control module + + Copyright (C) Stefan Metzmacher 2015 + + 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/>. +*/ + + +#include "includes.h" +#include "ldb/include/ldb.h" +#include "ldb/include/ldb_errors.h" +#include "ldb/include/ldb_module.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" + +struct dsdb_notification_cookie { + uint64_t known_usn; +}; + +static int dsdb_notification_verify_tree(struct ldb_parse_tree *tree) +{ + unsigned int i; + int ret; + unsigned int num_ok = 0; + /* + * these attributes are present on every object + * and windows accepts them. + * + * While [MS-ADTS] says only '(objectClass=*)' + * would be allowed. + */ + static const char * const attrs_ok[] = { + "objectClass", + "objectGUID", + "distinguishedName", + "name", + NULL, + }; + + switch (tree->operation) { + case LDB_OP_AND: + for (i = 0; i < tree->u.list.num_elements; i++) { + /* + * all elements need to be valid + */ + ret = dsdb_notification_verify_tree(tree->u.list.elements[i]); + if (ret != LDB_SUCCESS) { + return ret; + } + num_ok++; + } + break; + case LDB_OP_OR: + for (i = 0; i < tree->u.list.num_elements; i++) { + /* + * at least one element needs to be valid + */ + ret = dsdb_notification_verify_tree(tree->u.list.elements[i]); + if (ret == LDB_SUCCESS) { + num_ok++; + break; + } + } + break; + case LDB_OP_NOT: + case LDB_OP_EQUALITY: + case LDB_OP_GREATER: + case LDB_OP_LESS: + case LDB_OP_APPROX: + case LDB_OP_SUBSTRING: + case LDB_OP_EXTENDED: + break; + + case LDB_OP_PRESENT: + ret = ldb_attr_in_list(attrs_ok, tree->u.present.attr); + if (ret == 1) { + num_ok++; + } + break; + } + + if (num_ok != 0) { + return LDB_SUCCESS; + } + + return LDB_ERR_UNWILLING_TO_PERFORM; +} + +static int dsdb_notification_filter_search(struct ldb_module *module, + struct ldb_request *req, + struct ldb_control *control) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + char *filter_usn = NULL; + struct ldb_parse_tree *down_tree = NULL; + struct ldb_request *down_req = NULL; + struct dsdb_notification_cookie *cookie = NULL; + int ret; + + if (req->op.search.tree == NULL) { + return dsdb_module_werror(module, LDB_ERR_OTHER, + WERR_DS_NOTIFY_FILTER_TOO_COMPLEX, + "Search filter missing."); + } + + ret = dsdb_notification_verify_tree(req->op.search.tree); + if (ret != LDB_SUCCESS) { + return dsdb_module_werror(module, ret, + WERR_DS_NOTIFY_FILTER_TOO_COMPLEX, + "Search filter too complex."); + } + + /* + * For now we use a very simple design: + * + * - We don't do fully async ldb_requests, + * the caller needs to retry periodically! + * - The only useful caller is the LDAP server, which is a long + * running task that can do periodic retries. + * - We use a cookie in order to transfer state between the + * retries. + * - We just search the available new objects each time we're + * called. + * + * As the only valid search filter is '(objectClass=*)' or + * something similar that matches every object, we simply + * replace it with (uSNChanged >= ) filter. + * We could improve this later if required... + */ + + /* + * The ldap_control_handler() decode_flag_request for + * LDB_CONTROL_NOTIFICATION_OID. This makes sure + * notification_control->data is NULL when comming from + * the client. + */ + if (control->data == NULL) { + cookie = talloc_zero(control, struct dsdb_notification_cookie); + if (cookie == NULL) { + return ldb_module_oom(module); + } + control->data = (uint8_t *)cookie; + + /* mark the control as done */ + control->critical = 0; + } + + cookie = talloc_get_type_abort(control->data, + struct dsdb_notification_cookie); + + if (cookie->known_usn != 0) { + filter_usn = talloc_asprintf(req, "%llu", + (unsigned long long)(cookie->known_usn)+1); + if (filter_usn == NULL) { + return ldb_module_oom(module); + } + } + + ret = ldb_sequence_number(ldb, LDB_SEQ_HIGHEST_SEQ, + &cookie->known_usn); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (filter_usn == NULL) { + /* + * It's the first time, let the caller comeback later + * as we won't find any new objects. + */ + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS); + } + + down_tree = talloc_zero(req, struct ldb_parse_tree); + if (down_tree == NULL) { + return ldb_module_oom(module); + } + down_tree->operation = LDB_OP_GREATER; + down_tree->u.equality.attr = "uSNChanged"; + down_tree->u.equality.value = data_blob_string_const(filter_usn); + (void)talloc_move(down_req, &filter_usn); + + ret = ldb_build_search_req_ex(&down_req, ldb, req, + req->op.search.base, + req->op.search.scope, + down_tree, + req->op.search.attrs, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* perform the search */ + return ldb_next_request(module, down_req); +} + +static int dsdb_notification_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_control *control = NULL; + + if (ldb_dn_is_special(req->op.search.base)) { + return ldb_next_request(module, req); + } + + /* + * check if there's an extended dn control + */ + control = ldb_request_get_control(req, LDB_CONTROL_NOTIFICATION_OID); + if (control == NULL) { + /* not found go on */ + return ldb_next_request(module, req); + } + + return dsdb_notification_filter_search(module, req, control); +} + +static int dsdb_notification_init(struct ldb_module *module) +{ + int ret; + + ret = ldb_mod_register_control(module, LDB_CONTROL_NOTIFICATION_OID); + if (ret != LDB_SUCCESS) { + struct ldb_context *ldb = ldb_module_get_ctx(module); + + ldb_debug(ldb, LDB_DEBUG_ERROR, + "notification: Unable to register control with rootdse!\n"); + return ldb_module_operr(module); + } + + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_dsdb_notification_module_ops = { + .name = "dsdb_notification", + .search = dsdb_notification_search, + .init_context = dsdb_notification_init, +}; + +/* + initialise the module + */ +_PUBLIC_ int ldb_dsdb_notification_module_init(const char *version) +{ + int ret; + LDB_MODULE_CHECK_VERSION(version); + ret = ldb_register_module(&ldb_dsdb_notification_module_ops); + return ret; +} diff --git a/source4/dsdb/samdb/ldb_modules/encrypted_secrets.c b/source4/dsdb/samdb/ldb_modules/encrypted_secrets.c new file mode 100644 index 0000000..8c59418 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/encrypted_secrets.c @@ -0,0 +1,1401 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017 + + 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/>. +*/ + +/* + * Encrypt the samba secret attributes on disk. This is intended to + * mitigate the inadvertent disclosure of the sam.ldb file, and to mitigate + * memory read attacks. + * + * Currently the key file is stored in the same directory as sam.ldb but + * this could be changed at a later date to use an HSM or similar mechanism + * to protect the key. + * + * Data is encrypted with AES 128 GCM. The encryption uses gnutls where + * available and if it supports AES 128 GCM AEAD modes, otherwise the + * samba internal implementation is used. + * + */ + +#include "includes.h" +#include <ldb_module.h> + +#include "librpc/gen_ndr/ndr_drsblobs.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" + +#include <gnutls/gnutls.h> +#include <gnutls/crypto.h> + +static const char * const secret_attributes[] = {DSDB_SECRET_ATTRIBUTES}; +static const size_t num_secret_attributes = ARRAY_SIZE(secret_attributes); + +#define SECRET_ATTRIBUTE_VERSION 1 +#define SECRET_ENCRYPTION_ALGORITHM ENC_SECRET_AES_128_AEAD +#define NUMBER_OF_KEYS 1 +#define SECRETS_KEY_FILE "encrypted_secrets.key" + +#undef strcasecmp + +struct es_data { + /* + * Should secret attributes be encrypted and decrypted? + */ + bool encrypt_secrets; + /* + * Encryption keys for secret attributes + */ + DATA_BLOB keys[NUMBER_OF_KEYS]; + /* + * The gnutls algorithm used to encrypt attributes + */ + int encryption_algorithm; +}; + +/* + * @brief Get the key used to encrypt and decrypt secret attributes on disk. + * + * @param data the private context data for this module. + * + * @return A data blob containing the key. + * This should be treated as read only. + */ +static const DATA_BLOB get_key(const struct es_data *data) { + + return data->keys[0]; +} + +/* + * @brief Get the directory containing the key files. + * + * @param ctx talloc memory context that will own the return value + * @param ldb ldb context, to allow logging + * + * @return zero terminated string, the directory containing the key file + * allocated on ctx. + * + */ +static const char* get_key_directory(TALLOC_CTX *ctx, struct ldb_context *ldb) +{ + + const char *sam_ldb_path = NULL; + const char *private_dir = NULL; + char *p = NULL; + + + /* + * Work out where *our* key file is. It must be in + * the same directory as sam.ldb + */ + sam_ldb_path = ldb_get_opaque(ldb, "ldb_url"); + if (sam_ldb_path == NULL) { + ldb_set_errstring(ldb, "Unable to get ldb_url\n"); + return NULL; + } + + if (strncmp("tdb://", sam_ldb_path, 6) == 0) { + sam_ldb_path += 6; + } + else if (strncmp("ldb://", sam_ldb_path, 6) == 0) { + sam_ldb_path += 6; + } + else if (strncmp("mdb://", sam_ldb_path, 6) == 0) { + sam_ldb_path += 6; + } + private_dir = talloc_strdup(ctx, sam_ldb_path); + if (private_dir == NULL) { + ldb_set_errstring(ldb, + "Out of memory building encrypted " + "secrets key\n"); + return NULL; + } + + p = strrchr(private_dir, '/'); + if (p != NULL) { + *p = '\0'; + } else { + private_dir = talloc_strdup(ctx, "."); + } + + return private_dir; +} + +/* + * @brief log details of an error that set errno + * + * @param ldb ldb context, to allow logging. + * @param err the value of errno. + * @param desc extra text to help describe the error. + * + */ +static void log_error(struct ldb_context *ldb, int err, const char *desc) +{ + char buf[1024]; + int e = strerror_r(err, buf, sizeof(buf)); + if (e != 0) { + strlcpy(buf, "Unknown error", sizeof(buf)-1); + } + ldb_asprintf_errstring(ldb, "Error (%d) %s - %s\n", err, buf, desc); +} + +/* + * @brief Load the keys into the encrypted secrets module context. + * + * @param module the current ldb module + * @param data the private data for the current module + * + * Currently the keys are stored in a binary file in the same directory + * as the database. + * + * @return an LDB result code. + * + */ +static int load_keys(struct ldb_module *module, struct es_data *data) +{ + + const char *key_dir = NULL; + const char *key_path = NULL; + + struct ldb_context *ldb = NULL; + FILE *fp = NULL; + const int key_size = 16; + int read; + DATA_BLOB key = data_blob_null; + + TALLOC_CTX *frame = talloc_stackframe(); + + ldb = ldb_module_get_ctx(module); + key_dir = get_key_directory(frame, ldb); + if (key_dir == NULL) { + TALLOC_FREE(frame); + return LDB_ERR_OPERATIONS_ERROR; + } + + key_path = talloc_asprintf(frame, "%s/%s", key_dir, SECRETS_KEY_FILE); + if (key_path == NULL) { + TALLOC_FREE(frame); + return ldb_oom(ldb); + } + + + key = data_blob_talloc_zero(module, key_size); + key.length = key_size; + + fp = fopen(key_path, "rb"); + if (fp == NULL) { + TALLOC_FREE(frame); + data_blob_free(&key); + if (errno == ENOENT) { + ldb_debug(ldb, + LDB_DEBUG_WARNING, + "No encrypted secrets key file. " + "Secret attributes will not be encrypted or " + "decrypted\n"); + data->encrypt_secrets = false; + return LDB_SUCCESS; + } else { + log_error(ldb, + errno, + "Opening encrypted_secrets key file\n"); + return LDB_ERR_OPERATIONS_ERROR; + } + } + + read = fread(key.data, 1, key.length, fp); + fclose(fp); + if (read == 0) { + TALLOC_FREE(frame); + ldb_debug(ldb, + LDB_DEBUG_WARNING, + "Zero length encrypted secrets key file. " + "Secret attributes will not be encrypted or " + "decrypted\n"); + data->encrypt_secrets = false; + return LDB_SUCCESS; + } + if (read != key.length) { + TALLOC_FREE(frame); + if (errno) { + log_error(ldb, + errno, + "Reading encrypted_secrets key file\n"); + } else { + ldb_debug(ldb, + LDB_DEBUG_ERROR, + "Invalid encrypted_secrets key file, " + "only %d bytes read should be %d bytes\n", + read, + key_size); + } + return LDB_ERR_OPERATIONS_ERROR; + } + + data->keys[0] = key; + data->encrypt_secrets = true; + data->encryption_algorithm = GNUTLS_CIPHER_AES_128_GCM; + TALLOC_FREE(frame); + + return LDB_SUCCESS; + +} + +/* + * @brief should this element be encrypted. + * + * @param el the element to examine + * + * @return true if the element should be encrypted, + * false otherwise. + */ +static bool should_encrypt(const struct ldb_message_element *el) +{ + size_t i; + + for (i = 0; i < ARRAY_SIZE(secret_attributes); i++) { + if (strcasecmp(secret_attributes[i], el->name) == 0) { + return true; + } + } + return false; +} + +/* + * @brief Round a size up to a multiple of the encryption cipher block size. + * + * @param block_size The cipher block size + * @param size The size to round + * + * @return Size rounded up to the nearest multiple of block_size + */ +static size_t round_to_block_size(size_t block_size, size_t size) +{ + if ((size % block_size) == 0) { + return size; + } else { + return ((int)(size/block_size) + 1) * block_size; + } +} + +/* + * @brief Create an new EncryptedSecret owned by the supplied talloc context. + * + * Create a new encrypted secret and initialise the header. + * + * @param ldb ldb context, to allow logging. + * @param ctx The talloc memory context that will own the new EncryptedSecret + * + * @return pointer to the new encrypted secret, or NULL if there was an error + */ +static struct EncryptedSecret *makeEncryptedSecret(struct ldb_context *ldb, + TALLOC_CTX *ctx) +{ + struct EncryptedSecret *es = NULL; + + es = talloc_zero_size(ctx, sizeof(struct EncryptedSecret)); + if (es == NULL) { + ldb_set_errstring(ldb, + "Out of memory, allocating " + "struct EncryptedSecret\n"); + return NULL; + } + es->header.magic = ENCRYPTED_SECRET_MAGIC_VALUE; + es->header.version = SECRET_ATTRIBUTE_VERSION; + es->header.algorithm = SECRET_ENCRYPTION_ALGORITHM; + es->header.flags = 0; + return es; +} + +/* + * @brief Allocate and populate a data blob with a PlaintextSecret structure. + * + * Allocate a new data blob and populate it with a serialised PlaintextSecret, + * containing the ldb_val + * + * @param ctx The talloc memory context that will own the allocated memory. + * @param ldb ldb context, to allow logging. + * @param val The ldb value to serialise. + * + * @return The populated data blob or data_blob_null if there was an error. + */ +static DATA_BLOB makePlainText(TALLOC_CTX *ctx, + struct ldb_context *ldb, + const struct ldb_val val) +{ + struct PlaintextSecret ps = { .cleartext = data_blob_null}; + DATA_BLOB pt = data_blob_null; + int rc; + + ps.cleartext.length = val.length; + ps.cleartext.data = val.data; + + rc = ndr_push_struct_blob(&pt, + ctx, + &ps, + (ndr_push_flags_fn_t) + ndr_push_PlaintextSecret); + if (!NDR_ERR_CODE_IS_SUCCESS(rc)) { + ldb_set_errstring(ldb, + "Unable to ndr push PlaintextSecret\n"); + return data_blob_null; + } + return pt; +} + + +/* + * Helper function converts a data blob to a gnutls_datum_t. + * Note that this does not copy the data. + * So the returned value should be treated as read only. + * And that changes to the length of the underlying DATA_BLOB + * will not be reflected in the returned object. + * + */ +static const gnutls_datum_t convert_from_data_blob(DATA_BLOB blob) { + + const gnutls_datum_t datum = { + .size = blob.length, + .data = blob.data, + }; + return datum; +} + +/* + * @brief Get the gnutls algorithm needed to decrypt the EncryptedSecret + * + * @param ldb ldb context, to allow logging. + * @param es the encrypted secret + * + * @return The gnutls algoritm number, or 0 if there is no match. + * + */ +static int gnutls_get_algorithm(struct ldb_context *ldb, + struct EncryptedSecret *es) { + + switch (es->header.algorithm) { + case ENC_SECRET_AES_128_AEAD: + return GNUTLS_CIPHER_AES_128_GCM; + default: + ldb_asprintf_errstring(ldb, + "Unsupported encryption algorithm %d\n", + es->header.algorithm); + return 0; + } +} + +/* + * + * @param err Pointer to an error code, set to: + * LDB_SUCESS If the value was successfully encrypted + * LDB_ERR_OPERATIONS_ERROR If there was an error. + * + * @param ctx Talloc memory context the will own the memory allocated + * @param ldb ldb context, to allow logging. + * @param val The ldb value to encrypt, not altered or freed + * @param data The context data for this module. + * + * @return The encrypted ldb_val, or data_blob_null if there was an error. + */ +static struct ldb_val gnutls_encrypt_aead(int *err, + TALLOC_CTX *ctx, + struct ldb_context *ldb, + const struct ldb_val val, + const struct es_data *data) +{ + struct EncryptedSecret *es = NULL; + struct ldb_val enc = data_blob_null; + DATA_BLOB pt = data_blob_null; + gnutls_aead_cipher_hd_t cipher_hnd; + int rc; + + TALLOC_CTX *frame = talloc_stackframe(); + + es = makeEncryptedSecret(ldb, frame); + if (es == NULL) { + goto error_exit; + } + + pt = makePlainText(frame, ldb, val); + if (pt.length == 0) { + goto error_exit; + } + + /* + * Set the encryption key and initialize the encryption handle. + */ + { + const size_t key_size = gnutls_cipher_get_key_size( + data->encryption_algorithm); + gnutls_datum_t cipher_key; + DATA_BLOB key_blob = get_key(data); + + if (key_blob.length != key_size) { + ldb_asprintf_errstring(ldb, + "Invalid EncryptedSecrets key " + "size, expected %zu bytes and " + "it is %zu bytes\n", + key_size, + key_blob.length); + goto error_exit; + } + cipher_key = convert_from_data_blob(key_blob); + + rc = gnutls_aead_cipher_init(&cipher_hnd, + data->encryption_algorithm, + &cipher_key); + if (rc !=0) { + ldb_asprintf_errstring(ldb, + "gnutls_aead_cipher_init failed " + "%s - %s\n", + gnutls_strerror_name(rc), + gnutls_strerror(rc)); + goto error_exit; + } + + } + + /* + * Set the initialisation vector + */ + { + unsigned iv_size = gnutls_cipher_get_iv_size( + data->encryption_algorithm); + uint8_t *iv; + + iv = talloc_zero_size(frame, iv_size); + if (iv == NULL) { + ldb_set_errstring(ldb, + "Out of memory allocating IV\n"); + goto error_exit_handle; + } + + rc = gnutls_rnd(GNUTLS_RND_NONCE, iv, iv_size); + if (rc !=0) { + ldb_asprintf_errstring(ldb, + "gnutls_rnd failed %s - %s\n", + gnutls_strerror_name(rc), + gnutls_strerror(rc)); + goto error_exit_handle; + } + es->iv.length = iv_size; + es->iv.data = iv; + } + + /* + * Encrypt the value. + */ + { + const unsigned block_size = gnutls_cipher_get_block_size( + data->encryption_algorithm); + const unsigned tag_size = gnutls_cipher_get_tag_size( + data->encryption_algorithm); + const size_t ed_size = round_to_block_size( + block_size, + sizeof(struct PlaintextSecret) + val.length); + const size_t en_size = ed_size + tag_size; + uint8_t *ct = talloc_zero_size(frame, en_size); + size_t el = en_size; + + if (ct == NULL) { + ldb_set_errstring(ldb, + "Out of memory allocation cipher " + "text\n"); + goto error_exit_handle; + } + + rc = gnutls_aead_cipher_encrypt( + cipher_hnd, + es->iv.data, + es->iv.length, + &es->header, + sizeof(struct EncryptedSecretHeader), + tag_size, + pt.data, + pt.length, + ct, + &el); + if (rc !=0) { + ldb_asprintf_errstring(ldb, + "gnutls_aead_cipher_encrypt '" + "failed %s - %s\n", + gnutls_strerror_name(rc), + gnutls_strerror(rc)); + *err = LDB_ERR_OPERATIONS_ERROR; + return data_blob_null; + } + es->encrypted.length = el; + es->encrypted.data = ct; + gnutls_aead_cipher_deinit(cipher_hnd); + } + + rc = ndr_push_struct_blob(&enc, + ctx, + es, + (ndr_push_flags_fn_t) + ndr_push_EncryptedSecret); + if (!NDR_ERR_CODE_IS_SUCCESS(rc)) { + ldb_set_errstring(ldb, + "Unable to ndr push EncryptedSecret\n"); + goto error_exit; + } + TALLOC_FREE(frame); + return enc; + +error_exit_handle: + gnutls_aead_cipher_deinit(cipher_hnd); +error_exit: + *err = LDB_ERR_OPERATIONS_ERROR; + TALLOC_FREE(frame); + return data_blob_null; +} + +/* + * @brief Decrypt data encrypted using an aead algorithm. + * + * Decrypt the data in ed and insert it into ev. The data was encrypted + * with one of the gnutls aead compatable algorithms. + * + * @param err Pointer to an error code, set to: + * LDB_SUCESS If the value was successfully decrypted + * LDB_ERR_OPERATIONS_ERROR If there was an error. + * + * @param ctx The talloc context that will own the PlaintextSecret + * @param ldb ldb context, to allow logging. + * @param ev The value to be updated with the decrypted data. + * @param ed The data to decrypt. + * @param data The context data for this module. + * + * @return ev is updated with the unencrypted data. + */ +static void gnutls_decrypt_aead(int *err, + TALLOC_CTX *ctx, + struct ldb_context *ldb, + struct EncryptedSecret *es, + struct PlaintextSecret *ps, + const struct es_data *data) +{ + + gnutls_aead_cipher_hd_t cipher_hnd; + DATA_BLOB pt = data_blob_null; + const unsigned tag_size = + gnutls_cipher_get_tag_size(es->header.algorithm); + int rc; + + /* + * Get the encryption key and initialise the encryption handle + */ + { + gnutls_datum_t cipher_key; + DATA_BLOB key_blob; + const int algorithm = gnutls_get_algorithm(ldb, es); + const size_t key_size = gnutls_cipher_get_key_size(algorithm); + key_blob = get_key(data); + + if (algorithm == 0) { + goto error_exit; + } + + if (key_blob.length != key_size) { + ldb_asprintf_errstring(ldb, + "Invalid EncryptedSecrets key " + "size, expected %zu bytes and " + "it is %zu bytes\n", + key_size, + key_blob.length); + goto error_exit; + } + cipher_key = convert_from_data_blob(key_blob); + + rc = gnutls_aead_cipher_init( + &cipher_hnd, + algorithm, + &cipher_key); + if (rc != 0) { + ldb_asprintf_errstring(ldb, + "gnutls_aead_cipher_init failed " + "%s - %s\n", + gnutls_strerror_name(rc), + gnutls_strerror(rc)); + goto error_exit; + } + } + + /* + * Decrypt and validate the encrypted value + */ + + pt.length = es->encrypted.length; + pt.data = talloc_zero_size(ctx, es->encrypted.length); + + if (pt.data == NULL) { + ldb_set_errstring(ldb, + "Out of memory allocating plain text\n"); + goto error_exit_handle; + } + + rc = gnutls_aead_cipher_decrypt(cipher_hnd, + es->iv.data, + es->iv.length, + &es->header, + sizeof(struct EncryptedSecretHeader), + tag_size, + es->encrypted.data, + es->encrypted.length, + pt.data, + &pt.length); + if (rc != 0) { + /* + * Typically this will indicate that the data has been + * corrupted i.e. the tag comparison has failed. + * At the moment gnutls does not provide a separate + * error code to indicate this + */ + ldb_asprintf_errstring(ldb, + "gnutls_aead_cipher_decrypt failed " + "%s - %s. Data possibly corrupted or " + "altered\n", + gnutls_strerror_name(rc), + gnutls_strerror(rc)); + goto error_exit_handle; + } + gnutls_aead_cipher_deinit(cipher_hnd); + + rc = ndr_pull_struct_blob(&pt, + ctx, + ps, + (ndr_pull_flags_fn_t) + ndr_pull_PlaintextSecret); + if(!NDR_ERR_CODE_IS_SUCCESS(rc)) { + ldb_asprintf_errstring(ldb, + "Error(%d) unpacking decrypted data, " + "data possibly corrupted or altered\n", + rc); + goto error_exit; + } + return; + +error_exit_handle: + gnutls_aead_cipher_deinit(cipher_hnd); +error_exit: + *err = LDB_ERR_OPERATIONS_ERROR; + return; +} + +/* + * @brief Encrypt an attribute value using the default encryption algorithm. + * + * Returns an encrypted copy of the value, the original value is left intact. + * The original content of val is encrypted and wrapped in an encrypted_value + * structure. + * + * @param err Pointer to an error code, set to: + * LDB_SUCESS If the value was successfully encrypted + * LDB_ERR_OPERATIONS_ERROR If there was an error. + * + * @param ctx Talloc memory context the will own the memory allocated + * @param ldb ldb context, to allow logging. + * @param val The ldb value to encrypt, not altered or freed + * @param data The context data for this module. + * + * @return The encrypted ldb_val, or data_blob_null if there was an error. + */ +static struct ldb_val encrypt_value(int *err, + TALLOC_CTX *ctx, + struct ldb_context *ldb, + const struct ldb_val val, + const struct es_data *data) +{ + return gnutls_encrypt_aead(err, ctx, ldb, val, data); +} + +/* + * @brief Encrypt all the values on an ldb_message_element + * + * Returns a copy of the original attribute with all values encrypted + * by encrypt_value(), the original attribute is left intact. + * + * @param err Pointer to an error code, set to: + * LDB_SUCESS If the value was successfully encrypted + * LDB_ERR_OPERATIONS_ERROR If there was an error. + * + * @param ctx Talloc memory context the will own the memory allocated + * for the new ldb_message_element. + * @param ldb ldb context, to allow logging. + * @param el The ldb_message_elemen to encrypt, not altered or freed + * @param data The context data for this module. + * + * @return Pointer encrypted lsb_message_element, will be NULL if there was + * an error. + */ +static struct ldb_message_element *encrypt_element( + int *err, + TALLOC_CTX *ctx, + struct ldb_context *ldb, + const struct ldb_message_element *el, + const struct es_data *data) +{ + struct ldb_message_element* enc; + unsigned int i; + + enc = talloc_zero(ctx, struct ldb_message_element); + if (enc == NULL) { + ldb_set_errstring(ldb, + "Out of memory, allocating ldb_message_" + "element\n"); + *err = LDB_ERR_OPERATIONS_ERROR; + return NULL; + } + + enc->flags = el->flags; + enc->num_values = el->num_values; + enc->values = talloc_array(enc, struct ldb_val, enc->num_values); + if (enc->values == NULL) { + TALLOC_FREE(enc); + ldb_set_errstring(ldb, + "Out of memory, allocating values array\n"); + *err = LDB_ERR_OPERATIONS_ERROR; + return NULL; + } + + enc->name = talloc_strdup(enc, el->name); + if (enc->name == NULL) { + TALLOC_FREE(enc); + ldb_set_errstring(ldb, + "Out of memory, copying element name\n"); + *err = LDB_ERR_OPERATIONS_ERROR; + return NULL; + } + + for (i = 0; i < el->num_values; i++) { + enc->values[i] = + encrypt_value( + err, + enc->values, + ldb, + el->values[i], + data); + if (*err != LDB_SUCCESS) { + TALLOC_FREE(enc); + return NULL; + } + } + return enc; +} + +/* + * @brief Encrypt all the secret attributes on an ldb_message + * + * Encrypt all the secret attributes on an ldb_message. Any secret + * attributes are removed from message and encrypted copies of the + * attributes added. In the event of an error the contents of the + * message will be inconsistent. + * + * @param err Pointer to an error code, set to: + * LDB_SUCESS If the value was successfully encrypted + * LDB_ERR_OPERATIONS_ERROR If there was an error. + * @param ldb ldb context, to allow logging. + * @param msg The ldb_message to have it's secret attributes encrypted. + * + * @param data The context data for this module. + */ +static const struct ldb_message *encrypt_secret_attributes( + int *err, + TALLOC_CTX *ctx, + struct ldb_context *ldb, + const struct ldb_message *msg, + const struct es_data *data) +{ + struct ldb_message *encrypted_msg = NULL; + + unsigned int i; + + if (ldb_dn_is_special(msg->dn)) { + return NULL; + } + + for (i = 0; i < msg->num_elements; i++) { + + const struct ldb_message_element *el = &msg->elements[i]; + if (should_encrypt(el)) { + struct ldb_message_element* enc = NULL; + if (encrypted_msg == NULL) { + encrypted_msg = ldb_msg_copy_shallow(ctx, msg); + if (encrypted_msg == NULL) { + ldb_set_errstring( + ldb, + "Out of memory, allocating " + "ldb_message_element\n"); + *err = LDB_ERR_OPERATIONS_ERROR; + return NULL; + } + encrypted_msg->dn = msg->dn; + } + enc = encrypt_element(err, + msg->elements, + ldb, + el, + data); + if (*err != LDB_SUCCESS) { + return NULL; + } + encrypted_msg->elements[i] = *enc; + } + } + return encrypted_msg; +} + +/* + * @brief Check the encrypted secret header to ensure it's valid + * + * Check an Encrypted secret and ensure it's header is valid. + * A header is assumed to be valid if it: + * - it starts with the MAGIC_VALUE + * - The version number is valid + * - The algorithm is valid + * + * @param val The EncryptedSecret to check. + * + * @return true if the header is valid, false otherwise. + * + */ +static bool check_header(struct EncryptedSecret *es) +{ + struct EncryptedSecretHeader *eh; + + eh = &es->header; + if (eh->magic != ENCRYPTED_SECRET_MAGIC_VALUE) { + /* + * Does not start with the magic value so not + * an encrypted_value + */ + return false; + } + + if (eh->version > SECRET_ATTRIBUTE_VERSION) { + /* + * Invalid version, so not an encrypted value + */ + return false; + } + + if (eh->algorithm != ENC_SECRET_AES_128_AEAD) { + /* + * Invalid algorithm, so not an encrypted value + */ + return false; + } + /* + * Length looks ok, starts with magic value, and the version and + * algorithm are valid + */ + return true; +} +/* + * @brief Decrypt an attribute value. + * + * Returns a decrypted copy of the value, the original value is left intact. + * + * @param err Pointer to an error code, set to: + * LDB_SUCESS If the value was successfully decrypted + * LDB_ERR_OPERATIONS_ERROR If there was an error. + * + * @param ctx Talloc memory context the will own the memory allocated + * @param ldb ldb context, to allow logging. + * @param val The ldb value to decrypt, not altered or freed + * @param data The context data for this module. + * + * @return The decrypted ldb_val, or data_blob_null if there was an error. + */ +static struct ldb_val decrypt_value(int *err, + TALLOC_CTX *ctx, + struct ldb_context *ldb, + const struct ldb_val val, + const struct es_data *data) +{ + + struct ldb_val dec; + + struct EncryptedSecret es; + struct PlaintextSecret ps = { data_blob_null}; + int rc; + TALLOC_CTX *frame = talloc_stackframe(); + + rc = ndr_pull_struct_blob(&val, + frame, + &es, + (ndr_pull_flags_fn_t) + ndr_pull_EncryptedSecret); + if(!NDR_ERR_CODE_IS_SUCCESS(rc)) { + ldb_asprintf_errstring(ldb, + "Error(%d) unpacking encrypted secret, " + "data possibly corrupted or altered\n", + rc); + *err = LDB_ERR_OPERATIONS_ERROR; + TALLOC_FREE(frame); + return data_blob_null; + } + if (!check_header(&es)) { + /* + * Header is invalid so can't be an encrypted value + */ + ldb_set_errstring(ldb, "Invalid EncryptedSecrets header\n"); + *err = LDB_ERR_OPERATIONS_ERROR; + return data_blob_null; + } + gnutls_decrypt_aead(err, frame, ldb, &es, &ps, data); + + if (*err != LDB_SUCCESS) { + TALLOC_FREE(frame); + return data_blob_null; + } + + dec = data_blob_talloc(ctx, + ps.cleartext.data, + ps.cleartext.length); + if (dec.data == NULL) { + TALLOC_FREE(frame); + ldb_set_errstring(ldb, "Out of memory, copying value\n"); + *err = LDB_ERR_OPERATIONS_ERROR; + return data_blob_null; + } + + TALLOC_FREE(frame); + return dec; +} + +/* + * @brief Decrypt all the encrypted values on an ldb_message_element + * + * Returns a copy of the original attribute with all values decrypted by + * decrypt_value(), the original attribute is left intact. + * + * @param err Pointer to an error code, set to: + * LDB_SUCESS If the value was successfully encrypted + * LDB_ERR_OPERATIONS_ERROR If there was an error. + * + * @param ctx Talloc memory context the will own the memory allocated + * for the new ldb_message_element. + * @param ldb ldb context, to allow logging. + * @param el The ldb_message_elemen to decrypt, not altered or freed + * @param data The context data for this module. + * + * @return Pointer decrypted lsb_message_element, will be NULL if there was + * an error. + */ +static struct ldb_message_element *decrypt_element( + int *err, + TALLOC_CTX *ctx, + struct ldb_context *ldb, + struct ldb_message_element* el, + struct es_data *data) +{ + unsigned int i; + struct ldb_message_element* dec = + talloc_zero(ctx, struct ldb_message_element); + + *err = LDB_SUCCESS; + if (dec == NULL) { + ldb_set_errstring(ldb, + "Out of memory, allocating " + "ldb_message_element\n"); + *err = LDB_ERR_OPERATIONS_ERROR; + return NULL; + } + dec->num_values = el->num_values; + + dec->values = talloc_array(dec, struct ldb_val, dec->num_values); + if (dec->values == NULL) { + TALLOC_FREE(dec); + ldb_set_errstring(ldb, + "Out of memory, allocating values array\n"); + *err = LDB_ERR_OPERATIONS_ERROR; + return NULL; + } + + dec->name = talloc_strdup(dec, el->name); + if (dec->name == NULL) { + TALLOC_FREE(dec); + ldb_set_errstring(ldb, "Out of memory, copying element name\n"); + *err = LDB_ERR_OPERATIONS_ERROR; + return NULL; + } + + for (i = 0; i < el->num_values; i++) { + dec->values[i] = + decrypt_value(err, + el->values, + ldb, + el->values[i], + data); + if (*err != LDB_SUCCESS) { + TALLOC_FREE(dec); + return NULL; + } + } + return dec; +} + + +/* + * @brief Decrypt all the secret attributes on an ldb_message + * + * Decrypt all the secret attributes on an ldb_message. Any secret attributes + * are removed from message and decrypted copies of the attributes added. + * In the event of an error the contents of the message will be inconsistent. + * + * @param ldb ldb context, to allow logging. + * @param msg The ldb_message to have it's secret attributes encrypted. + * @param data The context data for this module. + * + * @returns ldb status code + * LDB_SUCESS If the value was successfully encrypted + * LDB_ERR_OPERATIONS_ERROR If there was an error. + */ +static int decrypt_secret_attributes(struct ldb_context *ldb, + struct ldb_message *msg, + struct es_data *data) +{ + size_t i; + int ret; + + if (ldb_dn_is_special(msg->dn)) { + return LDB_SUCCESS; + } + + for (i = 0; i < num_secret_attributes; i++) { + struct ldb_message_element *el = + ldb_msg_find_element(msg, secret_attributes[i]); + if (el != NULL) { + const int flags = el->flags; + struct ldb_message_element* dec = + decrypt_element(&ret, + msg->elements, + ldb, + el, + data); + if (ret != LDB_SUCCESS) { + return ret; + } + ldb_msg_remove_element(msg, el); + ret = ldb_msg_add(msg, dec, flags); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } + return LDB_SUCCESS; +} + +static int es_search_post_process(struct ldb_module *module, + struct ldb_message *msg) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct es_data *data = + talloc_get_type(ldb_module_get_private(module), + struct es_data); + + + /* + * Decrypt any encrypted secret attributes + */ + if (data && data->encrypt_secrets) { + int err = decrypt_secret_attributes(ldb, msg, data); + if (err != LDB_SUCCESS) { + return err; + } + } + return LDB_SUCCESS; +} + +/* + hook search operations +*/ +struct es_context { + struct ldb_module *module; + struct ldb_request *req; +}; + +static int es_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct es_context *ec; + int ret; + + + ec = talloc_get_type(req->context, struct es_context); + + if (!ares) { + return ldb_module_done(ec->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ec->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + /* + * for each record returned decrypt any encrypted attributes + */ + ret = es_search_post_process(ec->module, ares->message); + if (ret != 0) { + return ldb_module_done(ec->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + return ldb_module_send_entry(ec->req, + ares->message, ares->controls); + + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral(ec->req, ares->referral); + + case LDB_REPLY_DONE: + + return ldb_module_done(ec->req, ares->controls, + ares->response, LDB_SUCCESS); + } + + talloc_free(ares); + return LDB_SUCCESS; +} + +static int es_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct es_context *ec; + struct ldb_request *down_req; + int ret; + + /* There are no encrypted attributes on special DNs */ + if (ldb_dn_is_special(req->op.search.base)) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + ec = talloc(req, struct es_context); + if (ec == NULL) { + return ldb_oom(ldb); + } + + ec->module = module; + ec->req = req; + ret = ldb_build_search_req_ex(&down_req, + ldb, + ec, + req->op.search.base, + req->op.search.scope, + req->op.search.tree, + req->op.search.attrs, + req->controls, + ec, + es_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + /* perform the search */ + return ldb_next_request(module, down_req); +} +static int es_add(struct ldb_module *module, struct ldb_request *req) +{ + + struct es_data *data = + talloc_get_type(ldb_module_get_private(module), + struct es_data); + const struct ldb_message *encrypted_msg = NULL; + struct ldb_context *ldb = NULL; + int rc = LDB_SUCCESS; + + if (!data->encrypt_secrets) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + encrypted_msg = encrypt_secret_attributes(&rc, + req, + ldb, + req->op.add.message, + data); + if (rc != LDB_SUCCESS) { + return rc; + } + /* + * If we did not encrypt any of the attributes + * continue on to the next module + */ + if (encrypted_msg == NULL) { + return ldb_next_request(module, req); + } + + /* + * Encrypted an attribute, now need to build a copy of the request + * so that we're not altering the original callers copy + */ + { + struct ldb_request* new_req = NULL; + rc = ldb_build_add_req(&new_req, + ldb, + req, + encrypted_msg, + req->controls, + req, + dsdb_next_callback, + req); + if (rc != LDB_SUCCESS) { + return rc; + } + return ldb_next_request(module, new_req); + } +} + +static int es_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct es_data *data = + talloc_get_type(ldb_module_get_private(module), + struct es_data); + const struct ldb_message *encrypted_msg = NULL; + struct ldb_context *ldb = NULL; + int rc = LDB_SUCCESS; + + if (!data->encrypt_secrets) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + encrypted_msg = encrypt_secret_attributes(&rc, + req, + ldb, + req->op.mod.message, + data); + if (rc != LDB_SUCCESS) { + return rc; + } + /* + * If we did not encrypt any of the attributes + * continue on to the next module + */ + if (encrypted_msg == NULL) { + return ldb_next_request(module, req); + } + + + /* + * Encrypted an attribute, now need to build a copy of the request + * so that we're not altering the original callers copy + */ + { + struct ldb_request* new_req = NULL; + rc = ldb_build_mod_req(&new_req, + ldb, + req, + encrypted_msg, + req->controls, + req, + dsdb_next_callback, + req); + if (rc != LDB_SUCCESS) { + return rc; + } + return ldb_next_request(module, new_req); + } +} + +static int es_delete(struct ldb_module *module, struct ldb_request *req) +{ + return ldb_next_request(module, req); +} + +static int es_rename(struct ldb_module *module, struct ldb_request *req) +{ + return ldb_next_request(module, req); +} +static int es_init(struct ldb_module *ctx) +{ + struct es_data *data; + int ret; + + data = talloc_zero(ctx, struct es_data); + if (!data) { + return ldb_module_oom(ctx); + } + + { + struct ldb_context *ldb = ldb_module_get_ctx(ctx); + struct ldb_dn *samba_dsdb_dn; + struct ldb_result *res; + static const char *samba_dsdb_attrs[] = { + SAMBA_REQUIRED_FEATURES_ATTR, + NULL + }; + TALLOC_CTX *frame = talloc_stackframe(); + + samba_dsdb_dn = ldb_dn_new(frame, ldb, "@SAMBA_DSDB"); + if (!samba_dsdb_dn) { + TALLOC_FREE(frame); + return ldb_oom(ldb); + } + ret = dsdb_module_search_dn(ctx, + frame, + &res, + samba_dsdb_dn, + samba_dsdb_attrs, + DSDB_FLAG_NEXT_MODULE, + NULL); + if (ret != LDB_SUCCESS) { + TALLOC_FREE(frame); + return ret; + } + data->encrypt_secrets = + ldb_msg_check_string_attribute( + res->msgs[0], + SAMBA_REQUIRED_FEATURES_ATTR, + SAMBA_ENCRYPTED_SECRETS_FEATURE); + if (data->encrypt_secrets) { + ret = load_keys(ctx, data); + if (ret != LDB_SUCCESS) { + TALLOC_FREE(frame); + return ret; + } + } + TALLOC_FREE(frame); + } + ldb_module_set_private(ctx, data); + + ret = ldb_next_init(ctx); + if (ret != LDB_SUCCESS) { + return ret; + } + return LDB_SUCCESS; +} + +static const struct ldb_module_ops ldb_encrypted_secrets_module_ops = { + .name = "encrypted_secrets", + .search = es_search, + .add = es_add, + .modify = es_modify, + .del = es_delete, + .rename = es_rename, + .init_context = es_init +}; + +int ldb_encrypted_secrets_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_encrypted_secrets_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/extended_dn_in.c b/source4/dsdb/samdb/ldb_modules/extended_dn_in.c new file mode 100644 index 0000000..248bb66 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/extended_dn_in.c @@ -0,0 +1,801 @@ +/* + ldb database library + + Copyright (C) Simo Sorce 2005-2008 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2007-2008 + + 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 interprets DNs of the form <SID=S-1-2-4456> into normal DNs. + * + * Authors: Simo Sorce + * Andrew Bartlett + */ + +#include "includes.h" +#include <ldb.h> +#include <ldb_errors.h> +#include <ldb_module.h> +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "lib/ldb-samba/ldb_matching_rules.h" + +#undef strncasecmp + +/* + TODO: if relax is not set then we need to reject the fancy RMD_* and + DELETED extended DN codes + */ + +/* search */ +struct extended_search_context { + struct ldb_module *module; + struct ldb_request *req; + struct ldb_parse_tree *tree; + struct ldb_dn *basedn; + struct ldb_dn *dn; + char *wellknown_object; + int extended_type; +}; + +static const char *wkattr[] = { + "wellKnownObjects", + "otherWellKnownObjects", + NULL +}; + +static const struct ldb_module_ops ldb_extended_dn_in_openldap_module_ops; + +/* 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_search_context *ac; + ac = talloc_get_type(req->context, struct extended_search_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_base_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct extended_search_context *ac; + struct ldb_request *down_req; + struct ldb_message_element *el; + int ret; + unsigned int i, j; + size_t wkn_len = 0; + char *valstr = NULL; + const char *found = NULL; + + ac = talloc_get_type(req->context, struct extended_search_context); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + if (ac->basedn) { + /* we have more than one match! This can + happen as S-1-5-17 appears twice in a + normal provision. We need to return + NO_SUCH_OBJECT */ + const char *str = talloc_asprintf(req, "Duplicate base-DN matches found for '%s'", + ldb_dn_get_extended_linearized(req, ac->dn, 1)); + ldb_set_errstring(ldb_module_get_ctx(ac->module), str); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_NO_SUCH_OBJECT); + } + + if (!ac->wellknown_object) { + ac->basedn = talloc_steal(ac, ares->message->dn); + break; + } + + wkn_len = strlen(ac->wellknown_object); + + for (j=0; wkattr[j]; j++) { + + el = ldb_msg_find_element(ares->message, wkattr[j]); + if (!el) { + ac->basedn = NULL; + continue; + } + + for (i=0; i < el->num_values; i++) { + valstr = talloc_strndup(ac, + (const char *)el->values[i].data, + el->values[i].length); + if (!valstr) { + ldb_oom(ldb_module_get_ctx(ac->module)); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + if (strncasecmp(valstr, ac->wellknown_object, wkn_len) != 0) { + talloc_free(valstr); + continue; + } + + found = &valstr[wkn_len]; + break; + } + if (found) { + break; + } + } + + if (!found) { + break; + } + + ac->basedn = ldb_dn_new(ac, ldb_module_get_ctx(ac->module), found); + talloc_free(valstr); + if (!ac->basedn) { + ldb_oom(ldb_module_get_ctx(ac->module)); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + break; + + case LDB_REPLY_REFERRAL: + break; + + case LDB_REPLY_DONE: + + if (!ac->basedn) { + const char *str = talloc_asprintf(req, "Base-DN '%s' not found", + ldb_dn_get_extended_linearized(req, ac->dn, 1)); + ldb_set_errstring(ldb_module_get_ctx(ac->module), str); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_NO_SUCH_OBJECT); + } + + switch (ac->req->operation) { + case LDB_SEARCH: + ret = ldb_build_search_req_ex(&down_req, + ldb_module_get_ctx(ac->module), ac->req, + ac->basedn, + ac->req->op.search.scope, + ac->tree, + ac->req->op.search.attrs, + ac->req->controls, + ac, extended_final_callback, + ac->req); + LDB_REQ_SET_LOCATION(down_req); + break; + case LDB_ADD: + { + struct ldb_message *add_msg = ldb_msg_copy_shallow(ac, ac->req->op.add.message); + if (!add_msg) { + ldb_oom(ldb_module_get_ctx(ac->module)); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + add_msg->dn = ac->basedn; + + ret = ldb_build_add_req(&down_req, + ldb_module_get_ctx(ac->module), ac->req, + add_msg, + ac->req->controls, + ac, extended_final_callback, + ac->req); + LDB_REQ_SET_LOCATION(down_req); + break; + } + case LDB_MODIFY: + { + struct ldb_message *mod_msg = ldb_msg_copy_shallow(ac, ac->req->op.mod.message); + if (!mod_msg) { + ldb_oom(ldb_module_get_ctx(ac->module)); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + mod_msg->dn = ac->basedn; + + ret = ldb_build_mod_req(&down_req, + ldb_module_get_ctx(ac->module), ac->req, + mod_msg, + ac->req->controls, + ac, extended_final_callback, + ac->req); + LDB_REQ_SET_LOCATION(down_req); + break; + } + case LDB_DELETE: + ret = ldb_build_del_req(&down_req, + ldb_module_get_ctx(ac->module), ac->req, + ac->basedn, + ac->req->controls, + ac, extended_final_callback, + ac->req); + LDB_REQ_SET_LOCATION(down_req); + break; + case LDB_RENAME: + ret = ldb_build_rename_req(&down_req, + ldb_module_get_ctx(ac->module), ac->req, + ac->basedn, + ac->req->op.rename.newdn, + ac->req->controls, + ac, extended_final_callback, + ac->req); + LDB_REQ_SET_LOCATION(down_req); + break; + default: + return ldb_module_done(ac->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + return ldb_next_request(ac->module, down_req); + } + talloc_free(ares); + return LDB_SUCCESS; +} + + +/* + windows ldap searchs don't allow a baseDN with more + than one extended component, or an extended + component and a string DN + + We only enforce this over ldap, not for internal + use, as there are just too many places where we + internally want to use a DN that has come from a + search with extended DN enabled, or comes from a DRS + naming context. + + Enforcing this would also make debugging samba much + harder, as we'd need to use ldb_dn_minimise() in a + lot of places, and that would lose the DN string + which is so useful for working out what a request is + for +*/ +static bool ldb_dn_match_allowed(struct ldb_dn *dn, struct ldb_request *req) +{ + int num_components = ldb_dn_get_comp_num(dn); + int num_ex_components = ldb_dn_get_extended_comp_num(dn); + + if (num_ex_components == 0) { + return true; + } + + if ((num_components != 0 || num_ex_components != 1) && + ldb_req_is_untrusted(req)) { + return false; + } + return true; +} + + +struct extended_dn_filter_ctx { + bool test_only; + bool matched; + struct ldb_module *module; + struct ldb_request *req; + struct dsdb_schema *schema; + uint32_t dsdb_flags; +}; + +/* + create a always non-matching node from a equality node + */ +static void set_parse_tree_false(struct ldb_parse_tree *tree) +{ + const char *attr = tree->u.equality.attr; + struct ldb_val value = tree->u.equality.value; + tree->operation = LDB_OP_EXTENDED; + tree->u.extended.attr = attr; + tree->u.extended.value = value; + tree->u.extended.rule_id = SAMBA_LDAP_MATCH_ALWAYS_FALSE; + tree->u.extended.dnAttributes = 0; +} + +/* + called on all nodes in the parse tree + */ +static int extended_dn_filter_callback(struct ldb_parse_tree *tree, void *private_context) +{ + struct extended_dn_filter_ctx *filter_ctx; + int ret; + struct ldb_dn *dn = NULL; + const struct ldb_val *sid_val, *guid_val; + const char *no_attrs[] = { NULL }; + struct ldb_result *res; + const struct dsdb_attribute *attribute = NULL; + bool has_extended_component = false; + enum ldb_scope scope; + struct ldb_dn *base_dn; + const char *expression; + uint32_t dsdb_flags; + + if (tree->operation != LDB_OP_EQUALITY && tree->operation != LDB_OP_EXTENDED) { + return LDB_SUCCESS; + } + + filter_ctx = talloc_get_type_abort(private_context, struct extended_dn_filter_ctx); + + if (filter_ctx->test_only && filter_ctx->matched) { + /* the tree already matched */ + return LDB_SUCCESS; + } + + if (!filter_ctx->schema) { + /* Schema not setup yet */ + return LDB_SUCCESS; + } + if (tree->operation == LDB_OP_EQUALITY) { + attribute = dsdb_attribute_by_lDAPDisplayName(filter_ctx->schema, tree->u.equality.attr); + } else if (tree->operation == LDB_OP_EXTENDED) { + attribute = dsdb_attribute_by_lDAPDisplayName(filter_ctx->schema, tree->u.extended.attr); + } + if (attribute == NULL) { + return LDB_SUCCESS; + } + + if (attribute->dn_format != DSDB_NORMAL_DN) { + return LDB_SUCCESS; + } + + if (tree->operation == LDB_OP_EQUALITY) { + has_extended_component = (memchr(tree->u.equality.value.data, '<', + tree->u.equality.value.length) != NULL); + } else if (tree->operation == LDB_OP_EXTENDED) { + has_extended_component = (memchr(tree->u.extended.value.data, '<', + tree->u.extended.value.length) != NULL); + } + + /* + * Don't turn it into an extended DN if we're talking to OpenLDAP. + * We just check the module_ops pointer instead of adding a private + * pointer and a boolean to tell us the exact same thing. + */ + if (!has_extended_component) { + if (!attribute->one_way_link) { + return LDB_SUCCESS; + } + + if (ldb_module_get_ops(filter_ctx->module) == &ldb_extended_dn_in_openldap_module_ops) { + return LDB_SUCCESS; + } + } + + if (tree->operation == LDB_OP_EQUALITY) { + dn = ldb_dn_from_ldb_val(filter_ctx, ldb_module_get_ctx(filter_ctx->module), &tree->u.equality.value); + } else if (tree->operation == LDB_OP_EXTENDED + && (strcmp(tree->u.extended.rule_id, SAMBA_LDAP_MATCH_RULE_TRANSITIVE_EVAL) == 0)) { + dn = ldb_dn_from_ldb_val(filter_ctx, ldb_module_get_ctx(filter_ctx->module), &tree->u.extended.value); + } + if (dn == NULL) { + /* testing against windows shows that we don't raise + an error here */ + return LDB_SUCCESS; + } + + guid_val = ldb_dn_get_extended_component(dn, "GUID"); + sid_val = ldb_dn_get_extended_component(dn, "SID"); + + /* + * Is the attribute indexed? By treating confidential attributes + * as unindexed, we force searches to go through the unindexed + * search path, avoiding observable timing differences. + */ + if (!guid_val && !sid_val && + (attribute->searchFlags & SEARCH_FLAG_ATTINDEX) && + !(attribute->searchFlags & SEARCH_FLAG_CONFIDENTIAL)) + { + /* if it is indexed, then fixing the string DN will do + no good here, as we will not find the attribute in + the index. So for now fall through to a standard DN + component comparison */ + return LDB_SUCCESS; + } + + if (filter_ctx->test_only) { + /* we need to copy the tree */ + filter_ctx->matched = true; + return LDB_SUCCESS; + } + + if (!ldb_dn_match_allowed(dn, filter_ctx->req)) { + /* we need to make this element of the filter always + be false */ + set_parse_tree_false(tree); + return LDB_SUCCESS; + } + + dsdb_flags = filter_ctx->dsdb_flags | DSDB_FLAG_NEXT_MODULE; + + if (guid_val) { + expression = talloc_asprintf(filter_ctx, "objectGUID=%s", ldb_binary_encode(filter_ctx, *guid_val)); + scope = LDB_SCOPE_SUBTREE; + base_dn = NULL; + dsdb_flags |= DSDB_SEARCH_SEARCH_ALL_PARTITIONS; + } else if (sid_val) { + expression = talloc_asprintf(filter_ctx, "objectSID=%s", ldb_binary_encode(filter_ctx, *sid_val)); + scope = LDB_SCOPE_SUBTREE; + base_dn = NULL; + dsdb_flags |= DSDB_SEARCH_SEARCH_ALL_PARTITIONS; + } else { + /* fallback to searching using the string DN as the base DN */ + expression = "objectClass=*"; + base_dn = dn; + scope = LDB_SCOPE_BASE; + } + + ret = dsdb_module_search(filter_ctx->module, + filter_ctx, + &res, + base_dn, + scope, + no_attrs, + dsdb_flags, + filter_ctx->req, + "%s", expression); + if (scope == LDB_SCOPE_BASE && ret == LDB_ERR_NO_SUCH_OBJECT) { + /* note that this will need to change for multi-domain + support */ + set_parse_tree_false(tree); + return LDB_SUCCESS; + } + + if (ret != LDB_SUCCESS) { + return LDB_SUCCESS; + } + + + if (res->count != 1) { + return LDB_SUCCESS; + } + + /* replace the search expression element with the matching DN */ + if (tree->operation == LDB_OP_EQUALITY) { + tree->u.equality.value.data = + (uint8_t *)talloc_strdup(tree, ldb_dn_get_extended_linearized(tree, res->msgs[0]->dn, 1)); + if (tree->u.equality.value.data == NULL) { + return ldb_oom(ldb_module_get_ctx(filter_ctx->module)); + } + tree->u.equality.value.length = strlen((const char *)tree->u.equality.value.data); + } else if (tree->operation == LDB_OP_EXTENDED) { + tree->u.extended.value.data = + (uint8_t *)talloc_strdup(tree, ldb_dn_get_extended_linearized(tree, res->msgs[0]->dn, 1)); + if (tree->u.extended.value.data == NULL) { + return ldb_oom(ldb_module_get_ctx(filter_ctx->module)); + } + tree->u.extended.value.length = strlen((const char *)tree->u.extended.value.data); + } + talloc_free(res); + + filter_ctx->matched = true; + return LDB_SUCCESS; +} + +/* + fix the parse tree to change any extended DN components to their + canonical form + */ +static int extended_dn_fix_filter(struct ldb_module *module, + struct ldb_request *req, + uint32_t default_dsdb_flags, + struct ldb_parse_tree **down_tree) +{ + struct extended_dn_filter_ctx *filter_ctx; + int ret; + + *down_tree = NULL; + + filter_ctx = talloc_zero(req, struct extended_dn_filter_ctx); + if (filter_ctx == NULL) { + return ldb_module_oom(module); + } + + /* first pass through the existing tree to see if anything + needs to be modified. Filtering DNs on the input side is rare, + so this avoids copying the parse tree in most cases */ + filter_ctx->test_only = true; + filter_ctx->matched = false; + filter_ctx->module = module; + filter_ctx->req = req; + filter_ctx->schema = dsdb_get_schema(ldb_module_get_ctx(module), filter_ctx); + filter_ctx->dsdb_flags= default_dsdb_flags; + + ret = ldb_parse_tree_walk(req->op.search.tree, extended_dn_filter_callback, filter_ctx); + if (ret != LDB_SUCCESS) { + talloc_free(filter_ctx); + return ret; + } + + if (!filter_ctx->matched) { + /* nothing matched, no need for a new parse tree */ + talloc_free(filter_ctx); + return LDB_SUCCESS; + } + + filter_ctx->test_only = false; + filter_ctx->matched = false; + + *down_tree = ldb_parse_tree_copy_shallow(req, req->op.search.tree); + if (*down_tree == NULL) { + return ldb_oom(ldb_module_get_ctx(module)); + } + + ret = ldb_parse_tree_walk(*down_tree, extended_dn_filter_callback, filter_ctx); + if (ret != LDB_SUCCESS) { + talloc_free(filter_ctx); + return ret; + } + + talloc_free(filter_ctx); + return LDB_SUCCESS; +} + +/* + fix DNs and filter expressions to cope with the semantics of + extended DNs + */ +static int extended_dn_in_fix(struct ldb_module *module, struct ldb_request *req, struct ldb_dn *dn) +{ + struct extended_search_context *ac; + struct ldb_request *down_req = NULL; + struct ldb_parse_tree *down_tree = NULL; + int ret; + struct ldb_dn *base_dn = NULL; + enum ldb_scope base_dn_scope = LDB_SCOPE_BASE; + const char *base_dn_filter = NULL; + const char * const *base_dn_attrs = NULL; + char *wellknown_object = NULL; + static const char *no_attr[] = { + NULL + }; + uint32_t dsdb_flags = DSDB_FLAG_AS_SYSTEM | DSDB_SEARCH_SHOW_EXTENDED_DN; + + if (ldb_request_get_control(req, LDB_CONTROL_SHOW_DELETED_OID)) { + dsdb_flags |= DSDB_SEARCH_SHOW_DELETED; + } + if (ldb_request_get_control(req, LDB_CONTROL_SHOW_RECYCLED_OID)) { + dsdb_flags |= DSDB_SEARCH_SHOW_RECYCLED; + } + if (ldb_request_get_control(req, DSDB_CONTROL_DBCHECK)) { + dsdb_flags |= DSDB_SEARCH_SHOW_RECYCLED; + } + + if (req->operation == LDB_SEARCH) { + ret = extended_dn_fix_filter(module, req, dsdb_flags, &down_tree); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + if (!ldb_dn_has_extended(dn)) { + /* Move along there isn't anything to see here */ + if (down_tree == NULL) { + down_req = req; + } else { + ret = ldb_build_search_req_ex(&down_req, + ldb_module_get_ctx(module), req, + req->op.search.base, + req->op.search.scope, + down_tree, + req->op.search.attrs, + req->controls, + req, dsdb_next_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + LDB_REQ_SET_LOCATION(down_req); + } + + return ldb_next_request(module, down_req); + } else { + /* It looks like we need to map the DN */ + const struct ldb_val *sid_val, *guid_val, *wkguid_val; + + if (!ldb_dn_match_allowed(dn, req)) { + return ldb_error(ldb_module_get_ctx(module), + LDB_ERR_INVALID_DN_SYNTAX, "invalid number of DN components"); + } + + sid_val = ldb_dn_get_extended_component(dn, "SID"); + guid_val = ldb_dn_get_extended_component(dn, "GUID"); + wkguid_val = ldb_dn_get_extended_component(dn, "WKGUID"); + + /* + prioritise the GUID - we have had instances of + duplicate SIDs in the database in the + ForeignSecurityPrinciples due to provision errors + */ + if (guid_val) { + dsdb_flags |= DSDB_SEARCH_SEARCH_ALL_PARTITIONS; + base_dn = NULL; + base_dn_filter = talloc_asprintf(req, "(objectGUID=%s)", + ldb_binary_encode(req, *guid_val)); + if (!base_dn_filter) { + return ldb_oom(ldb_module_get_ctx(module)); + } + base_dn_scope = LDB_SCOPE_SUBTREE; + base_dn_attrs = no_attr; + + } else if (sid_val) { + dsdb_flags |= DSDB_SEARCH_SEARCH_ALL_PARTITIONS; + base_dn = NULL; + base_dn_filter = talloc_asprintf(req, "(objectSid=%s)", + ldb_binary_encode(req, *sid_val)); + if (!base_dn_filter) { + return ldb_oom(ldb_module_get_ctx(module)); + } + base_dn_scope = LDB_SCOPE_SUBTREE; + base_dn_attrs = no_attr; + + } else if (wkguid_val) { + char *wkguid_dup; + char *tail_str; + char *p; + + wkguid_dup = talloc_strndup(req, (char *)wkguid_val->data, wkguid_val->length); + + p = strchr(wkguid_dup, ','); + if (!p) { + return ldb_error(ldb_module_get_ctx(module), LDB_ERR_INVALID_DN_SYNTAX, + "Invalid WKGUID format"); + } + + p[0] = '\0'; + p++; + + wellknown_object = talloc_asprintf(req, "B:32:%s:", wkguid_dup); + if (!wellknown_object) { + return ldb_oom(ldb_module_get_ctx(module)); + } + + tail_str = p; + + base_dn = ldb_dn_new(req, ldb_module_get_ctx(module), tail_str); + talloc_free(wkguid_dup); + if (!base_dn) { + return ldb_oom(ldb_module_get_ctx(module)); + } + base_dn_filter = talloc_strdup(req, "(objectClass=*)"); + if (!base_dn_filter) { + return ldb_oom(ldb_module_get_ctx(module)); + } + base_dn_scope = LDB_SCOPE_BASE; + base_dn_attrs = wkattr; + } else { + return ldb_error(ldb_module_get_ctx(module), LDB_ERR_INVALID_DN_SYNTAX, + "Invalid extended DN component"); + } + + ac = talloc_zero(req, struct extended_search_context); + if (ac == NULL) { + return ldb_oom(ldb_module_get_ctx(module)); + } + + ac->module = module; + ac->req = req; + ac->tree = (down_tree != NULL) ? down_tree : req->op.search.tree; + ac->dn = dn; + ac->basedn = NULL; /* Filled in if the search finds the DN by SID/GUID etc */ + ac->wellknown_object = wellknown_object; + + /* If the base DN was an extended DN (perhaps a well known + * GUID) then search for that, so we can proceed with the original operation */ + + ret = ldb_build_search_req(&down_req, + ldb_module_get_ctx(module), ac, + base_dn, + base_dn_scope, + base_dn_filter, + base_dn_attrs, + NULL, + ac, extended_base_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb_module_get_ctx(module)); + } + + ret = dsdb_request_add_controls(down_req, dsdb_flags); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* perform the search */ + return ldb_next_request(module, down_req); + } +} + +static int extended_dn_in_search(struct ldb_module *module, struct ldb_request *req) +{ + return extended_dn_in_fix(module, req, req->op.search.base); +} + +static int extended_dn_in_modify(struct ldb_module *module, struct ldb_request *req) +{ + return extended_dn_in_fix(module, req, req->op.mod.message->dn); +} + +static int extended_dn_in_del(struct ldb_module *module, struct ldb_request *req) +{ + return extended_dn_in_fix(module, req, req->op.del.dn); +} + +static int extended_dn_in_rename(struct ldb_module *module, struct ldb_request *req) +{ + return extended_dn_in_fix(module, req, req->op.rename.olddn); +} + +static const struct ldb_module_ops ldb_extended_dn_in_module_ops = { + .name = "extended_dn_in", + .search = extended_dn_in_search, + .modify = extended_dn_in_modify, + .del = extended_dn_in_del, + .rename = extended_dn_in_rename, +}; + +static const struct ldb_module_ops ldb_extended_dn_in_openldap_module_ops = { + .name = "extended_dn_in_openldap", + .search = extended_dn_in_search, + .modify = extended_dn_in_modify, + .del = extended_dn_in_del, + .rename = extended_dn_in_rename, +}; + +int ldb_extended_dn_in_module_init(const char *version) +{ + int ret; + LDB_MODULE_CHECK_VERSION(version); + ret = ldb_register_module(&ldb_extended_dn_in_openldap_module_ops); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_register_module(&ldb_extended_dn_in_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/extended_dn_out.c b/source4/dsdb/samdb/ldb_modules/extended_dn_out.c new file mode 100644 index 0000000..53cbe34 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/extended_dn_out.c @@ -0,0 +1,648 @@ +/* + 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, and fixes some other aspects of the result (returned case issues) + * values. + * + * Authors: Simo Sorce + * Andrew Bartlett + */ + +#include "includes.h" +#include <ldb.h> +#include <ldb_errors.h> +#include <ldb_module.h> +#include "libcli/security/security.h" +#include "librpc/gen_ndr/ndr_misc.h" +#include "librpc/gen_ndr/ndr_security.h" +#include "librpc/ndr/libndr.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" + +#undef strcasecmp +#undef strncasecmp + +struct extended_dn_out_private { + bool dereference; + bool normalise; + const char **attrs; +}; + +static char **copy_attrs(void *mem_ctx, const char * const * attrs) +{ + char **nattrs; + unsigned int i, num; + + for (num = 0; attrs[num]; num++); + + nattrs = talloc_array(mem_ctx, char *, num + 1); + if (!nattrs) return NULL; + + for(i = 0; i < num; i++) { + nattrs[i] = talloc_strdup(nattrs, attrs[i]); + if (!nattrs[i]) { + talloc_free(nattrs); + return NULL; + } + } + nattrs[i] = NULL; + + return nattrs; +} + +static bool add_attrs(void *mem_ctx, char ***attrs, const char *attr) +{ + char **nattrs; + unsigned int num; + + for (num = 0; (*attrs)[num]; num++); + + nattrs = talloc_realloc(mem_ctx, *attrs, char *, num + 2); + if (!nattrs) return false; + + *attrs = nattrs; + + nattrs[num] = talloc_strdup(nattrs, attr); + if (!nattrs[num]) return false; + + nattrs[num + 1] = NULL; + + return true; +} + +/* Inject the extended DN components, so the DN cn=Adminstrator,cn=users,dc=samba,dc=example,dc=com becomes + <GUID=541203ae-f7d6-47ef-8390-bfcf019f9583>;<SID=S-1-5-21-4177067393-1453636373-93818737-500>;cn=Adminstrator,cn=users,dc=samba,dc=example,dc=com */ + +static int inject_extended_dn_out(struct ldb_reply *ares, + struct ldb_context *ldb, + int type, + bool remove_guid, + bool remove_sid) +{ + int ret; + const DATA_BLOB *guid_blob; + const DATA_BLOB *sid_blob; + + guid_blob = ldb_msg_find_ldb_val(ares->message, "objectGUID"); + sid_blob = ldb_msg_find_ldb_val(ares->message, "objectSid"); + + if (!guid_blob) { + ldb_set_errstring(ldb, "Did not find objectGUID to inject into extended DN"); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = ldb_dn_set_extended_component(ares->message->dn, "GUID", guid_blob); + if (ret != LDB_SUCCESS) { + return ret; + } + if (sid_blob) { + ret = ldb_dn_set_extended_component(ares->message->dn, "SID", sid_blob); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + if (remove_guid) { + ldb_msg_remove_attr(ares->message, "objectGUID"); + } + + if (sid_blob && remove_sid) { + ldb_msg_remove_attr(ares->message, "objectSid"); + } + + return LDB_SUCCESS; +} + +/* search */ +struct extended_search_context { + struct ldb_module *module; + const struct dsdb_schema *schema; + struct ldb_request *req; + bool inject; + bool remove_guid; + bool remove_sid; + int extended_type; +}; + + +/* + fix one-way links to have the right string DN, to cope with + renames of the target +*/ +static int fix_one_way_link(struct extended_search_context *ac, struct ldb_dn *dn, + bool is_deleted_objects, bool *remove_value, + uint32_t linkID) +{ + struct GUID guid; + NTSTATUS status; + int ret; + struct ldb_dn *real_dn; + uint32_t search_flags; + TALLOC_CTX *tmp_ctx = talloc_new(ac); + const char *attrs[] = { NULL }; + struct ldb_result *res; + + (*remove_value) = false; + + status = dsdb_get_extended_dn_guid(dn, &guid, "GUID"); + if (!NT_STATUS_IS_OK(status)) { + /* this is a strange DN that doesn't have a GUID! just + return the current DN string?? */ + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + + search_flags = DSDB_FLAG_NEXT_MODULE | DSDB_SEARCH_SEARCH_ALL_PARTITIONS | DSDB_SEARCH_ONE_ONLY; + + if (linkID == 0) { + /* You must ALWAYS show one-way links regardless of the state of the target */ + search_flags |= (DSDB_SEARCH_SHOW_DELETED | DSDB_SEARCH_SHOW_RECYCLED); + } + + ret = dsdb_module_search(ac->module, tmp_ctx, &res, NULL, LDB_SCOPE_SUBTREE, attrs, + search_flags, ac->req, "objectguid=%s", GUID_string(tmp_ctx, &guid)); + if (ret != LDB_SUCCESS || res->count != 1) { + /* if we can't resolve this GUID, then we don't + display the link. This could be a link to a NC that we don't + have, or it could be a link to a deleted object + */ + (*remove_value) = true; + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + real_dn = res->msgs[0]->dn; + + if (strcmp(ldb_dn_get_linearized(dn), ldb_dn_get_linearized(real_dn)) == 0) { + /* its already correct */ + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + + /* fix the DN by replacing its components with those from the + * real DN + */ + if (!ldb_dn_replace_components(dn, real_dn)) { + talloc_free(tmp_ctx); + return ldb_operr(ldb_module_get_ctx(ac->module)); + } + talloc_free(tmp_ctx); + + return LDB_SUCCESS; +} + + +/* + this is called to post-process the results from the search + */ +static int extended_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct extended_search_context *ac; + int ret; + unsigned int i, j, k; + struct ldb_message *msg; + struct extended_dn_out_private *p; + struct ldb_context *ldb; + bool have_reveal_control=false; + + ac = talloc_get_type(req->context, struct extended_search_context); + p = talloc_get_type(ldb_module_get_private(ac->module), struct extended_dn_out_private); + ldb = ldb_module_get_ctx(ac->module); + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + msg = ares->message; + + switch (ares->type) { + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral(ac->req, ares->referral); + + case LDB_REPLY_DONE: + return ldb_module_done(ac->req, ares->controls, + ares->response, LDB_SUCCESS); + case LDB_REPLY_ENTRY: + break; + } + + if (p && p->normalise) { + ret = dsdb_fix_dn_rdncase(ldb, ares->message->dn); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + } + + if (ac->inject) { + /* for each record returned post-process to add any derived + attributes that have been asked for */ + ret = inject_extended_dn_out(ares, ldb, + ac->extended_type, ac->remove_guid, + ac->remove_sid); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + } + + if ((p && p->normalise) || ac->inject) { + const struct ldb_val *val = ldb_msg_find_ldb_val(ares->message, "distinguishedName"); + if (val) { + ldb_msg_remove_attr(ares->message, "distinguishedName"); + if (ac->inject) { + ret = ldb_msg_add_steal_string(ares->message, "distinguishedName", + ldb_dn_get_extended_linearized(ares->message, ares->message->dn, ac->extended_type)); + } else { + ret = ldb_msg_add_linearized_dn(ares->message, + "distinguishedName", + ares->message->dn); + } + if (ret != LDB_SUCCESS) { + return ldb_oom(ldb); + } + } + } + + have_reveal_control = + dsdb_request_has_control(req, LDB_CONTROL_REVEAL_INTERNALS); + + /* + * Shortcut for repl_meta_data. We asked for the data + * 'as-is', so stop processing here! + */ + if (have_reveal_control && p->normalise == false && ac->inject == true) { + return ldb_module_send_entry(ac->req, msg, ares->controls); + } + + /* Walk the returned elements (but only if we have a schema to + * interpret the list with) */ + for (i = 0; ac->schema && i < msg->num_elements; i++) { + bool make_extended_dn; + const struct dsdb_attribute *attribute; + + attribute = dsdb_attribute_by_lDAPDisplayName(ac->schema, msg->elements[i].name); + if (!attribute) { + continue; + } + + if (p && p->normalise) { + /* If we are also in 'normalise' mode, then + * fix the attribute names to be in the + * correct case */ + msg->elements[i].name = talloc_strdup(msg->elements, attribute->lDAPDisplayName); + if (!msg->elements[i].name) { + ldb_oom(ldb); + return ldb_module_done(ac->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + } + + /* distinguishedName has been dealt with above */ + if (ldb_attr_cmp(msg->elements[i].name, "distinguishedName") == 0) { + continue; + } + + /* Look to see if this attributeSyntax is a DN */ + if (attribute->dn_format == DSDB_INVALID_DN) { + continue; + } + + make_extended_dn = ac->inject; + + /* Always show plain DN in case of Object(OR-Name) syntax */ + if (make_extended_dn) { + make_extended_dn = (strcmp(attribute->syntax->ldap_oid, DSDB_SYNTAX_OR_NAME) != 0); + } + + for (k = 0, j = 0; j < msg->elements[i].num_values; j++) { + const char *dn_str; + struct ldb_dn *dn; + struct dsdb_dn *dsdb_dn = NULL; + struct ldb_val *plain_dn = &msg->elements[i].values[j]; + bool is_deleted_objects = false; + + /* this is a fast method for detecting deleted + linked attributes, working on the unparsed + ldb_val */ + if (dsdb_dn_is_deleted_val(plain_dn) && !have_reveal_control) { + /* it's a deleted linked attribute, + and we don't have the reveal control */ + /* we won't keep this one, so not incrementing k */ + continue; + } + + + dsdb_dn = dsdb_dn_parse_trusted(msg, ldb, plain_dn, attribute->syntax->ldap_oid); + + if (!dsdb_dn) { + ldb_asprintf_errstring(ldb, + "could not parse %.*s in %s on %s as a %s DN", + (int)plain_dn->length, plain_dn->data, + msg->elements[i].name, ldb_dn_get_linearized(msg->dn), + attribute->syntax->ldap_oid); + talloc_free(dsdb_dn); + return ldb_module_done(ac->req, NULL, NULL, LDB_ERR_INVALID_DN_SYNTAX); + } + dn = dsdb_dn->dn; + + /* we need to know if this is a link to the + deleted objects container for fixing one way + links */ + if (dsdb_dn->extra_part.length == 16) { + char *hex_string = data_blob_hex_string_upper(req, &dsdb_dn->extra_part); + if (hex_string && strcmp(hex_string, DS_GUID_DELETED_OBJECTS_CONTAINER) == 0) { + is_deleted_objects = true; + } + talloc_free(hex_string); + } + + if (p->normalise) { + ret = dsdb_fix_dn_rdncase(ldb, dn); + if (ret != LDB_SUCCESS) { + talloc_free(dsdb_dn); + return ldb_module_done(ac->req, NULL, NULL, ret); + } + } + + /* Look for this value in the attribute */ + + /* note that we don't fixup objectCategory as + it should not be possible to move + objectCategory elements in the schema */ + if (attribute->one_way_link && + strcasecmp(attribute->lDAPDisplayName, "objectCategory") != 0) { + bool remove_value; + ret = fix_one_way_link(ac, dn, is_deleted_objects, &remove_value, + attribute->linkID); + if (ret != LDB_SUCCESS) { + talloc_free(dsdb_dn); + return ldb_module_done(ac->req, NULL, NULL, ret); + } + if (remove_value && + !ldb_request_get_control(req, LDB_CONTROL_REVEAL_INTERNALS)) { + /* we show these with REVEAL + to allow dbcheck to find and + cleanup these orphaned links */ + /* we won't keep this one, so not incrementing k */ + continue; + } + } + + if (make_extended_dn) { + if (!ldb_dn_validate(dsdb_dn->dn)) { + ldb_asprintf_errstring(ldb, + "could not parse %.*s in %s on %s as a %s DN", + (int)plain_dn->length, plain_dn->data, + msg->elements[i].name, ldb_dn_get_linearized(msg->dn), + attribute->syntax->ldap_oid); + talloc_free(dsdb_dn); + return ldb_module_done(ac->req, NULL, NULL, LDB_ERR_INVALID_DN_SYNTAX); + } + /* don't let users see the internal extended + GUID components */ + if (!have_reveal_control) { + const char *accept[] = { "GUID", "SID", NULL }; + ldb_dn_extended_filter(dn, accept); + } + dn_str = dsdb_dn_get_extended_linearized(msg->elements[i].values, + dsdb_dn, ac->extended_type); + } else { + dn_str = dsdb_dn_get_linearized(msg->elements[i].values, + dsdb_dn); + } + + if (!dn_str) { + ldb_oom(ldb); + talloc_free(dsdb_dn); + return ldb_module_done(ac->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + msg->elements[i].values[k] = data_blob_string_const(dn_str); + talloc_free(dsdb_dn); + k++; + } + + if (k == 0) { + /* we've deleted all of the values from this + * element - remove the element */ + ldb_msg_remove_element(msg, &msg->elements[i]); + i--; + } else { + msg->elements[i].num_values = k; + } + } + return ldb_module_send_entry(ac->req, msg, ares->controls); +} + +static int extended_callback_ldb(struct ldb_request *req, struct ldb_reply *ares) +{ + return extended_callback(req, ares); +} + +static int extended_dn_out_search(struct ldb_module *module, struct ldb_request *req, + int (*callback)(struct ldb_request *req, struct ldb_reply *ares)) +{ + struct ldb_control *control; + struct ldb_control *storage_format_control; + struct ldb_extended_dn_control *extended_ctrl = NULL; + struct extended_search_context *ac; + struct ldb_request *down_req; + char **new_attrs; + const char * const *const_attrs; + struct ldb_context *ldb = ldb_module_get_ctx(module); + int ret; + + struct extended_dn_out_private *p = talloc_get_type(ldb_module_get_private(module), struct extended_dn_out_private); + + /* The schema manipulation does not apply to special DNs */ + if (ldb_dn_is_special(req->op.search.base)) { + return ldb_next_request(module, req); + } + + /* check if there's an extended dn control */ + control = ldb_request_get_control(req, LDB_CONTROL_EXTENDED_DN_OID); + if (control && control->data) { + extended_ctrl = talloc_get_type(control->data, struct ldb_extended_dn_control); + if (!extended_ctrl) { + return LDB_ERR_PROTOCOL_ERROR; + } + } + + /* Look to see if, as we are in 'store DN+GUID+SID' mode, the + * client is after the storage format (to fill in linked + * attributes) */ + storage_format_control = ldb_request_get_control(req, DSDB_CONTROL_DN_STORAGE_FORMAT_OID); + if (!control && storage_format_control && storage_format_control->data) { + extended_ctrl = talloc_get_type(storage_format_control->data, struct ldb_extended_dn_control); + if (!extended_ctrl) { + ldb_set_errstring(ldb, "extended_dn_out: extended_ctrl was of the wrong data type"); + return LDB_ERR_PROTOCOL_ERROR; + } + } + + ac = talloc_zero(req, struct extended_search_context); + if (ac == NULL) { + return ldb_oom(ldb); + } + + ac->module = module; + ac->schema = dsdb_get_schema(ldb, ac); + ac->req = req; + ac->inject = false; + ac->remove_guid = false; + ac->remove_sid = false; + + const_attrs = req->op.search.attrs; + + /* We only need to do special processing if we were asked for + * the extended DN, or we are 'store DN+GUID+SID' + * (!dereference) mode. (This is the normal mode for LDB on + * tdb). */ + if (control || (storage_format_control && p)) { + ac->inject = true; + if (extended_ctrl) { + ac->extended_type = extended_ctrl->type; + } else { + ac->extended_type = 0; + } + + /* check if attrs only is specified, in that case check whether we need to modify them */ + if (req->op.search.attrs && !is_attr_in_list(req->op.search.attrs, "*")) { + if (! is_attr_in_list(req->op.search.attrs, "objectGUID")) { + ac->remove_guid = true; + } + if (! is_attr_in_list(req->op.search.attrs, "objectSid")) { + ac->remove_sid = true; + } + if (ac->remove_guid || ac->remove_sid) { + new_attrs = copy_attrs(ac, req->op.search.attrs); + if (new_attrs == NULL) { + return ldb_oom(ldb); + } + + if (ac->remove_guid) { + if (!add_attrs(ac, &new_attrs, "objectGUID")) + return ldb_operr(ldb); + } + if (ac->remove_sid) { + if (!add_attrs(ac, &new_attrs, "objectSid")) + return ldb_operr(ldb); + } + const_attrs = (const char * const *)new_attrs; + } + } + } + + ret = ldb_build_search_req_ex(&down_req, + ldb, ac, + req->op.search.base, + req->op.search.scope, + req->op.search.tree, + const_attrs, + req->controls, + ac, callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* mark extended DN and storage format controls as done */ + if (control) { + control->critical = 0; + } + + if (storage_format_control) { + storage_format_control->critical = 0; + } + + /* perform the search */ + return ldb_next_request(module, down_req); +} + +static int extended_dn_out_ldb_search(struct ldb_module *module, struct ldb_request *req) +{ + return extended_dn_out_search(module, req, extended_callback_ldb); +} + +static int extended_dn_out_ldb_init(struct ldb_module *module) +{ + int ret; + + struct extended_dn_out_private *p = talloc(module, struct extended_dn_out_private); + struct dsdb_extended_dn_store_format *dn_format; + + ldb_module_set_private(module, p); + + if (!p) { + return ldb_oom(ldb_module_get_ctx(module)); + } + + dn_format = talloc(p, struct dsdb_extended_dn_store_format); + if (!dn_format) { + talloc_free(p); + return ldb_oom(ldb_module_get_ctx(module)); + } + + dn_format->store_extended_dn_in_ldb = true; + ret = ldb_set_opaque(ldb_module_get_ctx(module), DSDB_EXTENDED_DN_STORE_FORMAT_OPAQUE_NAME, dn_format); + if (ret != LDB_SUCCESS) { + talloc_free(p); + return ret; + } + + p->dereference = false; + p->normalise = false; + + ret = ldb_mod_register_control(module, LDB_CONTROL_EXTENDED_DN_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb_module_get_ctx(module), LDB_DEBUG_ERROR, + "extended_dn_out: Unable to register control with rootdse!\n"); + return ldb_operr(ldb_module_get_ctx(module)); + } + + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_extended_dn_out_ldb_module_ops = { + .name = "extended_dn_out_ldb", + .search = extended_dn_out_ldb_search, + .init_context = extended_dn_out_ldb_init, +}; + +/* + initialise the module + */ +_PUBLIC_ int ldb_extended_dn_out_module_init(const char *version) +{ + int ret; + LDB_MODULE_CHECK_VERSION(version); + ret = ldb_register_module(&ldb_extended_dn_out_ldb_module_ops); + if (ret != LDB_SUCCESS) { + return ret; + } + return LDB_SUCCESS; +} 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..0b0e186 --- /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() garantees 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 becuase 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); +} diff --git a/source4/dsdb/samdb/ldb_modules/group_audit.c b/source4/dsdb/samdb/ldb_modules/group_audit.c new file mode 100644 index 0000000..a6ca25e --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/group_audit.c @@ -0,0 +1,1555 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018 + + 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/>. +*/ + +/* + * Provide an audit log of changes made to group memberships + * + */ + +#include "includes.h" +#include "ldb_module.h" +#include "lib/audit_logging/audit_logging.h" +#include "librpc/gen_ndr/windows_event_ids.h" + +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "dsdb/samdb/ldb_modules/audit_util_proto.h" +#include "libcli/security/dom_sid.h" +#include "auth/common_auth.h" +#include "param/param.h" + +#define AUDIT_JSON_TYPE "groupChange" +#define AUDIT_HR_TAG "Group Change" +#define AUDIT_MAJOR 1 +#define AUDIT_MINOR 1 +#define GROUP_LOG_LVL 5 + +static const char *const group_attrs[] = {"member", "groupType", NULL}; +static const char *const group_type_attr[] = {"groupType", NULL}; +static const char * const primary_group_attr[] = { + "primaryGroupID", + "objectSID", + NULL}; + +struct audit_context { + bool send_events; + struct imessaging_context *msg_ctx; +}; + +struct audit_callback_context { + struct ldb_request *request; + struct ldb_module *module; + struct ldb_message_element *members; + uint32_t primary_group; + void (*log_changes)( + struct audit_callback_context *acc, + const int status); +}; + +/* + * @brief get the transaction id. + * + * Get the id of the transaction that the current request is contained in. + * + * @param req the request. + * + * @return the transaction id GUID, or NULL if it is not there. + */ +static struct GUID *get_transaction_id( + const struct ldb_request *request) +{ + struct ldb_control *control; + struct dsdb_control_transaction_identifier *transaction_id; + + control = ldb_request_get_control( + discard_const(request), + DSDB_CONTROL_TRANSACTION_IDENTIFIER_OID); + if (control == NULL) { + return NULL; + } + transaction_id = talloc_get_type( + control->data, + struct dsdb_control_transaction_identifier); + if (transaction_id == NULL) { + return NULL; + } + return &transaction_id->transaction_guid; +} + +/* + * @brief generate a JSON log entry for a group change. + * + * Generate a JSON object containing details of a users group change. + * + * @param module the ldb module + * @param request the ldb_request + * @param action the change action being performed + * @param user the user name + * @param group the group name + * @param status the ldb status code for the ldb operation. + * + * @return A json object containing the details. + * NULL if an error was detected + */ +static struct json_object audit_group_json(const struct ldb_module *module, + const struct ldb_request *request, + const char *action, + const char *user, + const char *group, + const enum event_id_type event_id, + const int status) +{ + struct ldb_context *ldb = NULL; + const struct dom_sid *sid = NULL; + struct json_object wrapper = json_empty_object; + struct json_object audit = json_empty_object; + const struct tsocket_address *remote = NULL; + const struct GUID *unique_session_token = NULL; + struct GUID *transaction_id = NULL; + int rc = 0; + + ldb = ldb_module_get_ctx(discard_const(module)); + + remote = dsdb_audit_get_remote_address(ldb); + sid = dsdb_audit_get_user_sid(module); + unique_session_token = dsdb_audit_get_unique_session_token(module); + transaction_id = get_transaction_id(request); + + audit = json_new_object(); + if (json_is_invalid(&audit)) { + goto failure; + } + rc = json_add_version(&audit, AUDIT_MAJOR, AUDIT_MINOR); + if (rc != 0) { + goto failure; + } + if (event_id != EVT_ID_NONE) { + rc = json_add_int(&audit, "eventId", event_id); + if (rc != 0) { + goto failure; + } + } + rc = json_add_int(&audit, "statusCode", status); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "status", ldb_strerror(status)); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "action", action); + if (rc != 0) { + goto failure; + } + rc = json_add_address(&audit, "remoteAddress", remote); + if (rc != 0) { + goto failure; + } + rc = json_add_sid(&audit, "userSid", sid); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "group", group); + if (rc != 0) { + goto failure; + } + rc = json_add_guid(&audit, "transactionId", transaction_id); + if (rc != 0) { + goto failure; + } + rc = json_add_guid(&audit, "sessionId", unique_session_token); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "user", user); + if (rc != 0) { + goto failure; + } + + wrapper = json_new_object(); + if (json_is_invalid(&wrapper)) { + goto failure; + } + rc = json_add_timestamp(&wrapper); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&wrapper, "type", AUDIT_JSON_TYPE); + if (rc != 0) { + goto failure; + } + rc = json_add_object(&wrapper, AUDIT_JSON_TYPE, &audit); + if (rc != 0) { + goto failure; + } + + return wrapper; +failure: + /* + * On a failure audit will not have been added to wrapper so it + * needs to free it to avoid a leak. + * + * wrapper is freed to invalidate it as it will have only been + * partially constructed and may be inconsistent. + * + * All the json manipulation routines handle a freed object correctly + */ + json_free(&audit); + json_free(&wrapper); + DBG_ERR("Failed to create group change JSON log message\n"); + return wrapper; +} + +/* + * @brief generate a human readable log entry for a group change. + * + * Generate a human readable log entry containing details of a users group + * change. + * + * @param ctx the talloc context owning the returned log entry + * @param module the ldb module + * @param request the ldb_request + * @param action the change action being performed + * @param user the user name + * @param group the group name + * @param status the ldb status code for the ldb operation. + * + * @return A human readable log line. + */ +static char *audit_group_human_readable( + TALLOC_CTX *mem_ctx, + const struct ldb_module *module, + const struct ldb_request *request, + const char *action, + const char *user, + const char *group, + const int status) +{ + struct ldb_context *ldb = NULL; + const char *remote_host = NULL; + const struct dom_sid *sid = NULL; + const char *user_sid = NULL; + const char *timestamp = NULL; + char *log_entry = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_module_get_ctx(discard_const(module)); + + remote_host = dsdb_audit_get_remote_host(ldb, ctx); + sid = dsdb_audit_get_user_sid(module); + user_sid = dom_sid_string(ctx, sid); + timestamp = audit_get_timestamp(ctx); + + log_entry = talloc_asprintf( + mem_ctx, + "[%s] at [%s] status [%s] " + "Remote host [%s] SID [%s] Group [%s] User [%s]", + action, + timestamp, + ldb_strerror(status), + remote_host, + user_sid, + group, + user); + TALLOC_FREE(ctx); + return log_entry; +} + +/* + * @brief generate an array of parsed_dns, deferring the actual parsing. + * + * Get an array of 'struct parsed_dns' without the parsing. + * The parsed_dns are parsed only when needed to avoid the expense of parsing. + * + * This procedure assumes that the dn's are sorted in GUID order and contains + * no duplicates. This should be valid as the module sits below repl_meta_data + * which ensures this. + * + * @param mem_ctx The memory context that will own the generated array + * @param el The message element used to generate the array. + * + * @return an array of struct parsed_dns, or NULL in the event of an error + */ +static struct parsed_dn *get_parsed_dns( + TALLOC_CTX *mem_ctx, + struct ldb_message_element *el) +{ + int ret; + struct parsed_dn *pdn = NULL; + + if (el == NULL || el->num_values == 0) { + return NULL; + } + + ret = get_parsed_dns_trusted(mem_ctx, el, &pdn); + if (ret == LDB_ERR_OPERATIONS_ERROR) { + DBG_ERR("Out of memory\n"); + return NULL; + } + return pdn; + +} + +enum dn_compare_result { + LESS_THAN, + BINARY_EQUAL, + EQUAL, + GREATER_THAN +}; +/* + * @brief compare parsed_dn, using GUID ordering + * + * Compare two parsed_dn structures, using GUID ordering. + * To avoid the overhead of parsing the DN's this function does a binary + * compare first. The DN's tre only parsed if they are not equal at a binary + * level. + * + * @param ctx talloc context that will own the parsed dsdb_dn + * @param ldb ldb_context + * @param dn1 The first dn + * @param dn2 The second dn + * + * @return BINARY_EQUAL values are equal at a binary level + * EQUAL DN's are equal but the meta data is different + * LESS_THAN dn1's GUID is less than dn2's GUID + * GREATER_THAN dn1's GUID is greater than dn2's GUID + * + */ +static enum dn_compare_result dn_compare( + TALLOC_CTX *mem_ctx, + struct ldb_context *ldb, + struct parsed_dn *dn1, + struct parsed_dn *dn2) { + + int res = 0; + + /* + * Do a binary compare first to avoid unnecessary parsing + */ + if (data_blob_cmp(dn1->v, dn2->v) == 0) { + /* + * Values are equal at a binary level so no need + * for further processing + */ + return BINARY_EQUAL; + } + /* + * Values not equal at the binary level, so lets + * do a GUID ordering compare. To do this we will need to ensure + * that the dn's have been parsed. + */ + if (dn1->dsdb_dn == NULL) { + really_parse_trusted_dn( + mem_ctx, + ldb, + dn1, + LDB_SYNTAX_DN); + } + if (dn2->dsdb_dn == NULL) { + really_parse_trusted_dn( + mem_ctx, + ldb, + dn2, + LDB_SYNTAX_DN); + } + + res = ndr_guid_compare(&dn1->guid, &dn2->guid); + if (res < 0) { + return LESS_THAN; + } else if (res == 0) { + return EQUAL; + } else { + return GREATER_THAN; + } +} + +/* + * @brief Get the DN of a users primary group as a printable string. + * + * Get the DN of a users primary group as a printable string. + * + * @param mem_ctx Talloc context the the returned string will be allocated on. + * @param module The ldb module + * @param account_sid The SID for the uses account. + * @param primary_group_rid The RID for the users primary group. + * + * @return a formatted DN, or null if there is an error. + */ +static const char *get_primary_group_dn( + TALLOC_CTX *mem_ctx, + struct ldb_module *module, + struct dom_sid *account_sid, + uint32_t primary_group_rid) +{ + NTSTATUS status; + + struct ldb_context *ldb = NULL; + struct dom_sid *domain_sid = NULL; + struct dom_sid *primary_group_sid = NULL; + char *sid = NULL; + struct ldb_dn *dn = NULL; + struct ldb_message *msg = NULL; + int rc; + + ldb = ldb_module_get_ctx(module); + + status = dom_sid_split_rid(mem_ctx, account_sid, &domain_sid, NULL); + if (!NT_STATUS_IS_OK(status)) { + return NULL; + } + + primary_group_sid = dom_sid_add_rid( + mem_ctx, + domain_sid, + primary_group_rid); + if (!primary_group_sid) { + return NULL; + } + + sid = dom_sid_string(mem_ctx, primary_group_sid); + if (sid == NULL) { + return NULL; + } + + dn = ldb_dn_new_fmt(mem_ctx, ldb, "<SID=%s>", sid); + if(dn == NULL) { + return sid; + } + rc = dsdb_search_one( + ldb, + mem_ctx, + &msg, + dn, + LDB_SCOPE_BASE, + NULL, + 0, + NULL); + if (rc != LDB_SUCCESS) { + return NULL; + } + + return ldb_dn_get_linearized(msg->dn); +} + +/* + * @brief Log details of a change to a users primary group. + * + * Log details of a change to a users primary group. + * There is no windows event id associated with a Primary Group change. + * However for a new user we generate an added to group event. + * + * @param module The ldb module. + * @param request The request being logged. + * @param action Description of the action being performed. + * @param group The linearized for of the group DN + * @param status the LDB status code for the processing of the request. + * + */ +static void log_primary_group_change( + struct ldb_module *module, + const struct ldb_request *request, + const char *action, + const char *group, + const int status) +{ + const char *user = NULL; + + struct audit_context *ac = + talloc_get_type( + ldb_module_get_private(module), + struct audit_context); + + TALLOC_CTX *ctx = talloc_new(NULL); + + user = dsdb_audit_get_primary_dn(request); + if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL)) { + char *message = NULL; + message = audit_group_human_readable( + ctx, + module, + request, + action, + user, + group, + status); + audit_log_human_text( + AUDIT_HR_TAG, + message, + DBGC_DSDB_GROUP_AUDIT, + GROUP_LOG_LVL); + TALLOC_FREE(message); + } + + if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT_JSON, GROUP_LOG_LVL) || + (ac->msg_ctx && ac->send_events)) { + + struct json_object json; + json = audit_group_json( + module, request, action, user, group, EVT_ID_NONE, status); + audit_log_json( + &json, + DBGC_DSDB_GROUP_AUDIT_JSON, + GROUP_LOG_LVL); + if (ac->send_events) { + audit_message_send( + ac->msg_ctx, + DSDB_GROUP_EVENT_NAME, + MSG_GROUP_LOG, + &json); + } + json_free(&json); + if (request->operation == LDB_ADD) { + /* + * Have just added a user, generate a groupChange + * message indicating the user has been added to thier + * new PrimaryGroup. + */ + } + } + TALLOC_FREE(ctx); +} + +/* + * @brief Log details of a single change to a users group membership. + * + * Log details of a change to a users group membership, except for changes + * to their primary group which is handled by log_primary_group_change. + * + * @param module The ldb module. + * @param request The request being logged. + * @param action Description of the action being performed. + * @param user The linearized form of the users DN + * @param status the LDB status code for the processing of the request. + * + */ +static void log_membership_change(struct ldb_module *module, + const struct ldb_request *request, + const char *action, + const char *user, + const enum event_id_type event_id, + const int status) +{ + const char *group = NULL; + struct audit_context *ac = + talloc_get_type( + ldb_module_get_private(module), + struct audit_context); + + TALLOC_CTX *ctx = talloc_new(NULL); + group = dsdb_audit_get_primary_dn(request); + if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL)) { + char *message = NULL; + message = audit_group_human_readable( + ctx, + module, + request, + action, + user, + group, + status); + audit_log_human_text( + AUDIT_HR_TAG, + message, + DBGC_DSDB_GROUP_AUDIT, + GROUP_LOG_LVL); + TALLOC_FREE(message); + } + + if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT_JSON, GROUP_LOG_LVL) || + (ac->msg_ctx && ac->send_events)) { + struct json_object json; + json = audit_group_json( + module, request, action, user, group, event_id, status); + audit_log_json( + &json, + DBGC_DSDB_GROUP_AUDIT_JSON, + GROUP_LOG_LVL); + if (ac->send_events) { + audit_message_send( + ac->msg_ctx, + DSDB_GROUP_EVENT_NAME, + MSG_GROUP_LOG, + &json); + } + json_free(&json); + } + TALLOC_FREE(ctx); +} + +/* + * @brief Get the windows event type id for removing a user from a group type. + * + * @param group_type the type of the current group, see libds/common/flags.h + * + * @return the Windows Event Id + * + */ +static enum event_id_type get_remove_member_event(uint32_t group_type) +{ + + switch (group_type) { + case GTYPE_SECURITY_BUILTIN_LOCAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_LOCAL_SEC_GROUP; + case GTYPE_SECURITY_GLOBAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_GLOBAL_SEC_GROUP; + case GTYPE_SECURITY_DOMAIN_LOCAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_LOCAL_SEC_GROUP; + case GTYPE_SECURITY_UNIVERSAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_UNIVERSAL_SEC_GROUP; + case GTYPE_DISTRIBUTION_GLOBAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_GLOBAL_GROUP; + case GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_LOCAL_GROUP; + case GTYPE_DISTRIBUTION_UNIVERSAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_UNIVERSAL_GROUP; + default: + return EVT_ID_NONE; + } +} + +/* + * @brief Get the windows event type id for adding a user to a group type. + * + * @param group_type the type of the current group, see libds/common/flags.h + * + * @return the Windows Event Id + * + */ +static enum event_id_type get_add_member_event(uint32_t group_type) +{ + + switch (group_type) { + case GTYPE_SECURITY_BUILTIN_LOCAL_GROUP: + return EVT_ID_USER_ADDED_TO_LOCAL_SEC_GROUP; + case GTYPE_SECURITY_GLOBAL_GROUP: + return EVT_ID_USER_ADDED_TO_GLOBAL_SEC_GROUP; + case GTYPE_SECURITY_DOMAIN_LOCAL_GROUP: + return EVT_ID_USER_ADDED_TO_LOCAL_SEC_GROUP; + case GTYPE_SECURITY_UNIVERSAL_GROUP: + return EVT_ID_USER_ADDED_TO_UNIVERSAL_SEC_GROUP; + case GTYPE_DISTRIBUTION_GLOBAL_GROUP: + return EVT_ID_USER_ADDED_TO_GLOBAL_GROUP; + case GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP: + return EVT_ID_USER_ADDED_TO_LOCAL_GROUP; + case GTYPE_DISTRIBUTION_UNIVERSAL_GROUP: + return EVT_ID_USER_ADDED_TO_UNIVERSAL_GROUP; + default: + return EVT_ID_NONE; + } +} + +/* + * @brief Log all the changes to a users group membership. + * + * Log details of a change to a users group memberships, except for changes + * to their primary group which is handled by log_primary_group_change. + * + * @param module The ldb module. + * @param request The request being logged. + * @param action Description of the action being performed. + * @param user The linearized form of the users DN + * @param status the LDB status code for the processing of the request. + * + */ +static void log_membership_changes(struct ldb_module *module, + const struct ldb_request *request, + struct ldb_message_element *el, + struct ldb_message_element *old_el, + uint32_t group_type, + int status) +{ + unsigned int i, old_i, new_i; + unsigned int old_num_values; + unsigned int max_num_values; + unsigned int new_num_values; + struct parsed_dn *old_val = NULL; + struct parsed_dn *new_val = NULL; + struct parsed_dn *new_values = NULL; + struct parsed_dn *old_values = NULL; + struct ldb_context *ldb = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + old_num_values = old_el ? old_el->num_values : 0; + new_num_values = el ? el->num_values : 0; + max_num_values = old_num_values + new_num_values; + + if (max_num_values == 0) { + /* + * There is nothing to do! + */ + TALLOC_FREE(ctx); + return; + } + + old_values = get_parsed_dns(ctx, old_el); + new_values = get_parsed_dns(ctx, el); + ldb = ldb_module_get_ctx(module); + + old_i = 0; + new_i = 0; + for (i = 0; i < max_num_values; i++) { + enum dn_compare_result cmp; + if (old_i < old_num_values && new_i < new_num_values) { + /* + * Both list have values, so compare the values + */ + old_val = &old_values[old_i]; + new_val = &new_values[new_i]; + cmp = dn_compare(ctx, ldb, old_val, new_val); + } else if (old_i < old_num_values) { + /* + * the new list is empty, read the old list + */ + old_val = &old_values[old_i]; + new_val = NULL; + cmp = LESS_THAN; + } else if (new_i < new_num_values) { + /* + * the old list is empty, read new list + */ + old_val = NULL; + new_val = &new_values[new_i]; + cmp = GREATER_THAN; + } else { + break; + } + + if (cmp == LESS_THAN) { + /* + * Have an entry in the original record that is not in + * the new record. So it's been deleted + */ + const char *user = NULL; + enum event_id_type event_id; + if (old_val->dsdb_dn == NULL) { + really_parse_trusted_dn( + ctx, + ldb, + old_val, + LDB_SYNTAX_DN); + } + user = ldb_dn_get_linearized(old_val->dsdb_dn->dn); + event_id = get_remove_member_event(group_type); + log_membership_change( + module, request, "Removed", user, event_id, status); + old_i++; + } else if (cmp == BINARY_EQUAL) { + /* + * DN's unchanged at binary level so nothing to do. + */ + old_i++; + new_i++; + } else if (cmp == EQUAL) { + /* + * DN is unchanged now need to check the flags to + * determine if a record has been deleted or undeleted + */ + uint32_t old_flags; + uint32_t new_flags; + if (old_val->dsdb_dn == NULL) { + really_parse_trusted_dn( + ctx, + ldb, + old_val, + LDB_SYNTAX_DN); + } + if (new_val->dsdb_dn == NULL) { + really_parse_trusted_dn( + ctx, + ldb, + new_val, + LDB_SYNTAX_DN); + } + + dsdb_get_extended_dn_uint32( + old_val->dsdb_dn->dn, + &old_flags, + "RMD_FLAGS"); + dsdb_get_extended_dn_uint32( + new_val->dsdb_dn->dn, + &new_flags, + "RMD_FLAGS"); + if (new_flags == old_flags) { + /* + * No changes to the Repl meta data so can + * no need to log the change + */ + old_i++; + new_i++; + continue; + } + if (new_flags & DSDB_RMD_FLAG_DELETED) { + /* + * DN has been deleted. + */ + const char *user = NULL; + enum event_id_type event_id; + user = ldb_dn_get_linearized( + old_val->dsdb_dn->dn); + event_id = get_remove_member_event(group_type); + log_membership_change(module, + request, + "Removed", + user, + event_id, + status); + } else { + /* + * DN has been re-added + */ + const char *user = NULL; + enum event_id_type event_id; + user = ldb_dn_get_linearized( + new_val->dsdb_dn->dn); + event_id = get_add_member_event(group_type); + log_membership_change(module, + request, + "Added", + user, + event_id, + status); + } + old_i++; + new_i++; + } else { + /* + * Member in the updated record that's not in the + * original, so it must have been added. + */ + const char *user = NULL; + enum event_id_type event_id; + if ( new_val->dsdb_dn == NULL) { + really_parse_trusted_dn( + ctx, + ldb, + new_val, + LDB_SYNTAX_DN); + } + user = ldb_dn_get_linearized(new_val->dsdb_dn->dn); + event_id = get_add_member_event(group_type); + log_membership_change( + module, request, "Added", user, event_id, status); + new_i++; + } + } + + TALLOC_FREE(ctx); +} + +/* + * @brief log a group change message for a newly added user. + * + * When a user is added we need to generate a GroupChange Add message to + * log that the user has been added to their PrimaryGroup + */ +static void log_new_user_added_to_primary_group( + TALLOC_CTX *ctx, + struct audit_callback_context *acc, + const char *group, + const int status) +{ + uint32_t group_type; + enum event_id_type event_id = EVT_ID_NONE; + struct ldb_result *res = NULL; + struct ldb_dn *group_dn = NULL; + struct ldb_context *ldb = NULL; + int ret; + + ldb = ldb_module_get_ctx(acc->module); + group_dn = ldb_dn_new(ctx, ldb, group); + ret = dsdb_module_search_dn(acc->module, + ctx, + &res, + group_dn, + group_type_attr, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + NULL); + if (ret == LDB_SUCCESS) { + const char *user = NULL; + group_type = + ldb_msg_find_attr_as_uint(res->msgs[0], "groupType", 0); + event_id = get_add_member_event(group_type); + user = dsdb_audit_get_primary_dn(acc->request); + log_membership_change( + acc->module, acc->request, "Added", user, event_id, status); + } +} + +/* + * @brief Log the details of a primary group change. + * + * Retrieve the users primary groupo after the operation has completed + * and call log_primary_group_change to log the actual changes. + * + * @param acc details of the primary group before the operation. + * @param status The status code returned by the operation. + * + * @return an LDB status code. + */ +static void log_user_primary_group_change( + struct audit_callback_context *acc, + const int status) +{ + TALLOC_CTX *ctx = talloc_new(NULL); + uint32_t new_rid = UINT32_MAX; + struct dom_sid *account_sid = NULL; + int ret; + const struct ldb_message *msg = dsdb_audit_get_message(acc->request); + + if (status == LDB_SUCCESS && msg != NULL) { + struct ldb_result *res = NULL; + ret = dsdb_module_search_dn( + acc->module, + ctx, + &res, + msg->dn, + primary_group_attr, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + NULL); + if (ret == LDB_SUCCESS) { + new_rid = ldb_msg_find_attr_as_uint( + msg, + "primaryGroupID", + ~0); + account_sid = samdb_result_dom_sid( + ctx, + res->msgs[0], + "objectSid"); + } + } + /* + * If we don't have a new value then the user has been deleted + * which we currently do not log. + * Otherwise only log if the primary group has actually changed. + */ + if (account_sid != NULL && + new_rid != UINT32_MAX && + acc->primary_group != new_rid) { + const char* group = get_primary_group_dn( + ctx, + acc->module, + account_sid, + new_rid); + log_primary_group_change( + acc->module, + acc->request, + "PrimaryGroup", + group, + status); + /* + * Are we adding a new user with the primaryGroupID + * set. If so and we're generating JSON audit logs, will need to + * generate an "Add" message with the appropriate windows + * event id. + */ + if (acc->request->operation == LDB_ADD) { + log_new_user_added_to_primary_group( + ctx, acc, group, status); + } + } + TALLOC_FREE(ctx); +} + +/* + * @brief log the changes to users group membership. + * + * Retrieve the users group memberships after the operation has completed + * and call log_membership_changes to log the actual changes. + * + * @param acc details of the group memberships before the operation. + * @param status The status code returned by the operation. + * + */ +static void log_group_membership_changes( + struct audit_callback_context *acc, + const int status) +{ + TALLOC_CTX *ctx = talloc_new(NULL); + struct ldb_message_element *new_val = NULL; + int ret; + uint32_t group_type = 0; + const struct ldb_message *msg = dsdb_audit_get_message(acc->request); + if (status == LDB_SUCCESS && msg != NULL) { + struct ldb_result *res = NULL; + ret = dsdb_module_search_dn( + acc->module, + ctx, + &res, + msg->dn, + group_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + NULL); + if (ret == LDB_SUCCESS) { + new_val = ldb_msg_find_element(res->msgs[0], "member"); + group_type = ldb_msg_find_attr_as_uint( + res->msgs[0], "groupType", 0); + log_membership_changes(acc->module, + acc->request, + new_val, + acc->members, + group_type, + status); + TALLOC_FREE(ctx); + return; + } + } + /* + * If we get here either + * one of the lower level modules failed and the group record did + * not get updated + * or + * the updated group record could not be read. + * + * In both cases it does not make sense to log individual membership + * changes so we log a group membership change "Failure" message. + * + */ + log_membership_change(acc->module, + acc->request, + "Failure", + "", + EVT_ID_NONE, + status); + TALLOC_FREE(ctx); +} + +/* + * @brief call back function to log changes to the group memberships. + * + * Call back function to log changes to the uses broup memberships. + * + * @param req the ldb request. + * @param ares the ldb result + * + * @return am LDB status code. + */ +static int group_audit_callback( + struct ldb_request *req, + struct ldb_reply *ares) +{ + struct audit_callback_context *ac = NULL; + + ac = talloc_get_type( + req->context, + struct audit_callback_context); + + if (!ares) { + return ldb_module_done( + ac->request, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + /* pass on to the callback */ + switch (ares->type) { + case LDB_REPLY_ENTRY: + return ldb_module_send_entry( + ac->request, + ares->message, + ares->controls); + + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral( + ac->request, + ares->referral); + + case LDB_REPLY_DONE: + /* + * Log on DONE now we have a result code + */ + ac->log_changes(ac, ares->error); + return ldb_module_done( + ac->request, + ares->controls, + ares->response, + ares->error); + break; + + default: + /* Can't happen */ + return LDB_ERR_OPERATIONS_ERROR; + } +} + +/* + * @brief Does this request change the primary group. + * + * Does the request change the primary group, i.e. does it contain the + * primaryGroupID attribute. + * + * @param req the request to examine. + * + * @return True if the request modifies the primary group. + */ +static bool has_primary_group_id(struct ldb_request *req) +{ + struct ldb_message_element *el = NULL; + const struct ldb_message *msg = NULL; + + msg = dsdb_audit_get_message(req); + el = ldb_msg_find_element(msg, "primaryGroupID"); + + return (el != NULL); +} + +/* + * @brief Does this request change group membership. + * + * Does the request change the ses group memberships, i.e. does it contain the + * member attribute. + * + * @param req the request to examine. + * + * @return True if the request modifies the users group memberships. + */ +static bool has_group_membership_changes(struct ldb_request *req) +{ + struct ldb_message_element *el = NULL; + const struct ldb_message *msg = NULL; + + msg = dsdb_audit_get_message(req); + el = ldb_msg_find_element(msg, "member"); + + return (el != NULL); +} + + + +/* + * @brief Install the callback function to log an add request. + * + * Install the callback function to log an add request changing the users + * group memberships. As we want to log the returned status code, we need to + * register a callback function that will be called once the operation has + * completed. + * + * This function reads the current user record so that we can log the before + * and after state. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int set_group_membership_add_callback( + struct ldb_module *module, + struct ldb_request *req) +{ + struct audit_callback_context *context = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + int ret; + /* + * Adding group memberships so will need to log the changes. + */ + ldb = ldb_module_get_ctx(module); + context = talloc_zero(req, struct audit_callback_context); + + if (context == NULL) { + return ldb_oom(ldb); + } + context->request = req; + context->module = module; + context->log_changes = log_group_membership_changes; + /* + * We want to log the return code status, so we need to register + * a callback function to get the actual result. + * We need to take a new copy so that we don't alter the callers copy + */ + ret = ldb_build_add_req( + &new_req, + ldb, + req, + req->op.add.message, + req->controls, + context, + group_audit_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(module, new_req); +} + + +/* + * @brief Install the callback function to log a modify request. + * + * Install the callback function to log a modify request changing the primary + * group . As we want to log the returned status code, we need to register a + * callback function that will be called once the operation has completed. + * + * This function reads the current user record so that we can log the before + * and after state. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int set_primary_group_modify_callback( + struct ldb_module *module, + struct ldb_request *req) +{ + struct audit_callback_context *context = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + const struct ldb_message *msg = NULL; + struct ldb_result *res = NULL; + int ret; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_module_get_ctx(module); + + context = talloc_zero(req, struct audit_callback_context); + if (context == NULL) { + ret = ldb_oom(ldb); + goto exit; + } + context->request = req; + context->module = module; + context->log_changes = log_user_primary_group_change; + + msg = dsdb_audit_get_message(req); + ret = dsdb_module_search_dn( + module, + ctx, + &res, + msg->dn, + primary_group_attr, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + NULL); + if (ret == LDB_SUCCESS) { + uint32_t pg; + pg = ldb_msg_find_attr_as_uint( + res->msgs[0], + "primaryGroupID", + ~0); + context->primary_group = pg; + } + /* + * We want to log the return code status, so we need to register + * a callback function to get the actual result. + * We need to take a new copy so that we don't alter the callers copy + */ + ret = ldb_build_mod_req( + &new_req, + ldb, + req, + req->op.add.message, + req->controls, + context, + group_audit_callback, + req); + if (ret != LDB_SUCCESS) { + goto exit; + } + ret = ldb_next_request(module, new_req); +exit: + TALLOC_FREE(ctx); + return ret; +} + +/* + * @brief Install the callback function to log an add request. + * + * Install the callback function to log an add request changing the primary + * group . As we want to log the returned status code, we need to register a + * callback function that will be called once the operation has completed. + * + * This function reads the current user record so that we can log the before + * and after state. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int set_primary_group_add_callback( + struct ldb_module *module, + struct ldb_request *req) +{ + struct audit_callback_context *context = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + int ret; + /* + * Adding a user with a primary group. + */ + ldb = ldb_module_get_ctx(module); + context = talloc_zero(req, struct audit_callback_context); + + if (context == NULL) { + return ldb_oom(ldb); + } + context->request = req; + context->module = module; + context->log_changes = log_user_primary_group_change; + /* + * We want to log the return code status, so we need to register + * a callback function to get the actual result. + * We need to take a new copy so that we don't alter the callers copy + */ + ret = ldb_build_add_req( + &new_req, + ldb, + req, + req->op.add.message, + req->controls, + context, + group_audit_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(module, new_req); +} + +/* + * @brief Module handler for add operations. + * + * Inspect the current add request, and if needed log any group membership + * changes. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int group_add( + struct ldb_module *module, + struct ldb_request *req) +{ + + struct audit_context *ac = + talloc_get_type( + ldb_module_get_private(module), + struct audit_context); + /* + * Currently we don't log replicated group changes + */ + if (ldb_request_get_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID)) { + return ldb_next_request(module, req); + } + + if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL) || + CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT_JSON, GROUP_LOG_LVL) || + (ac->msg_ctx && ac->send_events)) { + /* + * Avoid the overheads of logging unless it has been + * enabled + */ + if (has_group_membership_changes(req)) { + return set_group_membership_add_callback(module, req); + } + if (has_primary_group_id(req)) { + return set_primary_group_add_callback(module, req); + } + } + return ldb_next_request(module, req); +} + +/* + * @brief Module handler for delete operations. + * + * Currently there is no logging for delete operations. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int group_delete( + struct ldb_module *module, + struct ldb_request *req) +{ + return ldb_next_request(module, req); +} + +/* + * @brief Install the callback function to log a modify request. + * + * Install the callback function to log a modify request. As we want to log the + * returned status code, we need to register a callback function that will be + * called once the operation has completed. + * + * This function reads the current user record so that we can log the before + * and after state. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int set_group_modify_callback( + struct ldb_module *module, + struct ldb_request *req) +{ + struct audit_callback_context *context = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + struct ldb_result *res = NULL; + int ret; + + ldb = ldb_module_get_ctx(module); + context = talloc_zero(req, struct audit_callback_context); + + if (context == NULL) { + return ldb_oom(ldb); + } + context->request = req; + context->module = module; + context->log_changes = log_group_membership_changes; + + /* + * About to change the group memberships need to read + * the current state from the database. + */ + ret = dsdb_module_search_dn( + module, + context, + &res, + req->op.add.message->dn, + group_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + NULL); + if (ret == LDB_SUCCESS) { + context->members = ldb_msg_find_element(res->msgs[0], "member"); + } + + ret = ldb_build_mod_req( + &new_req, + ldb, + req, + req->op.mod.message, + req->controls, + context, + group_audit_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(module, new_req); +} + +/* + * @brief Module handler for modify operations. + * + * Inspect the current modify request, and if needed log any group membership + * changes. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int group_modify( + struct ldb_module *module, + struct ldb_request *req) +{ + + struct audit_context *ac = + talloc_get_type( + ldb_module_get_private(module), + struct audit_context); + /* + * Currently we don't log replicated group changes + */ + if (ldb_request_get_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID)) { + return ldb_next_request(module, req); + } + + if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL) || + CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT_JSON, GROUP_LOG_LVL) || + (ac->msg_ctx && ac->send_events)) { + /* + * Avoid the overheads of logging unless it has been + * enabled + */ + if (has_group_membership_changes(req)) { + return set_group_modify_callback(module, req); + } + if (has_primary_group_id(req)) { + return set_primary_group_modify_callback(module, req); + } + } + return ldb_next_request(module, req); +} + +/* + * @brief ldb module initialisation + * + * Initialise the module, loading the private data etc. + * + * @param module The ldb module to initialise. + * + * @return An LDB status code. + */ +static int group_init(struct ldb_module *module) +{ + + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct audit_context *context = NULL; + struct loadparm_context *lp_ctx + = talloc_get_type_abort( + ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + struct tevent_context *ev = ldb_get_event_context(ldb); + + context = talloc_zero(module, struct audit_context); + if (context == NULL) { + return ldb_module_oom(module); + } + + if (lp_ctx && lpcfg_dsdb_group_change_notification(lp_ctx)) { + context->send_events = true; + context->msg_ctx = imessaging_client_init(context, + lp_ctx, + ev); + } + + ldb_module_set_private(module, context); + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_group_audit_log_module_ops = { + .name = "group_audit_log", + .add = group_add, + .modify = group_modify, + .del = group_delete, + .init_context = group_init, +}; + +int ldb_group_audit_log_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_group_audit_log_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/instancetype.c b/source4/dsdb/samdb/ldb_modules/instancetype.c new file mode 100644 index 0000000..9a3fd11 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/instancetype.c @@ -0,0 +1,173 @@ +/* + ldb database library + + Copyright (C) Simo Sorce 2004-2008 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2005 + Copyright (C) Andrew Tridgell 2005 + Copyright (C) Stefan Metzmacher <metze@samba.org> 2007 + + 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 instancetype module + * + * Description: add an instanceType onto every new record + * + * Author: Andrew Bartlett + */ + +#include "includes.h" +#include "ldb.h" +#include "ldb_module.h" +#include "librpc/gen_ndr/ndr_misc.h" +#include "dsdb/samdb/samdb.h" +#include "../libds/common/flags.h" +#include "dsdb/samdb/ldb_modules/util.h" + +/* add_record: add instancetype attribute */ +static int instancetype_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_request *down_req; + struct ldb_message *msg; + struct ldb_message_element *el; + uint32_t instanceType; + int ret; + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.add.message->dn)) { + return ldb_next_request(module, req); + } + + ldb_debug(ldb, LDB_DEBUG_TRACE, "instancetype_add\n"); + + el = ldb_msg_find_element(req->op.add.message, "instanceType"); + if (el != NULL) { + if (el->num_values != 1) { + ldb_set_errstring(ldb, "instancetype: the 'instanceType' attribute is single-valued!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + instanceType = ldb_msg_find_attr_as_uint(req->op.add.message, + "instanceType", 0); + if (!(instanceType & INSTANCE_TYPE_IS_NC_HEAD)) { + /* + * If we have no NC add operation (no TYPE_IS_NC_HEAD) + * then "instanceType" can only be "0" or "TYPE_WRITE". + */ + if ((instanceType != 0) && + ((instanceType & INSTANCE_TYPE_WRITE) == 0)) { + ldb_set_errstring(ldb, "instancetype: if TYPE_IS_NC_HEAD wasn't set, then only TYPE_WRITE or 0 are allowed!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } else { + /* + * If we have a NC add operation then we need also the + * "TYPE_WRITE" flag in order to succeed, + * unless this NC is not instantiated + */ + if (ldb_request_get_control(req, DSDB_CONTROL_PARTIAL_REPLICA)) { + if (!(instanceType & INSTANCE_TYPE_UNINSTANT)) { + ldb_set_errstring(ldb, "instancetype: if TYPE_IS_NC_HEAD " + "was set, and we are creating a new NC " + "over DsAddEntry then also TYPE_UNINSTANT is requested!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } else { + if (!(instanceType & INSTANCE_TYPE_WRITE)) { + ldb_set_errstring(ldb, "instancetype: if TYPE_IS_NC_HEAD " + "was set, then also TYPE_WRITE is requested!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + + /* + * TODO: Confirm we are naming master or start + * a remote call to the naming master to + * create the crossRef object + */ + } + + /* we did only tests, so proceed with the original request */ + return ldb_next_request(module, req); + } + + /* we have to copy the message as the caller might have it as a const */ + msg = ldb_msg_copy_shallow(req, req->op.add.message); + if (msg == NULL) { + return ldb_oom(ldb); + } + + /* + * TODO: calculate correct instance type + */ + instanceType = INSTANCE_TYPE_WRITE; + + ret = samdb_msg_add_uint(ldb, msg, msg, "instanceType", instanceType); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_build_add_req(&down_req, ldb, req, + msg, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* go on with the call chain */ + return ldb_next_request(module, down_req); +} + +/* deny instancetype modification */ +static int instancetype_mod(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_message_element *el; + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.mod.message->dn)) { + return ldb_next_request(module, req); + } + + ldb_debug(ldb, LDB_DEBUG_TRACE, "instancetype_mod\n"); + + el = ldb_msg_find_element(req->op.mod.message, "instanceType"); + if (el != NULL) { + /* Except to allow dbcheck to fix things, this must never be modified */ + if (!ldb_request_get_control(req, DSDB_CONTROL_DBCHECK)) { + ldb_set_errstring(ldb, "instancetype: the 'instanceType' attribute can never be changed!"); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } + return ldb_next_request(module, req); +} + +static const struct ldb_module_ops ldb_instancetype_module_ops = { + .name = "instancetype", + .add = instancetype_add, + .modify = instancetype_mod +}; + +int ldb_instancetype_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_instancetype_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/lazy_commit.c b/source4/dsdb/samdb/ldb_modules/lazy_commit.c new file mode 100644 index 0000000..24fc6dd --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/lazy_commit.c @@ -0,0 +1,128 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 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 lazy_commit module + * + * Description: module to pretend to support the 'lazy commit' control + * + * Author: Andrew Bartlett + */ + +#include "includes.h" +#include "ldb_module.h" +#include "dsdb/samdb/ldb_modules/util.h" + +static int unlazy_op(struct ldb_module *module, struct ldb_request *req) +{ + int ret; + struct ldb_request *new_req; + struct ldb_control *control = ldb_request_get_control(req, LDB_CONTROL_SERVER_LAZY_COMMIT); + if (!control) { + return ldb_next_request(module, req); + } + + switch (req->operation) { + case LDB_SEARCH: + ret = ldb_build_search_req_ex(&new_req, ldb_module_get_ctx(module), + req, + req->op.search.base, + req->op.search.scope, + req->op.search.tree, + req->op.search.attrs, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(new_req); + break; + case LDB_ADD: + ret = ldb_build_add_req(&new_req, ldb_module_get_ctx(module), req, + req->op.add.message, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(new_req); + break; + case LDB_MODIFY: + ret = ldb_build_mod_req(&new_req, ldb_module_get_ctx(module), req, + req->op.mod.message, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(new_req); + break; + case LDB_DELETE: + ret = ldb_build_del_req(&new_req, ldb_module_get_ctx(module), req, + req->op.del.dn, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(new_req); + break; + case LDB_RENAME: + ret = ldb_build_rename_req(&new_req, ldb_module_get_ctx(module), req, + req->op.rename.olddn, + req->op.rename.newdn, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(new_req); + break; + case LDB_EXTENDED: + ret = ldb_build_extended_req(&new_req, ldb_module_get_ctx(module), + req, + req->op.extended.oid, + req->op.extended.data, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(new_req); + break; + default: + ldb_set_errstring(ldb_module_get_ctx(module), + "Unsupported request type!"); + ret = LDB_ERR_UNWILLING_TO_PERFORM; + } + + if (ret != LDB_SUCCESS) { + return ret; + } + + control->critical = 0; + return ldb_next_request(module, new_req); +} + +static const struct ldb_module_ops ldb_lazy_commit_module_ops = { + .name = "lazy_commit", + .search = unlazy_op, + .add = unlazy_op, + .modify = unlazy_op, + .del = unlazy_op, + .rename = unlazy_op, + .request = unlazy_op, + .extended = unlazy_op, +}; + +int ldb_lazy_commit_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_lazy_commit_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/linked_attributes.c b/source4/dsdb/samdb/ldb_modules/linked_attributes.c new file mode 100644 index 0000000..317df9d --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/linked_attributes.c @@ -0,0 +1,1581 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2007 + Copyright (C) Simo Sorce <idra@samba.org> 2008 + Copyright (C) Matthieu Patou <mat@matws.net> 2011 + Copyright (C) Andrew Tridgell 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 linked_attributes module + * + * Description: Module to ensure linked attribute pairs (i.e. forward-links + * and backlinks) remain in sync. + * + * Backlinks are 'plain' links (without extra metadata). When the link target + * object is modified (e.g. renamed), we use the backlinks to keep the link + * source object updated. Note there are some cases where we can't do this: + * - one-way links, which don't have a corresponding backlink + * - two-way deactivated links, i.e. when a user is removed from a group, + * the forward 'member' link still exists (but is inactive), however, the + * 'memberOf' backlink is deleted. + * In these cases, we can end up with a dangling forward link which is + * incorrect (i.e. the target has been renamed or deleted). We have dbcheck + * rules to detect and fix this, and cope otherwise by filtering at runtime + * (i.e. in the extended_dn module). + * + * See also repl_meta_data.c, which handles updating links for deleted + * objects, as well as link changes received from another DC. + * + * Author: Andrew Bartlett + */ + +#include "includes.h" +#include "ldb_module.h" +#include "util/dlinklist.h" +#include "dsdb/samdb/samdb.h" +#include "librpc/gen_ndr/ndr_misc.h" +#include "dsdb/samdb/ldb_modules/util.h" + +#undef strcasecmp + +struct la_private_transaction { + struct la_context *la_list; +}; + + +struct la_private { + struct la_private_transaction *transaction; + bool sorted_links; +}; + +struct la_op_store { + struct la_op_store *next; + struct la_op_store *prev; + enum la_op {LA_OP_ADD, LA_OP_DEL} op; + struct GUID guid; + char *name; +}; + +struct replace_context { + struct la_context *ac; + unsigned int num_elements; + struct ldb_message_element *el; +}; + +struct la_context { + struct la_context *next, *prev; + const struct dsdb_schema *schema; + struct ldb_module *module; + struct ldb_request *req; + struct ldb_dn *mod_dn; + struct replace_context *rc; + struct la_op_store *ops; + struct ldb_extended *op_response; + struct ldb_control **op_controls; + /* + * For futur use + * will tell which GC to use for resolving links + */ + char *gc_dns_name; +}; + + +static int handle_verify_name_control(TALLOC_CTX *ctx, struct ldb_context *ldb, + struct ldb_control *control, struct la_context *ac) +{ + /* + * If we are a GC let's remove the control, + * if there is a specified GC check that is us. + */ + struct ldb_verify_name_control *lvnc = talloc_get_type_abort(control->data, struct ldb_verify_name_control); + if (samdb_is_gc(ldb)) { + /* Because we can't easily talloc a struct ldb_dn*/ + struct ldb_dn **dn = talloc_array(ctx, struct ldb_dn *, 1); + int ret = samdb_server_reference_dn(ldb, ctx, dn); + const char *dns; + + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + dns = samdb_dn_to_dnshostname(ldb, ctx, *dn); + if (!dns) { + return ldb_operr(ldb); + } + if (!lvnc->gc || strcasecmp(dns, lvnc->gc) == 0) { + if (!ldb_save_controls(control, ctx, NULL)) { + return ldb_operr(ldb); + } + } else { + control->critical = true; + } + talloc_free(dn); + } else { + /* For the moment we don't remove the control is this case in order + * to fail the request. It's better than having the client thinking + * that we honnor its control. + * Hopefully only a very small set of usecase should hit this problem. + */ + if (lvnc->gc) { + ac->gc_dns_name = talloc_strdup(ac, lvnc->gc); + } + control->critical = true; + } + + return LDB_SUCCESS; +} + +static struct la_context *linked_attributes_init(struct ldb_module *module, + struct ldb_request *req) +{ + struct ldb_context *ldb; + struct la_context *ac; + + ldb = ldb_module_get_ctx(module); + + ac = talloc_zero(req, struct la_context); + if (ac == NULL) { + ldb_oom(ldb); + return NULL; + } + + ac->schema = dsdb_get_schema(ldb, ac); + ac->module = module; + ac->req = req; + + return ac; +} + +/* + turn a DN into a GUID + */ +static int la_guid_from_dn(struct ldb_module *module, + struct ldb_request *parent, + struct ldb_dn *dn, struct GUID *guid) +{ + NTSTATUS status; + int ret; + + status = dsdb_get_extended_dn_guid(dn, guid, "GUID"); + if (NT_STATUS_IS_OK(status)) { + return LDB_SUCCESS; + } + if (!NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) { + DEBUG(4,(__location__ ": Unable to parse GUID for dn %s\n", + ldb_dn_get_linearized(dn))); + return ldb_operr(ldb_module_get_ctx(module)); + } + + ret = dsdb_module_guid_by_dn(module, dn, guid, parent); + if (ret != LDB_SUCCESS) { + DEBUG(4,(__location__ ": Failed to find GUID for dn %s\n", + ldb_dn_get_linearized(dn))); + return ret; + } + return LDB_SUCCESS; +} + + +/* Common routine to handle reading the attributes and creating a + * series of modify requests */ +static int la_store_op(struct la_context *ac, + enum la_op op, + const struct dsdb_attribute *schema_attr, + struct ldb_val *dn, + const char *name) +{ + struct ldb_context *ldb; + struct la_op_store *os; + struct ldb_dn *op_dn; + struct dsdb_dn *dsdb_dn; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + + os = talloc_zero(ac, struct la_op_store); + if (!os) { + return ldb_oom(ldb); + } + + dsdb_dn = dsdb_dn_parse(os, ldb, dn, schema_attr->syntax->ldap_oid); + + if (!dsdb_dn) { + ldb_asprintf_errstring(ldb, + "could not parse attribute as a DN"); + TALLOC_FREE(os); + return LDB_ERR_INVALID_DN_SYNTAX; + } + + op_dn = dsdb_dn->dn; + + os->op = op; + + ret = la_guid_from_dn(ac->module, ac->req, op_dn, &os->guid); + talloc_free(op_dn); + if (ret == LDB_ERR_NO_SUCH_OBJECT && ac->req->operation == LDB_DELETE) { + /* we are deleting an object, and we've found it has a + * forward link to a target that no longer + * exists. This is not an error in the delete, and we + * should just not do the deferred delete of the + * target attribute + */ + talloc_free(os); + return LDB_SUCCESS; + } + if (ret != LDB_SUCCESS) { + return ret; + } + + os->name = talloc_strdup(os, name); + if (!os->name) { + return ldb_oom(ldb); + } + + /* Do deletes before adds */ + if (op == LA_OP_ADD) { + DLIST_ADD_END(ac->ops, os); + } else { + /* By adding to the head of the list, we do deletes before + * adds when processing a replace */ + DLIST_ADD(ac->ops, os); + } + + return LDB_SUCCESS; +} + +static int la_queue_mod_request(struct la_context *ac); +static int la_down_req(struct la_context *ac); + + + +/* add */ +static int linked_attributes_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + const struct dsdb_attribute *target_attr; + struct la_context *ac; + const char *attr_name; + struct ldb_control *ctrl; + unsigned int i, j; + struct ldb_control *control; + int ret; + + ldb = ldb_module_get_ctx(module); + + if (ldb_dn_is_special(req->op.add.message->dn)) { + /* do not manipulate our control entries */ + return ldb_next_request(module, req); + } + + ac = linked_attributes_init(module, req); + if (!ac) { + return ldb_operr(ldb); + } + + control = ldb_request_get_control(req, LDB_CONTROL_VERIFY_NAME_OID); + if (control != NULL && control->data != NULL) { + ret = handle_verify_name_control(req, ldb, control, ac); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + } + + if (!(ctrl = ldb_request_get_control(req, DSDB_CONTROL_APPLY_LINKS))) { + /* don't do anything special for linked attributes, repl_meta_data has done it */ + talloc_free(ac); + return ldb_next_request(module, req); + } + ctrl->critical = false; + + if (!ac->schema) { + /* without schema, this doesn't make any sense */ + talloc_free(ac); + return ldb_next_request(module, req); + } + + + /* Need to ensure we only have forward links being specified */ + 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) { + ldb_asprintf_errstring(ldb, + "%s: attribute %s is not a valid attribute in schema", + __FUNCTION__, + el->name); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + + /* this could be a link with no partner, in which case + there is no special work to do */ + if (schema_attr->linkID == 0) { + continue; + } + + /* this part of the code should only be handling forward links */ + SMB_ASSERT((schema_attr->linkID & 1) == 0); + + /* Even link IDs are for the originating attribute */ + target_attr = dsdb_attribute_by_linkID(ac->schema, schema_attr->linkID ^ 1); + if (!target_attr) { + /* + * windows 2003 has a broken schema where + * the definition of msDS-IsDomainFor + * is missing (which is supposed to be + * the backlink of the msDS-HasDomainNCs + * attribute + */ + continue; + } + + attr_name = target_attr->lDAPDisplayName; + + for (j = 0; j < el->num_values; j++) { + ret = la_store_op(ac, LA_OP_ADD, + schema_attr, + &el->values[j], + attr_name); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } + + /* if no linked attributes are present continue */ + if (ac->ops == NULL) { + /* nothing to do for this module, proceed */ + talloc_free(ac); + return ldb_next_request(module, req); + } + + /* start with the original request */ + return la_down_req(ac); +} + +/* For a delete or rename, we need to find out what linked attributes + * are currently on this DN, and then deal with them. This is the + * callback to the base search */ + +static int la_mod_search_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct ldb_context *ldb; + const struct dsdb_attribute *schema_attr; + const struct dsdb_attribute *target_attr; + struct ldb_message_element *search_el; + struct replace_context *rc; + struct la_context *ac; + const char *attr_name; + unsigned int i, j; + int ret = LDB_SUCCESS; + + ac = talloc_get_type(req->context, struct la_context); + ldb = ldb_module_get_ctx(ac->module); + rc = ac->rc; + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(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: + + if (ldb_dn_compare(ares->message->dn, ac->req->op.mod.message->dn) != 0) { + ldb_asprintf_errstring(ldb, + "linked_attributes: %s is not the DN we were looking for", + ldb_dn_get_linearized(ares->message->dn)); + /* Guh? We only asked for this DN */ + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + ac->mod_dn = talloc_steal(ac, ares->message->dn); + + /* We don't populate 'rc' for ADD - it can't be deleting elements anyway */ + for (i = 0; rc && i < rc->num_elements; i++) { + + schema_attr = dsdb_attribute_by_lDAPDisplayName(ac->schema, rc->el[i].name); + if (!schema_attr) { + ldb_asprintf_errstring(ldb, + "%s: attribute %s is not a valid attribute in schema", + __FUNCTION__, + rc->el[i].name); + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OBJECT_CLASS_VIOLATION); + } + + search_el = ldb_msg_find_element(ares->message, + rc->el[i].name); + + /* See if this element already exists */ + /* otherwise just ignore as + * the add has already been scheduled */ + if ( ! search_el) { + continue; + } + + target_attr = dsdb_attribute_by_linkID(ac->schema, schema_attr->linkID ^ 1); + if (!target_attr) { + /* + * windows 2003 has a broken schema where + * the definition of msDS-IsDomainFor + * is missing (which is supposed to be + * the backlink of the msDS-HasDomainNCs + * attribute + */ + continue; + } + attr_name = target_attr->lDAPDisplayName; + + /* Now we know what was there, we can remove it for the re-add */ + for (j = 0; j < search_el->num_values; j++) { + ret = la_store_op(ac, LA_OP_DEL, + schema_attr, + &search_el->values[j], + attr_name); + if (ret != LDB_SUCCESS) { + talloc_free(ares); + return ldb_module_done(ac->req, + NULL, NULL, ret); + } + } + } + + break; + + case LDB_REPLY_REFERRAL: + /* ignore */ + break; + + case LDB_REPLY_DONE: + + talloc_free(ares); + + if (ac->req->operation == LDB_ADD) { + /* Start the modifies to the backlinks */ + ret = la_queue_mod_request(ac); + + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, + ret); + } + } else { + /* Start with the original request */ + ret = la_down_req(ac); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + } + return LDB_SUCCESS; + } + + talloc_free(ares); + return ret; +} + + +/* modify */ +static int linked_attributes_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 */ + + struct ldb_control *control; + struct ldb_context *ldb; + unsigned int i, j; + struct la_context *ac; + struct ldb_request *search_req; + const char **attrs; + struct ldb_control *ctrl; + int ret; + + ldb = ldb_module_get_ctx(module); + + if (ldb_dn_is_special(req->op.mod.message->dn)) { + /* do not manipulate our control entries */ + return ldb_next_request(module, req); + } + + ac = linked_attributes_init(module, req); + if (!ac) { + return ldb_operr(ldb); + } + + control = ldb_request_get_control(req, LDB_CONTROL_VERIFY_NAME_OID); + if (control != NULL && control->data != NULL) { + ret = handle_verify_name_control(req, ldb, control, ac); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + } + + if (!(ctrl = ldb_request_get_control(req, DSDB_CONTROL_APPLY_LINKS))) { + /* don't do anything special for linked attributes, repl_meta_data has done it */ + talloc_free(ac); + return ldb_next_request(module, req); + } + ctrl->critical = false; + + if (!ac->schema) { + /* without schema, this doesn't make any sense */ + return ldb_next_request(module, req); + } + + ac->rc = talloc_zero(ac, struct replace_context); + if (!ac->rc) { + return ldb_oom(ldb); + } + + for (i=0; i < req->op.mod.message->num_elements; i++) { + bool store_el = false; + const char *attr_name; + const struct dsdb_attribute *target_attr; + 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) { + ldb_asprintf_errstring(ldb, + "%s: attribute %s is not a valid attribute in schema", + __FUNCTION__, + el->name); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + /* We have a valid attribute, now find out if it is a forward link + (Even link IDs are for the originating attribute) */ + if (schema_attr->linkID == 0) { + continue; + } + + /* this part of the code should only be handling forward links */ + SMB_ASSERT((schema_attr->linkID & 1) == 0); + + /* Now find the target attribute */ + target_attr = dsdb_attribute_by_linkID(ac->schema, schema_attr->linkID ^ 1); + if (!target_attr) { + /* + * windows 2003 has a broken schema where + * the definition of msDS-IsDomainFor + * is missing (which is supposed to be + * the backlink of the msDS-HasDomainNCs + * attribute + */ + continue; + } + + attr_name = target_attr->lDAPDisplayName; + + switch (el->flags & LDB_FLAG_MOD_MASK) { + case LDB_FLAG_MOD_REPLACE: + /* treat as just a normal add the delete part is handled by the callback */ + store_el = true; + + FALL_THROUGH; + case LDB_FLAG_MOD_ADD: + + /* For each value being added, we need to setup the adds */ + for (j = 0; j < el->num_values; j++) { + ret = la_store_op(ac, LA_OP_ADD, + schema_attr, + &el->values[j], + attr_name); + if (ret != LDB_SUCCESS) { + return ret; + } + } + break; + + case LDB_FLAG_MOD_DELETE: + + if (el->num_values) { + /* For each value being deleted, we need to setup the delete */ + for (j = 0; j < el->num_values; j++) { + ret = la_store_op(ac, LA_OP_DEL, + schema_attr, + &el->values[j], + attr_name); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } else { + /* Flag that there was a DELETE + * without a value specified, so we + * need to look for the old value */ + store_el = true; + } + + break; + } + + if (store_el) { + struct ldb_message_element *search_el; + + search_el = talloc_realloc(ac->rc, ac->rc->el, + struct ldb_message_element, + ac->rc->num_elements +1); + if (!search_el) { + return ldb_oom(ldb); + } + ac->rc->el = search_el; + + ac->rc->el[ac->rc->num_elements] = *el; + ac->rc->num_elements++; + } + } + + if (ac->ops || ac->rc->el) { + /* both replace and delete without values are handled in the callback + * after the search on the entry to be modified is performed */ + + attrs = talloc_array(ac->rc, const char *, ac->rc->num_elements + 1); + if (!attrs) { + return ldb_oom(ldb); + } + for (i = 0; i < ac->rc->num_elements; i++) { + attrs[i] = ac->rc->el[i].name; + } + attrs[i] = NULL; + + /* The callback does all the hard work here */ + ret = ldb_build_search_req(&search_req, ldb, ac, + req->op.mod.message->dn, + LDB_SCOPE_BASE, + "(objectClass=*)", attrs, + NULL, + ac, la_mod_search_callback, + req); + LDB_REQ_SET_LOCATION(search_req); + + /* We need to figure out our own extended DN, to fill in as the backlink target */ + if (ret == LDB_SUCCESS) { + ret = dsdb_request_add_controls(search_req, + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_SHOW_EXTENDED_DN); + } + if (ret == LDB_SUCCESS) { + talloc_steal(search_req, attrs); + + ret = ldb_next_request(module, search_req); + } + + } else { + /* nothing to do for this module, proceed */ + talloc_free(ac); + ret = ldb_next_request(module, req); + } + + return ret; +} + + +static int linked_attributes_fix_link_slow(struct ldb_module *module, + struct ldb_request *parent, + struct ldb_message *msg, + struct ldb_dn *new_dn, + struct GUID self_guid, + const char *syntax_oid, + const char *reverse_syntax_oid) +{ + int ret; + unsigned int i; + struct GUID link_guid; + struct ldb_message_element *el = &msg->elements[0]; + struct ldb_context *ldb = ldb_module_get_ctx(module); + bool has_unique_value = strcmp(reverse_syntax_oid, LDB_SYNTAX_DN) == 0; + TALLOC_CTX *tmp_ctx = talloc_new(module); + if (tmp_ctx == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + /* + * The msg has one element (el) containing links of one particular + * type from the remote object. We know that at least one of those + * links points to the object being renamed (identified by self_guid, + * renamed to new_dn). Usually only one of the links will point back + * to renamed object, but there can be more when the reverse link is a + * DN+Binary link. + * + * This is used for unsorted links, which is to say back links and + * forward links on old databases. It necessarily involves a linear + * search, though when the link is a plain DN link, we can skip + * checking as soon as we find it. + * + * NOTE: if there are duplicate links, the extra ones will end up as + * dangling links to the old DN. This may or may not be worse than + * leaving them as duplicate links. + */ + for (i = 0; i < el->num_values; i++) { + struct dsdb_dn *dsdb_dn = dsdb_dn_parse(msg, + ldb, + &el->values[i], + syntax_oid); + if (dsdb_dn == NULL) { + talloc_free(tmp_ctx); + return LDB_ERR_INVALID_DN_SYNTAX; + } + + ret = la_guid_from_dn(module, parent, dsdb_dn->dn, &link_guid); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + /* + * By comparing using the GUID we ensure that even if somehow + * the name has got out of sync, this rename will fix it. + * + * If somehow we don't have a GUID on the DN in the DB, the + * la_guid_from_dn call will be more costly, but still give us + * a GUID. dbcheck will fix this if run. + */ + if (!GUID_equal(&self_guid, &link_guid)) { + continue; + } + + ret = ldb_dn_update_components(dsdb_dn->dn, new_dn); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + el->values[i] = data_blob_string_const( + dsdb_dn_get_extended_linearized(el->values, dsdb_dn, 1)); + if (has_unique_value) { + break; + } + } + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + + +static int linked_attributes_fix_forward_link(struct ldb_module *module, + struct ldb_message *msg, + struct ldb_dn *new_dn, + struct GUID self_guid, + const char *syntax_oid) +{ + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct parsed_dn *pdn_list = NULL; + struct parsed_dn *exact = NULL; + struct parsed_dn *next = NULL; + bool is_plain_dn; + struct ldb_message_element *el = &msg->elements[0]; + unsigned int num_parsed_dns = el->num_values; + + TALLOC_CTX *tmp_ctx = talloc_new(module); + if (tmp_ctx == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * The msg has a single element (el) containing forward links which we + * trust are sorted in GUID order. We know that at least one of those + * links points to the object being renamed (identified by self_guid, + * renamed to new_dn), because that object has a backlink pointing + * here. + * + * In most cases we assume there will only be one forward link, which + * is found by parsed_dn_find(), but in the case of DN+Binary links + * (e.g. msDS-RevealedUsers) there may be many forward links that + * share the same DN/GUID but differ in the binary part. For those we + * need to look around the link found by parsed_dn_find() and convert + * them all -- there is no way to know which forward link belongs to + * which backlink. + */ + + ret = get_parsed_dns_trusted(tmp_ctx, el, &pdn_list); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "get_parsed_dn_trusted() " + "error fixing %s links for %s", + el->name, + ldb_dn_get_linearized(msg->dn)); + talloc_free(tmp_ctx); + return ret; + } + + /* find our DN in the values */ + ret = parsed_dn_find(ldb, pdn_list, num_parsed_dns, + &self_guid, + NULL, + data_blob_null, 0, + &exact, &next, + syntax_oid, + false); + + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "parsed_dn_find() " + "error fixing %s links for %s", + el->name, + ldb_dn_get_linearized(msg->dn)); + talloc_free(tmp_ctx); + return ret; + } + + if (exact == NULL) { + ldb_asprintf_errstring( + ldb, + "parsed_dn_find could not find %s link for %s", + el->name, + ldb_dn_get_linearized(msg->dn)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + is_plain_dn = strcmp(syntax_oid, LDB_SYNTAX_DN) == 0; + + if (is_plain_dn) { + /* + * The common case -- we only have to update a single link + */ + ret = ldb_dn_update_components(exact->dsdb_dn->dn, new_dn); + if (ret != LDB_SUCCESS) { + DBG_ERR("could not update components %s %s\n", + ldb_dn_get_linearized(exact->dsdb_dn->dn), + ldb_dn_get_linearized(new_dn) + ); + + talloc_free(tmp_ctx); + return ret; + } + *(exact->v) = data_blob_string_const( + dsdb_dn_get_extended_linearized(el->values, + exact->dsdb_dn, + 1)); + } else { + /* + * The forward link is a DN+Binary (or in some alternate + * universes, DN+String), which means the parsed_dns are keyed + * on GUID+Binary. We don't know the binary part, which means + * from our point of view the list can have entries with + * duplicate GUIDs that we can't tell apart. We don't know + * which backlink belongs to which GUID+binary, and the binary + * search will always find the same one. That means one link + * link will get fixed n times, whil n-1 links get fixed + * never. + * + * If we instead fixing all the possible links, we end up + * fixing n links n times, which at least works and is + * probably not too costly because n is probably small. + */ + struct parsed_dn *first = exact; + struct parsed_dn *last = exact; + struct parsed_dn *p = NULL; + int cmp; + while (first > pdn_list) { + p = first - 1; + if (p->dsdb_dn == NULL) { + ret = really_parse_trusted_dn(tmp_ctx, + ldb, p, + syntax_oid); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + cmp = ndr_guid_compare(&exact->guid, &p->guid); + if (cmp != 0) { + break; + } + first = p; + } + + while (last < pdn_list + num_parsed_dns - 1) { + p = last + 1; + if (p->dsdb_dn == NULL) { + ret = really_parse_trusted_dn(tmp_ctx, + ldb, p, + syntax_oid); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + cmp = ndr_guid_compare(&exact->guid, &p->guid); + if (cmp != 0) { + break; + } + last = p; + } + + for (p = first; p <= last; p++) { + ret = ldb_dn_update_components(p->dsdb_dn->dn, new_dn); + if (ret != LDB_SUCCESS) { + DBG_ERR("could not update components %s %s\n", + ldb_dn_get_linearized(p->dsdb_dn->dn), + ldb_dn_get_linearized(new_dn) + ); + talloc_free(tmp_ctx); + return ret; + } + *(p->v) = data_blob_string_const( + dsdb_dn_get_extended_linearized(el->values, + p->dsdb_dn, + 1)); + } + } + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + + +static int linked_attributes_fix_links(struct ldb_module *module, + struct GUID self_guid, + struct ldb_dn *old_dn, + struct ldb_dn *new_dn, + struct ldb_message_element *el, + struct dsdb_schema *schema, + const struct dsdb_attribute *schema_attr, + struct ldb_request *parent) +{ + unsigned int i; + TALLOC_CTX *tmp_ctx = NULL; + struct ldb_context *ldb = ldb_module_get_ctx(module); + const struct dsdb_attribute *target = NULL; + const char *attrs[2]; + int ret; + struct la_private *la_private = NULL; + + target = dsdb_attribute_by_linkID(schema, schema_attr->linkID ^ 1); + if (target == NULL) { + /* there is no counterpart link to change */ + return LDB_SUCCESS; + } + + tmp_ctx = talloc_new(module); + if (tmp_ctx == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + la_private = talloc_get_type(ldb_module_get_private(module), + struct la_private); + if (la_private == NULL) { + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + attrs[0] = target->lDAPDisplayName; + attrs[1] = NULL; + + for (i=0; i<el->num_values; i++) { + struct dsdb_dn *dsdb_dn = NULL; + struct ldb_result *res = NULL; + struct ldb_message *msg = NULL; + struct ldb_message_element *el2 = NULL; + struct GUID link_guid; + char *link_guid_str = NULL; + + dsdb_dn = dsdb_dn_parse(tmp_ctx, ldb, &el->values[i], + schema_attr->syntax->ldap_oid); + if (dsdb_dn == NULL) { + talloc_free(tmp_ctx); + return LDB_ERR_INVALID_DN_SYNTAX; + } + + ret = la_guid_from_dn(module, parent, dsdb_dn->dn, &link_guid); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Linked attribute %s->%s between %s and %s - GUID not found - %s", + el->name, target->lDAPDisplayName, + ldb_dn_get_linearized(old_dn), + ldb_dn_get_linearized(dsdb_dn->dn), + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + link_guid_str = GUID_string(tmp_ctx, &link_guid); + if (link_guid_str == NULL) { + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * get the existing message from the db for the object with + * this GUID, returning attribute being modified. We will then + * use this msg as the basis for a modify call + */ + + ret = dsdb_module_search(module, tmp_ctx, &res, NULL, LDB_SCOPE_SUBTREE, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SEARCH_ALL_PARTITIONS | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT | + DSDB_SEARCH_REVEAL_INTERNALS, + parent, + "objectGUID=%s", link_guid_str); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Linked attribute %s->%s between %s and %s - target GUID %s not found - %s", + el->name, target->lDAPDisplayName, + ldb_dn_get_linearized(old_dn), + ldb_dn_get_linearized(dsdb_dn->dn), + link_guid_str, + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + if (res->count == 0) { + /* Forward link without backlink object remaining - nothing to do here */ + continue; + } + if (res->count != 1) { + ldb_asprintf_errstring(ldb, "Linked attribute %s->%s between %s and %s - target GUID %s found more than once!", + el->name, target->lDAPDisplayName, + ldb_dn_get_linearized(old_dn), + ldb_dn_get_linearized(dsdb_dn->dn), + link_guid_str); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + msg = res->msgs[0]; + + if (msg->num_elements == 0) { + /* Forward link without backlink remaining - nothing to do here */ + continue; + } else if (msg->num_elements != 1) { + ldb_asprintf_errstring(ldb, "Bad msg elements - got %u elements, expected one element to be returned in linked_attributes_fix_links for %s", + msg->num_elements, ldb_dn_get_linearized(msg->dn)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + if (ldb_attr_cmp(msg->elements[0].name, target->lDAPDisplayName) != 0) { + ldb_asprintf_errstring(ldb, "Bad returned attribute in linked_attributes_fix_links: got %s, expected %s for %s", msg->elements[0].name, target->lDAPDisplayName, ldb_dn_get_linearized(msg->dn)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + el2 = &msg->elements[0]; + + el2->flags = LDB_FLAG_MOD_REPLACE; + + if (target->linkID & 1 || + ! la_private->sorted_links) { + /* handle backlinks (which aren't sorted in the DB) + and forward links in old unsorted databases. */ + ret = linked_attributes_fix_link_slow( + module, + parent, + msg, + new_dn, + self_guid, + target->syntax->ldap_oid, + schema_attr->syntax->ldap_oid); + } else { + /* we can binary search to find forward links */ + ret = linked_attributes_fix_forward_link( + module, + msg, + new_dn, + self_guid, + target->syntax->ldap_oid); + } + ret = dsdb_check_single_valued_link(target, el2); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + /* we may be putting multiple values in an attribute - + disable checking for this attribute */ + el2->flags |= LDB_FLAG_INTERNAL_DISABLE_SINGLE_VALUE_CHECK; + + ret = dsdb_module_modify(module, msg, DSDB_FLAG_NEXT_MODULE, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Linked attribute %s->%s between %s and %s - update failed - %s", + el->name, target->lDAPDisplayName, + ldb_dn_get_linearized(old_dn), + ldb_dn_get_linearized(dsdb_dn->dn), + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + } + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + + +/* rename */ +static int linked_attributes_rename(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_result *res; + struct ldb_message *msg; + unsigned int i; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct dsdb_schema *schema; + int ret; + struct GUID guid; + + /* + - load the current msg + - find any linked attributes + - if its a link then find the target object + - modify the target linked attributes with the new DN + */ + ret = dsdb_module_search_dn(module, req, &res, req->op.rename.olddn, + NULL, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_EXTENDED_DN | + DSDB_SEARCH_SHOW_RECYCLED, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + schema = dsdb_get_schema(ldb, res); + if (!schema) { + return ldb_oom(ldb); + } + + msg = res->msgs[0]; + + ret = la_guid_from_dn(module, req, msg->dn, &guid); + if (ret != LDB_SUCCESS) { + return ret; + } + + for (i=0; i<msg->num_elements; i++) { + struct ldb_message_element *el = &msg->elements[i]; + const struct dsdb_attribute *schema_attr + = dsdb_attribute_by_lDAPDisplayName(schema, el->name); + if (!schema_attr || schema_attr->linkID == 0) { + continue; + } + ret = linked_attributes_fix_links(module, guid, msg->dn, req->op.rename.newdn, el, + schema, schema_attr, req); + if (ret != LDB_SUCCESS) { + talloc_free(res); + return ret; + } + } + + talloc_free(res); + + return ldb_next_request(module, req); +} + + +/* queue a linked attributes modify request in the la_private + structure */ +static int la_queue_mod_request(struct la_context *ac) +{ + struct la_private *la_private = + talloc_get_type(ldb_module_get_private(ac->module), + struct la_private); + + if (la_private == NULL || la_private->transaction == NULL) { + ldb_debug(ldb_module_get_ctx(ac->module), + LDB_DEBUG_ERROR, + __location__ ": No la_private transaction setup\n"); + return ldb_operr(ldb_module_get_ctx(ac->module)); + } + + talloc_steal(la_private->transaction, ac); + DLIST_ADD(la_private->transaction->la_list, ac); + + return ldb_module_done(ac->req, ac->op_controls, + ac->op_response, LDB_SUCCESS); +} + +/* Having done the original operation, then try to fix up all the linked attributes for modify and delete */ +static int la_mod_del_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct la_context *ac; + struct ldb_context *ldb; + int ret; + + ac = talloc_get_type(req->context, struct la_context); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + if (ares->type != LDB_REPLY_DONE) { + ldb_set_errstring(ldb, + "invalid reply type in linked attributes delete callback"); + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + ac->op_controls = talloc_steal(ac, ares->controls); + ac->op_response = talloc_steal(ac, ares->response); + + /* If we have modfies to make, this is the time to do them for modify and delete */ + ret = la_queue_mod_request(ac); + + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + talloc_free(ares); + + /* la_queue_mod_request has already sent the callbacks */ + return LDB_SUCCESS; + +} + +/* Having done the original add, then try to fix up all the linked attributes + + This is done after the add so the links can get the extended DNs correctly. + */ +static int la_add_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct la_context *ac; + struct ldb_context *ldb; + int ret; + + ac = talloc_get_type(req->context, struct la_context); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + if (ares->type != LDB_REPLY_DONE) { + ldb_set_errstring(ldb, + "invalid reply type in linked attributes add callback"); + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + if (ac->ops) { + struct ldb_request *search_req; + static const char *attrs[] = { NULL }; + + /* The callback does all the hard work here - we need + * the objectGUID and SID of the added record */ + ret = ldb_build_search_req(&search_req, ldb, ac, + ac->req->op.add.message->dn, + LDB_SCOPE_BASE, + "(objectClass=*)", attrs, + NULL, + ac, la_mod_search_callback, + ac->req); + LDB_REQ_SET_LOCATION(search_req); + + if (ret == LDB_SUCCESS) { + ret = dsdb_request_add_controls(search_req, + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_SHOW_EXTENDED_DN); + } + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, + ret); + } + + ac->op_controls = talloc_steal(ac, ares->controls); + ac->op_response = talloc_steal(ac, ares->response); + + return ldb_next_request(ac->module, search_req); + + } else { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } +} + +/* Reconstruct the original request, but pointing at our local callback to finish things off */ +static int la_down_req(struct la_context *ac) +{ + struct ldb_request *down_req; + struct ldb_context *ldb; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + switch (ac->req->operation) { + case LDB_ADD: + ret = ldb_build_add_req(&down_req, ldb, ac, + ac->req->op.add.message, + ac->req->controls, + ac, la_add_callback, + ac->req); + LDB_REQ_SET_LOCATION(down_req); + break; + case LDB_MODIFY: + ret = ldb_build_mod_req(&down_req, ldb, ac, + ac->req->op.mod.message, + ac->req->controls, + ac, la_mod_del_callback, + ac->req); + LDB_REQ_SET_LOCATION(down_req); + break; + default: + ret = LDB_ERR_OPERATIONS_ERROR; + } + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(ac->module, down_req); +} + +/* + use the GUID part of an extended DN to find the target DN, in case + it has moved + */ +static int la_find_dn_target(struct ldb_module *module, struct la_context *ac, + struct GUID *guid, struct ldb_dn **dn) +{ + return dsdb_module_dn_by_guid(ac->module, ac, guid, dn, ac->req); +} + +/* apply one la_context op change */ +static int la_do_op_request(struct ldb_module *module, struct la_context *ac, struct la_op_store *op) +{ + struct ldb_message_element *ret_el; + struct ldb_message *new_msg; + struct ldb_context *ldb; + int ret; + + if (ac->mod_dn == NULL) { + /* we didn't find the DN that we searched for */ + return LDB_SUCCESS; + } + + ldb = ldb_module_get_ctx(ac->module); + + /* Create the modify request */ + new_msg = ldb_msg_new(ac); + if (!new_msg) { + return ldb_oom(ldb); + } + + ret = la_find_dn_target(module, ac, &op->guid, &new_msg->dn); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (op->op == LA_OP_ADD) { + ret = ldb_msg_add_empty(new_msg, op->name, + LDB_FLAG_MOD_ADD, &ret_el); + } else { + ret = ldb_msg_add_empty(new_msg, op->name, + LDB_FLAG_MOD_DELETE, &ret_el); + } + if (ret != LDB_SUCCESS) { + return ret; + } + ret_el->values = talloc_array(new_msg, struct ldb_val, 1); + if (!ret_el->values) { + return ldb_oom(ldb); + } + ret_el->num_values = 1; + ret_el->values[0] = data_blob_string_const(ldb_dn_get_extended_linearized(new_msg, ac->mod_dn, 1)); + + /* a backlink should never be single valued. Unfortunately the + exchange schema has a attribute + msExchBridgeheadedLocalConnectorsDNBL which is single + valued and a backlink. We need to cope with that by + ignoring the single value flag */ + ret_el->flags |= LDB_FLAG_INTERNAL_DISABLE_SINGLE_VALUE_CHECK; + +#if 0 + ldb_debug(ldb, LDB_DEBUG_WARNING, + "link on %s %s: %s %s\n", + ldb_dn_get_linearized(new_msg->dn), ret_el->name, + ret_el->values[0].data, ac->ops->op == LA_OP_ADD ? "added" : "deleted"); +#endif + + if (DEBUGLVL(4)) { + DEBUG(4,("Applying linked attribute change:\n%s\n", + ldb_ldif_message_redacted_string(ldb, op, + LDB_CHANGETYPE_MODIFY, + new_msg))); + } + + ret = dsdb_module_modify(module, new_msg, DSDB_FLAG_NEXT_MODULE, ac->req); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_WARNING, __location__ ": failed to apply linked attribute change '%s'\n%s\n", + ldb_errstring(ldb), + ldb_ldif_message_redacted_string(ldb, op, + LDB_CHANGETYPE_MODIFY, + new_msg)); + } + + return ret; +} + +/* apply one set of la_context changes */ +static int la_do_mod_request(struct ldb_module *module, struct la_context *ac) +{ + struct la_op_store *op; + + for (op = ac->ops; op; op=op->next) { + int ret = la_do_op_request(module, ac, op); + if (ret != LDB_SUCCESS) { + if (ret != LDB_ERR_NO_SUCH_OBJECT) { + return ret; + } + } + } + + return LDB_SUCCESS; +} + + +/* + we hook into the transaction operations to allow us to + perform the linked attribute updates at the end of the whole + transaction. This allows a forward linked attribute to be created + before the target is created, as long as the target is created + in the same transaction + */ +static int linked_attributes_start_transaction(struct ldb_module *module) +{ + /* create our private structure for this transaction */ + struct la_private *la_private = + talloc_get_type(ldb_module_get_private(module), + struct la_private); + + if (la_private == NULL) { + return ldb_oom(ldb_module_get_ctx(module)); + } + talloc_free(la_private->transaction); + la_private->transaction = talloc(module, struct la_private_transaction); + if (la_private->transaction == NULL) { + return ldb_oom(ldb_module_get_ctx(module)); + } + la_private->transaction->la_list = NULL; + return ldb_next_start_trans(module); +} + +/* + on prepare commit we loop over our queued la_context structures + and apply each of them + */ +static int linked_attributes_prepare_commit(struct ldb_module *module) +{ + struct la_context *ac; + struct la_private *la_private = + talloc_get_type(ldb_module_get_private(module), + struct la_private); + if (la_private == NULL || la_private->transaction == NULL) { + DBG_ERR("prepare_commit without begin_transaction\n"); + /* prepare commit without begin_transaction - let someone else + * return the error, just don't segfault */ + return ldb_next_prepare_commit(module); + } + /* walk the list backwards, to do the first entry first, as we + * added the entries with DLIST_ADD() which puts them at the + * start of the list */ + + /* Start at the end of the list - so we can start + * there, but ensure we don't create a loop by NULLing + * it out in the first element */ + ac = DLIST_TAIL(la_private->transaction->la_list); + + for (; ac; ac=DLIST_PREV(ac)) { + int ret; + ac->req = NULL; + ret = la_do_mod_request(module, ac); + if (ret != LDB_SUCCESS) { + DEBUG(0,(__location__ ": Failed mod request ret=%d\n", ret)); + TALLOC_FREE(la_private->transaction); + return ret; + } + } + + TALLOC_FREE(la_private->transaction); + + return ldb_next_prepare_commit(module); +} + +static int linked_attributes_del_transaction(struct ldb_module *module) +{ + struct la_private *la_private = + talloc_get_type(ldb_module_get_private(module), + struct la_private); + TALLOC_FREE(la_private->transaction); + return ldb_next_del_trans(module); +} + +static int linked_attributes_ldb_init(struct ldb_module *module) +{ + int ret; + struct la_private *la_private = NULL; + struct ldb_context *ldb = ldb_module_get_ctx(module); + + ret = ldb_mod_register_control(module, LDB_CONTROL_VERIFY_NAME_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb_module_get_ctx(module), LDB_DEBUG_ERROR, + "verify_name: Unable to register control with rootdse!\n"); + return ldb_operr(ldb_module_get_ctx(module)); + } + + la_private = talloc_zero(module, struct la_private); + if (la_private == NULL) { + ldb_oom(ldb); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = dsdb_check_samba_compatible_feature(module, + SAMBA_SORTED_LINKS_FEATURE, + &la_private->sorted_links); + if (ret != LDB_SUCCESS) { + talloc_free(la_private); + return ret; + } + + ldb_module_set_private(module, la_private); + return ldb_next_init(module); +} + + +static const struct ldb_module_ops ldb_linked_attributes_module_ops = { + .name = "linked_attributes", + .add = linked_attributes_add, + .modify = linked_attributes_modify, + .rename = linked_attributes_rename, + .init_context = linked_attributes_ldb_init, + .start_transaction = linked_attributes_start_transaction, + .prepare_commit = linked_attributes_prepare_commit, + .del_transaction = linked_attributes_del_transaction, +}; + +int ldb_linked_attributes_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_linked_attributes_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/netlogon.c b/source4/dsdb/samdb/ldb_modules/netlogon.c new file mode 100644 index 0000000..4864ae4 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/netlogon.c @@ -0,0 +1,510 @@ +/* + Unix SMB/CIFS implementation. + + CLDAP server - netlogon handling + + Copyright (C) Andrew Tridgell 2005 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2008 + + 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/>. +*/ + +#include "includes.h" +#include <ldb.h> +#include <ldb_errors.h> +#include "lib/events/events.h" +#include "samba/service_task.h" +#include "librpc/gen_ndr/ndr_misc.h" +#include "libcli/ldap/ldap_ndr.h" +#include "libcli/security/security.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "auth/auth.h" +#include "ldb_wrap.h" +#include "system/network.h" +#include "lib/socket/netif.h" +#include "param/param.h" +#include "../lib/tsocket/tsocket.h" +#include "libds/common/flag_mapping.h" +#include "lib/util/util_net.h" + +#undef strcasecmp + +/* + fill in the cldap netlogon union for a given version +*/ +NTSTATUS fill_netlogon_samlogon_response(struct ldb_context *sam_ctx, + TALLOC_CTX *mem_ctx, + const char *domain, + const char *netbios_domain, + struct dom_sid *domain_sid, + const char *domain_guid, + const char *user, + uint32_t acct_control, + const char *src_address, + uint32_t version, + struct loadparm_context *lp_ctx, + struct netlogon_samlogon_response *netlogon, + bool fill_on_blank_request) +{ + const char *dom_attrs[] = {"objectGUID", NULL}; + const char *none_attrs[] = {NULL}; + struct ldb_result *dom_res = NULL; + int ret; + const char **services = lpcfg_server_services(lp_ctx); + uint32_t server_type; + const char *pdc_name; + struct GUID domain_uuid; + const char *dns_domain; + const char *forest_domain; + const char *pdc_dns_name; + const char *flatname; + const char *server_site; + const char *client_site; + const char *pdc_ip; + struct ldb_dn *domain_dn = NULL; + struct interface *ifaces; + bool user_known = false, am_rodc = false; + uint32_t uac = 0; + int dc_level; + NTSTATUS status; + + /* the domain parameter could have an optional trailing "." */ + if (domain && domain[strlen(domain)-1] == '.') { + domain = talloc_strndup(mem_ctx, domain, strlen(domain)-1); + NT_STATUS_HAVE_NO_MEMORY(domain); + } + + /* Lookup using long or short domainname */ + if (domain && (strcasecmp_m(domain, lpcfg_dnsdomain(lp_ctx)) == 0)) { + domain_dn = ldb_get_default_basedn(sam_ctx); + } + if (netbios_domain && (strcasecmp_m(netbios_domain, lpcfg_sam_name(lp_ctx)) == 0)) { + domain_dn = ldb_get_default_basedn(sam_ctx); + } + if (domain_dn) { + const char *domain_identifier = domain != NULL ? domain + : netbios_domain; + ret = ldb_search(sam_ctx, mem_ctx, &dom_res, + domain_dn, LDB_SCOPE_BASE, dom_attrs, + "objectClass=domain"); + if (ret != LDB_SUCCESS) { + DEBUG(2,("Error finding domain '%s'/'%s' in sam: %s\n", + domain_identifier, + ldb_dn_get_linearized(domain_dn), + ldb_errstring(sam_ctx))); + return NT_STATUS_NO_SUCH_DOMAIN; + } + if (dom_res->count != 1) { + DEBUG(2,("Error finding domain '%s'/'%s' in sam\n", + domain_identifier, + ldb_dn_get_linearized(domain_dn))); + return NT_STATUS_NO_SUCH_DOMAIN; + } + } + + /* Lookup using GUID or SID */ + if ((dom_res == NULL) && (domain_guid || domain_sid)) { + if (domain_guid) { + struct GUID binary_guid; + struct ldb_val guid_val; + + /* By this means, we ensure we don't have funny stuff in the GUID */ + + status = GUID_from_string(domain_guid, &binary_guid); + if (!NT_STATUS_IS_OK(status)) { + return status; + } + + /* And this gets the result into the binary format we want anyway */ + status = GUID_to_ndr_blob(&binary_guid, mem_ctx, &guid_val); + if (!NT_STATUS_IS_OK(status)) { + return status; + } + ret = ldb_search(sam_ctx, mem_ctx, &dom_res, + NULL, LDB_SCOPE_SUBTREE, + dom_attrs, + "(&(objectCategory=DomainDNS)(objectGUID=%s))", + ldb_binary_encode(mem_ctx, guid_val)); + } else { /* domain_sid case */ + ret = ldb_search(sam_ctx, mem_ctx, &dom_res, + NULL, LDB_SCOPE_SUBTREE, + dom_attrs, + "(&(objectCategory=DomainDNS)(objectSid=%s))", + dom_sid_string(mem_ctx, domain_sid)); + } + + if (ret != LDB_SUCCESS) { + DEBUG(2,("Unable to find a correct reference to GUID '%s' or SID '%s' in sam: %s\n", + domain_guid, dom_sid_string(mem_ctx, domain_sid), + ldb_errstring(sam_ctx))); + return NT_STATUS_NO_SUCH_DOMAIN; + } else if (dom_res->count == 1) { + /* Ok, now just check it is our domain */ + if (ldb_dn_compare(ldb_get_default_basedn(sam_ctx), + dom_res->msgs[0]->dn) != 0) { + DEBUG(2,("The GUID '%s' or SID '%s' doesn't identify our domain\n", + domain_guid, + dom_sid_string(mem_ctx, domain_sid))); + return NT_STATUS_NO_SUCH_DOMAIN; + } + } else { + DEBUG(2,("Unable to find a correct reference to GUID '%s' or SID '%s' in sam\n", + domain_guid, dom_sid_string(mem_ctx, domain_sid))); + return NT_STATUS_NO_SUCH_DOMAIN; + } + } + + if (dom_res == NULL && fill_on_blank_request) { + /* blank inputs gives our domain - tested against + w2k8r2. Without this ADUC on Win7 won't start */ + domain_dn = ldb_get_default_basedn(sam_ctx); + ret = ldb_search(sam_ctx, mem_ctx, &dom_res, + domain_dn, LDB_SCOPE_BASE, dom_attrs, + "objectClass=domain"); + if (ret != LDB_SUCCESS) { + DEBUG(2,("Error finding domain '%s'/'%s' in sam: %s\n", + lpcfg_dnsdomain(lp_ctx), + ldb_dn_get_linearized(domain_dn), + ldb_errstring(sam_ctx))); + return NT_STATUS_NO_SUCH_DOMAIN; + } + } + + if (dom_res == NULL) { + DEBUG(2,(__location__ ": Unable to get domain information with no inputs\n")); + return NT_STATUS_NO_SUCH_DOMAIN; + } + + /* work around different inputs for not-specified users */ + if (!user) { + user = ""; + } + + /* Enquire about any valid username with just a CLDAP packet - + * if kerberos didn't also do this, the security folks would + * scream... */ + if (user[0]) { + /* Only allow some bits to be enquired: [MS-ATDS] 7.3.3.2 */ + if (acct_control == (uint32_t)-1) { + acct_control = 0; + } + /* + * ACB_AUTOLOCK/UF_LOCKOUT seems to be a special + * hack for SEC_CHAN_DNS_DOMAIN. + * + * It's used together with user = "example.com." + */ + if (acct_control != ACB_AUTOLOCK) { + acct_control &= (ACB_TEMPDUP | ACB_NORMAL | ACB_DOMTRUST | ACB_WSTRUST | ACB_SVRTRUST); + } + uac = ds_acb2uf(acct_control); + } + + if (uac == UF_LOCKOUT) { + struct ldb_message *tdo_msg = NULL; + + /* + * ACB_AUTOLOCK/UF_LOCKOUT seems to be a special + * hack for SEC_CHAN_DNS_DOMAIN. + * + * It's used together with user = "example.com." + */ + status = dsdb_trust_search_tdo_by_type(sam_ctx, + SEC_CHAN_DNS_DOMAIN, + user, none_attrs, + mem_ctx, &tdo_msg); + if (NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) { + user_known = false; + } else if (NT_STATUS_IS_OK(status)) { + TALLOC_FREE(tdo_msg); + user_known = true; + } else { + DEBUG(2,("Unable to find reference to TDO '%s' - %s\n", + user, nt_errstr(status))); + return status; + } + } else if (user[0]) { + struct ldb_result *user_res = NULL; + + /* We must exclude disabled accounts, but otherwise do the bitwise match the client asked for */ + ret = ldb_search(sam_ctx, mem_ctx, &user_res, + dom_res->msgs[0]->dn, LDB_SCOPE_SUBTREE, + none_attrs, + "(&(objectClass=user)(samAccountName=%s)" + "(!(userAccountControl:" LDB_OID_COMPARATOR_AND ":=%u))" + "(userAccountControl:" LDB_OID_COMPARATOR_OR ":=%u))", + ldb_binary_encode_string(mem_ctx, user), + UF_ACCOUNTDISABLE, uac); + if (ret != LDB_SUCCESS) { + DEBUG(2,("Unable to find reference to user '%s' with ACB 0x%8x under %s: %s\n", + user, acct_control, ldb_dn_get_linearized(dom_res->msgs[0]->dn), + ldb_errstring(sam_ctx))); + return NT_STATUS_NO_SUCH_USER; + } else if (user_res->count == 1) { + user_known = true; + } else { + user_known = false; + } + TALLOC_FREE(user_res); + } else { + user_known = true; + } + + server_type = DS_SERVER_DS; + + if (samdb_is_pdc(sam_ctx)) { + server_type |= DS_SERVER_PDC; + } + + if (samdb_is_gc(sam_ctx)) { + server_type |= DS_SERVER_GC; + } + + if (str_list_check(services, "ldap")) { + server_type |= DS_SERVER_LDAP; + } + + if (str_list_check(services, "kdc")) { + server_type |= DS_SERVER_KDC; + } + + if (str_list_check(services, "ntp_signd")) { + server_type |= DS_SERVER_TIMESERV | DS_SERVER_GOOD_TIMESERV; + } + + if (samdb_rodc(sam_ctx, &am_rodc) == LDB_SUCCESS && !am_rodc) { + server_type |= DS_SERVER_WRITABLE; + } + + dc_level = dsdb_dc_functional_level(sam_ctx); + if (dc_level >= DS_DOMAIN_FUNCTION_2008) { + if (server_type & DS_SERVER_WRITABLE) { + server_type |= DS_SERVER_FULL_SECRET_DOMAIN_6; + } else { + server_type |= DS_SERVER_SELECT_SECRET_DOMAIN_6; + } + } + + if (dc_level >= DS_DOMAIN_FUNCTION_2012) { + server_type |= DS_SERVER_DS_8; + } + + if (dc_level >= DS_DOMAIN_FUNCTION_2012_R2) { + server_type |= DS_SERVER_DS_9; + } + + if (dc_level >= DS_DOMAIN_FUNCTION_2016) { + server_type |= DS_SERVER_DS_10; + } + + if (version & (NETLOGON_NT_VERSION_5EX|NETLOGON_NT_VERSION_5EX_WITH_IP)) { + pdc_name = lpcfg_netbios_name(lp_ctx); + } else { + pdc_name = talloc_asprintf(mem_ctx, "\\\\%s", + lpcfg_netbios_name(lp_ctx)); + NT_STATUS_HAVE_NO_MEMORY(pdc_name); + } + domain_uuid = samdb_result_guid(dom_res->msgs[0], "objectGUID"); + dns_domain = lpcfg_dnsdomain(lp_ctx); + forest_domain = samdb_forest_name(sam_ctx, mem_ctx); + NT_STATUS_HAVE_NO_MEMORY(forest_domain); + pdc_dns_name = talloc_asprintf(mem_ctx, "%s.%s", + strlower_talloc(mem_ctx, + lpcfg_netbios_name(lp_ctx)), + dns_domain); + NT_STATUS_HAVE_NO_MEMORY(pdc_dns_name); + flatname = lpcfg_workgroup(lp_ctx); + + server_site = samdb_server_site_name(sam_ctx, mem_ctx); + NT_STATUS_HAVE_NO_MEMORY(server_site); + client_site = samdb_client_site_name(sam_ctx, mem_ctx, + src_address, NULL, + true); + NT_STATUS_HAVE_NO_MEMORY(client_site); + if (strcasecmp(server_site, client_site) == 0) { + server_type |= DS_SERVER_CLOSEST; + } + + load_interface_list(mem_ctx, lp_ctx, &ifaces); + if (src_address) { + pdc_ip = iface_list_best_ip(ifaces, src_address); + } else { + pdc_ip = iface_list_first_v4(ifaces); + } + if (pdc_ip == NULL || !is_ipaddress_v4(pdc_ip)) { + /* this matches windows behaviour */ + pdc_ip = "127.0.0.1"; + } + + ZERO_STRUCTP(netlogon); + + /* check if either of these bits is present */ + if (version & (NETLOGON_NT_VERSION_5EX|NETLOGON_NT_VERSION_5EX_WITH_IP)) { + uint32_t extra_flags = 0; + netlogon->ntver = NETLOGON_NT_VERSION_5EX; + + /* could check if the user exists */ + if (user_known) { + netlogon->data.nt5_ex.command = LOGON_SAM_LOGON_RESPONSE_EX; + } else { + netlogon->data.nt5_ex.command = LOGON_SAM_LOGON_USER_UNKNOWN_EX; + } + netlogon->data.nt5_ex.pdc_name = pdc_name; + netlogon->data.nt5_ex.user_name = user; + netlogon->data.nt5_ex.domain_name = flatname; + netlogon->data.nt5_ex.domain_uuid = domain_uuid; + netlogon->data.nt5_ex.forest = forest_domain; + netlogon->data.nt5_ex.dns_domain = dns_domain; + netlogon->data.nt5_ex.pdc_dns_name = pdc_dns_name; + netlogon->data.nt5_ex.server_site = server_site; + netlogon->data.nt5_ex.client_site = client_site; + if (version & NETLOGON_NT_VERSION_5EX_WITH_IP) { + /* note that this is always a IPV4 address */ + extra_flags = NETLOGON_NT_VERSION_5EX_WITH_IP; + netlogon->data.nt5_ex.sockaddr.sockaddr_family = 2; + netlogon->data.nt5_ex.sockaddr.pdc_ip = pdc_ip; + netlogon->data.nt5_ex.sockaddr.remaining = data_blob_talloc_zero(mem_ctx, 8); + } + netlogon->data.nt5_ex.server_type = server_type; + netlogon->data.nt5_ex.nt_version = NETLOGON_NT_VERSION_1|NETLOGON_NT_VERSION_5EX|extra_flags; + netlogon->data.nt5_ex.lmnt_token = 0xFFFF; + netlogon->data.nt5_ex.lm20_token = 0xFFFF; + + } else if (version & NETLOGON_NT_VERSION_5) { + netlogon->ntver = NETLOGON_NT_VERSION_5; + + /* could check if the user exists */ + if (user_known) { + netlogon->data.nt5.command = LOGON_SAM_LOGON_RESPONSE; + } else { + netlogon->data.nt5.command = LOGON_SAM_LOGON_USER_UNKNOWN; + } + netlogon->data.nt5.pdc_name = pdc_name; + netlogon->data.nt5.user_name = user; + netlogon->data.nt5.domain_name = flatname; + netlogon->data.nt5.domain_uuid = domain_uuid; + netlogon->data.nt5.forest = forest_domain; + netlogon->data.nt5.dns_domain = dns_domain; + netlogon->data.nt5.pdc_dns_name = pdc_dns_name; + netlogon->data.nt5.pdc_ip = pdc_ip; + netlogon->data.nt5.server_type = server_type; + netlogon->data.nt5.nt_version = NETLOGON_NT_VERSION_1|NETLOGON_NT_VERSION_5; + netlogon->data.nt5.lmnt_token = 0xFFFF; + netlogon->data.nt5.lm20_token = 0xFFFF; + + } else /* (version & NETLOGON_NT_VERSION_1) and all other cases */ { + netlogon->ntver = NETLOGON_NT_VERSION_1; + /* could check if the user exists */ + if (user_known) { + netlogon->data.nt4.command = LOGON_SAM_LOGON_RESPONSE; + } else { + netlogon->data.nt4.command = LOGON_SAM_LOGON_USER_UNKNOWN; + } + netlogon->data.nt4.pdc_name = pdc_name; + netlogon->data.nt4.user_name = user; + netlogon->data.nt4.domain_name = flatname; + netlogon->data.nt4.nt_version = NETLOGON_NT_VERSION_1; + netlogon->data.nt4.lmnt_token = 0xFFFF; + netlogon->data.nt4.lm20_token = 0xFFFF; + } + + return NT_STATUS_OK; +} + +NTSTATUS parse_netlogon_request(struct ldb_parse_tree *tree, + struct loadparm_context *lp_ctx, + TALLOC_CTX *tmp_ctx, + const char **domain, + const char **host, + const char **user, + const char **domain_guid, + struct dom_sid **domain_sid, + int *acct_control, + int *version) +{ + unsigned int i; + + *domain = NULL; + *host = NULL; + *user = NULL; + *domain_guid = NULL; + *domain_sid = NULL; + *acct_control = -1; + *version = NETLOGON_NT_VERSION_5; + + if (tree->operation != LDB_OP_AND) goto failed; + + /* extract the query elements */ + for (i=0;i<tree->u.list.num_elements;i++) { + struct ldb_parse_tree *t = tree->u.list.elements[i]; + if (t->operation != LDB_OP_EQUALITY) goto failed; + if (strcasecmp(t->u.equality.attr, "DnsDomain") == 0) { + *domain = talloc_strndup(tmp_ctx, + (const char *)t->u.equality.value.data, + t->u.equality.value.length); + } + if (strcasecmp(t->u.equality.attr, "Host") == 0) { + *host = talloc_strndup(tmp_ctx, + (const char *)t->u.equality.value.data, + t->u.equality.value.length); + } + if (strcasecmp(t->u.equality.attr, "DomainGuid") == 0) { + NTSTATUS enc_status; + struct GUID guid; + enc_status = ldap_decode_ndr_GUID(tmp_ctx, + t->u.equality.value, &guid); + if (NT_STATUS_IS_OK(enc_status)) { + *domain_guid = GUID_string(tmp_ctx, &guid); + } + } + if (strcasecmp(t->u.equality.attr, "DomainSid") == 0) { + enum ndr_err_code ndr_err; + + *domain_sid = talloc(tmp_ctx, struct dom_sid); + if (*domain_sid == NULL) { + goto failed; + } + ndr_err = ndr_pull_struct_blob(&t->u.equality.value, + *domain_sid, *domain_sid, + (ndr_pull_flags_fn_t)ndr_pull_dom_sid); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + talloc_free(*domain_sid); + goto failed; + } + } + if (strcasecmp(t->u.equality.attr, "User") == 0) { + *user = talloc_strndup(tmp_ctx, + (const char *)t->u.equality.value.data, + t->u.equality.value.length); + } + if (strcasecmp(t->u.equality.attr, "NtVer") == 0 && + t->u.equality.value.length == 4) { + *version = IVAL(t->u.equality.value.data, 0); + } + if (strcasecmp(t->u.equality.attr, "AAC") == 0 && + t->u.equality.value.length == 4) { + *acct_control = IVAL(t->u.equality.value.data, 0); + } + } + + if ((*domain == NULL) && (*domain_guid == NULL) && (*domain_sid == NULL)) { + *domain = lpcfg_dnsdomain(lp_ctx); + } + + return NT_STATUS_OK; + +failed: + return NT_STATUS_UNSUCCESSFUL; +} diff --git a/source4/dsdb/samdb/ldb_modules/new_partition.c b/source4/dsdb/samdb/ldb_modules/new_partition.c new file mode 100644 index 0000000..eaf7d43 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/new_partition.c @@ -0,0 +1,213 @@ +/* + ldb database library + + Copyright (C) Simo Sorce 2004-2008 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2005 + Copyright (C) Andrew Tridgell 2005 + Copyright (C) Stefan Metzmacher <metze@samba.org> 2007 + + 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 new partition module + * + * Description: Handle the add of new partitions + * + * Author: Andrew Bartlett + */ + +#include "includes.h" +#include "ldb.h" +#include "ldb_module.h" +#include "librpc/gen_ndr/ndr_misc.h" +#include "dsdb/samdb/samdb.h" +#include "../libds/common/flags.h" +#include "dsdb/common/util.h" + +struct np_context { + struct ldb_module *module; + struct ldb_request *req; + struct ldb_request *search_req; + struct ldb_request *part_add; +}; + +static int np_part_mod_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct np_context *ac; + + ac = talloc_get_type(req->context, struct np_context); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + /* We just want to update the @PARTITIONS record if the value does not exist */ + if (ares->error != LDB_SUCCESS && ares->error != LDB_ERR_ATTRIBUTE_OR_VALUE_EXISTS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + if (ares->type != LDB_REPLY_DONE) { + ldb_asprintf_errstring(ldb, "Invalid LDB reply type %d", ares->type); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + ldb_reset_err_string(ldb); + + /* Do the original add */ + return ldb_next_request(ac->module, ac->req); +} + +static int np_part_search_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct np_context *ac; + struct dsdb_create_partition_exop *ex_op; + int ret; + + ac = talloc_get_type(req->context, struct np_context); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + /* If this already exists, we really don't want to create a + * partition - it would allow a duplicate entry to be + * created */ + if (ares->error != LDB_ERR_NO_SUCH_OBJECT) { + if (ares->error == LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, LDB_ERR_ENTRY_ALREADY_EXISTS); + } else { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + } + + if (ares->type != LDB_REPLY_DONE) { + ldb_set_errstring(ldb, "Invalid reply type - we must not get a result here!"); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + ldb_reset_err_string(ldb); + + /* Now that we know it does not exist, we can try and create the partition */ + ex_op = talloc(ac, struct dsdb_create_partition_exop); + if (ex_op == NULL) { + return ldb_oom(ldb); + } + + ex_op->new_dn = ac->req->op.add.message->dn; + + ret = ldb_build_extended_req(&ac->part_add, + ldb, ac, DSDB_EXTENDED_CREATE_PARTITION_OID, ex_op, + NULL, ac, np_part_mod_callback, req); + + /* if the parent was asking for a partial replica, then we + * need the extended operation to also ask for a partial + * replica */ + if (ldb_request_get_control(req, DSDB_CONTROL_PARTIAL_REPLICA)) { + ret = dsdb_request_add_controls(ac->part_add, DSDB_MODIFY_PARTIAL_REPLICA); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + + LDB_REQ_SET_LOCATION(ac->part_add); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(ac->module, ac->part_add); +} + +/* add_record: add instancetype attribute */ +static int new_partition_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct np_context *ac; + int ret; + + ldb = ldb_module_get_ctx(module); + + ldb_debug(ldb, LDB_DEBUG_TRACE, "new_partition_add\n"); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.add.message->dn)) { + return ldb_next_request(module, req); + } + + if (ldb_msg_find_element(req->op.add.message, "instanceType")) { + /* This needs to be 'static' to ensure it does not move, and is not on the stack */ + static const char *no_attrs[] = { NULL }; + uint32_t instanceType = ldb_msg_find_attr_as_uint(req->op.add.message, "instanceType", 0); + + if (!(instanceType & INSTANCE_TYPE_IS_NC_HEAD)) { + return ldb_next_request(module, req); + } + + if (ldb_msg_find_attr_as_bool(req->op.add.message, "isDeleted", false)) { + DEBUG(0,(__location__ ": Skipping deleted partition %s\n", + ldb_dn_get_linearized(req->op.add.message->dn))); + return ldb_next_request(module, req); + } + + /* Create an @PARTITIONS record for this partition - + * by asking the partitions module to do so via an + * extended operation, after first checking if the + * record already exists */ + ac = talloc(req, struct np_context); + if (ac == NULL) { + return ldb_oom(ldb); + } + ac->module = module; + ac->req = req; + + ret = ldb_build_search_req(&ac->search_req, ldb, ac, req->op.add.message->dn, + LDB_SCOPE_BASE, NULL, no_attrs, req->controls, ac, + np_part_search_callback, + req); + LDB_REQ_SET_LOCATION(ac->search_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, ac->search_req); + } + + /* go on with the call chain */ + return ldb_next_request(module, req); +} + +static const struct ldb_module_ops ldb_new_partition_module_ops = { + .name = "new_partition", + .add = new_partition_add, +}; + +int ldb_new_partition_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_new_partition_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/objectclass.c b/source4/dsdb/samdb/ldb_modules/objectclass.c new file mode 100644 index 0000000..5b97c9d --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/objectclass.c @@ -0,0 +1,1477 @@ +/* + ldb database library + + Copyright (C) Simo Sorce 2006-2008 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2005-2009 + Copyright (C) Matthias Dieter Wallnöfer 2010-2011 + + 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: objectClass sorting and constraint checking module + * + * Description: + * - sort the objectClass attribute into the class + * hierarchy and perform constraint checks (correct RDN name, + * valid parent), + * - fix DNs into 'standard' case + * - Add objectCategory and some other attribute defaults + * + * Author: Andrew Bartlett + */ + + +#include "includes.h" +#include "ldb_module.h" +#include "dsdb/samdb/samdb.h" +#include "librpc/ndr/libndr.h" +#include "librpc/gen_ndr/ndr_security.h" +#include "libcli/security/security.h" +#include "auth/auth.h" +#include "param/param.h" +#include "../libds/common/flags.h" +#include "dsdb/samdb/ldb_modules/util.h" + +#undef strcasecmp + +struct oc_context { + + struct ldb_module *module; + struct ldb_request *req; + const struct dsdb_schema *schema; + + struct ldb_reply *search_res; + struct ldb_reply *search_res2; + + int (*step_fn)(struct oc_context *); +}; + +static struct oc_context *oc_init_context(struct ldb_module *module, + struct ldb_request *req) +{ + struct ldb_context *ldb; + struct oc_context *ac; + + ldb = ldb_module_get_ctx(module); + + ac = talloc_zero(req, struct oc_context); + if (ac == NULL) { + ldb_oom(ldb); + return NULL; + } + + ac->module = module; + ac->req = req; + ac->schema = dsdb_get_schema(ldb, ac); + + return ac; +} + +static int objectclass_do_add(struct oc_context *ac); + +/* + * This checks if we have unrelated object classes in our entry's "objectClass" + * attribute. That means "unsatisfied" abstract classes (no concrete subclass) + * or two or more disjunct structural ones. + * If one of these conditions are true, blame. + */ +static int check_unrelated_objectclasses(struct ldb_module *module, + const struct dsdb_schema *schema, + const struct dsdb_class *struct_objectclass, + struct ldb_message_element *objectclass_element) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + unsigned int i; + bool found; + + if (schema == NULL) { + return LDB_SUCCESS; + } + + for (i = 0; i < objectclass_element->num_values; i++) { + const struct dsdb_class *tmp_class = dsdb_class_by_lDAPDisplayName_ldb_val(schema, + &objectclass_element->values[i]); + const struct dsdb_class *tmp_class2 = struct_objectclass; + + /* Pointer comparison can be used due to the same schema str. */ + if (tmp_class == NULL || + tmp_class == struct_objectclass || + tmp_class->objectClassCategory > 2 || + ldb_attr_cmp(tmp_class->lDAPDisplayName, "top") == 0) { + continue; + } + + found = false; + while (!found && + ldb_attr_cmp(tmp_class2->lDAPDisplayName, "top") != 0) { + tmp_class2 = dsdb_class_by_lDAPDisplayName(schema, + tmp_class2->subClassOf); + if (tmp_class2 == tmp_class) { + found = true; + } + } + if (found) { + continue; + } + + ldb_asprintf_errstring(ldb, + "objectclass: the objectclass '%s' seems to be unrelated to %s!", + tmp_class->lDAPDisplayName, + struct_objectclass->lDAPDisplayName); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + + return LDB_SUCCESS; +} + +static int get_search_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct oc_context *ac; + int ret; + + ac = talloc_get_type(req->context, struct oc_context); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS && + ares->error != LDB_ERR_NO_SUCH_OBJECT) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + ldb_reset_err_string(ldb); + + switch (ares->type) { + case LDB_REPLY_ENTRY: + if (ac->search_res != NULL) { + ldb_set_errstring(ldb, "Too many results"); + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + ac->search_res = talloc_steal(ac, ares); + break; + + case LDB_REPLY_REFERRAL: + /* ignore */ + talloc_free(ares); + break; + + case LDB_REPLY_DONE: + talloc_free(ares); + ret = ac->step_fn(ac); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + break; + } + + return LDB_SUCCESS; +} + +/* Fix up the DN to be in the standard form, taking particular care to match the parent DN + + This should mean that if the parent is: + CN=Users,DC=samba,DC=example,DC=com + and a proposed child is + cn=Admins ,cn=USERS,dc=Samba,dc=example,dc=COM + + The resulting DN should be: + + CN=Admins,CN=Users,DC=samba,DC=example,DC=com + + */ +static int fix_dn(struct ldb_context *ldb, + TALLOC_CTX *mem_ctx, + struct ldb_dn *newdn, struct ldb_dn *parent_dn, + struct ldb_dn **fixed_dn) +{ + char *upper_rdn_attr; + const struct ldb_val *rdn_val; + + /* Fix up the DN to be in the standard form, taking particular care to + * match the parent DN */ + *fixed_dn = ldb_dn_copy(mem_ctx, parent_dn); + if (*fixed_dn == NULL) { + return ldb_oom(ldb); + } + + /* We need the attribute name in upper case */ + upper_rdn_attr = strupper_talloc(*fixed_dn, + ldb_dn_get_rdn_name(newdn)); + if (upper_rdn_attr == NULL) { + return ldb_oom(ldb); + } + + /* Create a new child */ + if (ldb_dn_add_child_fmt(*fixed_dn, "X=X") == false) { + return ldb_operr(ldb); + } + + rdn_val = ldb_dn_get_rdn_val(newdn); + if (rdn_val == NULL) { + return ldb_operr(ldb); + } + +#if 0 + /* the rules for rDN length constraints are more complex than + this. Until we understand them we need to leave this + constraint out. Otherwise we break replication, as windows + does sometimes send us rDNs longer than 64 */ + if (!rdn_val || rdn_val->length > 64) { + DEBUG(2,(__location__ ": WARNING: rDN longer than 64 limit for '%s'\n", ldb_dn_get_linearized(newdn))); + } +#endif + + + /* And replace it with CN=foo (we need the attribute in upper case) */ + return ldb_dn_set_component(*fixed_dn, 0, upper_rdn_attr, *rdn_val); +} + + +static int objectclass_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct ldb_request *search_req; + struct oc_context *ac; + struct ldb_dn *parent_dn; + const struct ldb_val *val; + int ret; + static const char * const parent_attrs[] = { "objectClass", NULL }; + + ldb = ldb_module_get_ctx(module); + + ldb_debug(ldb, LDB_DEBUG_TRACE, "objectclass_add\n"); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.add.message->dn)) { + return ldb_next_request(module, req); + } + + /* An add operation on the basedn without "NC-add" operation isn't + * allowed. */ + if (ldb_dn_compare(ldb_get_default_basedn(ldb), req->op.add.message->dn) == 0) { + unsigned int instanceType; + + instanceType = ldb_msg_find_attr_as_uint(req->op.add.message, + "instanceType", 0); + if (!(instanceType & INSTANCE_TYPE_IS_NC_HEAD)) { + char *referral_uri; + /* When we are trying to readd the root basedn then + * this is denied, but with an interesting mechanism: + * there is generated a referral with the last + * component value as hostname. */ + val = ldb_dn_get_component_val(req->op.add.message->dn, + ldb_dn_get_comp_num(req->op.add.message->dn) - 1); + if (val == NULL) { + return ldb_operr(ldb); + } + referral_uri = talloc_asprintf(req, "ldap://%s/%s", val->data, + ldb_dn_get_linearized(req->op.add.message->dn)); + if (referral_uri == NULL) { + return ldb_module_oom(module); + } + + return ldb_module_send_referral(req, referral_uri); + } + } + + ac = oc_init_context(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + /* If there isn't a parent, just go on to the add processing */ + if (ldb_dn_get_comp_num(ac->req->op.add.message->dn) == 1) { + return objectclass_do_add(ac); + } + + /* get copy of parent DN */ + parent_dn = ldb_dn_get_parent(ac, ac->req->op.add.message->dn); + if (parent_dn == NULL) { + return ldb_operr(ldb); + } + + ret = ldb_build_search_req(&search_req, ldb, + ac, parent_dn, LDB_SCOPE_BASE, + "(objectClass=*)", parent_attrs, + NULL, + ac, get_search_callback, + req); + LDB_REQ_SET_LOCATION(search_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = dsdb_request_add_controls(search_req, + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED); + if (ret != LDB_SUCCESS) { + return ret; + } + + ac->step_fn = objectclass_do_add; + + return ldb_next_request(ac->module, search_req); +} + + +/* + check if this is a special RODC nTDSDSA add + */ +static bool check_rodc_ntdsdsa_add(struct oc_context *ac, + const struct dsdb_class *objectclass) +{ + struct ldb_control *rodc_control; + + if (ldb_attr_cmp(objectclass->lDAPDisplayName, "nTDSDSA") != 0) { + return false; + } + rodc_control = ldb_request_get_control(ac->req, LDB_CONTROL_RODC_DCPROMO_OID); + if (!rodc_control) { + return false; + } + + rodc_control->critical = false; + return true; +} + +static int objectclass_do_add(struct oc_context *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + struct ldb_request *add_req; + struct ldb_message_element *objectclass_element, *el; + struct ldb_message *msg; + const char *rdn_name = NULL; + char *value; + const struct dsdb_class *objectclass; + struct ldb_dn *objectcategory; + int32_t systemFlags = 0; + unsigned int i, j; + bool found; + int ret; + + msg = ldb_msg_copy_shallow(ac, ac->req->op.add.message); + if (msg == NULL) { + return ldb_module_oom(ac->module); + } + + /* Check if we have a valid parent - this check is needed since + * we don't get a LDB_ERR_NO_SUCH_OBJECT error. */ + if (ac->search_res == NULL) { + unsigned int instanceType; + + /* An add operation on partition DNs without "NC-add" operation + * isn't allowed. */ + instanceType = ldb_msg_find_attr_as_uint(msg, "instanceType", + 0); + if (!(instanceType & INSTANCE_TYPE_IS_NC_HEAD)) { + ldb_asprintf_errstring(ldb, "objectclass: Cannot add %s, parent does not exist!", + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_NO_SUCH_OBJECT; + } + + /* Don't keep any error messages - we've to add a partition */ + ldb_set_errstring(ldb, NULL); + } else { + /* Fix up the DN to be in the standard form, taking + * particular care to match the parent DN */ + ret = fix_dn(ldb, msg, + ac->req->op.add.message->dn, + ac->search_res->message->dn, + &msg->dn); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "objectclass: Could not munge DN %s into normal form", + ldb_dn_get_linearized(ac->req->op.add.message->dn)); + return ret; + } + } + + if (ac->schema != NULL) { + unsigned int linkID = 0; + /* + * Notice: by the normalization function call in "ldb_request()" + * case "LDB_ADD" we have always only *one* "objectClass" + * attribute at this stage! + */ + + objectclass_element = ldb_msg_find_element(msg, "objectClass"); + if (!objectclass_element) { + ldb_asprintf_errstring(ldb, "objectclass: Cannot add %s, no objectclass specified!", + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + if (objectclass_element->num_values == 0) { + ldb_asprintf_errstring(ldb, "objectclass: Cannot add %s, at least one (structural) objectclass has to be specified!", + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + /* Now do the sorting */ + ret = dsdb_sort_objectClass_attr(ldb, ac->schema, + objectclass_element, msg, + objectclass_element); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + * Get the new top-most structural object class and check for + * unrelated structural classes + */ + objectclass = dsdb_get_last_structural_class(ac->schema, + objectclass_element); + if (objectclass == NULL) { + ldb_asprintf_errstring(ldb, + "Failed to find a structural class for %s", + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + ret = check_unrelated_objectclasses(ac->module, ac->schema, + objectclass, + objectclass_element); + if (ret != LDB_SUCCESS) { + return ret; + } + + rdn_name = ldb_dn_get_rdn_name(msg->dn); + if (rdn_name == NULL) { + return ldb_operr(ldb); + } + found = false; + for (i = 0; (!found) && (i < objectclass_element->num_values); + i++) { + const struct dsdb_class *tmp_class = + dsdb_class_by_lDAPDisplayName_ldb_val(ac->schema, + &objectclass_element->values[i]); + + if (tmp_class == NULL) continue; + + if (ldb_attr_cmp(rdn_name, tmp_class->rDNAttID) == 0) + found = true; + } + if (!found) { + ldb_asprintf_errstring(ldb, + "objectclass: Invalid RDN '%s' for objectclass '%s'!", + rdn_name, objectclass->lDAPDisplayName); + return LDB_ERR_NAMING_VIOLATION; + } + + if (objectclass->systemOnly && + !ldb_request_get_control(ac->req, LDB_CONTROL_RELAX_OID) && + !check_rodc_ntdsdsa_add(ac, objectclass)) { + ldb_asprintf_errstring(ldb, + "objectclass: object class '%s' is system-only, rejecting creation of '%s'!", + objectclass->lDAPDisplayName, + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + if (ac->search_res && ac->search_res->message) { + struct ldb_message_element *oc_el + = ldb_msg_find_element(ac->search_res->message, "objectClass"); + + bool allowed_class = false; + for (i=0; allowed_class == false && oc_el && i < oc_el->num_values; i++) { + const struct dsdb_class *sclass; + + sclass = dsdb_class_by_lDAPDisplayName_ldb_val(ac->schema, + &oc_el->values[i]); + if (!sclass) { + /* We don't know this class? what is going on? */ + continue; + } + for (j=0; sclass->systemPossibleInferiors && sclass->systemPossibleInferiors[j]; j++) { + if (ldb_attr_cmp(objectclass->lDAPDisplayName, sclass->systemPossibleInferiors[j]) == 0) { + allowed_class = true; + break; + } + } + } + + if (!allowed_class) { + ldb_asprintf_errstring(ldb, "structural objectClass %s is not a valid child class for %s", + objectclass->lDAPDisplayName, ldb_dn_get_linearized(ac->search_res->message->dn)); + return LDB_ERR_NAMING_VIOLATION; + } + } + + objectcategory = ldb_msg_find_attr_as_dn(ldb, ac, msg, + "objectCategory"); + if (objectcategory == NULL) { + struct dsdb_extended_dn_store_format *dn_format = + talloc_get_type(ldb_module_get_private(ac->module), + struct dsdb_extended_dn_store_format); + if (dn_format && dn_format->store_extended_dn_in_ldb == false) { + /* Strip off extended components */ + struct ldb_dn *dn = ldb_dn_new(ac, ldb, + objectclass->defaultObjectCategory); + value = ldb_dn_alloc_linearized(msg, dn); + talloc_free(dn); + } else { + value = talloc_strdup(msg, + objectclass->defaultObjectCategory); + } + if (value == NULL) { + return ldb_module_oom(ac->module); + } + + ret = ldb_msg_add_string(msg, "objectCategory", value); + if (ret != LDB_SUCCESS) { + return ret; + } + } else { + const struct dsdb_class *ocClass = + dsdb_class_by_cn_ldb_val(ac->schema, + ldb_dn_get_rdn_val(objectcategory)); + if (ocClass != NULL) { + struct ldb_dn *dn = ldb_dn_new(ac, ldb, + ocClass->defaultObjectCategory); + if (ldb_dn_compare(objectcategory, dn) != 0) { + ocClass = NULL; + } + } + talloc_free(objectcategory); + if (ocClass == NULL) { + ldb_asprintf_errstring(ldb, "objectclass: Cannot add %s, 'objectCategory' attribute invalid!", + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + } + + if (!ldb_msg_find_element(msg, "showInAdvancedViewOnly") && (objectclass->defaultHidingValue == true)) { + ldb_msg_add_string(msg, "showInAdvancedViewOnly", + "TRUE"); + } + + /* There are very special rules for systemFlags, see MS-ADTS + * MS-ADTS 3.1.1.5.2.4 */ + + el = ldb_msg_find_element(msg, "systemFlags"); + if ((el != NULL) && (el->num_values > 1)) { + ldb_asprintf_errstring(ldb, "objectclass: Cannot add %s, 'systemFlags' attribute multivalued!", + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + systemFlags = ldb_msg_find_attr_as_int(msg, "systemFlags", 0); + + ldb_msg_remove_attr(msg, "systemFlags"); + + /* Only the following flags may be set by a client */ + if (ldb_request_get_control(ac->req, + LDB_CONTROL_RELAX_OID) == NULL) { + systemFlags &= ( SYSTEM_FLAG_CONFIG_ALLOW_RENAME + | SYSTEM_FLAG_CONFIG_ALLOW_MOVE + | SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE + | SYSTEM_FLAG_ATTR_IS_RDN ); + } + + /* But the last one ("ATTR_IS_RDN") is only allowed on + * "attributeSchema" objects. So truncate if it does not fit. */ + if (ldb_attr_cmp(objectclass->lDAPDisplayName, "attributeSchema") != 0) { + systemFlags &= ~SYSTEM_FLAG_ATTR_IS_RDN; + } + + if (ldb_attr_cmp(objectclass->lDAPDisplayName, "server") == 0) { + systemFlags |= (int32_t)(SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE | SYSTEM_FLAG_CONFIG_ALLOW_RENAME | SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE); + } else if (ldb_attr_cmp(objectclass->lDAPDisplayName, "site") == 0 + || ldb_attr_cmp(objectclass->lDAPDisplayName, "serversContainer") == 0 + || ldb_attr_cmp(objectclass->lDAPDisplayName, "nTDSDSA") == 0) { + if (ldb_attr_cmp(objectclass->lDAPDisplayName, "site") == 0) + systemFlags |= (int32_t)(SYSTEM_FLAG_CONFIG_ALLOW_RENAME); + systemFlags |= (int32_t)(SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE); + } else if (ldb_attr_cmp(objectclass->lDAPDisplayName, "siteLink") == 0 + || ldb_attr_cmp(objectclass->lDAPDisplayName, "subnet") == 0 + || ldb_attr_cmp(objectclass->lDAPDisplayName, "siteLinkBridge") == 0 + || ldb_attr_cmp(objectclass->lDAPDisplayName, "nTDSConnection") == 0) { + systemFlags |= (int32_t)(SYSTEM_FLAG_CONFIG_ALLOW_RENAME); + } + /* TODO: If parent object is site or subnet, also add (SYSTEM_FLAG_CONFIG_ALLOW_RENAME) */ + + linkID = ldb_msg_find_attr_as_int(msg, "linkID", 0); + if (linkID > 0 && linkID % 2 == 1) { + systemFlags |= DS_FLAG_ATTR_NOT_REPLICATED; + } + + if (el || systemFlags != 0) { + ret = samdb_msg_add_int(ldb, msg, msg, "systemFlags", + systemFlags); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* make sure that "isCriticalSystemObject" is not specified! */ + el = ldb_msg_find_element(msg, "isCriticalSystemObject"); + if ((el != NULL) && + !ldb_request_get_control(ac->req, LDB_CONTROL_RELAX_OID)) { + ldb_set_errstring(ldb, + "objectclass: 'isCriticalSystemObject' must not be specified!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + + ret = ldb_build_add_req(&add_req, ldb, ac, + msg, + ac->req->controls, + ac->req, dsdb_next_callback, + ac->req); + LDB_REQ_SET_LOCATION(add_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* perform the add */ + return ldb_next_request(ac->module, add_req); +} + +static int oc_modify_callback(struct ldb_request *req, + struct ldb_reply *ares); +static int objectclass_do_mod(struct oc_context *ac); + +static int objectclass_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_message_element *objectclass_element; + struct ldb_message *msg; + struct ldb_request *down_req; + struct oc_context *ac; + bool oc_changes = false; + int ret; + + ldb_debug(ldb, LDB_DEBUG_TRACE, "objectclass_modify\n"); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.mod.message->dn)) { + return ldb_next_request(module, req); + } + + /* As with the "real" AD we don't accept empty messages */ + if (req->op.mod.message->num_elements == 0) { + ldb_set_errstring(ldb, "objectclass: modify message must have " + "elements/attributes!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + ac = oc_init_context(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + /* Without schema, there isn't much to do here */ + if (ac->schema == NULL) { + talloc_free(ac); + return ldb_next_request(module, req); + } + + msg = ldb_msg_copy_shallow(ac, req->op.mod.message); + if (msg == NULL) { + return ldb_module_oom(ac->module); + } + + /* For now change everything except the objectclasses */ + + objectclass_element = ldb_msg_find_element(msg, "objectClass"); + if (objectclass_element != NULL) { + ldb_msg_remove_attr(msg, "objectClass"); + oc_changes = true; + } + + /* MS-ADTS 3.1.1.5.3.5 - on a forest level < 2003 we do allow updates + * only on application NCs - not on the default ones */ + if (oc_changes && + (dsdb_forest_functional_level(ldb) < DS_DOMAIN_FUNCTION_2003)) { + struct ldb_dn *nc_root; + + ret = dsdb_find_nc_root(ldb, ac, req->op.mod.message->dn, + &nc_root); + if (ret != LDB_SUCCESS) { + return ret; + } + + if ((ldb_dn_compare(nc_root, ldb_get_default_basedn(ldb)) == 0) || + (ldb_dn_compare(nc_root, ldb_get_config_basedn(ldb)) == 0) || + (ldb_dn_compare(nc_root, ldb_get_schema_basedn(ldb)) == 0)) { + ldb_set_errstring(ldb, + "objectclass: object class changes on objects under the standard name contexts not allowed!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + talloc_free(nc_root); + } + + if (oc_changes) { + ret = ldb_build_mod_req(&down_req, ldb, ac, + msg, + req->controls, ac, + oc_modify_callback, + req); + } else { + ret = ldb_build_mod_req(&down_req, ldb, ac, + msg, + req->controls, req, + dsdb_next_callback, + req); + } + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, down_req); +} + +static int oc_modify_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + static const char * const attrs[] = { "objectClass", NULL }; + struct ldb_context *ldb; + struct ldb_request *search_req; + struct oc_context *ac; + int ret; + + ac = talloc_get_type(req->context, struct oc_context); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + if (ares->type == LDB_REPLY_REFERRAL) { + return ldb_module_send_referral(ac->req, ares->referral); + } + + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + if (ares->type != LDB_REPLY_DONE) { + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + talloc_free(ares); + + /* this looks up the real existing object for fetching some important + * information (objectclasses) */ + ret = ldb_build_search_req(&search_req, ldb, + ac, ac->req->op.mod.message->dn, + LDB_SCOPE_BASE, + "(objectClass=*)", + attrs, NULL, + ac, get_search_callback, + ac->req); + LDB_REQ_SET_LOCATION(search_req); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + ret = dsdb_request_add_controls(search_req, + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + ac->step_fn = objectclass_do_mod; + + ret = ldb_next_request(ac->module, search_req); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + return LDB_SUCCESS; +} + +static int objectclass_do_mod(struct oc_context *ac) +{ + struct ldb_context *ldb; + struct ldb_request *mod_req; + struct ldb_message_element *oc_el_entry, *oc_el_change; + struct ldb_val *vals; + struct ldb_message *msg; + const struct dsdb_class *current_structural_objectclass; + const struct dsdb_class *objectclass; + unsigned int i, j, k; + bool found; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + /* we should always have a valid entry when we enter here */ + if (ac->search_res == NULL) { + return ldb_operr(ldb); + } + + oc_el_entry = ldb_msg_find_element(ac->search_res->message, + "objectClass"); + if (oc_el_entry == NULL) { + /* existing entry without a valid object class? */ + return ldb_operr(ldb); + } + + /* + * Get the current new top-most structural object class + * + * We must not allow this to change + */ + + current_structural_objectclass + = dsdb_get_last_structural_class(ac->schema, + oc_el_entry); + if (current_structural_objectclass == NULL) { + ldb_asprintf_errstring(ldb, + "objectclass: cannot find current structural objectclass on %s!", + ldb_dn_get_linearized(ac->search_res->message->dn)); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + + /* use a new message structure */ + msg = ldb_msg_new(ac); + if (msg == NULL) { + return ldb_module_oom(ac->module); + } + + msg->dn = ac->req->op.mod.message->dn; + + /* We've to walk over all "objectClass" message elements */ + for (k = 0; k < ac->req->op.mod.message->num_elements; k++) { + if (ldb_attr_cmp(ac->req->op.mod.message->elements[k].name, + "objectClass") != 0) { + continue; + } + + oc_el_change = &ac->req->op.mod.message->elements[k]; + + switch (oc_el_change->flags & LDB_FLAG_MOD_MASK) { + case LDB_FLAG_MOD_ADD: + /* Merge the two message elements */ + for (i = 0; i < oc_el_change->num_values; i++) { + for (j = 0; j < oc_el_entry->num_values; j++) { + if (ldb_attr_cmp((char *)oc_el_change->values[i].data, + (char *)oc_el_entry->values[j].data) == 0) { + ldb_asprintf_errstring(ldb, + "objectclass: cannot re-add an existing objectclass: '%.*s'!", + (int)oc_el_change->values[i].length, + (const char *)oc_el_change->values[i].data); + return LDB_ERR_ATTRIBUTE_OR_VALUE_EXISTS; + } + } + /* append the new object class value - code was + * copied from "ldb_msg_add_value" */ + vals = talloc_realloc(oc_el_entry, oc_el_entry->values, + struct ldb_val, + oc_el_entry->num_values + 1); + if (vals == NULL) { + return ldb_module_oom(ac->module); + } + oc_el_entry->values = vals; + oc_el_entry->values[oc_el_entry->num_values] = + oc_el_change->values[i]; + ++(oc_el_entry->num_values); + } + + break; + + case LDB_FLAG_MOD_REPLACE: + /* + * In this case the new "oc_el_entry" is simply + * "oc_el_change" + */ + oc_el_entry = oc_el_change; + + break; + + case LDB_FLAG_MOD_DELETE: + /* Merge the two message elements */ + for (i = 0; i < oc_el_change->num_values; i++) { + found = false; + for (j = 0; j < oc_el_entry->num_values; j++) { + if (ldb_attr_cmp((char *)oc_el_change->values[i].data, + (char *)oc_el_entry->values[j].data) == 0) { + found = true; + /* delete the object class value + * - code was copied from + * "ldb_msg_remove_element" */ + if (j != oc_el_entry->num_values - 1) { + memmove(&oc_el_entry->values[j], + &oc_el_entry->values[j+1], + ((oc_el_entry->num_values-1) - j)*sizeof(struct ldb_val)); + } + --(oc_el_entry->num_values); + break; + } + } + if (!found) { + /* we cannot delete a not existing + * object class */ + ldb_asprintf_errstring(ldb, + "objectclass: cannot delete this objectclass: '%.*s'!", + (int)oc_el_change->values[i].length, + (const char *)oc_el_change->values[i].data); + return LDB_ERR_NO_SUCH_ATTRIBUTE; + } + } + + break; + } + + /* Now do the sorting */ + ret = dsdb_sort_objectClass_attr(ldb, ac->schema, oc_el_entry, + msg, oc_el_entry); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + * Get the new top-most structural object class and check for + * unrelated structural classes + */ + objectclass = dsdb_get_last_structural_class(ac->schema, + oc_el_entry); + if (objectclass == NULL) { + ldb_set_errstring(ldb, + "objectclass: cannot delete all structural objectclasses!"); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + + /* + * Has (so far, we re-check for each and every + * "objectclass" in the message) the structural + * objectclass changed? + */ + + if (objectclass != current_structural_objectclass) { + const char *dn + = ldb_dn_get_linearized(ac->search_res->message->dn); + ldb_asprintf_errstring(ldb, + "objectclass: not permitted " + "to change the structural " + "objectClass on %s [%s] => [%s]!", + dn, + current_structural_objectclass->lDAPDisplayName, + objectclass->lDAPDisplayName); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + + /* Check for unrelated objectclasses */ + ret = check_unrelated_objectclasses(ac->module, ac->schema, + objectclass, + oc_el_entry); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* Now add the new object class attribute to the change message */ + ret = ldb_msg_add(msg, oc_el_entry, LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + ldb_module_oom(ac->module); + return ret; + } + + /* Now we have the real and definitive change left to do */ + + ret = ldb_build_mod_req(&mod_req, ldb, ac, + msg, + ac->req->controls, + ac->req, dsdb_next_callback, + ac->req); + LDB_REQ_SET_LOCATION(mod_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(ac->module, mod_req); +} + +static int objectclass_do_rename(struct oc_context *ac); + +static int objectclass_rename(struct ldb_module *module, struct ldb_request *req) +{ + static const char * const attrs[] = { "objectClass", NULL }; + struct ldb_context *ldb; + struct ldb_request *search_req; + struct oc_context *ac; + struct ldb_dn *parent_dn; + int ret; + + ldb = ldb_module_get_ctx(module); + + ldb_debug(ldb, LDB_DEBUG_TRACE, "objectclass_rename\n"); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.rename.olddn)) { + return ldb_next_request(module, req); + } + + /* + * Bypass the constraint checks when we do have the "DBCHECK" control + * set, so we can force objects under the deleted objects container. + */ + if (ldb_request_get_control(req, DSDB_CONTROL_DBCHECK) != NULL) { + return ldb_next_request(module, req); + } + + ac = oc_init_context(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + parent_dn = ldb_dn_get_parent(ac, req->op.rename.newdn); + if (parent_dn == NULL) { + ldb_asprintf_errstring(ldb, "objectclass: Cannot rename %s, the parent DN does not exist!", + ldb_dn_get_linearized(req->op.rename.olddn)); + return LDB_ERR_NO_SUCH_OBJECT; + } + + /* this looks up the parent object for fetching some important + * information (objectclasses, DN normalisation...) */ + ret = ldb_build_search_req(&search_req, ldb, + ac, parent_dn, LDB_SCOPE_BASE, + "(objectClass=*)", + attrs, NULL, + ac, get_search_callback, + req); + LDB_REQ_SET_LOCATION(search_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* we have to add the show recycled control, as otherwise DRS + deletes will be refused as we will think the target parent + does not exist */ + ret = dsdb_request_add_controls(search_req, + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED); + if (ret != LDB_SUCCESS) { + return ret; + } + + ac->step_fn = objectclass_do_rename; + + return ldb_next_request(ac->module, search_req); +} + +static int objectclass_do_rename2(struct oc_context *ac); + +static int objectclass_do_rename(struct oc_context *ac) +{ + static const char * const attrs[] = { "objectClass", NULL }; + struct ldb_context *ldb; + struct ldb_request *search_req; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + /* Check if we have a valid parent - this check is needed since + * we don't get a LDB_ERR_NO_SUCH_OBJECT error. */ + if (ac->search_res == NULL) { + ldb_asprintf_errstring(ldb, "objectclass: Cannot rename %s, parent does not exist!", + ldb_dn_get_linearized(ac->req->op.rename.olddn)); + return LDB_ERR_OTHER; + } + + /* now assign "search_res2" to the parent entry to have "search_res" + * free for another lookup */ + ac->search_res2 = ac->search_res; + ac->search_res = NULL; + + /* this looks up the real existing object for fetching some important + * information (objectclasses) */ + ret = ldb_build_search_req(&search_req, ldb, + ac, ac->req->op.rename.olddn, + LDB_SCOPE_BASE, + "(objectClass=*)", + attrs, NULL, + ac, get_search_callback, + ac->req); + LDB_REQ_SET_LOCATION(search_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = dsdb_request_add_controls(search_req, + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED); + if (ret != LDB_SUCCESS) { + return ret; + } + + ac->step_fn = objectclass_do_rename2; + + return ldb_next_request(ac->module, search_req); +} + +static int objectclass_do_rename2(struct oc_context *ac) +{ + struct ldb_context *ldb; + struct ldb_request *rename_req; + struct ldb_dn *fixed_dn; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + /* Check if we have a valid entry - this check is needed since + * we don't get a LDB_ERR_NO_SUCH_OBJECT error. */ + if (ac->search_res == NULL) { + ldb_asprintf_errstring(ldb, "objectclass: Cannot rename %s, entry does not exist!", + ldb_dn_get_linearized(ac->req->op.rename.olddn)); + return LDB_ERR_NO_SUCH_OBJECT; + } + + if (ac->schema != NULL) { + struct ldb_message_element *oc_el_entry, *oc_el_parent; + const struct dsdb_class *objectclass; + const char *rdn_name; + bool allowed_class = false; + unsigned int i, j; + bool found; + + oc_el_entry = ldb_msg_find_element(ac->search_res->message, + "objectClass"); + if (oc_el_entry == NULL) { + /* existing entry without a valid object class? */ + return ldb_operr(ldb); + } + objectclass = dsdb_get_last_structural_class(ac->schema, + oc_el_entry); + if (objectclass == NULL) { + /* existing entry without a valid object class? */ + return ldb_operr(ldb); + } + + rdn_name = ldb_dn_get_rdn_name(ac->req->op.rename.newdn); + if (rdn_name == NULL) { + return ldb_operr(ldb); + } + found = false; + for (i = 0; (!found) && (i < oc_el_entry->num_values); i++) { + const struct dsdb_class *tmp_class = + dsdb_class_by_lDAPDisplayName_ldb_val(ac->schema, + &oc_el_entry->values[i]); + + if (tmp_class == NULL) continue; + + if (ldb_attr_cmp(rdn_name, tmp_class->rDNAttID) == 0) + found = true; + } + if (!found) { + ldb_asprintf_errstring(ldb, + "objectclass: Invalid RDN '%s' for objectclass '%s'!", + rdn_name, objectclass->lDAPDisplayName); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + oc_el_parent = ldb_msg_find_element(ac->search_res2->message, + "objectClass"); + if (oc_el_parent == NULL) { + /* existing entry without a valid object class? */ + return ldb_operr(ldb); + } + + for (i=0; allowed_class == false && i < oc_el_parent->num_values; i++) { + const struct dsdb_class *sclass; + + sclass = dsdb_class_by_lDAPDisplayName_ldb_val(ac->schema, + &oc_el_parent->values[i]); + if (!sclass) { + /* We don't know this class? what is going on? */ + continue; + } + for (j=0; sclass->systemPossibleInferiors && sclass->systemPossibleInferiors[j]; j++) { + if (ldb_attr_cmp(objectclass->lDAPDisplayName, sclass->systemPossibleInferiors[j]) == 0) { + allowed_class = true; + break; + } + } + } + + if (!allowed_class) { + ldb_asprintf_errstring(ldb, + "objectclass: structural objectClass %s is not a valid child class for %s", + objectclass->lDAPDisplayName, ldb_dn_get_linearized(ac->search_res2->message->dn)); + return LDB_ERR_NAMING_VIOLATION; + } + } + + /* Ensure we are not trying to rename it to be a child of itself */ + if ((ldb_dn_compare_base(ac->req->op.rename.olddn, + ac->req->op.rename.newdn) == 0) && + (ldb_dn_compare(ac->req->op.rename.olddn, + ac->req->op.rename.newdn) != 0)) { + ldb_asprintf_errstring(ldb, "objectclass: Cannot rename %s to be a child of itself", + ldb_dn_get_linearized(ac->req->op.rename.olddn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* Fix up the DN to be in the standard form, taking + * particular care to match the parent DN */ + ret = fix_dn(ldb, ac, + ac->req->op.rename.newdn, + ac->search_res2->message->dn, + &fixed_dn); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "objectclass: Could not munge DN %s into normal form", + ldb_dn_get_linearized(ac->req->op.rename.newdn)); + return ret; + + } + + ret = ldb_build_rename_req(&rename_req, ldb, ac, + ac->req->op.rename.olddn, fixed_dn, + ac->req->controls, + ac->req, dsdb_next_callback, + ac->req); + LDB_REQ_SET_LOCATION(rename_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* perform the rename */ + return ldb_next_request(ac->module, rename_req); +} + +static int objectclass_do_delete(struct oc_context *ac); + +static int objectclass_delete(struct ldb_module *module, struct ldb_request *req) +{ + static const char * const attrs[] = { "nCName", "objectClass", + "systemFlags", + "isDeleted", + "isCriticalSystemObject", NULL }; + struct ldb_context *ldb; + struct ldb_request *search_req; + struct oc_context *ac; + int ret; + + ldb = ldb_module_get_ctx(module); + + ldb_debug(ldb, LDB_DEBUG_TRACE, "objectclass_delete\n"); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.del.dn)) { + return ldb_next_request(module, req); + } + + /* Bypass the constraint checks when we do have the "RELAX" control + * set. */ + if (ldb_request_get_control(req, LDB_CONTROL_RELAX_OID) != NULL) { + return ldb_next_request(module, req); + } + + ac = oc_init_context(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + /* this looks up the entry object for fetching some important + * information (object classes, system flags...) */ + ret = ldb_build_search_req(&search_req, ldb, + ac, req->op.del.dn, LDB_SCOPE_BASE, + "(objectClass=*)", + attrs, NULL, + ac, get_search_callback, + req); + LDB_REQ_SET_LOCATION(search_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = dsdb_request_add_controls(search_req, + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_RECYCLED); + if (ret != LDB_SUCCESS) { + return ret; + } + + ac->step_fn = objectclass_do_delete; + + return ldb_next_request(ac->module, search_req); +} + +static int objectclass_do_delete(struct oc_context *ac) +{ + struct ldb_context *ldb; + struct ldb_dn *dn; + int32_t systemFlags; + bool isCriticalSystemObject; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + /* Check if we have a valid entry - this check is needed since + * we don't get a LDB_ERR_NO_SUCH_OBJECT error. */ + if (ac->search_res == NULL) { + ldb_asprintf_errstring(ldb, "objectclass: Cannot delete %s, entry does not exist!", + ldb_dn_get_linearized(ac->req->op.del.dn)); + return LDB_ERR_NO_SUCH_OBJECT; + } + + /* DC's ntDSDSA object */ + if (ldb_dn_compare(ac->req->op.del.dn, samdb_ntds_settings_dn(ldb, ac)) == 0) { + ldb_asprintf_errstring(ldb, "objectclass: Cannot delete %s, it's the DC's ntDSDSA object!", + ldb_dn_get_linearized(ac->req->op.del.dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* DC's rIDSet object */ + /* Perform this check only when it does exist - this is needed in order + * to don't let existing provisions break, and to delete . */ + ret = samdb_rid_set_dn(ldb, ac, &dn); + if ((ret != LDB_SUCCESS) && (ret != LDB_ERR_NO_SUCH_ATTRIBUTE) + && (ret != LDB_ERR_NO_SUCH_OBJECT)) { + ldb_asprintf_errstring(ldb, "objectclass: Unable to determine if %s, is this DC's rIDSet object: %s ", + ldb_dn_get_linearized(ac->req->op.del.dn), + ldb_errstring(ldb)); + return ret; + } + if (ret == LDB_SUCCESS) { + if (ldb_dn_compare(ac->req->op.del.dn, dn) == 0) { + talloc_free(dn); + ldb_asprintf_errstring(ldb, "objectclass: Cannot delete %s, it's the DC's rIDSet object!", + ldb_dn_get_linearized(ac->req->op.del.dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + talloc_free(dn); + } + + /* Only trusted request from system account are allowed to delete + * deleted objects. + */ + if (ldb_msg_check_string_attribute(ac->search_res->message, "isDeleted", "TRUE") && + (ldb_req_is_untrusted(ac->req) || + !dsdb_module_am_system(ac->module))) { + ldb_asprintf_errstring(ldb, "Delete of '%s' failed", + ldb_dn_get_linearized(ac->req->op.del.dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* crossRef objects regarding config, schema and default domain NCs */ + if (samdb_find_attribute(ldb, ac->search_res->message, "objectClass", + "crossRef") != NULL) { + dn = ldb_msg_find_attr_as_dn(ldb, ac, ac->search_res->message, + "nCName"); + if ((ldb_dn_compare(dn, ldb_get_default_basedn(ldb)) == 0) || + (ldb_dn_compare(dn, ldb_get_config_basedn(ldb)) == 0)) { + talloc_free(dn); + + ldb_asprintf_errstring(ldb, "objectclass: Cannot delete %s, it's a crossRef object to the main or configuration partition!", + ldb_dn_get_linearized(ac->req->op.del.dn)); + return LDB_ERR_NOT_ALLOWED_ON_NON_LEAF; + } + if (ldb_dn_compare(dn, ldb_get_schema_basedn(ldb)) == 0) { + talloc_free(dn); + + ldb_asprintf_errstring(ldb, "objectclass: Cannot delete %s, it's a crossRef object to the schema partition!", + ldb_dn_get_linearized(ac->req->op.del.dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + talloc_free(dn); + } + + /* systemFlags */ + + systemFlags = ldb_msg_find_attr_as_int(ac->search_res->message, + "systemFlags", 0); + if ((systemFlags & SYSTEM_FLAG_DISALLOW_DELETE) != 0) { + ldb_asprintf_errstring(ldb, "objectclass: Cannot delete %s, it isn't permitted!", + ldb_dn_get_linearized(ac->req->op.del.dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* isCriticalSystemObject - but this only applies on tree delete + * operations - MS-ADTS 3.1.1.5.5.7.2 */ + if (ldb_request_get_control(ac->req, LDB_CONTROL_TREE_DELETE_OID) != NULL) { + isCriticalSystemObject = ldb_msg_find_attr_as_bool(ac->search_res->message, + "isCriticalSystemObject", false); + if (isCriticalSystemObject) { + /* + * Following the explaination from Microsoft + * https://lists.samba.org/archive/cifs-protocol/2011-August/002046.html + * "I finished the investigation on this behavior. + * As per MS-ADTS 3.1.5.5.7.2 , when a tree deletion is performed , + * every object in the tree will be checked to see if it has isCriticalSystemObject + * set to TRUE, including the root node on which the delete operation is performed + * But there is an exception if the root object is a SAM specific objects(3.1.1.5.2.3 MS-ADTS) + * Its deletion is done through SAM manger and isCriticalSystemObject attribute is not checked + * The root node of the tree delete in your case is CN=ARES,OU=Domain Controllers,DC=w2k8r2,DC=home,DC=matws,DC=net + * which is a SAM object with user class. Therefore the tree deletion is performed without any error + */ + + if (samdb_find_attribute(ldb, ac->search_res->message, "objectClass", "group") == NULL && + samdb_find_attribute(ldb, ac->search_res->message, "objectClass", "samDomain") == NULL && + samdb_find_attribute(ldb, ac->search_res->message, "objectClass", "samServer") == NULL && + samdb_find_attribute(ldb, ac->search_res->message, "objectClass", "user") == NULL) { + ldb_asprintf_errstring(ldb, + "objectclass: Cannot tree-delete %s, it's a critical system object!", + ldb_dn_get_linearized(ac->req->op.del.dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + } + + return ldb_next_request(ac->module, ac->req); +} + +static int objectclass_init(struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + int ret; + + /* Init everything else */ + ret = ldb_next_init(module); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* Look for the opaque to indicate we might have to cut down the DN of defaultObjectCategory */ + ldb_module_set_private(module, ldb_get_opaque(ldb, DSDB_EXTENDED_DN_STORE_FORMAT_OPAQUE_NAME)); + + ret = ldb_mod_register_control(module, LDB_CONTROL_RODC_DCPROMO_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "objectclass_init: Unable to register control DCPROMO with rootdse\n"); + return ldb_operr(ldb); + } + + return ret; +} + +static const struct ldb_module_ops ldb_objectclass_module_ops = { + .name = "objectclass", + .add = objectclass_add, + .modify = objectclass_modify, + .rename = objectclass_rename, + .del = objectclass_delete, + .init_context = objectclass_init +}; + +int ldb_objectclass_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_objectclass_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/objectclass_attrs.c b/source4/dsdb/samdb/ldb_modules/objectclass_attrs.c new file mode 100644 index 0000000..2a77353 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/objectclass_attrs.c @@ -0,0 +1,744 @@ +/* + ldb database library + + Copyright (C) Simo Sorce 2006-2008 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2005-2009 + Copyright (C) Stefan Metzmacher 2009 + Copyright (C) Matthias Dieter Wallnöfer 2010 + + 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 Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ + +/* + * Name: ldb + * + * Component: objectclass attribute checking module + * + * Description: this checks the attributes on a directory entry (if they're + * allowed, if the syntax is correct, if mandatory ones are missing, + * denies the deletion of mandatory ones...). The module contains portions + * of the "objectclass" and the "validate_update" LDB module. + * + * Author: Matthias Dieter Wallnöfer + */ + +#include "includes.h" +#include "ldb_module.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" + +#undef strcasecmp + +struct oc_context { + + struct ldb_module *module; + struct ldb_request *req; + const struct dsdb_schema *schema; + + struct ldb_message *msg; + + struct ldb_reply *search_res; + struct ldb_reply *mod_ares; +}; + +static struct oc_context *oc_init_context(struct ldb_module *module, + struct ldb_request *req) +{ + struct ldb_context *ldb; + struct oc_context *ac; + + ldb = ldb_module_get_ctx(module); + + ac = talloc_zero(req, struct oc_context); + if (ac == NULL) { + ldb_oom(ldb); + return NULL; + } + + ac->module = module; + ac->req = req; + ac->schema = dsdb_get_schema(ldb, ac); + + return ac; +} + +static int oc_op_callback(struct ldb_request *req, struct ldb_reply *ares); + +/* + * Checks the correctness of the "dSHeuristics" attribute as described in both + * MS-ADTS 7.1.1.2.4.1.2 dSHeuristics and MS-ADTS 3.1.1.5.3.2 Constraints + */ +static int oc_validate_dsheuristics(struct ldb_message_element *el) +{ + if (el->num_values > 0) { + if ((el->values[0].length >= DS_HR_NINETIETH_CHAR) && + (el->values[0].data[DS_HR_NINETIETH_CHAR-1] != '9')) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + if ((el->values[0].length >= DS_HR_EIGHTIETH_CHAR) && + (el->values[0].data[DS_HR_EIGHTIETH_CHAR-1] != '8')) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + if ((el->values[0].length >= DS_HR_SEVENTIETH_CHAR) && + (el->values[0].data[DS_HR_SEVENTIETH_CHAR-1] != '7')) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + if ((el->values[0].length >= DS_HR_SIXTIETH_CHAR) && + (el->values[0].data[DS_HR_SIXTIETH_CHAR-1] != '6')) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + if ((el->values[0].length >= DS_HR_FIFTIETH_CHAR) && + (el->values[0].data[DS_HR_FIFTIETH_CHAR-1] != '5')) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + if ((el->values[0].length >= DS_HR_FOURTIETH_CHAR) && + (el->values[0].data[DS_HR_FOURTIETH_CHAR-1] != '4')) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + if ((el->values[0].length >= DS_HR_THIRTIETH_CHAR) && + (el->values[0].data[DS_HR_THIRTIETH_CHAR-1] != '3')) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + if ((el->values[0].length >= DS_HR_TWENTIETH_CHAR) && + (el->values[0].data[DS_HR_TWENTIETH_CHAR-1] != '2')) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + if ((el->values[0].length >= DS_HR_TENTH_CHAR) && + (el->values[0].data[DS_HR_TENTH_CHAR-1] != '1')) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } + + return LDB_SUCCESS; +} + +/* + auto normalise values on input + */ +static int oc_auto_normalise(struct ldb_context *ldb, const struct dsdb_attribute *attr, + struct ldb_message *msg, struct ldb_message_element *el) +{ + int i; + bool values_copied = false; + + for (i=0; i<el->num_values; i++) { + struct ldb_val v; + int ret; + /* + * We use msg->elements (owned by this module due to + * ldb_msg_copy_shallow()) as a memory context and + * then steal from there to the right spot if we don't + * free it. + */ + ret = attr->ldb_schema_attribute->syntax->canonicalise_fn(ldb, + msg->elements, + &el->values[i], + &v); + if (ret != LDB_SUCCESS) { + return ret; + } + if (data_blob_cmp(&v, &el->values[i]) == 0) { + /* no need to replace it */ + talloc_free(v.data); + continue; + } + + /* we need to copy the values array on the first change */ + if (!values_copied) { + struct ldb_val *v2; + v2 = talloc_array(msg->elements, struct ldb_val, el->num_values); + if (v2 == NULL) { + return ldb_oom(ldb); + } + memcpy(v2, el->values, sizeof(struct ldb_val) * el->num_values); + el->values = v2; + values_copied = true; + } + + el->values[i] = v; + + /* + * By now el->values is a talloc pointer under + * msg->elements and may now be used + */ + talloc_steal(el->values, v.data); + } + return LDB_SUCCESS; +} + +static int attr_handler(struct oc_context *ac) +{ + struct ldb_context *ldb; + struct ldb_message *msg; + struct ldb_request *child_req; + const struct dsdb_attribute *attr; + unsigned int i; + int ret; + WERROR werr; + struct dsdb_syntax_ctx syntax_ctx; + + ldb = ldb_module_get_ctx(ac->module); + + if (ac->req->operation == LDB_ADD) { + msg = ldb_msg_copy_shallow(ac, ac->req->op.add.message); + } else { + msg = ldb_msg_copy_shallow(ac, ac->req->op.mod.message); + } + if (msg == NULL) { + return ldb_oom(ldb); + } + ac->msg = msg; + + /* initialize syntax checking context */ + dsdb_syntax_ctx_init(&syntax_ctx, ldb, ac->schema); + + /* Check if attributes exist in the schema, if the values match, + * if they're not operational and fix the names to the match the schema + * case */ + for (i = 0; i < msg->num_elements; i++) { + attr = dsdb_attribute_by_lDAPDisplayName(ac->schema, + msg->elements[i].name); + if (attr == NULL) { + if (ldb_request_get_control(ac->req, DSDB_CONTROL_DBCHECK) && + ac->req->operation != LDB_ADD) { + /* we allow this for dbcheck to fix + broken attributes */ + goto no_attribute; + } + ldb_asprintf_errstring(ldb, "objectclass_attrs: attribute '%s' on entry '%s' was not found in the schema!", + msg->elements[i].name, + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_NO_SUCH_ATTRIBUTE; + } + + if ((attr->linkID & 1) == 1 && + !ldb_request_get_control(ac->req, LDB_CONTROL_RELAX_OID) && + !ldb_request_get_control(ac->req, DSDB_CONTROL_DBCHECK)) { + /* Odd is for the target. Illegal to modify */ + ldb_asprintf_errstring(ldb, + "objectclass_attrs: attribute '%s' on entry '%s' must not be modified directly, it is a linked attribute", + msg->elements[i].name, + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* + * Enforce systemOnly checks from [ADTS] 3.1.1.5.3.2 + * Constraints in Modify Operation + */ + if (ac->req->operation == LDB_MODIFY && attr->systemOnly) { + /* + * Allow dbcheck and relax to bypass. objectClass, name + * and distinguishedName are generally handled + * elsewhere. + * + * The remaining cases, undelete, msDS-AdditionalDnsHostName + * and wellKnownObjects are documented in the specification. + */ + if (!ldb_request_get_control(ac->req, LDB_CONTROL_RELAX_OID) && + !ldb_request_get_control(ac->req, DSDB_CONTROL_DBCHECK) && + !ldb_request_get_control(ac->req, DSDB_CONTROL_RESTORE_TOMBSTONE_OID) && + ldb_attr_cmp(attr->lDAPDisplayName, "objectClass") != 0 && + ldb_attr_cmp(attr->lDAPDisplayName, "name") != 0 && + ldb_attr_cmp(attr->lDAPDisplayName, "distinguishedName") != 0 && + ldb_attr_cmp(attr->lDAPDisplayName, "msDS-AdditionalDnsHostName") != 0 && + ldb_attr_cmp(attr->lDAPDisplayName, "wellKnownObjects") != 0) { + /* + * Comparison against base schema DN is used as a substitute for + * fschemaUpgradeInProgress and other specific schema checks. + */ + if (ldb_dn_compare_base(ldb_get_schema_basedn(ldb), msg->dn) != 0) { + struct ldb_control *as_system = ldb_request_get_control(ac->req, + LDB_CONTROL_AS_SYSTEM_OID); + if (!dsdb_module_am_system(ac->module) && !as_system) { + ldb_asprintf_errstring(ldb, + "objectclass_attrs: attribute '%s' on entry '%s' can only be modified as system", + msg->elements[i].name, + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } + } + } + + if (!(msg->elements[i].flags & LDB_FLAG_INTERNAL_DISABLE_VALIDATION)) { + werr = attr->syntax->validate_ldb(&syntax_ctx, attr, + &msg->elements[i]); + if (!W_ERROR_IS_OK(werr) && + !ldb_request_get_control(ac->req, DSDB_CONTROL_DBCHECK)) { + ldb_asprintf_errstring(ldb, "objectclass_attrs: attribute '%s' on entry '%s' contains at least one invalid value!", + msg->elements[i].name, + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_INVALID_ATTRIBUTE_SYNTAX; + } + } + + if ((attr->systemFlags & DS_FLAG_ATTR_IS_CONSTRUCTED) != 0) { + ldb_asprintf_errstring(ldb, "objectclass_attrs: attribute '%s' on entry '%s' is constructed!", + msg->elements[i].name, + ldb_dn_get_linearized(msg->dn)); + if (ac->req->operation == LDB_ADD) { + return LDB_ERR_UNDEFINED_ATTRIBUTE_TYPE; + } else { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } + + /* "dSHeuristics" syntax check */ + if (ldb_attr_cmp(attr->lDAPDisplayName, "dSHeuristics") == 0) { + ret = oc_validate_dsheuristics(&(msg->elements[i])); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* auto normalise some attribute values */ + if (attr->syntax->auto_normalise) { + ret = oc_auto_normalise(ldb, attr, msg, &msg->elements[i]); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* Substitute the attribute name to match in case */ + msg->elements[i].name = attr->lDAPDisplayName; + } + +no_attribute: + if (ac->req->operation == LDB_ADD) { + ret = ldb_build_add_req(&child_req, ldb, ac, + msg, ac->req->controls, + ac, oc_op_callback, ac->req); + LDB_REQ_SET_LOCATION(child_req); + } else { + ret = ldb_build_mod_req(&child_req, ldb, ac, + msg, ac->req->controls, + ac, oc_op_callback, ac->req); + LDB_REQ_SET_LOCATION(child_req); + } + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(ac->module, child_req); +} + +/* + these are attributes which are left over from old ways of doing + things in ldb, and are harmless + */ +static const char *harmless_attrs[] = { "parentGUID", NULL }; + +static int attr_handler2(struct oc_context *ac) +{ + struct ldb_context *ldb; + struct ldb_message_element *oc_element; + struct ldb_message *msg; + const char **must_contain, **may_contain, **found_must_contain; + /* There exists a hardcoded delete-protected attributes list in AD */ + const char *del_prot_attributes[] = { "nTSecurityDescriptor", + "objectSid", "sAMAccountType", "sAMAccountName", "groupType", + "primaryGroupID", "userAccountControl", "accountExpires", + "badPasswordTime", "badPwdCount", "codePage", "countryCode", + "lastLogoff", "lastLogon", "logonCount", "pwdLastSet", NULL }, + **l; + const struct dsdb_attribute *attr; + unsigned int i; + bool found; + bool isSchemaAttr = false; + + ldb = ldb_module_get_ctx(ac->module); + + if (ac->search_res == NULL) { + return ldb_operr(ldb); + } + + /* We rely here on the preceding "objectclass" LDB module which did + * already fix up the objectclass list (inheritance, order...). */ + oc_element = ldb_msg_find_element(ac->search_res->message, + "objectClass"); + if (oc_element == NULL) { + return ldb_operr(ldb); + } + + /* LSA-specific object classes are not allowed to be created over LDAP, + * so we need to tell if this connection is internal (trusted) or not + * (untrusted). + * + * Hongwei Sun from Microsoft explains: + * The constraint in 3.1.1.5.2.2 MS-ADTS means that LSA objects cannot + * be added or modified through the LDAP interface, instead they can + * only be handled through LSA Policy API. This is also explained in + * 7.1.6.9.7 MS-ADTS as follows: + * "Despite being replicated normally between peer DCs in a domain, + * the process of creating or manipulating TDOs is specifically + * restricted to the LSA Policy APIs, as detailed in [MS-LSAD] section + * 3.1.1.5. Unlike other objects in the DS, TDOs may not be created or + * manipulated by client machines over the LDAPv3 transport." + */ + for (i = 0; i < oc_element->num_values; i++) { + char * attname = (char *)oc_element->values[i].data; + if (ldb_req_is_untrusted(ac->req)) { + if (strcmp(attname, "secret") == 0 || + strcmp(attname, "trustedDomain") == 0) { + ldb_asprintf_errstring(ldb, "objectclass_attrs: LSA objectclasses (entry '%s') cannot be created or changed over LDAP!", + ldb_dn_get_linearized(ac->search_res->message->dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + if (strcmp(attname, "attributeSchema") == 0) { + isSchemaAttr = true; + } + } + + must_contain = dsdb_full_attribute_list(ac, ac->schema, oc_element, + DSDB_SCHEMA_ALL_MUST); + may_contain = dsdb_full_attribute_list(ac, ac->schema, oc_element, + DSDB_SCHEMA_ALL_MAY); + found_must_contain = const_str_list(str_list_copy(ac, must_contain)); + if ((must_contain == NULL) || (may_contain == NULL) + || (found_must_contain == NULL)) { + return ldb_operr(ldb); + } + + /* Check the delete-protected attributes list */ + msg = ac->search_res->message; + for (l = del_prot_attributes; *l != NULL; l++) { + struct ldb_message_element *el; + + el = ldb_msg_find_element(ac->msg, *l); + if (el == NULL) { + /* + * It was not specified in the add or modify, + * so it doesn't need to be in the stored record + */ + continue; + } + + found = str_list_check_ci(must_contain, *l); + if (!found) { + found = str_list_check_ci(may_contain, *l); + } + if (found && (ldb_msg_find_element(msg, *l) == NULL)) { + ldb_asprintf_errstring(ldb, "objectclass_attrs: delete protected attribute '%s' on entry '%s' missing!", + *l, + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + + /* Check if all specified attributes are valid in the given + * objectclasses and if they meet additional schema restrictions. */ + for (i = 0; i < msg->num_elements; i++) { + attr = dsdb_attribute_by_lDAPDisplayName(ac->schema, + msg->elements[i].name); + if (attr == NULL) { + if (ldb_request_get_control(ac->req, DSDB_CONTROL_DBCHECK)) { + /* allow this to make it possible for dbcheck + to remove bad attributes */ + continue; + } + return ldb_operr(ldb); + } + + /* We can use "str_list_check" with "strcmp" here since the + * attribute information from the schema are always equal + * up-down-cased. */ + found = str_list_check(must_contain, attr->lDAPDisplayName); + if (found) { + str_list_remove(found_must_contain, attr->lDAPDisplayName); + } else { + found = str_list_check(may_contain, attr->lDAPDisplayName); + } + if (!found) { + found = str_list_check(harmless_attrs, attr->lDAPDisplayName); + } + if (!found) { + /* we allow this for dbcheck to fix the rest of this broken entry */ + if (!ldb_request_get_control(ac->req, DSDB_CONTROL_DBCHECK) || + ac->req->operation == LDB_ADD) { + ldb_asprintf_errstring(ldb, "objectclass_attrs: attribute '%s' on entry '%s' does not exist in the specified objectclasses!", + msg->elements[i].name, + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + } + } + + /* + * We skip this check under dbcheck to allow fixing of other + * attributes even if an attribute is missing. This matters + * for CN=RID Set as the required attribute rIDNextRid is not + * replicated. + */ + if (found_must_contain[0] != NULL && + ldb_msg_check_string_attribute(msg, "isDeleted", "TRUE") == 0) { + + for (i = 0; found_must_contain[i] != NULL; i++) { + const struct dsdb_attribute *broken_attr = dsdb_attribute_by_lDAPDisplayName(ac->schema, + found_must_contain[i]); + + bool replicated = (broken_attr->systemFlags & + (DS_FLAG_ATTR_NOT_REPLICATED | DS_FLAG_ATTR_IS_CONSTRUCTED)) == 0; + + if (replicated) { + ldb_asprintf_errstring(ldb, "objectclass_attrs: at least one mandatory " + "attribute ('%s') on entry '%s' wasn't specified!", + found_must_contain[i], + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + } + } + + if (isSchemaAttr) { + /* + * Before really adding an attribute in the database, + * let's check that we can translate it into a dsdb_attribute and + * that we can find a valid syntax object. + * If not it's better to reject this attribute than not be able + * to start samba next time due to schema being unloadable. + */ + struct dsdb_attribute *att = talloc(ac, struct dsdb_attribute); + const struct dsdb_syntax *attrSyntax; + WERROR status; + + status = dsdb_attribute_from_ldb(NULL, msg, att); + if (!W_ERROR_IS_OK(status)) { + ldb_set_errstring(ldb, + "objectclass: failed to translate the schemaAttribute to a dsdb_attribute"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + attrSyntax = dsdb_syntax_for_attribute(att); + if (!attrSyntax) { + ldb_set_errstring(ldb, + "objectclass: unknown attribute syntax"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + return ldb_module_done(ac->req, ac->mod_ares->controls, + ac->mod_ares->response, LDB_SUCCESS); +} + +static int get_search_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct oc_context *ac; + int ret; + + ac = talloc_get_type(req->context, struct oc_context); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + ldb_reset_err_string(ldb); + + switch (ares->type) { + case LDB_REPLY_ENTRY: + if (ac->search_res != NULL) { + ldb_set_errstring(ldb, "Too many results"); + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + ac->search_res = talloc_steal(ac, ares); + break; + + case LDB_REPLY_REFERRAL: + /* ignore */ + talloc_free(ares); + break; + + case LDB_REPLY_DONE: + talloc_free(ares); + ret = attr_handler2(ac); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + break; + } + + return LDB_SUCCESS; +} + +static int oc_op_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct oc_context *ac; + struct ldb_context *ldb; + struct ldb_request *search_req; + struct ldb_dn *base_dn; + int ret; + static const char *attrs[] = {"nTSecurityDescriptor", "*", NULL}; + + ac = talloc_get_type(req->context, struct oc_context); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + if (ares->type == LDB_REPLY_REFERRAL) { + return ldb_module_send_referral(ac->req, ares->referral); + } + + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, ares->response, + ares->error); + } + + if (ares->type != LDB_REPLY_DONE) { + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + ac->search_res = NULL; + ac->mod_ares = talloc_steal(ac, ares); + + /* This looks up all attributes of our just added/modified entry */ + base_dn = ac->req->operation == LDB_ADD ? ac->req->op.add.message->dn + : ac->req->op.mod.message->dn; + ret = ldb_build_search_req(&search_req, ldb, ac, base_dn, + LDB_SCOPE_BASE, "(objectClass=*)", + attrs, NULL, ac, + get_search_callback, ac->req); + LDB_REQ_SET_LOCATION(search_req); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + ret = ldb_request_add_control(search_req, LDB_CONTROL_SHOW_RECYCLED_OID, + true, NULL); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + /* + * This ensures we see if there was a DN, that pointed at an + * object that is now deleted, that we still consider the + * schema check to have passed + */ + ret = ldb_request_add_control(search_req, LDB_CONTROL_REVEAL_INTERNALS, + false, NULL); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + ret = ldb_next_request(ac->module, search_req); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + /* "ldb_module_done" isn't called here since we need to do additional + * checks. It is called at the end of "attr_handler2". */ + return LDB_SUCCESS; +} + +static int objectclass_attrs_add(struct ldb_module *module, + struct ldb_request *req) +{ + struct ldb_context *ldb; + struct oc_context *ac; + + ldb = ldb_module_get_ctx(module); + + ldb_debug(ldb, LDB_DEBUG_TRACE, "objectclass_attrs_add\n"); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.add.message->dn)) { + return ldb_next_request(module, req); + } + + ac = oc_init_context(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + /* without schema, there isn't much to do here */ + if (ac->schema == NULL) { + talloc_free(ac); + return ldb_next_request(module, req); + } + + return attr_handler(ac); +} + +static int objectclass_attrs_modify(struct ldb_module *module, + struct ldb_request *req) +{ + struct ldb_context *ldb; + struct ldb_control *sd_propagation_control; + int ret; + + struct oc_context *ac; + + ldb = ldb_module_get_ctx(module); + + ldb_debug(ldb, LDB_DEBUG_TRACE, "objectclass_attrs_modify\n"); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.mod.message->dn)) { + return ldb_next_request(module, req); + } + + sd_propagation_control = ldb_request_get_control(req, + DSDB_CONTROL_SEC_DESC_PROPAGATION_OID); + if (sd_propagation_control != NULL) { + if (req->op.mod.message->num_elements != 1) { + return ldb_module_operr(module); + } + ret = strcmp(req->op.mod.message->elements[0].name, + "nTSecurityDescriptor"); + if (ret != 0) { + return ldb_module_operr(module); + } + + return ldb_next_request(module, req); + } + + ac = oc_init_context(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + /* without schema, there isn't much to do here */ + if (ac->schema == NULL) { + talloc_free(ac); + return ldb_next_request(module, req); + } + + return attr_handler(ac); +} + +static const struct ldb_module_ops ldb_objectclass_attrs_module_ops = { + .name = "objectclass_attrs", + .add = objectclass_attrs_add, + .modify = objectclass_attrs_modify +}; + +int ldb_objectclass_attrs_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_objectclass_attrs_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/objectguid.c b/source4/dsdb/samdb/ldb_modules/objectguid.c new file mode 100644 index 0000000..cfc8918 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/objectguid.c @@ -0,0 +1,252 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2005 + Copyright (C) Andrew Tridgell 2005 + Copyright (C) Simo Sorce 2004-2008 + + 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 objectguid module + * + * Description: add a unique objectGUID onto every new record + * + * Author: Simo Sorce + */ + +#include "includes.h" +#include "ldb_module.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "librpc/gen_ndr/ndr_misc.h" +#include "param/param.h" + +/* + add a time element to a record +*/ +static int add_time_element(struct ldb_message *msg, const char *attr, time_t t) +{ + char *s; + int ret; + + if (ldb_msg_find_element(msg, attr) != NULL) { + return LDB_SUCCESS; + } + + s = ldb_timestring(msg, t); + if (s == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + /* always set as replace. This works because on add ops, the flag + is ignored */ + ret = ldb_msg_append_string(msg, attr, s, LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ret; + } + + return LDB_SUCCESS; +} + +/* + add a uint64_t element to a record +*/ +static int add_uint64_element(struct ldb_context *ldb, struct ldb_message *msg, + const char *attr, uint64_t v) +{ + int ret; + + if (ldb_msg_find_element(msg, attr) != NULL) { + return LDB_SUCCESS; + } + + /* always set as replace. This works because on add ops, the flag + is ignored */ + ret = samdb_msg_append_uint64(ldb, msg, msg, attr, v, LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ret; + } + + return LDB_SUCCESS; +} + +struct og_context { + struct ldb_module *module; + struct ldb_request *req; +}; + +/* add_record: add objectGUID and timestamp attributes */ +static int objectguid_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct ldb_request *down_req; + struct ldb_message *msg; + struct ldb_message_element *el; + struct GUID guid; + uint64_t seq_num; + int ret; + time_t t = time(NULL); + struct og_context *ac; + + ldb = ldb_module_get_ctx(module); + + ldb_debug(ldb, LDB_DEBUG_TRACE, "objectguid_add_record\n"); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.add.message->dn)) { + return ldb_next_request(module, req); + } + + el = ldb_msg_find_element(req->op.add.message, "objectGUID"); + if (el != NULL) { + ldb_set_errstring(ldb, + "objectguid: objectGUID must not be specified!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + ac = talloc(req, struct og_context); + if (ac == NULL) { + return ldb_oom(ldb); + } + ac->module = module; + ac->req = req; + + /* we have to copy the message as the caller might have it as a const */ + msg = ldb_msg_copy_shallow(ac, req->op.add.message); + if (msg == NULL) { + talloc_free(ac); + return ldb_operr(ldb); + } + + /* a new GUID */ + guid = GUID_random(); + + ret = dsdb_msg_add_guid(msg, &guid, "objectGUID"); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (add_time_element(msg, "whenCreated", t) != LDB_SUCCESS || + add_time_element(msg, "whenChanged", t) != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + /* Get a sequence number from the backend */ + ret = ldb_sequence_number(ldb, LDB_SEQ_NEXT, &seq_num); + if (ret == LDB_SUCCESS) { + if (add_uint64_element(ldb, msg, "uSNCreated", + seq_num) != LDB_SUCCESS || + add_uint64_element(ldb, msg, "uSNChanged", + seq_num) != LDB_SUCCESS) { + return ldb_operr(ldb); + } + } + + ret = ldb_build_add_req(&down_req, ldb, ac, + msg, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* go on with the call chain */ + return ldb_next_request(module, down_req); +} + +/* modify_record: update timestamps */ +static int objectguid_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct ldb_request *down_req; + struct ldb_message *msg; + struct ldb_message_element *el; + int ret; + time_t t = time(NULL); + uint64_t seq_num; + struct og_context *ac; + + ldb = ldb_module_get_ctx(module); + + ldb_debug(ldb, LDB_DEBUG_TRACE, "objectguid_modify_record\n"); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.mod.message->dn)) { + return ldb_next_request(module, req); + } + + el = ldb_msg_find_element(req->op.mod.message, "objectGUID"); + if (el != NULL) { + ldb_set_errstring(ldb, + "objectguid: objectGUID must not be specified!"); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + ac = talloc(req, struct og_context); + if (ac == NULL) { + return ldb_oom(ldb); + } + ac->module = module; + ac->req = req; + + /* we have to copy the message as the caller might have it as a const */ + msg = ldb_msg_copy_shallow(ac, req->op.mod.message); + if (msg == NULL) { + return ldb_operr(ldb); + } + + if (add_time_element(msg, "whenChanged", t) != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + /* Get a sequence number from the backend */ + ret = ldb_sequence_number(ldb, LDB_SEQ_NEXT, &seq_num); + if (ret == LDB_SUCCESS) { + if (add_uint64_element(ldb, msg, "uSNChanged", + seq_num) != LDB_SUCCESS) { + return ldb_operr(ldb); + } + } + + ret = ldb_build_mod_req(&down_req, ldb, ac, + msg, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* go on with the call chain */ + return ldb_next_request(module, down_req); +} + +static const struct ldb_module_ops ldb_objectguid_module_ops = { + .name = "objectguid", + .add = objectguid_add, + .modify = objectguid_modify +}; + +int ldb_objectguid_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_objectguid_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/operational.c b/source4/dsdb/samdb/ldb_modules/operational.c new file mode 100644 index 0000000..214079c --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/operational.c @@ -0,0 +1,1829 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2001-2010 + Copyright (C) Andrew Tridgell 2005 + Copyright (C) Simo Sorce 2006-2008 + Copyright (C) Matthias Dieter Wallnöfer 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/>. +*/ + +/* + handle operational attributes + */ + +/* + createTimeStamp: HIDDEN, searchable, ldaptime, alias for whenCreated + modifyTimeStamp: HIDDEN, searchable, ldaptime, alias for whenChanged + + for the above two, we do the search as normal, and if + createTimeStamp or modifyTimeStamp is asked for, then do + additional searches for whenCreated and whenChanged and fill in + the resulting values + + we also need to replace these with the whenCreated/whenChanged + equivalent in the search expression trees + + whenCreated: not-HIDDEN, CONSTRUCTED, SEARCHABLE + whenChanged: not-HIDDEN, CONSTRUCTED, SEARCHABLE + + on init we need to setup attribute handlers for these so + comparisons are done correctly. The resolution is 1 second. + + on add we need to add both the above, for current time + + on modify we need to change whenChanged + + structuralObjectClass: HIDDEN, CONSTRUCTED, not-searchable. always same as objectclass? + + for this one we do the search as normal, then if requested ask + for objectclass, change the attribute name, and add it + + primaryGroupToken: HIDDEN, CONSTRUCTED, SEARCHABLE + + contains the RID of a certain group object + + + attributeTypes: in schema only + objectClasses: in schema only + matchingRules: in schema only + matchingRuleUse: in schema only + creatorsName: not supported by w2k3? + modifiersName: not supported by w2k3? +*/ + +#include "includes.h" +#include <ldb.h> +#include <ldb_module.h> + +#include "librpc/gen_ndr/ndr_misc.h" +#include "librpc/gen_ndr/ndr_drsblobs.h" +#include "param/param.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" + +#include "libcli/security/security.h" + +#ifndef ARRAY_SIZE +#define ARRAY_SIZE(a) (sizeof(a)/sizeof(a[0])) +#endif + +#undef strcasecmp + +struct operational_data { + struct ldb_dn *aggregate_dn; +}; + +enum search_type { + TOKEN_GROUPS, + TOKEN_GROUPS_GLOBAL_AND_UNIVERSAL, + TOKEN_GROUPS_NO_GC_ACCEPTABLE, + + /* + * MS-DRSR 4.1.8.1.3 RevMembGetAccountGroups: Transitive membership in + * all account groups in a given domain, excluding built-in groups. + * (Used internally for msDS-ResultantPSO support) + */ + ACCOUNT_GROUPS +}; + +static int get_pso_for_user(struct ldb_module *module, + struct ldb_message *user_msg, + struct ldb_request *parent, + struct ldb_message **pso_msg); + +/* + construct a canonical name from a message +*/ +static int construct_canonical_name(struct ldb_module *module, + struct ldb_message *msg, enum ldb_scope scope, + struct ldb_request *parent) +{ + char *canonicalName; + canonicalName = ldb_dn_canonical_string(msg, msg->dn); + if (canonicalName == NULL) { + return ldb_operr(ldb_module_get_ctx(module)); + } + return ldb_msg_add_steal_string(msg, "canonicalName", canonicalName); +} + +/* + construct a primary group token for groups from a message +*/ +static int construct_primary_group_token(struct ldb_module *module, + struct ldb_message *msg, enum ldb_scope scope, + struct ldb_request *parent) +{ + struct ldb_context *ldb; + uint32_t primary_group_token; + + ldb = ldb_module_get_ctx(module); + if (ldb_match_msg_objectclass(msg, "group") == 1) { + primary_group_token + = samdb_result_rid_from_sid(msg, msg, "objectSid", 0); + if (primary_group_token == 0) { + return LDB_SUCCESS; + } + + return samdb_msg_add_uint(ldb, msg, msg, "primaryGroupToken", + primary_group_token); + } else { + return LDB_SUCCESS; + } +} + +/* + * Returns the group SIDs for the user in the given LDB message + */ +static int get_group_sids(struct ldb_context *ldb, TALLOC_CTX *mem_ctx, + struct ldb_message *msg, const char *attribute_string, + enum search_type type, struct dom_sid **groupSIDs, + unsigned int *num_groupSIDs) +{ + const char *filter = NULL; + NTSTATUS status; + struct dom_sid *primary_group_sid; + const char *primary_group_string; + const char *primary_group_dn; + DATA_BLOB primary_group_blob; + struct dom_sid *account_sid; + const char *account_sid_string; + const char *account_sid_dn; + DATA_BLOB account_sid_blob; + struct dom_sid *domain_sid; + + /* If it's not a user, it won't have a primaryGroupID */ + if (ldb_msg_find_element(msg, "primaryGroupID") == NULL) { + return LDB_SUCCESS; + } + + /* Ensure it has an objectSID too */ + account_sid = samdb_result_dom_sid(mem_ctx, msg, "objectSid"); + if (account_sid == NULL) { + return LDB_SUCCESS; + } + + status = dom_sid_split_rid(mem_ctx, account_sid, &domain_sid, NULL); + if (NT_STATUS_EQUAL(status, NT_STATUS_INVALID_PARAMETER)) { + return LDB_ERR_INVALID_ATTRIBUTE_SYNTAX; + } else if (!NT_STATUS_IS_OK(status)) { + return LDB_ERR_OPERATIONS_ERROR; + } + + primary_group_sid = dom_sid_add_rid(mem_ctx, + domain_sid, + ldb_msg_find_attr_as_uint(msg, "primaryGroupID", ~0)); + if (!primary_group_sid) { + return ldb_oom(ldb); + } + + /* only return security groups */ + switch(type) { + case TOKEN_GROUPS_GLOBAL_AND_UNIVERSAL: + filter = talloc_asprintf(mem_ctx, "(&(objectClass=group)(groupType:1.2.840.113556.1.4.803:=%u)(|(groupType:1.2.840.113556.1.4.803:=%u)(groupType:1.2.840.113556.1.4.803:=%u)))", + GROUP_TYPE_SECURITY_ENABLED, GROUP_TYPE_ACCOUNT_GROUP, GROUP_TYPE_UNIVERSAL_GROUP); + break; + case TOKEN_GROUPS_NO_GC_ACCEPTABLE: + case TOKEN_GROUPS: + filter = talloc_asprintf(mem_ctx, "(&(objectClass=group)(groupType:1.2.840.113556.1.4.803:=%u))", + GROUP_TYPE_SECURITY_ENABLED); + break; + + /* for RevMembGetAccountGroups, exclude built-in groups */ + case ACCOUNT_GROUPS: + filter = talloc_asprintf(mem_ctx, "(&(objectClass=group)(!(groupType:1.2.840.113556.1.4.803:=%u))(groupType:1.2.840.113556.1.4.803:=%u))", + GROUP_TYPE_BUILTIN_LOCAL_GROUP, GROUP_TYPE_SECURITY_ENABLED); + break; + } + + if (!filter) { + return ldb_oom(ldb); + } + + primary_group_string = dom_sid_string(mem_ctx, primary_group_sid); + if (!primary_group_string) { + return ldb_oom(ldb); + } + + primary_group_dn = talloc_asprintf(mem_ctx, "<SID=%s>", primary_group_string); + if (!primary_group_dn) { + return ldb_oom(ldb); + } + + primary_group_blob = data_blob_string_const(primary_group_dn); + + account_sid_string = dom_sid_string(mem_ctx, account_sid); + if (!account_sid_string) { + return ldb_oom(ldb); + } + + account_sid_dn = talloc_asprintf(mem_ctx, "<SID=%s>", account_sid_string); + if (!account_sid_dn) { + return ldb_oom(ldb); + } + + account_sid_blob = data_blob_string_const(account_sid_dn); + + status = dsdb_expand_nested_groups(ldb, &account_sid_blob, + true, /* We don't want to add the object's SID itself, + it's not returend in this attribute */ + filter, + mem_ctx, groupSIDs, num_groupSIDs); + + if (!NT_STATUS_IS_OK(status)) { + ldb_asprintf_errstring(ldb, "Failed to construct %s: expanding groups of SID %s failed: %s", + attribute_string, account_sid_string, + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* Expands the primary group - this function takes in + * memberOf-like values, so we fake one up with the + * <SID=S-...> format of DN and then let it expand + * them, as long as they meet the filter - so only + * domain groups, not builtin groups + */ + status = dsdb_expand_nested_groups(ldb, &primary_group_blob, false, filter, + mem_ctx, groupSIDs, num_groupSIDs); + if (!NT_STATUS_IS_OK(status)) { + ldb_asprintf_errstring(ldb, "Failed to construct %s: expanding groups of SID %s failed: %s", + attribute_string, account_sid_string, + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + + return LDB_SUCCESS; +} + +/* + construct the token groups for SAM objects from a message +*/ +static int construct_generic_token_groups(struct ldb_module *module, + struct ldb_message *msg, enum ldb_scope scope, + struct ldb_request *parent, + const char *attribute_string, + enum search_type type) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + TALLOC_CTX *tmp_ctx = talloc_new(msg); + unsigned int i; + int ret; + struct dom_sid *groupSIDs = NULL; + unsigned int num_groupSIDs = 0; + + if (scope != LDB_SCOPE_BASE) { + ldb_set_errstring(ldb, "Cannot provide tokenGroups attribute, this is not a BASE search"); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* calculate the group SIDs for this object */ + ret = get_group_sids(ldb, tmp_ctx, msg, attribute_string, type, + &groupSIDs, &num_groupSIDs); + + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* add these SIDs to the search result */ + for (i=0; i < num_groupSIDs; i++) { + ret = samdb_msg_add_dom_sid(ldb, msg, msg, attribute_string, &groupSIDs[i]); + if (ret) { + talloc_free(tmp_ctx); + return ret; + } + } + + return LDB_SUCCESS; +} + +static int construct_token_groups(struct ldb_module *module, + struct ldb_message *msg, enum ldb_scope scope, + struct ldb_request *parent) +{ + /** + * TODO: Add in a limiting domain when we start to support + * trusted domains. + */ + return construct_generic_token_groups(module, msg, scope, parent, + "tokenGroups", + TOKEN_GROUPS); +} + +static int construct_token_groups_no_gc(struct ldb_module *module, + struct ldb_message *msg, enum ldb_scope scope, + struct ldb_request *parent) +{ + /** + * TODO: Add in a limiting domain when we start to support + * trusted domains. + */ + return construct_generic_token_groups(module, msg, scope, parent, + "tokenGroupsNoGCAcceptable", + TOKEN_GROUPS); +} + +static int construct_global_universal_token_groups(struct ldb_module *module, + struct ldb_message *msg, enum ldb_scope scope, + struct ldb_request *parent) +{ + return construct_generic_token_groups(module, msg, scope, parent, + "tokenGroupsGlobalAndUniversal", + TOKEN_GROUPS_GLOBAL_AND_UNIVERSAL); +} +/* + construct the parent GUID for an entry from a message +*/ +static int construct_parent_guid(struct ldb_module *module, + struct ldb_message *msg, enum ldb_scope scope, + struct ldb_request *parent) +{ + struct ldb_result *res, *parent_res; + const struct ldb_val *parent_guid; + const char *attrs[] = { "instanceType", NULL }; + const char *attrs2[] = { "objectGUID", NULL }; + uint32_t instanceType; + int ret; + struct ldb_dn *parent_dn; + struct ldb_val v; + + /* determine if the object is NC by instance type */ + ret = dsdb_module_search_dn(module, msg, &res, msg->dn, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_RECYCLED, parent); + if (ret != LDB_SUCCESS) { + return ret; + } + + instanceType = ldb_msg_find_attr_as_uint(res->msgs[0], + "instanceType", 0); + talloc_free(res); + if (instanceType & INSTANCE_TYPE_IS_NC_HEAD) { + DEBUG(4,(__location__ ": Object %s is NC\n", + ldb_dn_get_linearized(msg->dn))); + return LDB_SUCCESS; + } + parent_dn = ldb_dn_get_parent(msg, msg->dn); + + if (parent_dn == NULL) { + DEBUG(4,(__location__ ": Failed to find parent for dn %s\n", + ldb_dn_get_linearized(msg->dn))); + return LDB_ERR_OTHER; + } + ret = dsdb_module_search_dn(module, msg, &parent_res, parent_dn, attrs2, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_RECYCLED, parent); + /* not NC, so the object should have a parent*/ + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + ret = ldb_error(ldb_module_get_ctx(module), LDB_ERR_OPERATIONS_ERROR, + talloc_asprintf(msg, "Parent dn %s for %s does not exist", + ldb_dn_get_linearized(parent_dn), + ldb_dn_get_linearized(msg->dn))); + talloc_free(parent_dn); + return ret; + } else if (ret != LDB_SUCCESS) { + talloc_free(parent_dn); + return ret; + } + talloc_free(parent_dn); + + parent_guid = ldb_msg_find_ldb_val(parent_res->msgs[0], "objectGUID"); + if (!parent_guid) { + talloc_free(parent_res); + return LDB_ERR_INVALID_ATTRIBUTE_SYNTAX; + } + + v = data_blob_dup_talloc(parent_res, *parent_guid); + if (!v.data) { + talloc_free(parent_res); + return ldb_oom(ldb_module_get_ctx(module)); + } + ret = ldb_msg_add_steal_value(msg, "parentGUID", &v); + talloc_free(parent_res); + return ret; +} + +static int construct_modifyTimeStamp(struct ldb_module *module, + struct ldb_message *msg, enum ldb_scope scope, + struct ldb_request *parent) +{ + struct operational_data *data = talloc_get_type(ldb_module_get_private(module), struct operational_data); + struct ldb_context *ldb = ldb_module_get_ctx(module); + + /* We may be being called before the init function has finished */ + if (!data) { + return LDB_SUCCESS; + } + + /* Try and set this value up, if possible. Don't worry if it + * fails, we may not have the DB set up yet. + */ + if (!data->aggregate_dn) { + data->aggregate_dn = samdb_aggregate_schema_dn(ldb, data); + } + + if (data->aggregate_dn && ldb_dn_compare(data->aggregate_dn, msg->dn) == 0) { + /* + * If we have the DN for the object with common name = Aggregate and + * the request is for this DN then let's do the following: + * 1) search the object which changedUSN correspond to the one of the loaded + * schema. + * 2) Get the whenChanged attribute + * 3) Generate the modifyTimestamp out of the whenChanged attribute + */ + const struct dsdb_schema *schema = dsdb_get_schema(ldb, NULL); + char *value = ldb_timestring(msg, schema->ts_last_change); + + if (value == NULL) { + return ldb_oom(ldb_module_get_ctx(module)); + } + + return ldb_msg_add_string(msg, "modifyTimeStamp", value); + } + return ldb_msg_copy_attr(msg, "whenChanged", "modifyTimeStamp"); +} + +/* + construct a subSchemaSubEntry +*/ +static int construct_subschema_subentry(struct ldb_module *module, + struct ldb_message *msg, enum ldb_scope scope, + struct ldb_request *parent) +{ + struct operational_data *data = talloc_get_type(ldb_module_get_private(module), struct operational_data); + char *subSchemaSubEntry; + + /* We may be being called before the init function has finished */ + if (!data) { + return LDB_SUCCESS; + } + + /* Try and set this value up, if possible. Don't worry if it + * fails, we may not have the DB set up yet, and it's not + * really vital anyway */ + if (!data->aggregate_dn) { + struct ldb_context *ldb = ldb_module_get_ctx(module); + data->aggregate_dn = samdb_aggregate_schema_dn(ldb, data); + } + + if (data->aggregate_dn) { + subSchemaSubEntry = ldb_dn_alloc_linearized(msg, data->aggregate_dn); + return ldb_msg_add_steal_string(msg, "subSchemaSubEntry", subSchemaSubEntry); + } + return LDB_SUCCESS; +} + + +static int construct_msds_isrodc_with_dn(struct ldb_module *module, + struct ldb_message *msg, + struct ldb_message_element *object_category) +{ + struct ldb_context *ldb; + struct ldb_dn *dn; + const struct ldb_val *val; + + ldb = ldb_module_get_ctx(module); + if (!ldb) { + DEBUG(4, (__location__ ": Failed to get ldb \n")); + return LDB_ERR_OPERATIONS_ERROR; + } + + dn = ldb_dn_new(msg, ldb, (const char *)object_category->values[0].data); + if (!dn) { + DEBUG(4, (__location__ ": Failed to create dn from %s \n", + (const char *)object_category->values[0].data)); + return ldb_operr(ldb); + } + + val = ldb_dn_get_rdn_val(dn); + if (!val) { + DEBUG(4, (__location__ ": Failed to get rdn val from %s \n", + ldb_dn_get_linearized(dn))); + return ldb_operr(ldb); + } + + if (strequal((const char *)val->data, "NTDS-DSA")) { + ldb_msg_add_string(msg, "msDS-isRODC", "FALSE"); + } else { + ldb_msg_add_string(msg, "msDS-isRODC", "TRUE"); + } + return LDB_SUCCESS; +} + +static int construct_msds_isrodc_with_server_dn(struct ldb_module *module, + struct ldb_message *msg, + struct ldb_dn *dn, + struct ldb_request *parent) +{ + struct ldb_dn *server_dn; + const char *attr_obj_cat[] = { "objectCategory", NULL }; + struct ldb_result *res; + struct ldb_message_element *object_category; + int ret; + + server_dn = ldb_dn_copy(msg, dn); + if (!ldb_dn_add_child_fmt(server_dn, "CN=NTDS Settings")) { + DEBUG(4, (__location__ ": Failed to add child to %s \n", + ldb_dn_get_linearized(server_dn))); + return ldb_operr(ldb_module_get_ctx(module)); + } + + ret = dsdb_module_search_dn(module, msg, &res, server_dn, attr_obj_cat, + DSDB_FLAG_NEXT_MODULE, parent); + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + DEBUG(4,(__location__ ": Can't get objectCategory for %s \n", + ldb_dn_get_linearized(server_dn))); + return LDB_SUCCESS; + } else if (ret != LDB_SUCCESS) { + return ret; + } + + object_category = ldb_msg_find_element(res->msgs[0], "objectCategory"); + if (!object_category) { + DEBUG(4,(__location__ ": Can't find objectCategory for %s \n", + ldb_dn_get_linearized(res->msgs[0]->dn))); + return LDB_SUCCESS; + } + return construct_msds_isrodc_with_dn(module, msg, object_category); +} + +static int construct_msds_isrodc_with_computer_dn(struct ldb_module *module, + struct ldb_message *msg, + struct ldb_request *parent) +{ + int ret; + struct ldb_dn *server_dn; + + ret = dsdb_module_reference_dn(module, msg, msg->dn, "serverReferenceBL", + &server_dn, parent); + if (ret == LDB_ERR_NO_SUCH_OBJECT || ret == LDB_ERR_NO_SUCH_ATTRIBUTE) { + /* it's OK if we can't find serverReferenceBL attribute */ + DEBUG(4,(__location__ ": Can't get serverReferenceBL for %s \n", + ldb_dn_get_linearized(msg->dn))); + return LDB_SUCCESS; + } else if (ret != LDB_SUCCESS) { + return ret; + } + + return construct_msds_isrodc_with_server_dn(module, msg, server_dn, parent); +} + +/* + construct msDS-isRODC attr +*/ +static int construct_msds_isrodc(struct ldb_module *module, + struct ldb_message *msg, enum ldb_scope scope, + struct ldb_request *parent) +{ + struct ldb_message_element * object_class; + struct ldb_message_element * object_category; + unsigned int i; + + object_class = ldb_msg_find_element(msg, "objectClass"); + if (!object_class) { + DEBUG(4,(__location__ ": Can't get objectClass for %s \n", + ldb_dn_get_linearized(msg->dn))); + return ldb_operr(ldb_module_get_ctx(module)); + } + + for (i=0; i<object_class->num_values; i++) { + if (strequal((const char*)object_class->values[i].data, "nTDSDSA")) { + /* If TO!objectCategory equals the DN of the classSchema object for the nTDSDSA + * object class, then TO!msDS-isRODC is false. Otherwise, TO!msDS-isRODC is true. + */ + object_category = ldb_msg_find_element(msg, "objectCategory"); + if (!object_category) { + DEBUG(4,(__location__ ": Can't get objectCategory for %s \n", + ldb_dn_get_linearized(msg->dn))); + return LDB_SUCCESS; + } + return construct_msds_isrodc_with_dn(module, msg, object_category); + } + if (strequal((const char*)object_class->values[i].data, "server")) { + /* Let TN be the nTDSDSA object whose DN is "CN=NTDS Settings," prepended to + * the DN of TO. Apply the previous rule for the "TO is an nTDSDSA object" case, + * substituting TN for TO. + */ + return construct_msds_isrodc_with_server_dn(module, msg, msg->dn, parent); + } + if (strequal((const char*)object_class->values[i].data, "computer")) { + /* Let TS be the server object named by TO!serverReferenceBL. Apply the previous + * rule for the "TO is a server object" case, substituting TS for TO. + */ + return construct_msds_isrodc_with_computer_dn(module, msg, parent); + } + } + + return LDB_SUCCESS; +} + + +/* + construct msDS-keyVersionNumber attr + + TODO: Make this based on the 'win2k' DS huristics bit... + +*/ +static int construct_msds_keyversionnumber(struct ldb_module *module, + struct ldb_message *msg, + enum ldb_scope scope, + struct ldb_request *parent) +{ + uint32_t i; + enum ndr_err_code ndr_err; + const struct ldb_val *omd_value; + struct replPropertyMetaDataBlob *omd; + int ret; + + omd_value = ldb_msg_find_ldb_val(msg, "replPropertyMetaData"); + if (!omd_value) { + /* We can't make up a key version number without meta data */ + return LDB_SUCCESS; + } + + omd = talloc(msg, struct replPropertyMetaDataBlob); + if (!omd) { + ldb_module_oom(module); + return LDB_SUCCESS; + } + + ndr_err = ndr_pull_struct_blob(omd_value, omd, omd, + (ndr_pull_flags_fn_t)ndr_pull_replPropertyMetaDataBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + DEBUG(0,(__location__ ": Failed to parse replPropertyMetaData for %s when trying to add msDS-KeyVersionNumber\n", + ldb_dn_get_linearized(msg->dn))); + return ldb_operr(ldb_module_get_ctx(module)); + } + + if (omd->version != 1) { + DEBUG(0,(__location__ ": bad version %u in replPropertyMetaData for %s when trying to add msDS-KeyVersionNumber\n", + omd->version, ldb_dn_get_linearized(msg->dn))); + talloc_free(omd); + return LDB_SUCCESS; + } + for (i=0; i<omd->ctr.ctr1.count; i++) { + if (omd->ctr.ctr1.array[i].attid == DRSUAPI_ATTID_unicodePwd) { + ret = samdb_msg_add_uint(ldb_module_get_ctx(module), + msg, msg, + "msDS-KeyVersionNumber", + omd->ctr.ctr1.array[i].version); + if (ret != LDB_SUCCESS) { + talloc_free(omd); + return ret; + } + break; + } + } + return LDB_SUCCESS; + +} + +#define _UF_TRUST_ACCOUNTS ( \ + UF_WORKSTATION_TRUST_ACCOUNT | \ + UF_SERVER_TRUST_ACCOUNT | \ + UF_INTERDOMAIN_TRUST_ACCOUNT \ +) +#define _UF_NO_EXPIRY_ACCOUNTS ( \ + UF_SMARTCARD_REQUIRED | \ + UF_DONT_EXPIRE_PASSWD | \ + _UF_TRUST_ACCOUNTS \ +) + + +/* + * Returns the Effective-MaximumPasswordAge for a user + */ +static int64_t get_user_max_pwd_age(struct ldb_module *module, + struct ldb_message *user_msg, + struct ldb_request *parent, + struct ldb_dn *nc_root) +{ + int ret; + struct ldb_message *pso = NULL; + struct ldb_context *ldb = ldb_module_get_ctx(module); + + /* if a PSO applies to the user, use its maxPwdAge */ + ret = get_pso_for_user(module, user_msg, parent, &pso); + if (ret != LDB_SUCCESS) { + + /* log the error, but fallback to the domain default */ + DBG_ERR("Error retrieving PSO for %s\n", + ldb_dn_get_linearized(user_msg->dn)); + } + + if (pso != NULL) { + return ldb_msg_find_attr_as_int64(pso, + "msDS-MaximumPasswordAge", 0); + } + + /* otherwise return the default domain value */ + return samdb_search_int64(ldb, user_msg, 0, nc_root, "maxPwdAge", NULL); +} + +/* + calculate msDS-UserPasswordExpiryTimeComputed +*/ +static NTTIME get_msds_user_password_expiry_time_computed(struct ldb_module *module, + struct ldb_message *msg, + struct ldb_request *parent, + struct ldb_dn *domain_dn) +{ + int64_t pwdLastSet, maxPwdAge; + uint32_t userAccountControl; + NTTIME ret; + + userAccountControl = ldb_msg_find_attr_as_uint(msg, + "userAccountControl", + 0); + if (userAccountControl & _UF_NO_EXPIRY_ACCOUNTS) { + return 0x7FFFFFFFFFFFFFFFULL; + } + + pwdLastSet = ldb_msg_find_attr_as_int64(msg, "pwdLastSet", 0); + if (pwdLastSet == 0) { + return 0; + } + + if (pwdLastSet <= -1) { + /* + * This can't really happen... + */ + return 0x7FFFFFFFFFFFFFFFULL; + } + + if (pwdLastSet >= 0x7FFFFFFFFFFFFFFFLL) { + /* + * Somethings wrong with the clock... + */ + return 0x7FFFFFFFFFFFFFFFULL; + } + + /* + * Note that maxPwdAge is a stored as negative value. + * + * Possible values are in the range of: + * + * maxPwdAge: -864000000001 + * to + * maxPwdAge: -9223372036854775808 (-0x8000000000000000ULL) + * + */ + maxPwdAge = get_user_max_pwd_age(module, msg, parent, domain_dn); + if (maxPwdAge >= -864000000000) { + /* + * This is not really possible... + */ + return 0x7FFFFFFFFFFFFFFFULL; + } + + if (maxPwdAge == -0x8000000000000000LL) { + return 0x7FFFFFFFFFFFFFFFULL; + } + + /* + * Note we already caught maxPwdAge == -0x8000000000000000ULL + * and pwdLastSet >= 0x7FFFFFFFFFFFFFFFULL above. + * + * Remember maxPwdAge is a negative number, + * so it results in the following. + * + * 0x7FFFFFFFFFFFFFFEULL + 0x7FFFFFFFFFFFFFFFULL + * = + * 0xFFFFFFFFFFFFFFFDULL + * + * or to put it another way, adding two numbers less than 1<<63 can't + * ever be more than 1<<64, therefore this result can't wrap. + */ + ret = (NTTIME)pwdLastSet - (NTTIME)maxPwdAge; + if (ret >= 0x7FFFFFFFFFFFFFFFULL) { + return 0x7FFFFFFFFFFFFFFFULL; + } + + return ret; +} + +/* + * Returns the Effective-LockoutDuration for a user + */ +static int64_t get_user_lockout_duration(struct ldb_module *module, + struct ldb_message *user_msg, + struct ldb_request *parent, + struct ldb_dn *nc_root) +{ + int ret; + struct ldb_message *pso = NULL; + struct ldb_context *ldb = ldb_module_get_ctx(module); + + /* if a PSO applies to the user, use its lockoutDuration */ + ret = get_pso_for_user(module, user_msg, parent, &pso); + if (ret != LDB_SUCCESS) { + + /* log the error, but fallback to the domain default */ + DBG_ERR("Error retrieving PSO for %s\n", + ldb_dn_get_linearized(user_msg->dn)); + } + + if (pso != NULL) { + return ldb_msg_find_attr_as_int64(pso, + "msDS-LockoutDuration", 0); + } + + /* otherwise return the default domain value */ + return samdb_search_int64(ldb, user_msg, 0, nc_root, "lockoutDuration", + NULL); +} + +/* + construct msDS-User-Account-Control-Computed attr +*/ +static int construct_msds_user_account_control_computed(struct ldb_module *module, + struct ldb_message *msg, enum ldb_scope scope, + struct ldb_request *parent) +{ + uint32_t userAccountControl; + uint32_t msDS_User_Account_Control_Computed = 0; + struct ldb_context *ldb = ldb_module_get_ctx(module); + NTTIME now; + struct ldb_dn *nc_root; + int ret; + + ret = dsdb_find_nc_root(ldb, msg, msg->dn, &nc_root); + if (ret != 0) { + ldb_asprintf_errstring(ldb, + "Failed to find NC root of DN: %s: %s", + ldb_dn_get_linearized(msg->dn), + ldb_errstring(ldb_module_get_ctx(module))); + return ret; + } + if (ldb_dn_compare(nc_root, ldb_get_default_basedn(ldb)) != 0) { + /* Only calculate this on our default NC */ + return 0; + } + /* Test account expire time */ + unix_to_nt_time(&now, time(NULL)); + + userAccountControl = ldb_msg_find_attr_as_uint(msg, + "userAccountControl", + 0); + if (!(userAccountControl & _UF_TRUST_ACCOUNTS)) { + + int64_t lockoutTime = ldb_msg_find_attr_as_int64(msg, "lockoutTime", 0); + if (lockoutTime != 0) { + int64_t lockoutDuration; + + lockoutDuration = get_user_lockout_duration(module, msg, + parent, + nc_root); + + /* zero locks out until the administrator intervenes */ + if (lockoutDuration >= 0) { + msDS_User_Account_Control_Computed |= UF_LOCKOUT; + } else if (lockoutTime - lockoutDuration >= now) { + msDS_User_Account_Control_Computed |= UF_LOCKOUT; + } + } + } + + if (!(userAccountControl & _UF_NO_EXPIRY_ACCOUNTS)) { + NTTIME must_change_time + = get_msds_user_password_expiry_time_computed(module, + msg, + parent, + nc_root); + /* check for expired password */ + if (must_change_time < now) { + msDS_User_Account_Control_Computed |= UF_PASSWORD_EXPIRED; + } + } + + return samdb_msg_add_int64(ldb, + msg->elements, msg, + "msDS-User-Account-Control-Computed", + msDS_User_Account_Control_Computed); +} + +/* + construct msDS-UserPasswordExpiryTimeComputed +*/ +static int construct_msds_user_password_expiry_time_computed(struct ldb_module *module, + struct ldb_message *msg, enum ldb_scope scope, + struct ldb_request *parent) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_dn *nc_root; + int64_t password_expiry_time; + int ret; + + ret = dsdb_find_nc_root(ldb, msg, msg->dn, &nc_root); + if (ret != 0) { + ldb_asprintf_errstring(ldb, + "Failed to find NC root of DN: %s: %s", + ldb_dn_get_linearized(msg->dn), + ldb_errstring(ldb)); + return ret; + } + + if (ldb_dn_compare(nc_root, ldb_get_default_basedn(ldb)) != 0) { + /* Only calculate this on our default NC */ + return 0; + } + + password_expiry_time + = get_msds_user_password_expiry_time_computed(module, msg, + parent, nc_root); + + return samdb_msg_add_int64(ldb, + msg->elements, msg, + "msDS-UserPasswordExpiryTimeComputed", + password_expiry_time); +} + +/* + * Checks whether the msDS-ResultantPSO attribute is supported for a given + * user object. As per MS-ADTS, section 3.1.1.4.5.36 msDS-ResultantPSO. + */ +static bool pso_is_supported(struct ldb_context *ldb, struct ldb_message *msg) +{ + int functional_level; + uint32_t uac; + uint32_t user_rid; + + functional_level = dsdb_functional_level(ldb); + if (functional_level < DS_DOMAIN_FUNCTION_2008) { + return false; + } + + /* msDS-ResultantPSO is only supported for user objects */ + if (!ldb_match_msg_objectclass(msg, "user")) { + return false; + } + + /* ...and only if the ADS_UF_NORMAL_ACCOUNT bit is set */ + uac = ldb_msg_find_attr_as_uint(msg, "userAccountControl", 0); + if (!(uac & UF_NORMAL_ACCOUNT)) { + return false; + } + + /* skip it if it's the special KRBTGT default account */ + user_rid = samdb_result_rid_from_sid(msg, msg, "objectSid", 0); + if (user_rid == DOMAIN_RID_KRBTGT) { + return false; + } + + /* ...or if it's a special KRBTGT account for an RODC KDC */ + if (ldb_msg_find_ldb_val(msg, "msDS-SecondaryKrbTgtNumber") != NULL) { + return false; + } + + return true; +} + +/* + * Returns the number of PSO objects that exist in the DB + */ +static int get_pso_count(struct ldb_module *module, TALLOC_CTX *mem_ctx, + struct ldb_request *parent, int *pso_count) +{ + static const char * const attrs[] = { NULL }; + int ret; + struct ldb_dn *psc_dn = NULL; + struct ldb_result *res = NULL; + struct ldb_context *ldb = ldb_module_get_ctx(module); + bool psc_ok; + + *pso_count = 0; + psc_dn = samdb_system_container_dn(ldb, mem_ctx); + if (psc_dn == NULL) { + return ldb_oom(ldb); + } + psc_ok = ldb_dn_add_child_fmt(psc_dn, "CN=Password Settings Container"); + if (psc_ok == false) { + return ldb_oom(ldb); + } + + /* get the number of PSO children */ + ret = dsdb_module_search(module, mem_ctx, &res, psc_dn, + LDB_SCOPE_ONELEVEL, attrs, + DSDB_FLAG_NEXT_MODULE, parent, + "(objectClass=msDS-PasswordSettings)"); + + /* + * Just ignore PSOs if the container doesn't exist. This is a weird + * corner-case where the AD DB was created from a pre-2008 base schema, + * and then the FL was manually upgraded. + */ + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + DBG_NOTICE("No Password Settings Container exists\n"); + return LDB_SUCCESS; + } + + if (ret != LDB_SUCCESS) { + return ret; + } + + *pso_count = res->count; + talloc_free(res); + talloc_free(psc_dn); + + return LDB_SUCCESS; +} + +/* + * Compares two PSO objects returned by a search, to work out the better PSO. + * The PSO with the lowest precedence is better, otherwise (if the precedence + * is equal) the PSO with the lower GUID wins. + */ +static int pso_compare(struct ldb_message **m1, struct ldb_message **m2) +{ + uint32_t prec1; + uint32_t prec2; + + prec1 = ldb_msg_find_attr_as_uint(*m1, "msDS-PasswordSettingsPrecedence", + 0xffffffff); + prec2 = ldb_msg_find_attr_as_uint(*m2, "msDS-PasswordSettingsPrecedence", + 0xffffffff); + + /* if precedence is equal, use the lowest GUID */ + if (prec1 == prec2) { + struct GUID guid1 = samdb_result_guid(*m1, "objectGUID"); + struct GUID guid2 = samdb_result_guid(*m2, "objectGUID"); + + return ndr_guid_compare(&guid1, &guid2); + } else { + return prec1 - prec2; + } +} + +/* + * Search for PSO objects that apply to the object SIDs specified + */ +static int pso_search_by_sids(struct ldb_module *module, TALLOC_CTX *mem_ctx, + struct ldb_request *parent, + struct dom_sid *sid_array, unsigned int num_sids, + struct ldb_result **result) +{ + int ret; + int i; + struct ldb_context *ldb = ldb_module_get_ctx(module); + char *sid_filter = NULL; + struct ldb_dn *psc_dn = NULL; + bool psc_ok; + const char *attrs[] = { + "msDS-PasswordSettingsPrecedence", + "objectGUID", + "msDS-LockoutDuration", + "msDS-MaximumPasswordAge", + NULL + }; + + /* build a query for PSO objects that apply to any of the SIDs given */ + sid_filter = talloc_strdup(mem_ctx, ""); + + for (i = 0; sid_filter && i < num_sids; i++) { + struct dom_sid_buf sid_buf; + + sid_filter = talloc_asprintf_append( + sid_filter, + "(msDS-PSOAppliesTo=<SID=%s>)", + dom_sid_str_buf(&sid_array[i], &sid_buf)); + } + + if (sid_filter == NULL) { + return ldb_oom(ldb); + } + + /* only PSOs located in the Password Settings Container are valid */ + psc_dn = samdb_system_container_dn(ldb, mem_ctx); + if (psc_dn == NULL) { + return ldb_oom(ldb); + } + psc_ok = ldb_dn_add_child_fmt(psc_dn, "CN=Password Settings Container"); + if (psc_ok == false) { + return ldb_oom(ldb); + } + + ret = dsdb_module_search(module, mem_ctx, result, psc_dn, + LDB_SCOPE_ONELEVEL, attrs, + DSDB_FLAG_NEXT_MODULE, parent, + "(&(objectClass=msDS-PasswordSettings)(|%s))", + sid_filter); + talloc_free(sid_filter); + return ret; +} + +/* + * Returns the best PSO object that applies to the object SID(s) specified + */ +static int pso_find_best(struct ldb_module *module, TALLOC_CTX *mem_ctx, + struct ldb_request *parent, struct dom_sid *sid_array, + unsigned int num_sids, struct ldb_message **best_pso) +{ + struct ldb_result *res = NULL; + int ret; + + *best_pso = NULL; + + /* find any PSOs that apply to the SIDs specified */ + ret = pso_search_by_sids(module, mem_ctx, parent, sid_array, num_sids, + &res); + if (ret != LDB_SUCCESS) { + DBG_ERR("Error %d retrieving PSO for SID(s)\n", ret); + return ret; + } + + /* sort the list so that the best PSO is first */ + TYPESAFE_QSORT(res->msgs, res->count, pso_compare); + + if (res->count > 0) { + *best_pso = res->msgs[0]; + } + + return LDB_SUCCESS; +} + +/* + * Determines the Password Settings Object (PSO) that applies to the given user + */ +static int get_pso_for_user(struct ldb_module *module, + struct ldb_message *user_msg, + struct ldb_request *parent, + struct ldb_message **pso_msg) +{ + bool pso_supported; + struct dom_sid *groupSIDs = NULL; + unsigned int num_groupSIDs = 0; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_message *best_pso = NULL; + struct ldb_dn *pso_dn = NULL; + int ret; + struct ldb_message_element *el = NULL; + TALLOC_CTX *tmp_ctx = NULL; + int pso_count = 0; + struct ldb_result *res = NULL; + static const char *attrs[] = { + "msDS-LockoutDuration", + "msDS-MaximumPasswordAge", + NULL + }; + + *pso_msg = NULL; + + /* first, check msDS-ResultantPSO is supported for this object */ + pso_supported = pso_is_supported(ldb, user_msg); + + if (!pso_supported) { + return LDB_SUCCESS; + } + + tmp_ctx = talloc_new(user_msg); + + /* + * Several different constructed attributes try to use the PSO info. If + * we've already constructed the msDS-ResultantPSO for this user, we can + * just re-use the result, rather than calculating it from scratch again + */ + pso_dn = ldb_msg_find_attr_as_dn(ldb, tmp_ctx, user_msg, + "msDS-ResultantPSO"); + + if (pso_dn != NULL) { + ret = dsdb_module_search_dn(module, tmp_ctx, &res, pso_dn, + attrs, DSDB_FLAG_NEXT_MODULE, + parent); + if (ret != LDB_SUCCESS) { + DBG_ERR("Error %d retrieving PSO %s\n", ret, + ldb_dn_get_linearized(pso_dn)); + talloc_free(tmp_ctx); + return ret; + } + + if (res->count == 1) { + *pso_msg = res->msgs[0]; + return LDB_SUCCESS; + } + } + + /* + * if any PSOs apply directly to the user, they are considered first + * before we check group membership PSOs + */ + el = ldb_msg_find_element(user_msg, "msDS-PSOApplied"); + + if (el != NULL && el->num_values > 0) { + struct dom_sid *user_sid = NULL; + + /* lookup the best PSO object, based on the user's SID */ + user_sid = samdb_result_dom_sid(tmp_ctx, user_msg, "objectSid"); + + ret = pso_find_best(module, tmp_ctx, parent, user_sid, 1, + &best_pso); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + if (best_pso != NULL) { + *pso_msg = best_pso; + return LDB_SUCCESS; + } + } + + /* + * If no valid PSO applies directly to the user, then try its groups. + * The group expansion is expensive, so check there are actually + * PSOs in the DB first (which is a quick search). Note in the above + * cases we could tell that a PSO applied to the user, based on info + * already retrieved by the user search. + */ + ret = get_pso_count(module, tmp_ctx, parent, &pso_count); + if (ret != LDB_SUCCESS) { + DBG_ERR("Error %d determining PSOs in system\n", ret); + talloc_free(tmp_ctx); + return ret; + } + + if (pso_count == 0) { + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + + /* Work out the SIDs of any account groups the user is a member of */ + ret = get_group_sids(ldb, tmp_ctx, user_msg, + "msDS-ResultantPSO", ACCOUNT_GROUPS, + &groupSIDs, &num_groupSIDs); + if (ret != LDB_SUCCESS) { + DBG_ERR("Error %d determining group SIDs for %s\n", ret, + ldb_dn_get_linearized(user_msg->dn)); + talloc_free(tmp_ctx); + return ret; + } + + /* lookup the best PSO that applies to any of these groups */ + ret = pso_find_best(module, tmp_ctx, parent, groupSIDs, + num_groupSIDs, &best_pso); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + *pso_msg = best_pso; + return LDB_SUCCESS; +} + +/* + * Constructs the msDS-ResultantPSO attribute, which is the DN of the Password + * Settings Object (PSO) that applies to that user. + */ +static int construct_resultant_pso(struct ldb_module *module, + struct ldb_message *msg, + enum ldb_scope scope, + struct ldb_request *parent) +{ + struct ldb_message *pso = NULL; + int ret; + + /* work out the PSO (if any) that applies to this user */ + ret = get_pso_for_user(module, msg, parent, &pso); + if (ret != LDB_SUCCESS) { + DBG_ERR("Couldn't determine PSO for %s\n", + ldb_dn_get_linearized(msg->dn)); + return ret; + } + + if (pso != NULL) { + DBG_INFO("%s is resultant PSO for user %s\n", + ldb_dn_get_linearized(pso->dn), + ldb_dn_get_linearized(msg->dn)); + return ldb_msg_add_string(msg, "msDS-ResultantPSO", + ldb_dn_get_linearized(pso->dn)); + } + + /* no PSO applies to this user */ + return LDB_SUCCESS; +} + +struct op_controls_flags { + bool sd; + bool bypassoperational; +}; + +static bool check_keep_control_for_attribute(struct op_controls_flags* controls_flags, const char* attr) { + if (controls_flags->bypassoperational && ldb_attr_cmp(attr, "msDS-KeyVersionNumber") == 0 ) { + return true; + } + return false; +} + +/* + a list of attribute names that should be substituted in the parse + tree before the search is done +*/ +static const struct { + const char *attr; + const char *replace; +} parse_tree_sub[] = { + { "createTimeStamp", "whenCreated" }, + { "modifyTimeStamp", "whenChanged" } +}; + + +struct op_attributes_replace { + const char *attr; + const char *replace; + const char * const *extra_attrs; + int (*constructor)(struct ldb_module *, struct ldb_message *, enum ldb_scope, struct ldb_request *); +}; + +/* the 'extra_attrs' required for msDS-ResultantPSO */ +#define RESULTANT_PSO_COMPUTED_ATTRS \ + "msDS-PSOApplied", \ + "userAccountControl", \ + "objectSid", \ + "msDS-SecondaryKrbTgtNumber", \ + "primaryGroupID" + +/* + * any other constructed attributes that want to work out the PSO also need to + * include objectClass (this gets included via 'replace' for msDS-ResultantPSO) + */ +#define PSO_ATTR_DEPENDENCIES \ + RESULTANT_PSO_COMPUTED_ATTRS, \ + "objectClass" + +static const char *objectSid_attr[] = +{ + "objectSid", + NULL +}; + + +static const char *objectCategory_attr[] = +{ + "objectCategory", + NULL +}; + + +static const char *user_account_control_computed_attrs[] = +{ + "lockoutTime", + "pwdLastSet", + PSO_ATTR_DEPENDENCIES, + NULL +}; + + +static const char *user_password_expiry_time_computed_attrs[] = +{ + "pwdLastSet", + PSO_ATTR_DEPENDENCIES, + NULL +}; + +static const char *resultant_pso_computed_attrs[] = +{ + RESULTANT_PSO_COMPUTED_ATTRS, + NULL +}; + +/* + a list of attribute names that are hidden, but can be searched for + using another (non-hidden) name to produce the correct result +*/ +static const struct op_attributes_replace search_sub[] = { + { "createTimeStamp", "whenCreated", NULL , NULL }, + { "modifyTimeStamp", "whenChanged", NULL , construct_modifyTimeStamp}, + { "structuralObjectClass", "objectClass", NULL , NULL }, + { "canonicalName", NULL, NULL , construct_canonical_name }, + { "primaryGroupToken", "objectClass", objectSid_attr, construct_primary_group_token }, + { "tokenGroups", "primaryGroupID", objectSid_attr, construct_token_groups }, + { "tokenGroupsNoGCAcceptable", "primaryGroupID", objectSid_attr, construct_token_groups_no_gc}, + { "tokenGroupsGlobalAndUniversal", "primaryGroupID", objectSid_attr, construct_global_universal_token_groups }, + { "parentGUID", "objectGUID", NULL, construct_parent_guid }, + { "subSchemaSubEntry", NULL, NULL, construct_subschema_subentry }, + { "msDS-isRODC", "objectClass", objectCategory_attr, construct_msds_isrodc }, + { "msDS-KeyVersionNumber", "replPropertyMetaData", NULL, construct_msds_keyversionnumber }, + { "msDS-User-Account-Control-Computed", "userAccountControl", user_account_control_computed_attrs, + construct_msds_user_account_control_computed }, + { "msDS-UserPasswordExpiryTimeComputed", "userAccountControl", user_password_expiry_time_computed_attrs, + construct_msds_user_password_expiry_time_computed }, + { "msDS-ResultantPSO", "objectClass", resultant_pso_computed_attrs, + construct_resultant_pso } +}; + + +enum op_remove { + OPERATIONAL_REMOVE_ALWAYS, /* remove always */ + OPERATIONAL_REMOVE_UNASKED,/* remove if not requested */ + OPERATIONAL_SD_FLAGS, /* show if SD_FLAGS_OID set, or asked for */ + OPERATIONAL_REMOVE_UNLESS_CONTROL /* remove always unless an adhoc control has been specified */ +}; + +/* + a list of attributes that may need to be removed from the + underlying db return + + Some of these are attributes that were once stored, but are now calculated +*/ +struct op_attributes_operations { + const char *attr; + enum op_remove op; +}; + +static const struct op_attributes_operations operational_remove[] = { + { "nTSecurityDescriptor", OPERATIONAL_SD_FLAGS }, + { "msDS-KeyVersionNumber", OPERATIONAL_REMOVE_UNLESS_CONTROL }, + { "parentGUID", OPERATIONAL_REMOVE_ALWAYS }, + { "replPropertyMetaData", OPERATIONAL_REMOVE_UNASKED }, +#define _SEP ,OPERATIONAL_REMOVE_UNASKED},{ + { DSDB_SECRET_ATTRIBUTES_EX(_SEP), OPERATIONAL_REMOVE_UNASKED } +}; + + +/* + post process a search result record. For any search_sub[] attributes that were + asked for, we need to call the appropriate copy routine to copy the result + into the message, then remove any attributes that we added to the search but + were not asked for by the user +*/ +static int operational_search_post_process(struct ldb_module *module, + struct ldb_message *msg, + enum ldb_scope scope, + const char * const *attrs_from_user, + const char * const *attrs_searched_for, + struct op_controls_flags* controls_flags, + struct op_attributes_operations *list, + unsigned int list_size, + struct op_attributes_replace *list_replace, + unsigned int list_replace_size, + struct ldb_request *parent) +{ + struct ldb_context *ldb; + unsigned int i, a = 0; + bool constructed_attributes = false; + + ldb = ldb_module_get_ctx(module); + + /* removed any attrs that should not be shown to the user */ + for (i=0; i < list_size; i++) { + ldb_msg_remove_attr(msg, list[i].attr); + } + + for (a=0; a < list_replace_size; a++) { + if (check_keep_control_for_attribute(controls_flags, + list_replace[a].attr)) { + continue; + } + + /* construct the new attribute, using either a supplied + constructor or a simple copy */ + constructed_attributes = true; + if (list_replace[a].constructor != NULL) { + if (list_replace[a].constructor(module, msg, scope, parent) != LDB_SUCCESS) { + goto failed; + } + } else if (ldb_msg_copy_attr(msg, + list_replace[a].replace, + list_replace[a].attr) != LDB_SUCCESS) { + goto failed; + } + } + + /* Deletion of the search helper attributes are needed if: + * - we generated constructed attributes and + * - we aren't requesting all attributes + */ + if ((constructed_attributes) && (!ldb_attr_in_list(attrs_from_user, "*"))) { + for (i=0; i < list_replace_size; i++) { + /* remove the added search helper attributes, unless + * they were asked for by the user */ + if (list_replace[i].replace != NULL && + !ldb_attr_in_list(attrs_from_user, list_replace[i].replace)) { + ldb_msg_remove_attr(msg, list_replace[i].replace); + } + if (list_replace[i].extra_attrs != NULL) { + unsigned int j; + for (j=0; list_replace[i].extra_attrs[j]; j++) { + if (!ldb_attr_in_list(attrs_from_user, list_replace[i].extra_attrs[j])) { + ldb_msg_remove_attr(msg, list_replace[i].extra_attrs[j]); + } + } + } + } + } + + return 0; + +failed: + ldb_debug_set(ldb, LDB_DEBUG_WARNING, + "operational_search_post_process failed for attribute '%s' - %s", + list_replace[a].attr, ldb_errstring(ldb)); + return -1; +} + +/* + hook search operations +*/ + +struct operational_context { + struct ldb_module *module; + struct ldb_request *req; + enum ldb_scope scope; + const char * const *attrs; + struct op_controls_flags* controls_flags; + struct op_attributes_operations *list_operations; + unsigned int list_operations_size; + struct op_attributes_replace *attrs_to_replace; + unsigned int attrs_to_replace_size; +}; + +static int operational_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct operational_context *ac; + int ret; + + ac = talloc_get_type(req->context, struct operational_context); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + /* for each record returned post-process to add any derived + attributes that have been asked for */ + ret = operational_search_post_process(ac->module, + ares->message, + ac->scope, + ac->attrs, + req->op.search.attrs, + ac->controls_flags, + ac->list_operations, + ac->list_operations_size, + ac->attrs_to_replace, + ac->attrs_to_replace_size, + req); + if (ret != 0) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + return ldb_module_send_entry(ac->req, ares->message, ares->controls); + + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral(ac->req, ares->referral); + + case LDB_REPLY_DONE: + + return ldb_module_done(ac->req, ares->controls, + ares->response, LDB_SUCCESS); + } + + talloc_free(ares); + return LDB_SUCCESS; +} + +static struct op_attributes_operations* operation_get_op_list(TALLOC_CTX *ctx, + const char* const* attrs, + const char* const* searched_attrs, + struct op_controls_flags* controls_flags) +{ + int idx = 0; + int i; + struct op_attributes_operations *list = talloc_zero_array(ctx, + struct op_attributes_operations, + ARRAY_SIZE(operational_remove) + 1); + + if (list == NULL) { + return NULL; + } + + for (i=0; i<ARRAY_SIZE(operational_remove); i++) { + switch (operational_remove[i].op) { + case OPERATIONAL_REMOVE_UNASKED: + if (ldb_attr_in_list(attrs, operational_remove[i].attr)) { + continue; + } + if (ldb_attr_in_list(searched_attrs, operational_remove[i].attr)) { + continue; + } + list[idx].attr = operational_remove[i].attr; + list[idx].op = OPERATIONAL_REMOVE_UNASKED; + idx++; + break; + + case OPERATIONAL_REMOVE_ALWAYS: + list[idx].attr = operational_remove[i].attr; + list[idx].op = OPERATIONAL_REMOVE_ALWAYS; + idx++; + break; + + case OPERATIONAL_REMOVE_UNLESS_CONTROL: + if (!check_keep_control_for_attribute(controls_flags, operational_remove[i].attr)) { + list[idx].attr = operational_remove[i].attr; + list[idx].op = OPERATIONAL_REMOVE_UNLESS_CONTROL; + idx++; + } + break; + + case OPERATIONAL_SD_FLAGS: + if (ldb_attr_in_list(attrs, operational_remove[i].attr)) { + continue; + } + if (controls_flags->sd) { + if (attrs == NULL) { + continue; + } + if (attrs[0] == NULL) { + continue; + } + if (ldb_attr_in_list(attrs, "*")) { + continue; + } + } + list[idx].attr = operational_remove[i].attr; + list[idx].op = OPERATIONAL_SD_FLAGS; + idx++; + break; + } + } + + return list; +} + +static int operational_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct operational_context *ac; + struct ldb_request *down_req; + const char **search_attrs = NULL; + unsigned int i, a; + int ret; + + /* There are no operational attributes on special DNs */ + if (ldb_dn_is_special(req->op.search.base)) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + ac = talloc(req, struct operational_context); + if (ac == NULL) { + return ldb_oom(ldb); + } + + ac->module = module; + ac->req = req; + ac->scope = req->op.search.scope; + ac->attrs = req->op.search.attrs; + + /* FIXME: We must copy the tree and keep the original + * unmodified. SSS */ + /* replace any attributes in the parse tree that are + searchable, but are stored using a different name in the + backend */ + for (i=0;i<ARRAY_SIZE(parse_tree_sub);i++) { + ldb_parse_tree_attr_replace(req->op.search.tree, + parse_tree_sub[i].attr, + parse_tree_sub[i].replace); + } + + ac->controls_flags = talloc(ac, struct op_controls_flags); + /* remember if the SD_FLAGS_OID was set */ + ac->controls_flags->sd = (ldb_request_get_control(req, LDB_CONTROL_SD_FLAGS_OID) != NULL); + /* remember if the LDB_CONTROL_BYPASS_OPERATIONAL_OID */ + ac->controls_flags->bypassoperational = + (ldb_request_get_control(req, LDB_CONTROL_BYPASS_OPERATIONAL_OID) != NULL); + + ac->attrs_to_replace = NULL; + ac->attrs_to_replace_size = 0; + /* in the list of attributes we are looking for, rename any + attributes to the alias for any hidden attributes that can + be fetched directly using non-hidden names. + Note that order here can affect performance, e.g. we should process + msDS-ResultantPSO before msDS-User-Account-Control-Computed (as the + latter is also dependent on the PSO information) */ + for (a=0;ac->attrs && ac->attrs[a];a++) { + if (check_keep_control_for_attribute(ac->controls_flags, ac->attrs[a])) { + continue; + } + for (i=0;i<ARRAY_SIZE(search_sub);i++) { + + if (ldb_attr_cmp(ac->attrs[a], search_sub[i].attr) != 0 ) { + continue; + } + + ac->attrs_to_replace = talloc_realloc(ac, + ac->attrs_to_replace, + struct op_attributes_replace, + ac->attrs_to_replace_size + 1); + + ac->attrs_to_replace[ac->attrs_to_replace_size] = search_sub[i]; + ac->attrs_to_replace_size++; + if (!search_sub[i].replace) { + continue; + } + + if (search_sub[i].extra_attrs && search_sub[i].extra_attrs[0]) { + unsigned int j; + const char **search_attrs2; + /* Only adds to the end of the list */ + for (j = 0; search_sub[i].extra_attrs[j]; j++) { + search_attrs2 = ldb_attr_list_copy_add(req, search_attrs + ? search_attrs + : ac->attrs, + search_sub[i].extra_attrs[j]); + if (search_attrs2 == NULL) { + return ldb_operr(ldb); + } + /* may be NULL, talloc_free() doesn't mind */ + talloc_free(search_attrs); + search_attrs = search_attrs2; + } + } + + if (!search_attrs) { + search_attrs = ldb_attr_list_copy(req, ac->attrs); + if (search_attrs == NULL) { + return ldb_operr(ldb); + } + } + /* Despite the ldb_attr_list_copy_add, this is safe as that fn only adds to the end */ + search_attrs[a] = search_sub[i].replace; + } + } + ac->list_operations = operation_get_op_list(ac, ac->attrs, + search_attrs == NULL?req->op.search.attrs:search_attrs, + ac->controls_flags); + ac->list_operations_size = 0; + i = 0; + + while (ac->list_operations && ac->list_operations[i].attr != NULL) { + i++; + } + ac->list_operations_size = i; + ret = ldb_build_search_req_ex(&down_req, ldb, ac, + req->op.search.base, + req->op.search.scope, + req->op.search.tree, + /* use new set of attrs if any */ + search_attrs == NULL?req->op.search.attrs:search_attrs, + req->controls, + ac, operational_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + /* perform the search */ + return ldb_next_request(module, down_req); +} + +static int operational_init(struct ldb_module *ctx) +{ + struct operational_data *data; + int ret; + + ret = ldb_next_init(ctx); + + if (ret != LDB_SUCCESS) { + return ret; + } + + data = talloc_zero(ctx, struct operational_data); + if (!data) { + return ldb_module_oom(ctx); + } + + ldb_module_set_private(ctx, data); + + return LDB_SUCCESS; +} + +static const struct ldb_module_ops ldb_operational_module_ops = { + .name = "operational", + .search = operational_search, + .init_context = operational_init +}; + +int ldb_operational_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_operational_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/paged_results.c b/source4/dsdb/samdb/ldb_modules/paged_results.c new file mode 100644 index 0000000..2063e84 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/paged_results.c @@ -0,0 +1,855 @@ +/* + ldb database library + + Copyright (C) Simo Sorce 2005-2008 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018 + + ** NOTE! The following LGPL license applies to the ldb + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ + +/* + * Name: paged_result + * + * Component: ldb paged results control module + * + * Description: this module caches a complete search and sends back + * results in chunks as asked by the client + * + * Author: Garming Sam and Aaron Haslett + * + * Note: Based on the original paged_results.c by Simo Sorce and + * vlv_pagination.c by Douglas Bagnall and Garming Sam. + */ + +#include "includes.h" +#include "auth/auth.h" +#include <ldb.h> +#include "dsdb/samdb/samdb.h" +#include "libcli/security/security.h" +#include "libcli/ldap/ldap_errors.h" +#include "replace.h" +#include "system/filesys.h" +#include "system/time.h" +#include "ldb_module.h" +#include "dsdb/samdb/samdb.h" + +#include "dsdb/common/util.h" +#include "lib/util/dlinklist.h" + +/* Referrals are temporarily stored in a linked list */ +struct referral_store { + char *ref; + struct referral_store *next; +}; + +struct private_data; + +struct results_store { + struct results_store *prev, *next; + + struct private_data *priv; + + char *cookie; + time_t timestamp; + + struct referral_store *first_ref; + struct referral_store *last_ref; + + struct ldb_control **controls; + + /* from VLV */ + struct GUID *results; + size_t num_entries; + size_t result_array_size; + + struct ldb_control **down_controls; + const char * const *attrs; + + unsigned last_i; + struct ldb_parse_tree *expr; + char *expr_str; +}; + +struct private_data { + uint32_t next_free_id; + size_t num_stores; + struct results_store *store; +}; + +static int store_destructor(struct results_store *del) +{ + struct private_data *priv = del->priv; + DLIST_REMOVE(priv->store, del); + + priv->num_stores -= 1; + + return 0; +} + +static struct results_store *new_store(struct private_data *priv) +{ + struct results_store *newr; + uint32_t new_id = priv->next_free_id++; + + /* TODO: we should have a limit on the number of + * outstanding paged searches + */ + + newr = talloc_zero(priv, struct results_store); + if (!newr) return NULL; + + newr->priv = priv; + + newr->cookie = talloc_asprintf(newr, "%d", new_id); + if (!newr->cookie) { + talloc_free(newr); + return NULL; + } + + newr->timestamp = time(NULL); + + DLIST_ADD(priv->store, newr); + + priv->num_stores += 1; + + talloc_set_destructor(newr, store_destructor); + + if (priv->num_stores > 10) { + struct results_store *last; + /* + * 10 is the default for MaxResultSetsPerConn -- + * possibly need to parameterize it. + */ + last = DLIST_TAIL(priv->store); + TALLOC_FREE(last); + } + + return newr; +} + +struct paged_context { + struct ldb_module *module; + struct ldb_request *req; + + struct results_store *store; + int size; + struct ldb_control **controls; +}; + +static int send_referrals(struct results_store *store, + struct ldb_request *req) +{ + int ret; + struct referral_store *node; + while (store->first_ref != NULL) { + node = store->first_ref; + ret = ldb_module_send_referral(req, node->ref); + if (ret != LDB_SUCCESS) { + return ret; + } + store->first_ref = node->next; + talloc_free(node); + } + return LDB_SUCCESS; +} + +/* Start an ldb request for a single object by GUID */ +static int paged_search_by_dn_guid(struct ldb_module *module, + struct paged_context *ac, + struct ldb_result **result, + const struct GUID *guid, + const char * const *attrs, + struct ldb_parse_tree *expr) +{ + struct ldb_dn *dn; + struct ldb_request *req; + struct ldb_result *res; + int ret; + struct GUID_txt_buf guid_str; + + /* Use controls passed in on the downreq */ + struct ldb_control **controls = ac->store->down_controls; + + struct ldb_context *ldb = ldb_module_get_ctx(module); + + dn = ldb_dn_new_fmt(ac, ldb, "<GUID=%s>", + GUID_buf_string(guid, &guid_str)); + if (dn == NULL) { + return ldb_oom(ldb); + } + + res = talloc_zero(ac, struct ldb_result); + if (res == NULL) { + TALLOC_FREE(dn); + return ldb_oom(ldb); + } + + ret = ldb_build_search_req_ex(&req, ldb, ac, + dn, + LDB_SCOPE_BASE, + expr, + attrs, + controls, + res, + ldb_search_default_callback, + ac->req); + if (ret != LDB_SUCCESS) { + TALLOC_FREE(dn); + TALLOC_FREE(res); + return ret; + } + + /* + * Ensure the dn lasts only as long as the request, + * as we will have a lot of these (one per object + * being returned) + */ + + talloc_steal(req, dn); + + ret = ldb_request(ldb, req); + if (ret == LDB_SUCCESS) { + ret = ldb_wait(req->handle, LDB_WAIT_ALL); + } + + talloc_free(req); + if (ret != LDB_SUCCESS) { + talloc_free(res); + return ret; + } + + *result = res; + return ret; +} + +static int paged_results(struct paged_context *ac, struct ldb_reply *ares) +{ + struct ldb_extended *response = (ares != NULL ? ares->response : NULL); + struct ldb_paged_control *paged; + unsigned int i, num_ctrls; + int ret; + + if (ac->store == NULL) { + ret = LDB_ERR_OPERATIONS_ERROR; + return ldb_module_done( + ac->req, ac->controls, response, ret); + } + + while (ac->store->last_i < ac->store->num_entries && ac->size > 0) { + struct GUID *guid = &ac->store->results[ac->store->last_i++]; + struct ldb_result *result = NULL; + + ac->size--; + + /* + * Note: In the case that an object has been moved to a + * different place in the LDAP tree, we might expect the object + * to disappear from paged results. If we were going to + * implement that behaviour, we would do it here by passing + * down the original container DN to the search. + * However, testing shows that, on Windows, the moved object + * remains in the paged results. So, we are matching Windows + * behaviour here by leaving out the scope. + */ + ret = paged_search_by_dn_guid(ac->module, ac, &result, guid, + ac->req->op.search.attrs, + ac->store->expr); + if (ret == LDAP_NO_SUCH_OBJECT || + (ret == LDB_SUCCESS && result->count == 0)) { + /* The thing isn't there TODO, which we quietly + ignore and go on to send an extra one + instead. */ + continue; + } else if (ret != LDB_SUCCESS) { + return ldb_module_done( + ac->req, ac->controls, response, ret); + } + + ret = ldb_module_send_entry(ac->req, result->msgs[0], + NULL); + if (ret != LDB_SUCCESS) { + /* + * ldb_module_send_entry will have called + * ldb_module_done if an error occurred. + */ + return ret; + } + } + + if (ac->store->first_ref) { + /* There is no right place to put references in the sorted + results, so we send them as soon as possible. + */ + ret = send_referrals(ac->store, ac->req); + if (ret != LDB_SUCCESS) { + /* + * send_referrals will have called ldb_module_done + * if an error occurred. + */ + return ret; + } + } + + /* return result done */ + num_ctrls = 1; + i = 0; + + if (ac->store->controls != NULL) { + while (ac->store->controls[i]) i++; /* counting */ + + num_ctrls += i; + } + + ac->controls = talloc_array(ac, struct ldb_control *, num_ctrls +1); + if (ac->controls == NULL) { + ret = LDB_ERR_OPERATIONS_ERROR; + return ldb_module_done( + ac->req, ac->controls, response, ret); + } + ac->controls[num_ctrls] = NULL; + + for (i = 0; i < (num_ctrls -1); i++) { + ac->controls[i] = talloc_reference(ac->controls, + ac->store->controls[i]); + } + + ac->controls[i] = talloc(ac->controls, struct ldb_control); + if (ac->controls[i] == NULL) { + ret = LDB_ERR_OPERATIONS_ERROR; + return ldb_module_done( + ac->req, ac->controls, response, ret); + } + + ac->controls[i]->oid = talloc_strdup(ac->controls[i], + LDB_CONTROL_PAGED_RESULTS_OID); + if (ac->controls[i]->oid == NULL) { + ret = LDB_ERR_OPERATIONS_ERROR; + return ldb_module_done( + ac->req, ac->controls, response, ret); + } + + ac->controls[i]->critical = 0; + + paged = talloc(ac->controls[i], struct ldb_paged_control); + if (paged == NULL) { + ret = LDB_ERR_OPERATIONS_ERROR; + return ldb_module_done( + ac->req, ac->controls, response, ret); + } + + ac->controls[i]->data = paged; + + if (ac->size > 0) { + paged->size = 0; + paged->cookie = NULL; + paged->cookie_len = 0; + } else { + paged->size = ac->store->num_entries; + paged->cookie = talloc_strdup(paged, ac->store->cookie); + paged->cookie_len = strlen(paged->cookie) + 1; + } + + return LDB_SUCCESS; +} + +static int save_referral(struct results_store *store, char *ref) +{ + struct referral_store *node = talloc(store, + struct referral_store); + if (node == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + node->next = NULL; + node->ref = talloc_steal(node, ref); + + if (store->first_ref == NULL) { + store->first_ref = node; + } else { + store->last_ref->next = node; + } + store->last_ref = node; + return LDB_SUCCESS; +} + +static int paged_search_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct paged_context *ac; + struct results_store *store; + int ret; + const struct ldb_val *guid_blob; + struct GUID guid; + NTSTATUS status; + + ac = talloc_get_type(req->context, struct paged_context); + store = ac->store; + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + if (store->results == NULL) { + store->num_entries = 0; + store->result_array_size = 16; + store->results = talloc_array(store, struct GUID, + store->result_array_size); + if (store->results == NULL) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + } else if (store->num_entries == store->result_array_size) { + if (store->result_array_size > INT_MAX/2) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + store->result_array_size *= 2; + store->results = talloc_realloc(store, store->results, + struct GUID, + store->result_array_size); + if (store->results == NULL) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + } + + guid_blob = ldb_dn_get_extended_component(ares->message->dn, + "GUID"); + if (guid_blob == NULL) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + status = GUID_from_ndr_blob(guid_blob, &guid); + if (!NT_STATUS_IS_OK(status)) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + /* Redundant paranoid check */ + if (store->num_entries > store->result_array_size) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + store->results[store->num_entries] = guid; + store->num_entries++; + break; + + case LDB_REPLY_REFERRAL: + ret = save_referral(store, ares->referral); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + break; + + case LDB_REPLY_DONE: + if (store->num_entries != 0) { + store->results = talloc_realloc(store, store->results, + struct GUID, + store->num_entries); + if (store->results == NULL) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + } + store->result_array_size = store->num_entries; + + ac->store->controls = talloc_move(ac->store, &ares->controls); + ret = paged_results(ac, ares); + if (ret != LDB_SUCCESS) { + /* paged_results will have called ldb_module_done + * if an error occurred + */ + return ret; + } + return ldb_module_done(ac->req, ac->controls, + ares->response, ret); + } + + return LDB_SUCCESS; +} + +static struct ldb_control ** +paged_results_copy_down_controls(TALLOC_CTX *mem_ctx, + struct ldb_control **controls) +{ + + struct ldb_control **new_controls; + unsigned int i, j, num_ctrls; + if (controls == NULL) { + return NULL; + } + + for (num_ctrls = 0; controls[num_ctrls]; num_ctrls++); + + new_controls = talloc_array(mem_ctx, struct ldb_control *, num_ctrls); + if (new_controls == NULL) { + return NULL; + } + + for (j = 0, i = 0; i < (num_ctrls); i++) { + struct ldb_control *control = controls[i]; + if (control->oid == NULL) { + continue; + } + if (strcmp(control->oid, LDB_CONTROL_PAGED_RESULTS_OID) == 0) { + continue; + } + /* + * ASQ changes everything, do not copy it down for the + * per-GUID search + */ + if (strcmp(control->oid, LDB_CONTROL_ASQ_OID) == 0) { + continue; + } + new_controls[j] = talloc_steal(new_controls, control); + + /* + * Sadly the caller is not obliged to make this a + * proper talloc tree, so we do so here. + */ + if (control->data) { + talloc_steal(control, control->data); + } + j++; + } + new_controls[j] = NULL; + return new_controls; +} + +static const char * const *paged_copy_attrs(TALLOC_CTX *mem_ctx, + const char * const *attrs) { + int i; + const char **new_attrs; + if (attrs == NULL) { + return NULL; + } + new_attrs = ldb_attr_list_copy(mem_ctx, attrs); + + for (i=0; attrs[i] != NULL; i++) { + new_attrs[i] = talloc_strdup(mem_ctx, attrs[i]); + } + new_attrs[i] = NULL; + return new_attrs; +} + +/* + * Check if two sets of controls are the same except for the paged results + * control in the request controls. This function is messy because request + * control lists can contain controls that were NULL'd by the rootdse. We + * must ignore those entries. This function is not portable. + */ +static bool paged_controls_same(struct ldb_request *req, + struct ldb_control **down_controls) { + int i; + unsigned int num_down_controls, num_non_null_req_controls; + struct ldb_control *ctrl; + + num_down_controls = 0; + for (i=0; down_controls[i] != NULL; i++) { + num_down_controls++; + + ctrl = ldb_request_get_control(req, down_controls[i]->oid); + if (ctrl == NULL) { + return false; + } + } + + num_non_null_req_controls = 0; + for (i=0; req->controls[i] != NULL; i++) { + if (req->controls[i]->oid != NULL && + strcmp(req->controls[i]->oid, + LDB_CONTROL_ASQ_OID) != 0) { + num_non_null_req_controls++; + } + } + + /* At this point we have the number of non-null entries for both + * control lists and we know that: + * 1. down_controls does not contain the paged control or ASQ + * (because paged_results_copy_down_controls excludes it) + * 2. req->controls does contain the paged control + * (because this function is only called if this is true) + * 3. down_controls is a subset of non-null controls in req->controls + * (checked above) + * So to confirm that the two lists are identical except for the paged + * control and possibly ASQ, all we need to check is: */ + if (num_non_null_req_controls == num_down_controls + 1) { + return true; + } + return false; +} + +static bool paged_attrs_same(const char * const *attrs_1, + const char * const *attrs_2) { + int i; + if (attrs_1 == NULL || attrs_2 == NULL) { + if (attrs_1 == NULL && attrs_2 == NULL) { + return true; + } + return false; + } + + for (i=0; attrs_1[i] != NULL; i++) { + if (!ldb_attr_in_list(attrs_2, attrs_1[i])) { + return false; + } + } + return true; +} + +static int paged_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct ldb_control *control; + struct ldb_control *vlv_control; + struct private_data *private_data; + struct ldb_paged_control *paged_ctrl; + struct ldb_request *search_req; + struct paged_context *ac; + int ret; + + ldb = ldb_module_get_ctx(module); + + /* check if there's a paged request control */ + control = ldb_request_get_control(req, LDB_CONTROL_PAGED_RESULTS_OID); + if (control == NULL) { + /* not found go on */ + return ldb_next_request(module, req); + } + + paged_ctrl = talloc_get_type(control->data, struct ldb_paged_control); + if (!paged_ctrl) { + return LDB_ERR_PROTOCOL_ERROR; + } + + private_data = talloc_get_type(ldb_module_get_private(module), + struct private_data); + + vlv_control = ldb_request_get_control(req, LDB_CONTROL_VLV_REQ_OID); + if (vlv_control != NULL) { + /* + * VLV and paged_results are not allowed at the same + * time + */ + return LDB_ERR_UNSUPPORTED_CRITICAL_EXTENSION; + } + + ac = talloc_zero(req, struct paged_context); + if (ac == NULL) { + ldb_set_errstring(ldb, "Out of Memory"); + return LDB_ERR_OPERATIONS_ERROR; + } + + ac->module = module; + ac->req = req; + ac->size = paged_ctrl->size; + if (ac->size < 0) { + /* + * Apparently some clients send more than 2^31. This + * violates the ldap standard, but we need to cope. + * In the future, if maximum result sizes are implemented in + * Samba, we should also clamp the page size to the maximum + * result size. + */ + ac->size = 0x7FFFFFFF; + } + + /* check if it is a continuation search the store */ + if (paged_ctrl->cookie_len == 0) { + struct ldb_control *ext_ctrl; + struct ldb_control **controls; + static const char * const attrs[1] = { NULL }; + + if (paged_ctrl->size == 0) { + return LDB_ERR_OPERATIONS_ERROR; + } + + ac->store = new_store(private_data); + if (ac->store == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + controls = req->controls; + ext_ctrl = ldb_request_get_control(req, + LDB_CONTROL_EXTENDED_DN_OID); + if (ext_ctrl == NULL) { + /* + * Add extended_dn control to the request if there + * isn't already one. We'll get the GUID out of it in + * the callback. This is a workaround for the case + * where ntsecuritydescriptor forbids fetching GUIDs + * for the current user. + */ + struct ldb_request *req_extended_dn; + struct ldb_extended_dn_control *ext_ctrl_data; + req_extended_dn = talloc_zero(req, struct ldb_request); + req_extended_dn->controls = req->controls; + ext_ctrl_data = talloc_zero(req, + struct ldb_extended_dn_control); + ext_ctrl_data->type = 1; + + ret = ldb_request_add_control(req_extended_dn, + LDB_CONTROL_EXTENDED_DN_OID, + true, + ext_ctrl_data); + if (ret != LDB_SUCCESS) { + return ret; + } + controls = req_extended_dn->controls; + } + + ret = ldb_build_search_req_ex(&search_req, ldb, ac, + req->op.search.base, + req->op.search.scope, + req->op.search.tree, + attrs, + controls, + ac, + paged_search_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ac->store->expr = talloc_steal(ac->store, req->op.search.tree); + ac->store->expr_str = ldb_filter_from_tree(ac->store, + req->op.search.tree); + ac->store->attrs = paged_copy_attrs(ac->store, + req->op.search.attrs); + + /* save it locally and remove it from the list */ + /* we do not need to replace them later as we + * are keeping the original req intact */ + if (!ldb_save_controls(control, search_req, NULL)) { + return LDB_ERR_OPERATIONS_ERROR; + } + ac->store->down_controls = + paged_results_copy_down_controls(ac->store, req->controls); + if (ac->store->down_controls == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + return ldb_next_request(module, search_req); + + } else { + struct results_store *current = NULL; + char *expr_str; + bool bool_ret; + + /* TODO: age out old outstanding requests */ + for (current = private_data->store; current != NULL; + current = current->next) { + if (strcmp(current->cookie, paged_ctrl->cookie) == 0) { + current->timestamp = time(NULL); + break; + } + } + if (current == NULL) { + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* Get the expression string and make sure it didn't change */ + expr_str = ldb_filter_from_tree(ac, req->op.search.tree); + if (expr_str == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = strcmp(current->expr_str, expr_str); + if (ret != 0) { + return LDB_ERR_UNSUPPORTED_CRITICAL_EXTENSION; + } + + bool_ret = paged_controls_same(req, current->down_controls); + if (bool_ret == false) { + return LDB_ERR_UNSUPPORTED_CRITICAL_EXTENSION; + } + + bool_ret = paged_attrs_same(req->op.search.attrs, + current->attrs); + if (bool_ret == false) { + return LDB_ERR_UNSUPPORTED_CRITICAL_EXTENSION; + } + + DLIST_PROMOTE(private_data->store, current); + + ac->store = current; + + /* check if it is an abandon */ + if (ac->size == 0) { + return ldb_module_done(req, NULL, NULL, + LDB_SUCCESS); + } + + ret = paged_results(ac, NULL); + if (ret != LDB_SUCCESS) { + /* + * paged_results() will have called ldb_module_done + * if an error occurred + */ + return ret; + } + return ldb_module_done(req, ac->controls, NULL, LDB_SUCCESS); + } +} + +static int paged_request_init(struct ldb_module *module) +{ + struct ldb_context *ldb; + struct private_data *data; + int ret; + + ldb = ldb_module_get_ctx(module); + + data = talloc(module, struct private_data); + if (data == NULL) { + return LDB_ERR_OTHER; + } + + data->next_free_id = 1; + data->num_stores = 0; + data->store = NULL; + ldb_module_set_private(module, data); + + ret = ldb_mod_register_control(module, LDB_CONTROL_PAGED_RESULTS_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_WARNING, + "paged_results:" + "Unable to register control with rootdse!"); + } + + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_paged_results_module_ops = { + .name = "dsdb_paged_results", + .search = paged_search, + .init_context = paged_request_init +}; + +int ldb_dsdb_paged_results_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_paged_results_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/partition.c b/source4/dsdb/samdb/ldb_modules/partition.c new file mode 100644 index 0000000..9ed0990 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/partition.c @@ -0,0 +1,1721 @@ +/* + Partitions ldb module + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2006 + Copyright (C) Stefan Metzmacher <metze@samba.org> 2007 + + 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 partitions module + * + * Description: Implement LDAP partitions + * + * Author: Andrew Bartlett + * Author: Stefan Metzmacher + */ + +#include "dsdb/samdb/ldb_modules/partition.h" + +struct part_request { + struct ldb_module *module; + struct ldb_request *req; +}; + +struct partition_context { + struct ldb_module *module; + struct ldb_request *req; + + struct part_request *part_req; + unsigned int num_requests; + unsigned int finished_requests; + + const char **referrals; +}; + +static struct partition_context *partition_init_ctx(struct ldb_module *module, struct ldb_request *req) +{ + struct partition_context *ac; + + ac = talloc_zero(req, struct partition_context); + if (ac == NULL) { + ldb_set_errstring(ldb_module_get_ctx(module), "Out of Memory"); + return NULL; + } + + ac->module = module; + ac->req = req; + + return ac; +} + +/* + * helper functions to call the next module in chain + */ +int partition_request(struct ldb_module *module, struct ldb_request *request) +{ + if ((module && ldb_module_flags(ldb_module_get_ctx(module)) & LDB_FLG_ENABLE_TRACING)) { \ + const struct dsdb_control_current_partition *partition = NULL; + struct ldb_control *partition_ctrl = ldb_request_get_control(request, DSDB_CONTROL_CURRENT_PARTITION_OID); + if (partition_ctrl) { + partition = talloc_get_type(partition_ctrl->data, + struct dsdb_control_current_partition); + } + + if (partition != NULL) { + ldb_debug(ldb_module_get_ctx(module), LDB_DEBUG_TRACE, "partition_request() -> %s", + ldb_dn_get_linearized(partition->dn)); + } else { + ldb_debug(ldb_module_get_ctx(module), LDB_DEBUG_TRACE, "partition_request() -> (metadata partition)"); + } + } + + return ldb_next_request(module, request); +} + +static struct dsdb_partition *find_partition(struct partition_private_data *data, + struct ldb_dn *dn, + struct ldb_request *req) +{ + unsigned int i; + struct ldb_control *partition_ctrl; + + /* see if the request has the partition DN specified in a + * control. The repl_meta_data module can specify this to + * ensure that replication happens to the right partition + */ + partition_ctrl = ldb_request_get_control(req, DSDB_CONTROL_CURRENT_PARTITION_OID); + if (partition_ctrl) { + const struct dsdb_control_current_partition *partition; + partition = talloc_get_type(partition_ctrl->data, + struct dsdb_control_current_partition); + if (partition != NULL) { + dn = partition->dn; + } + } + + if (dn == NULL) { + return NULL; + } + + /* Look at base DN */ + /* Figure out which partition it is under */ + /* Skip the lot if 'data' isn't here yet (initialisation) */ + for (i=0; data && data->partitions && data->partitions[i]; i++) { + if (ldb_dn_compare_base(data->partitions[i]->ctrl->dn, dn) == 0) { + return data->partitions[i]; + } + } + + return NULL; +} + +/** + * fire the caller's callback for every entry, but only send 'done' once. + */ +static int partition_req_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct partition_context *ac; + struct ldb_module *module; + struct ldb_request *nreq; + int ret; + struct ldb_control *partition_ctrl; + + ac = talloc_get_type(req->context, struct partition_context); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + partition_ctrl = ldb_request_get_control(req, DSDB_CONTROL_CURRENT_PARTITION_OID); + if (partition_ctrl && (ac->num_requests == 1 || ares->type == LDB_REPLY_ENTRY)) { + /* If we didn't fan this request out to mulitple partitions, + * or this is an individual search result, we can + * deterministically tell the caller what partition this was + * written to (repl_meta_data likes to know) */ + ret = ldb_reply_add_control(ares, + DSDB_CONTROL_CURRENT_PARTITION_OID, + false, partition_ctrl->data); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, + ret); + } + } + + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral(ac->req, ares->referral); + + case LDB_REPLY_ENTRY: + if (ac->req->operation != LDB_SEARCH) { + ldb_set_errstring(ldb_module_get_ctx(ac->module), + "partition_req_callback:" + " Unsupported reply type for this request"); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + return ldb_module_send_entry(ac->req, ares->message, ares->controls); + + case LDB_REPLY_DONE: + if (ac->req->operation == LDB_EXTENDED) { + /* FIXME: check for ares->response, replmd does not fill it ! */ + if (ares->response) { + if (strcmp(ares->response->oid, LDB_EXTENDED_START_TLS_OID) != 0) { + ldb_set_errstring(ldb_module_get_ctx(ac->module), + "partition_req_callback:" + " Unknown extended reply, " + "only supports START_TLS"); + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + } + } + + ac->finished_requests++; + if (ac->finished_requests == ac->num_requests) { + /* Send back referrals if they do exist (search ops) */ + if (ac->referrals != NULL) { + const char **ref; + for (ref = ac->referrals; *ref != NULL; ++ref) { + ret = ldb_module_send_referral(ac->req, + talloc_strdup(ac->req, *ref)); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, + ret); + } + } + } + + /* this was the last one, call callback */ + return ldb_module_done(ac->req, ares->controls, + ares->response, + ares->error); + } + + /* not the last, now call the next one */ + module = ac->part_req[ac->finished_requests].module; + nreq = ac->part_req[ac->finished_requests].req; + + ret = partition_request(module, nreq); + if (ret != LDB_SUCCESS) { + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + break; + } + + talloc_free(ares); + return LDB_SUCCESS; +} + +static int partition_prep_request(struct partition_context *ac, + struct dsdb_partition *partition) +{ + int ret; + struct ldb_request *req; + struct ldb_control *partition_ctrl = NULL; + void *part_data = NULL; + + ac->part_req = talloc_realloc(ac, ac->part_req, + struct part_request, + ac->num_requests + 1); + if (ac->part_req == NULL) { + return ldb_oom(ldb_module_get_ctx(ac->module)); + } + + switch (ac->req->operation) { + case LDB_SEARCH: + ret = ldb_build_search_req_ex(&req, ldb_module_get_ctx(ac->module), + ac->part_req, + ac->req->op.search.base, + ac->req->op.search.scope, + ac->req->op.search.tree, + ac->req->op.search.attrs, + ac->req->controls, + ac, partition_req_callback, + ac->req); + LDB_REQ_SET_LOCATION(req); + break; + case LDB_ADD: + ret = ldb_build_add_req(&req, ldb_module_get_ctx(ac->module), ac->part_req, + ac->req->op.add.message, + ac->req->controls, + ac, partition_req_callback, + ac->req); + LDB_REQ_SET_LOCATION(req); + break; + case LDB_MODIFY: + ret = ldb_build_mod_req(&req, ldb_module_get_ctx(ac->module), ac->part_req, + ac->req->op.mod.message, + ac->req->controls, + ac, partition_req_callback, + ac->req); + LDB_REQ_SET_LOCATION(req); + break; + case LDB_DELETE: + ret = ldb_build_del_req(&req, ldb_module_get_ctx(ac->module), ac->part_req, + ac->req->op.del.dn, + ac->req->controls, + ac, partition_req_callback, + ac->req); + LDB_REQ_SET_LOCATION(req); + break; + case LDB_RENAME: + ret = ldb_build_rename_req(&req, ldb_module_get_ctx(ac->module), ac->part_req, + ac->req->op.rename.olddn, + ac->req->op.rename.newdn, + ac->req->controls, + ac, partition_req_callback, + ac->req); + LDB_REQ_SET_LOCATION(req); + break; + case LDB_EXTENDED: + ret = ldb_build_extended_req(&req, ldb_module_get_ctx(ac->module), + ac->part_req, + ac->req->op.extended.oid, + ac->req->op.extended.data, + ac->req->controls, + ac, partition_req_callback, + ac->req); + LDB_REQ_SET_LOCATION(req); + break; + default: + ldb_set_errstring(ldb_module_get_ctx(ac->module), + "Unsupported request type!"); + ret = LDB_ERR_UNWILLING_TO_PERFORM; + } + + if (ret != LDB_SUCCESS) { + return ret; + } + + ac->part_req[ac->num_requests].req = req; + + if (ac->req->controls) { + /* Duplicate everything beside the current partition control */ + partition_ctrl = ldb_request_get_control(ac->req, + DSDB_CONTROL_CURRENT_PARTITION_OID); + if (!ldb_save_controls(partition_ctrl, req, NULL)) { + return ldb_module_oom(ac->module); + } + } + + part_data = partition->ctrl; + + ac->part_req[ac->num_requests].module = partition->module; + + if (partition_ctrl != NULL) { + if (partition_ctrl->data != NULL) { + part_data = partition_ctrl->data; + } + + /* + * If the provided current partition control is without + * data then use the calculated one. + */ + ret = ldb_request_add_control(req, + DSDB_CONTROL_CURRENT_PARTITION_OID, + false, part_data); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + if (req->operation == LDB_SEARCH) { + /* + * If the search is for 'more' than this partition, + * then change the basedn, so the check of the BASE DN + * still passes in the ldb_key_value layer + */ + if (ldb_dn_compare_base(partition->ctrl->dn, + req->op.search.base) != 0) { + req->op.search.base = partition->ctrl->dn; + } + } + + ac->num_requests++; + + return LDB_SUCCESS; +} + +static int partition_call_first(struct partition_context *ac) +{ + return partition_request(ac->part_req[0].module, ac->part_req[0].req); +} + +/** + * Send a request down to all the partitions (but not the sam.ldb file) + */ +static int partition_send_all(struct ldb_module *module, + struct partition_context *ac, + struct ldb_request *req) +{ + unsigned int i; + struct partition_private_data *data = talloc_get_type(ldb_module_get_private(module), + struct partition_private_data); + int ret; + + for (i=0; data && data->partitions && data->partitions[i]; i++) { + ret = partition_prep_request(ac, data->partitions[i]); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* fire the first one */ + return partition_call_first(ac); +} + +struct partition_copy_context { + struct ldb_module *module; + struct partition_context *partition_context; + struct ldb_request *request; + struct ldb_dn *dn; +}; + +/* + * A special DN has been updated in the primary partition. Now propagate those + * changes to the remaining partitions. + * + * Note: that the operations are asynchronous and this function is called + * from partition_copy_all_callback_handler in response to an async + * callback. + */ +static int partition_copy_all_callback_action( + struct ldb_module *module, + struct partition_context *ac, + struct ldb_request *req, + struct ldb_dn *dn) + +{ + + unsigned int i; + struct partition_private_data *data = + talloc_get_type( + ldb_module_get_private(module), + struct partition_private_data); + int search_ret; + struct ldb_result *res; + /* now fetch the resulting object, and then copy it to all the + * other partitions. We need this approach to cope with the + * partitions getting out of sync. If for example the + * @ATTRIBUTES object exists on one partition but not the + * others then just doing each of the partitions in turn will + * lead to an error + */ + search_ret = dsdb_module_search_dn(module, ac, &res, dn, NULL, DSDB_FLAG_NEXT_MODULE, req); + if (search_ret != LDB_SUCCESS) { + return search_ret; + } + + /* now delete the object in the other partitions, if requried + */ + if (search_ret == LDB_ERR_NO_SUCH_OBJECT) { + for (i=0; data->partitions && data->partitions[i]; i++) { + int pret; + pret = dsdb_module_del(data->partitions[i]->module, + dn, + DSDB_FLAG_NEXT_MODULE, + req); + if (pret != LDB_SUCCESS && pret != LDB_ERR_NO_SUCH_OBJECT) { + /* we should only get success or no + such object from the other partitions */ + return pret; + } + } + + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS); + } + + /* now add/modify in the other partitions */ + for (i=0; data->partitions && data->partitions[i]; i++) { + struct ldb_message *modify_msg = NULL; + int pret; + unsigned int el_idx; + + pret = dsdb_module_add(data->partitions[i]->module, + res->msgs[0], + DSDB_FLAG_NEXT_MODULE, + req); + if (pret == LDB_SUCCESS) { + continue; + } + + if (pret != LDB_ERR_ENTRY_ALREADY_EXISTS) { + return pret; + } + + modify_msg = ldb_msg_copy(req, res->msgs[0]); + if (modify_msg == NULL) { + return ldb_module_oom(module); + } + + /* + * mark all the message elements as + * LDB_FLAG_MOD_REPLACE + */ + for (el_idx=0; + el_idx < modify_msg->num_elements; + el_idx++) { + modify_msg->elements[el_idx].flags + = LDB_FLAG_MOD_REPLACE; + } + + if (req->operation == LDB_MODIFY) { + const struct ldb_message *req_msg = req->op.mod.message; + /* + * mark elements to be removed, if there were + * deleted entirely above we need to delete + * them here too + */ + for (el_idx=0; el_idx < req_msg->num_elements; el_idx++) { + if (LDB_FLAG_MOD_TYPE(req_msg->elements[el_idx].flags) == LDB_FLAG_MOD_DELETE + || ((LDB_FLAG_MOD_TYPE(req_msg->elements[el_idx].flags) == LDB_FLAG_MOD_REPLACE) && + req_msg->elements[el_idx].num_values == 0)) { + if (ldb_msg_find_element(modify_msg, + req_msg->elements[el_idx].name) != NULL) { + continue; + } + pret = ldb_msg_add_empty( + modify_msg, + req_msg->elements[el_idx].name, + LDB_FLAG_MOD_REPLACE, + NULL); + if (pret != LDB_SUCCESS) { + return pret; + } + } + } + } + + pret = dsdb_module_modify(data->partitions[i]->module, + modify_msg, + DSDB_FLAG_NEXT_MODULE, + req); + + if (pret != LDB_SUCCESS) { + return pret; + } + } + + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS); +} + + +/* + * @brief call back function for the ldb operations on special DN's. + * + * As the LDB operations are async, and we wish to use the result + * the operations, a callback needs to be registered to process the results + * of the LDB operations. + * + * @param req the ldb request + * @param res the result of the operation + * + * @return the LDB_STATUS + */ +static int partition_copy_all_callback_handler( + struct ldb_request *req, + struct ldb_reply *ares) +{ + struct partition_copy_context *ac = NULL; + + ac = talloc_get_type( + req->context, + struct partition_copy_context); + + if (!ares) { + return ldb_module_done( + ac->request, + NULL, + NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + /* pass on to the callback */ + switch (ares->type) { + case LDB_REPLY_ENTRY: + return ldb_module_send_entry( + ac->request, + ares->message, + ares->controls); + + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral( + ac->request, + ares->referral); + + case LDB_REPLY_DONE: { + int error = ares->error; + if (error == LDB_SUCCESS) { + error = partition_copy_all_callback_action( + ac->module, + ac->partition_context, + ac->request, + ac->dn); + } + return ldb_module_done( + ac->request, + ares->controls, + ares->response, + error); + } + + default: + /* Can't happen */ + return LDB_ERR_OPERATIONS_ERROR; + } +} + +/** + * send an operation to the top partition, then copy the resulting + * object to all other partitions. + */ +static int partition_copy_all( + struct ldb_module *module, + struct partition_context *partition_context, + struct ldb_request *req, + struct ldb_dn *dn) +{ + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + struct partition_copy_context *context = NULL; + + int ret; + + ldb = ldb_module_get_ctx(module); + + context = talloc_zero(req, struct partition_copy_context); + if (context == NULL) { + return ldb_oom(ldb); + } + context->module = module; + context->request = req; + context->dn = dn; + context->partition_context = partition_context; + + switch (req->operation) { + case LDB_ADD: + ret = ldb_build_add_req( + &new_req, + ldb, + req, + req->op.add.message, + req->controls, + context, + partition_copy_all_callback_handler, + req); + break; + case LDB_MODIFY: + ret = ldb_build_mod_req( + &new_req, + ldb, + req, + req->op.mod.message, + req->controls, + context, + partition_copy_all_callback_handler, + req); + break; + case LDB_DELETE: + ret = ldb_build_del_req( + &new_req, + ldb, + req, + req->op.del.dn, + req->controls, + context, + partition_copy_all_callback_handler, + req); + break; + case LDB_RENAME: + ret = ldb_build_rename_req( + &new_req, + ldb, + req, + req->op.rename.olddn, + req->op.rename.newdn, + req->controls, + context, + partition_copy_all_callback_handler, + req); + break; + default: + /* + * Shouldn't happen. + */ + ldb_debug( + ldb, + LDB_DEBUG_ERROR, + "Unexpected operation type (%d)\n", req->operation); + ret = LDB_ERR_OPERATIONS_ERROR; + break; + } + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(module, new_req); +} +/** + * Figure out which backend a request needs to be aimed at. Some + * requests must be replicated to all backends + */ +static int partition_replicate(struct ldb_module *module, struct ldb_request *req, struct ldb_dn *dn) +{ + struct partition_context *ac; + unsigned int i; + int ret; + struct dsdb_partition *partition; + struct partition_private_data *data = talloc_get_type(ldb_module_get_private(module), + struct partition_private_data); + + /* if we aren't initialised yet go further */ + if (!data || !data->partitions) { + return ldb_next_request(module, req); + } + + if (ldb_dn_is_special(dn)) { + /* Is this a special DN, we need to replicate to every backend? */ + for (i=0; data->replicate && data->replicate[i]; i++) { + if (ldb_dn_compare(data->replicate[i], + dn) == 0) { + + ac = partition_init_ctx(module, req); + if (!ac) { + return ldb_operr(ldb_module_get_ctx(module)); + } + + return partition_copy_all(module, ac, req, dn); + } + } + } + + /* Otherwise, we need to find the partition to fire it to */ + + /* Find partition */ + partition = find_partition(data, dn, req); + if (!partition) { + /* + * if we haven't found a matching partition + * pass the request to the main ldb + * + * TODO: we should maybe return an error here + * if it's not a special dn + */ + + return ldb_next_request(module, req); + } + + ac = partition_init_ctx(module, req); + if (!ac) { + return ldb_operr(ldb_module_get_ctx(module)); + } + + /* we need to add a control but we never touch the original request */ + ret = partition_prep_request(ac, partition); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* fire the first one */ + return partition_call_first(ac); +} + +/* search */ +static int partition_search(struct ldb_module *module, struct ldb_request *req) +{ + /* Find backend */ + struct partition_private_data *data = talloc_get_type(ldb_module_get_private(module), + struct partition_private_data); + struct partition_context *ac; + struct ldb_context *ldb; + struct loadparm_context *lp_ctx; + + struct ldb_control *search_control = ldb_request_get_control(req, LDB_CONTROL_SEARCH_OPTIONS_OID); + struct ldb_control *domain_scope_control = ldb_request_get_control(req, LDB_CONTROL_DOMAIN_SCOPE_OID); + struct ldb_control *no_gc_control = ldb_request_get_control(req, DSDB_CONTROL_NO_GLOBAL_CATALOG); + + struct ldb_search_options_control *search_options = NULL; + struct dsdb_partition *p; + unsigned int i, j; + int ret; + bool domain_scope = false, phantom_root = false; + + p = find_partition(data, NULL, req); + if (p != NULL) { + /* the caller specified what partition they want the + * search - just pass it on + */ + return ldb_next_request(p->module, req); + } + + /* Get back the search options from the search control, and mark it as + * non-critical (to make backends and also dcpromo happy). + */ + if (search_control) { + search_options = talloc_get_type(search_control->data, struct ldb_search_options_control); + search_control->critical = 0; + + } + + /* if we aren't initialised yet go further */ + if (!data || !data->partitions) { + return ldb_next_request(module, req); + } + + /* Special DNs without specified partition should go further */ + if (ldb_dn_is_special(req->op.search.base)) { + return ldb_next_request(module, req); + } + + /* Locate the options */ + domain_scope = (search_options + && (search_options->search_options & LDB_SEARCH_OPTION_DOMAIN_SCOPE)) + || domain_scope_control; + phantom_root = search_options + && (search_options->search_options & LDB_SEARCH_OPTION_PHANTOM_ROOT); + + /* Remove handled options from the search control flag */ + if (search_options) { + search_options->search_options = search_options->search_options + & ~LDB_SEARCH_OPTION_DOMAIN_SCOPE + & ~LDB_SEARCH_OPTION_PHANTOM_ROOT; + } + + ac = partition_init_ctx(module, req); + if (!ac) { + return ldb_operr(ldb_module_get_ctx(module)); + } + + ldb = ldb_module_get_ctx(ac->module); + lp_ctx = talloc_get_type(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + + /* Search from the base DN */ + if (ldb_dn_is_null(req->op.search.base)) { + if (!phantom_root) { + return ldb_error(ldb, LDB_ERR_NO_SUCH_OBJECT, "empty base DN"); + } + return partition_send_all(module, ac, req); + } + + for (i=0; data->partitions[i]; i++) { + bool match = false, stop = false; + + if (data->partitions[i]->partial_replica && no_gc_control != NULL) { + if (ldb_dn_compare_base(data->partitions[i]->ctrl->dn, + req->op.search.base) == 0) { + /* base DN is in a partial replica + with the NO_GLOBAL_CATALOG + control. This partition is invisible */ + /* DEBUG(0,("DENYING NON-GC OP: %s\n", ldb_module_call_chain(req, req))); */ + continue; + } + } + + if (phantom_root) { + /* Phantom root: Find all partitions under the + * search base. We match if: + * + * 1) the DN we are looking for exactly matches a + * certain partition and always stop + * 2) the DN we are looking for is a parent of certain + * partitions and it isn't a scope base search + * 3) the DN we are looking for is a child of a certain + * partition and always stop + * - we don't need to go any further up in the + * hierarchy! + */ + if (ldb_dn_compare(data->partitions[i]->ctrl->dn, + req->op.search.base) == 0) { + match = true; + stop = true; + } + if (!match && + (ldb_dn_compare_base(req->op.search.base, + data->partitions[i]->ctrl->dn) == 0 && + req->op.search.scope != LDB_SCOPE_BASE)) { + match = true; + } + if (!match && + ldb_dn_compare_base(data->partitions[i]->ctrl->dn, + req->op.search.base) == 0) { + match = true; + stop = true; /* note that this relies on partition ordering */ + } + } else { + /* Domain scope: Find all partitions under the search + * base. + * + * We generate referral candidates if we haven't + * specified the domain scope control, haven't a base + * search* scope and the DN we are looking for is a real + * predecessor of certain partitions. When a new + * referral candidate is nearer to the DN than an + * existing one delete the latter (we want to have only + * the closest ones). When we checked this for all + * candidates we have the final referrals. + * + * We match if the DN we are looking for is a child of + * a certain partition or the partition + * DN itself - we don't need to go any further + * up in the hierarchy! + */ + if ((!domain_scope) && + (req->op.search.scope != LDB_SCOPE_BASE) && + (ldb_dn_compare_base(req->op.search.base, + data->partitions[i]->ctrl->dn) == 0) && + (ldb_dn_compare(req->op.search.base, + data->partitions[i]->ctrl->dn) != 0)) { + const char *scheme = ldb_get_opaque( + ldb, LDAP_REFERRAL_SCHEME_OPAQUE); + char *ref = talloc_asprintf( + ac, + "%s://%s/%s%s", + scheme == NULL ? "ldap" : scheme, + lpcfg_dnsdomain(lp_ctx), + ldb_dn_get_linearized( + data->partitions[i]->ctrl->dn), + req->op.search.scope == + LDB_SCOPE_ONELEVEL ? "??base" : ""); + + if (ref == NULL) { + return ldb_oom(ldb); + } + + /* Initialise the referrals list */ + if (ac->referrals == NULL) { + char **l = str_list_make_empty(ac); + ac->referrals = discard_const_p(const char *, l); + if (ac->referrals == NULL) { + return ldb_oom(ldb); + } + } + + /* Check if the new referral candidate is + * closer to the base DN than already + * saved ones and delete the latters */ + j = 0; + while (ac->referrals[j] != NULL) { + if (strstr(ac->referrals[j], + ldb_dn_get_linearized(data->partitions[i]->ctrl->dn)) != NULL) { + str_list_remove(ac->referrals, + ac->referrals[j]); + } else { + ++j; + } + } + + /* Add our new candidate */ + ac->referrals = str_list_add(ac->referrals, ref); + + talloc_free(ref); + + if (ac->referrals == NULL) { + return ldb_oom(ldb); + } + } + if (ldb_dn_compare_base(data->partitions[i]->ctrl->dn, req->op.search.base) == 0) { + match = true; + stop = true; /* note that this relies on partition ordering */ + } + } + + if (match) { + ret = partition_prep_request(ac, data->partitions[i]); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + if (stop) break; + } + + /* Perhaps we didn't match any partitions. Try the main partition */ + if (ac->num_requests == 0) { + talloc_free(ac); + return ldb_next_request(module, req); + } + + /* fire the first one */ + return partition_call_first(ac); +} + +/* add */ +static int partition_add(struct ldb_module *module, struct ldb_request *req) +{ + return partition_replicate(module, req, req->op.add.message->dn); +} + +/* modify */ +static int partition_modify(struct ldb_module *module, struct ldb_request *req) +{ + return partition_replicate(module, req, req->op.mod.message->dn); +} + +/* delete */ +static int partition_delete(struct ldb_module *module, struct ldb_request *req) +{ + return partition_replicate(module, req, req->op.del.dn); +} + +/* rename */ +static int partition_rename(struct ldb_module *module, struct ldb_request *req) +{ + /* Find backend */ + struct dsdb_partition *backend, *backend2; + + struct partition_private_data *data = talloc_get_type(ldb_module_get_private(module), + struct partition_private_data); + + /* Skip the lot if 'data' isn't here yet (initialisation) */ + if (!data) { + return ldb_operr(ldb_module_get_ctx(module)); + } + + backend = find_partition(data, req->op.rename.olddn, req); + backend2 = find_partition(data, req->op.rename.newdn, req); + + if ((backend && !backend2) || (!backend && backend2)) { + return LDB_ERR_AFFECTS_MULTIPLE_DSAS; + } + + if (backend != backend2) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "Cannot rename from %s in %s to %s in %s: %s", + ldb_dn_get_linearized(req->op.rename.olddn), + ldb_dn_get_linearized(backend->ctrl->dn), + ldb_dn_get_linearized(req->op.rename.newdn), + ldb_dn_get_linearized(backend2->ctrl->dn), + ldb_strerror(LDB_ERR_AFFECTS_MULTIPLE_DSAS)); + return LDB_ERR_AFFECTS_MULTIPLE_DSAS; + } + + return partition_replicate(module, req, req->op.rename.olddn); +} + +/* start a transaction */ +int partition_start_trans(struct ldb_module *module) +{ + int i = 0; + int ret = 0; + struct partition_private_data *data = talloc_get_type(ldb_module_get_private(module), + struct partition_private_data); + /* Look at base DN */ + /* Figure out which partition it is under */ + /* Skip the lot if 'data' isn't here yet (initialization) */ + if (ldb_module_flags(ldb_module_get_ctx(module)) & LDB_FLG_ENABLE_TRACING) { + ldb_debug(ldb_module_get_ctx(module), LDB_DEBUG_TRACE, "partition_start_trans() -> (metadata partition)"); + } + + /* + * We start a transaction on metadata.tdb first and end it last in + * end_trans. This makes locking semantics follow TDB rather than MDB, + * and effectively locks all partitions at once. + * Detail: + * Samba AD is special in that the partitions module (this file) + * combines multiple independently locked databases into one overall + * transaction. Changes across multiple partition DBs in a single + * transaction must ALL be either visible or invisible. + * The way this is achieved is by taking out a write lock on + * metadata.tdb at the start of prepare_commit, while unlocking it at + * the end of end_trans. This is matched by read_lock, ensuring it + * can't progress until that write lock is released. + * + * metadata.tdb needs to be a TDB file because MDB uses independent + * locks, which means a read lock and a write lock can be held at the + * same time, whereas in TDB, the two locks block each other. The TDB + * behaviour is required to implement the functionality described + * above. + * + * An important additional detail here is that if prepare_commit is + * called on a TDB without any changes being made, no write lock is + * taken. We address this by storing a sequence number in metadata.tdb + * which is updated every time a replicated attribute is modified. + * The possibility of a few unreplicated attributes being out of date + * turns out not to be a problem. + * For this reason, a lock on sam.ldb (which is a TDB) won't achieve + * the same end as locking metadata.tdb, unless we made a modification + * to the @ records found there before every prepare_commit. + */ + ret = partition_metadata_start_trans(module); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_next_start_trans(module); + if (ret != LDB_SUCCESS) { + partition_metadata_del_trans(module); + return ret; + } + + ret = partition_reload_if_required(module, data, NULL); + if (ret != LDB_SUCCESS) { + ldb_next_del_trans(module); + partition_metadata_del_trans(module); + return ret; + } + + /* + * The following per partition locks are required mostly because TDB + * and MDB require locks before read and write ops are permitted. + */ + for (i=0; data && data->partitions && data->partitions[i]; i++) { + if ((module && ldb_module_flags(ldb_module_get_ctx(module)) & LDB_FLG_ENABLE_TRACING)) { + ldb_debug(ldb_module_get_ctx(module), LDB_DEBUG_TRACE, "partition_start_trans() -> %s", + ldb_dn_get_linearized(data->partitions[i]->ctrl->dn)); + } + ret = ldb_next_start_trans(data->partitions[i]->module); + if (ret != LDB_SUCCESS) { + /* Back it out, if it fails on one */ + for (i--; i >= 0; i--) { + ldb_next_del_trans(data->partitions[i]->module); + } + ldb_next_del_trans(module); + partition_metadata_del_trans(module); + return ret; + } + } + + data->in_transaction++; + + return LDB_SUCCESS; +} + +/* prepare for a commit */ +int partition_prepare_commit(struct ldb_module *module) +{ + unsigned int i; + struct partition_private_data *data = talloc_get_type(ldb_module_get_private(module), + struct partition_private_data); + int ret; + + /* + * Order of prepare_commit calls must match that in + * partition_start_trans. See comment in that function for detail. + */ + ret = partition_metadata_prepare_commit(module); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_next_prepare_commit(module); + if (ret != LDB_SUCCESS) { + return ret; + } + + for (i=0; data && data->partitions && data->partitions[i]; i++) { + if ((module && ldb_module_flags(ldb_module_get_ctx(module)) & LDB_FLG_ENABLE_TRACING)) { + ldb_debug(ldb_module_get_ctx(module), LDB_DEBUG_TRACE, "partition_prepare_commit() -> %s", + ldb_dn_get_linearized(data->partitions[i]->ctrl->dn)); + } + ret = ldb_next_prepare_commit(data->partitions[i]->module); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), "prepare_commit error on %s: %s", + ldb_dn_get_linearized(data->partitions[i]->ctrl->dn), + ldb_errstring(ldb_module_get_ctx(module))); + return ret; + } + } + + if ((module && ldb_module_flags(ldb_module_get_ctx(module)) & LDB_FLG_ENABLE_TRACING)) { + ldb_debug(ldb_module_get_ctx(module), LDB_DEBUG_TRACE, "partition_prepare_commit() -> (metadata partition)"); + } + + return LDB_SUCCESS; +} + + +/* end a transaction */ +int partition_end_trans(struct ldb_module *module) +{ + int ret, ret2; + int i; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct partition_private_data *data = talloc_get_type(ldb_module_get_private(module), + struct partition_private_data); + bool trace = module && ldb_module_flags(ldb) & LDB_FLG_ENABLE_TRACING; + + ret = LDB_SUCCESS; + + if (data->in_transaction == 0) { + DEBUG(0,("partition end transaction mismatch\n")); + ret = LDB_ERR_OPERATIONS_ERROR; + } else { + data->in_transaction--; + } + + /* + * Order of end_trans calls must be the reverse of that in + * partition_start_trans. See comment in that function for detail. + */ + if (data && data->partitions) { + /* Just counting the partitions */ + for (i=0; data->partitions[i]; i++) {} + + /* now walk them backwards */ + for (i--; i>=0; i--) { + struct dsdb_partition *p = data->partitions[i]; + if (trace) { + ldb_debug(ldb, + LDB_DEBUG_TRACE, + "partition_end_trans() -> %s", + ldb_dn_get_linearized(p->ctrl->dn)); + } + ret2 = ldb_next_end_trans(p->module); + if (ret2 != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "end_trans error on %s: %s", + ldb_dn_get_linearized(p->ctrl->dn), + ldb_errstring(ldb)); + ret = ret2; + } + } + } + + if (trace) { + ldb_debug(ldb_module_get_ctx(module), LDB_DEBUG_TRACE, "partition_end_trans() -> (metadata partition)"); + } + ret2 = ldb_next_end_trans(module); + if (ret2 != LDB_SUCCESS) { + ret = ret2; + } + + ret2 = partition_metadata_end_trans(module); + if (ret2 != LDB_SUCCESS) { + ret = ret2; + } + + return ret; +} + +/* delete a transaction */ +int partition_del_trans(struct ldb_module *module) +{ + int ret, final_ret = LDB_SUCCESS; + int i; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct partition_private_data *data = talloc_get_type(ldb_module_get_private(module), + struct partition_private_data); + bool trace = module && ldb_module_flags(ldb) & LDB_FLG_ENABLE_TRACING; + + if (data == NULL) { + DEBUG(0,("partition delete transaction with no private data\n")); + return ldb_operr(ldb); + } + + /* + * Order of del_trans calls must be the reverse of that in + * partition_start_trans. See comment in that function for detail. + */ + if (data->partitions) { + /* Just counting the partitions */ + for (i=0; data->partitions[i]; i++) {} + + /* now walk them backwards */ + for (i--; i>=0; i--) { + struct dsdb_partition *p = data->partitions[i]; + if (trace) { + ldb_debug(ldb, + LDB_DEBUG_TRACE, + "partition_del_trans() -> %s", + ldb_dn_get_linearized(p->ctrl->dn)); + } + ret = ldb_next_del_trans(p->module); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "del_trans error on %s: %s", + ldb_dn_get_linearized(p->ctrl->dn), + ldb_errstring(ldb)); + final_ret = ret; + } + } + } + + if (trace) { + ldb_debug(ldb_module_get_ctx(module), LDB_DEBUG_TRACE, "partition_del_trans() -> (metadata partition)"); + } + ret = ldb_next_del_trans(module); + if (ret != LDB_SUCCESS) { + final_ret = ret; + } + + ret = partition_metadata_del_trans(module); + if (ret != LDB_SUCCESS) { + final_ret = ret; + } + + if (data->in_transaction == 0) { + DEBUG(0,("partition del transaction mismatch\n")); + return ldb_operr(ldb_module_get_ctx(module)); + } + data->in_transaction--; + + return final_ret; +} + +int partition_primary_sequence_number(struct ldb_module *module, TALLOC_CTX *mem_ctx, + uint64_t *seq_number, + struct ldb_request *parent) +{ + int ret; + struct ldb_result *res; + struct ldb_seqnum_request *tseq; + struct ldb_seqnum_result *seqr; + + tseq = talloc_zero(mem_ctx, struct ldb_seqnum_request); + if (tseq == NULL) { + return ldb_oom(ldb_module_get_ctx(module)); + } + tseq->type = LDB_SEQ_HIGHEST_SEQ; + + ret = dsdb_module_extended(module, tseq, &res, + LDB_EXTENDED_SEQUENCE_NUMBER, + tseq, + DSDB_FLAG_NEXT_MODULE, + parent); + if (ret != LDB_SUCCESS) { + talloc_free(tseq); + return ret; + } + + seqr = talloc_get_type_abort(res->extended->data, + struct ldb_seqnum_result); + if (seqr->flags & LDB_SEQ_TIMESTAMP_SEQUENCE) { + talloc_free(res); + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "Primary backend in partition module returned a timestamp based seq"); + } + + *seq_number = seqr->seq_num; + talloc_free(tseq); + return LDB_SUCCESS; +} + + +/* + * Older version of sequence number as sum of sequence numbers for each partition + */ +int partition_sequence_number_from_partitions(struct ldb_module *module, + uint64_t *seqr) +{ + int ret; + unsigned int i; + uint64_t seq_number = 0; + struct partition_private_data *data = talloc_get_type(ldb_module_get_private(module), + struct partition_private_data); + + ret = partition_primary_sequence_number(module, data, &seq_number, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* Skip the lot if 'data' isn't here yet (initialisation) */ + for (i=0; data && data->partitions && data->partitions[i]; i++) { + struct ldb_seqnum_request *tseq; + struct ldb_seqnum_result *tseqr; + struct ldb_request *treq; + struct ldb_result *res = talloc_zero(data, struct ldb_result); + if (res == NULL) { + return ldb_oom(ldb_module_get_ctx(module)); + } + tseq = talloc_zero(res, struct ldb_seqnum_request); + if (tseq == NULL) { + talloc_free(res); + return ldb_oom(ldb_module_get_ctx(module)); + } + tseq->type = LDB_SEQ_HIGHEST_SEQ; + + ret = ldb_build_extended_req(&treq, ldb_module_get_ctx(module), res, + LDB_EXTENDED_SEQUENCE_NUMBER, + tseq, + NULL, + res, + ldb_extended_default_callback, + NULL); + LDB_REQ_SET_LOCATION(treq); + if (ret != LDB_SUCCESS) { + talloc_free(res); + return ret; + } + + ret = partition_request(data->partitions[i]->module, treq); + if (ret != LDB_SUCCESS) { + talloc_free(res); + return ret; + } + ret = ldb_wait(treq->handle, LDB_WAIT_ALL); + if (ret != LDB_SUCCESS) { + talloc_free(res); + return ret; + } + tseqr = talloc_get_type(res->extended->data, + struct ldb_seqnum_result); + seq_number += tseqr->seq_num; + talloc_free(res); + } + + *seqr = seq_number; + return LDB_SUCCESS; +} + + +/* + * Newer version of sequence number using metadata tdb + */ +static int partition_sequence_number(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_extended *ext; + struct ldb_seqnum_request *seq; + struct ldb_seqnum_result *seqr; + uint64_t seq_number; + int ret; + + seq = talloc_get_type_abort(req->op.extended.data, struct ldb_seqnum_request); + switch (seq->type) { + case LDB_SEQ_NEXT: + ret = partition_metadata_sequence_number_increment(module, &seq_number); + if (ret != LDB_SUCCESS) { + return ret; + } + break; + + case LDB_SEQ_HIGHEST_SEQ: + ret = partition_metadata_sequence_number(module, &seq_number); + if (ret != LDB_SUCCESS) { + return ret; + } + break; + + case LDB_SEQ_HIGHEST_TIMESTAMP: + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "LDB_SEQ_HIGHEST_TIMESTAMP not supported"); + } + + ext = talloc_zero(req, struct ldb_extended); + if (!ext) { + return ldb_module_oom(module); + } + seqr = talloc_zero(ext, struct ldb_seqnum_result); + if (seqr == NULL) { + talloc_free(ext); + return ldb_module_oom(module); + } + ext->oid = LDB_EXTENDED_SEQUENCE_NUMBER; + ext->data = seqr; + + seqr->seq_num = seq_number; + seqr->flags |= LDB_SEQ_GLOBAL_SEQUENCE; + + /* send request done */ + return ldb_module_done(req, NULL, ext, LDB_SUCCESS); +} + +/* lock all the backends */ +int partition_read_lock(struct ldb_module *module) +{ + int i = 0; + int ret = 0; + int ret2 = 0; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct partition_private_data *data = \ + talloc_get_type(ldb_module_get_private(module), + struct partition_private_data); + + if (ldb_module_flags(ldb) & LDB_FLG_ENABLE_TRACING) { + ldb_debug(ldb, LDB_DEBUG_TRACE, + "partition_read_lock() -> (metadata partition)"); + } + + /* + * It is important to only do this for LOCK because: + * - we don't want to unlock what we did not lock + * + * - we don't want to make a new lock on the sam.ldb + * (triggered inside this routine due to the seq num check) + * during an unlock phase as that will violate the lock + * ordering + */ + + if (data == NULL) { + TALLOC_CTX *mem_ctx = talloc_new(module); + + data = talloc_zero(mem_ctx, struct partition_private_data); + if (data == NULL) { + talloc_free(mem_ctx); + return ldb_operr(ldb); + } + + /* + * When used from Samba4, this message is set by the + * samba4 module, as a fixed value not read from the + * DB. This avoids listing modules in the DB + */ + data->forced_module_msg = talloc_get_type( + ldb_get_opaque(ldb, + DSDB_OPAQUE_PARTITION_MODULE_MSG_OPAQUE_NAME), + struct ldb_message); + + ldb_module_set_private(module, talloc_steal(module, + data)); + talloc_free(mem_ctx); + } + + /* + * This will lock sam.ldb and will also call event loops, + * so we do it before we get the whole db lock. + */ + ret = partition_reload_if_required(module, data, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + * Order of read_lock calls must match that in partition_start_trans. + * See comment in that function for detail. + */ + ret = partition_metadata_read_lock(module); + if (ret != LDB_SUCCESS) { + goto failed; + } + + /* + * The top level DB (sam.ldb) lock is not enough to block another + * process in prepare_commit(), because if nothing was changed in the + * specific backend, then prepare_commit() is a no-op. Therefore the + * metadata.tdb lock is taken out above, as it is the best we can do + * right now. + */ + ret = ldb_next_read_lock(module); + if (ret != LDB_SUCCESS) { + ldb_debug_set(ldb, + LDB_DEBUG_FATAL, + "Failed to lock db: %s / %s for metadata partition", + ldb_errstring(ldb), + ldb_strerror(ret)); + + return ret; + } + + /* + * The following per partition locks are required mostly because TDB + * and MDB require locks before reads are permitted. + */ + for (i=0; data && data->partitions && data->partitions[i]; i++) { + if ((module && ldb_module_flags(ldb) & LDB_FLG_ENABLE_TRACING)) { + ldb_debug(ldb, LDB_DEBUG_TRACE, + "partition_read_lock() -> %s", + ldb_dn_get_linearized( + data->partitions[i]->ctrl->dn)); + } + ret = ldb_next_read_lock(data->partitions[i]->module); + if (ret == LDB_SUCCESS) { + continue; + } + + ldb_debug_set(ldb, + LDB_DEBUG_FATAL, + "Failed to lock db: %s / %s for %s", + ldb_errstring(ldb), + ldb_strerror(ret), + ldb_dn_get_linearized( + data->partitions[i]->ctrl->dn)); + + goto failed; + } + + return LDB_SUCCESS; + +failed: + /* Back it out, if it fails on one */ + for (i--; i >= 0; i--) { + ret2 = ldb_next_read_unlock(data->partitions[i]->module); + if (ret2 != LDB_SUCCESS) { + ldb_debug(ldb, + LDB_DEBUG_FATAL, + "Failed to unlock db: %s / %s", + ldb_errstring(ldb), + ldb_strerror(ret2)); + } + } + ret2 = ldb_next_read_unlock(module); + if (ret2 != LDB_SUCCESS) { + ldb_debug(ldb, + LDB_DEBUG_FATAL, + "Failed to unlock db: %s / %s", + ldb_errstring(ldb), + ldb_strerror(ret2)); + } + return ret; +} + +/* unlock all the backends */ +int partition_read_unlock(struct ldb_module *module) +{ + int i; + int ret = LDB_SUCCESS; + int ret2; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct partition_private_data *data = \ + talloc_get_type(ldb_module_get_private(module), + struct partition_private_data); + bool trace = module && ldb_module_flags(ldb) & LDB_FLG_ENABLE_TRACING; + + /* + * Order of read_unlock calls must be the reverse of that in + * partition_start_trans. See comment in that function for detail. + */ + if (data && data->partitions) { + /* Just counting the partitions */ + for (i=0; data->partitions[i]; i++) {} + + /* now walk them backwards */ + for (i--; i>=0; i--) { + struct dsdb_partition *p = data->partitions[i]; + if (trace) { + ldb_debug(ldb, LDB_DEBUG_TRACE, + "partition_read_unlock() -> %s", + ldb_dn_get_linearized(p->ctrl->dn)); + } + ret2 = ldb_next_read_unlock(p->module); + if (ret2 != LDB_SUCCESS) { + ldb_debug_set(ldb, + LDB_DEBUG_FATAL, + "Failed to lock db: %s / %s for %s", + ldb_errstring(ldb), + ldb_strerror(ret), + ldb_dn_get_linearized(p->ctrl->dn)); + + /* + * Don't overwrite the original failure code + * if there was one + */ + if (ret == LDB_SUCCESS) { + ret = ret2; + } + } + } + } + + if (trace) { + ldb_debug(ldb, LDB_DEBUG_TRACE, + "partition_read_unlock() -> (metadata partition)"); + } + + ret2 = ldb_next_read_unlock(module); + if (ret2 != LDB_SUCCESS) { + ldb_debug_set(ldb, + LDB_DEBUG_FATAL, + "Failed to unlock db: %s / %s for metadata partition", + ldb_errstring(ldb), + ldb_strerror(ret2)); + + /* + * Don't overwrite the original failure code + * if there was one + */ + if (ret == LDB_SUCCESS) { + ret = ret2; + } + } + + ret = partition_metadata_read_unlock(module); + + /* + * Don't overwrite the original failure code + * if there was one + */ + if (ret == LDB_SUCCESS) { + ret = ret2; + } + + return ret; +} + +/* extended */ +static int partition_extended(struct ldb_module *module, struct ldb_request *req) +{ + struct partition_private_data *data = talloc_get_type(ldb_module_get_private(module), + struct partition_private_data); + struct partition_context *ac; + int ret; + + /* if we aren't initialised yet go further */ + if (!data) { + return ldb_next_request(module, req); + } + + if (strcmp(req->op.extended.oid, DSDB_EXTENDED_SCHEMA_UPDATE_NOW_OID) == 0) { + /* Update the metadata.tdb to increment the schema version if needed*/ + DEBUG(10, ("Incrementing the sequence_number after schema_update_now\n")); + ret = partition_metadata_inc_schema_sequence(module); + return ldb_module_done(req, NULL, NULL, ret); + } + + if (strcmp(req->op.extended.oid, LDB_EXTENDED_SEQUENCE_NUMBER) == 0) { + return partition_sequence_number(module, req); + } + + if (strcmp(req->op.extended.oid, DSDB_EXTENDED_CREATE_PARTITION_OID) == 0) { + return partition_create(module, req); + } + + /* + * as the extended operation has no dn + * we need to send it to all partitions + */ + + ac = partition_init_ctx(module, req); + if (!ac) { + return ldb_operr(ldb_module_get_ctx(module)); + } + + return partition_send_all(module, ac, req); +} + +static const struct ldb_module_ops ldb_partition_module_ops = { + .name = "partition", + .init_context = partition_init, + .search = partition_search, + .add = partition_add, + .modify = partition_modify, + .del = partition_delete, + .rename = partition_rename, + .extended = partition_extended, + .start_transaction = partition_start_trans, + .prepare_commit = partition_prepare_commit, + .end_transaction = partition_end_trans, + .del_transaction = partition_del_trans, + .read_lock = partition_read_lock, + .read_unlock = partition_read_unlock +}; + +int ldb_partition_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_partition_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/partition.h b/source4/dsdb/samdb/ldb_modules/partition.h new file mode 100644 index 0000000..e6b5187 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/partition.h @@ -0,0 +1,64 @@ +/* + Partitions ldb module + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2006 + Copyright (C) Stefan Metzmacher <metze@samba.org> 2007 + + 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/>. +*/ +#include "includes.h" +#include <ldb.h> +#include <ldb_errors.h> +#include <ldb_module.h> +#include "lib/tdb_wrap/tdb_wrap.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "system/locale.h" +#include "param/param.h" + +struct dsdb_partition { + struct ldb_module *module; + struct dsdb_control_current_partition *ctrl; + const char *backend_url; + DATA_BLOB orig_record; + bool partial_replica; /* a GC partition */ +}; + +struct partition_module { + const char **modules; + struct ldb_dn *dn; +}; + +struct partition_metadata { + struct tdb_wrap *db; + int in_transaction; + int read_lock_count; +}; + +struct partition_private_data { + struct dsdb_partition **partitions; + struct ldb_dn **replicate; + struct partition_metadata *metadata; + + struct partition_module **modules; + + uint64_t metadata_seq; + uint32_t in_transaction; + + struct ldb_message *forced_module_msg; + + const char *backend_db_store; +}; + +#include "dsdb/samdb/ldb_modules/partition_proto.h" diff --git a/source4/dsdb/samdb/ldb_modules/partition_init.c b/source4/dsdb/samdb/ldb_modules/partition_init.c new file mode 100644 index 0000000..484b5bf --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/partition_init.c @@ -0,0 +1,885 @@ +/* + Partitions ldb module + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2006 + Copyright (C) Stefan Metzmacher <metze@samba.org> 2007 + + 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 partitions module + * + * Description: Implement LDAP partitions + * + * Author: Andrew Bartlett + * Author: Stefan Metzmacher + */ + +#include "dsdb/samdb/ldb_modules/partition.h" +#include "lib/util/tsort.h" +#include "lib/ldb-samba/ldb_wrap.h" +#include "system/filesys.h" + +static int partition_sort_compare(const void *v1, const void *v2) +{ + const struct dsdb_partition *p1; + const struct dsdb_partition *p2; + + p1 = *((struct dsdb_partition * const*)v1); + p2 = *((struct dsdb_partition * const*)v2); + + return ldb_dn_compare(p1->ctrl->dn, p2->ctrl->dn); +} + +/* Load the list of DNs that we must replicate to all partitions */ +static int partition_load_replicate_dns(struct ldb_context *ldb, + struct partition_private_data *data, + struct ldb_message *msg) +{ + struct ldb_message_element *replicate_attributes = ldb_msg_find_element(msg, "replicateEntries"); + + talloc_free(data->replicate); + if (!replicate_attributes) { + data->replicate = NULL; + } else { + unsigned int i; + data->replicate = talloc_array(data, struct ldb_dn *, replicate_attributes->num_values + 1); + if (!data->replicate) { + return ldb_oom(ldb); + } + + for (i=0; i < replicate_attributes->num_values; i++) { + data->replicate[i] = ldb_dn_from_ldb_val(data->replicate, ldb, &replicate_attributes->values[i]); + if (!ldb_dn_validate(data->replicate[i])) { + ldb_asprintf_errstring(ldb, + "partition_init: " + "invalid DN in partition replicate record: %s", + replicate_attributes->values[i].data); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } + data->replicate[i] = NULL; + } + return LDB_SUCCESS; +} + +/* Load the list of modules for the partitions */ +static int partition_load_modules(struct ldb_context *ldb, + struct partition_private_data *data, struct ldb_message *msg) +{ + unsigned int i; + struct ldb_message_element *modules_attributes = ldb_msg_find_element(msg, "modules"); + talloc_free(data->modules); + if (!modules_attributes) { + return LDB_SUCCESS; + } + + data->modules = talloc_array(data, struct partition_module *, modules_attributes->num_values + 1); + if (!data->modules) { + return ldb_oom(ldb); + } + + for (i=0; i < modules_attributes->num_values; i++) { + char *p; + DATA_BLOB dn_blob; + data->modules[i] = talloc(data->modules, struct partition_module); + if (!data->modules[i]) { + return ldb_oom(ldb); + } + + dn_blob = modules_attributes->values[i]; + + p = strchr((const char *)dn_blob.data, ':'); + if (!p) { + ldb_asprintf_errstring(ldb, + "partition_load_modules: " + "invalid form for partition module record (missing ':'): %s", (const char *)dn_blob.data); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + /* Now trim off the filename */ + dn_blob.length = ((uint8_t *)p - dn_blob.data); + + p++; + data->modules[i]->modules = ldb_modules_list_from_string(ldb, data->modules[i], + p); + + if (dn_blob.length == 1 && dn_blob.data[0] == '*') { + data->modules[i]->dn = NULL; + } else { + data->modules[i]->dn = ldb_dn_from_ldb_val(data->modules[i], ldb, &dn_blob); + if (!data->modules[i]->dn || !ldb_dn_validate(data->modules[i]->dn)) { + return ldb_operr(ldb); + } + } + } + data->modules[i] = NULL; + return LDB_SUCCESS; +} + +static int partition_reload_metadata(struct ldb_module *module, struct partition_private_data *data, + TALLOC_CTX *mem_ctx, struct ldb_message **_msg, + struct ldb_request *parent) +{ + int ret; + struct ldb_message *msg, *module_msg; + struct ldb_result *res; + struct ldb_context *ldb = ldb_module_get_ctx(module); + const char *attrs[] = { "partition", "replicateEntries", "modules", + "partialReplica", "backendStore", NULL }; + /* perform search for @PARTITION, looking for module, replicateEntries and ldapBackend */ + ret = dsdb_module_search_dn(module, mem_ctx, &res, + ldb_dn_new(mem_ctx, ldb, DSDB_PARTITION_DN), + attrs, + DSDB_FLAG_NEXT_MODULE, parent); + if (ret != LDB_SUCCESS) { + return ret; + } + + msg = res->msgs[0]; + + ret = partition_load_replicate_dns(ldb, data, msg); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* When used from Samba4, this message is set by the samba4 + * module, as a fixed value not read from the DB. This avoids + * listing modules in the DB */ + if (data->forced_module_msg) { + module_msg = data->forced_module_msg; + } else { + module_msg = msg; + } + + ret = partition_load_modules(ldb, data, module_msg); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (_msg) { + *_msg = msg; + } else { + talloc_free(msg); + } + + return LDB_SUCCESS; +} + +static const char **find_modules_for_dn(struct partition_private_data *data, struct ldb_dn *dn) +{ + unsigned int i; + struct partition_module *default_mod = NULL; + for (i=0; data->modules && data->modules[i]; i++) { + if (!data->modules[i]->dn) { + default_mod = data->modules[i]; + } else if (ldb_dn_compare(dn, data->modules[i]->dn) == 0) { + return data->modules[i]->modules; + } + } + if (default_mod) { + return default_mod->modules; + } else { + return NULL; + } +} + +static int new_partition_from_dn(struct ldb_context *ldb, struct partition_private_data *data, + TALLOC_CTX *mem_ctx, + struct ldb_dn *dn, const char *filename, + const char *backend_db_store, + struct dsdb_partition **partition) { + struct dsdb_control_current_partition *ctrl; + struct ldb_module *backend_module; + char *backend_path; + struct ldb_module *module_chain; + const char **modules; + const char **options = NULL; + int ret; + + (*partition) = talloc_zero(mem_ctx, struct dsdb_partition); + if (!*partition) { + return ldb_oom(ldb); + } + + (*partition)->ctrl = ctrl = talloc((*partition), struct dsdb_control_current_partition); + if (!ctrl) { + talloc_free(*partition); + return ldb_oom(ldb); + } + + /* the backend LDB is the DN (base64 encoded if not 'plain') followed by .ldb */ + backend_path = ldb_relative_path(ldb, + *partition, + filename); + if (!backend_path) { + ldb_asprintf_errstring(ldb, + "partition_init: unable to determine an relative path for partition: %s", + filename); + talloc_free(*partition); + return LDB_ERR_OPERATIONS_ERROR; + } + (*partition)->backend_url = talloc_asprintf(*partition, "%s://%s", + backend_db_store, + backend_path); + + if (!(ldb_module_flags(ldb) & LDB_FLG_RDONLY)) { + char *p; + char *backend_dir; + + p = strrchr(backend_path, '/'); + if (p) { + p[0] = '\0'; + } + backend_dir = backend_path; + + /* Failure is quite reasonable, it might alredy exist */ + mkdir(backend_dir, 0700); + } + + ctrl->version = DSDB_CONTROL_CURRENT_PARTITION_VERSION; + ctrl->dn = talloc_steal(ctrl, dn); + + options = ldb_options_get(ldb); + ret = ldb_module_connect_backend( + ldb, (*partition)->backend_url, options, &backend_module); + if (ret != LDB_SUCCESS) { + return ret; + } + talloc_steal((*partition), backend_module); + + modules = find_modules_for_dn(data, dn); + + if (!modules) { + DEBUG(0, ("Unable to load partition modules for new DN %s, perhaps you need to reprovision? See partition-upgrade.txt for instructions\n", ldb_dn_get_linearized(dn))); + talloc_free(*partition); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + ret = ldb_module_load_list(ldb, modules, backend_module, &module_chain); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "partition_init: " + "loading backend for %s failed: %s", + ldb_dn_get_linearized(dn), ldb_errstring(ldb)); + talloc_free(*partition); + return ret; + } + ret = ldb_module_init_chain(ldb, module_chain); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "partition_init: " + "initialising backend for %s failed: %s", + ldb_dn_get_linearized(dn), ldb_errstring(ldb)); + talloc_free(*partition); + return ret; + } + + /* This weirdness allows us to use ldb_next_request() in partition.c */ + (*partition)->module = ldb_module_new(*partition, ldb, "partition_next", NULL); + if (!(*partition)->module) { + talloc_free(*partition); + return ldb_oom(ldb); + } + ldb_module_set_next((*partition)->module, talloc_steal((*partition)->module, module_chain)); + + /* if we were in a transaction then we need to start a + transaction on this new partition, otherwise we'll get a + transaction mismatch when we end the transaction */ + if (data->in_transaction) { + if (ldb_module_flags(ldb) & LDB_FLG_ENABLE_TRACING) { + ldb_debug(ldb, LDB_DEBUG_TRACE, "partition_start_trans() -> %s (new partition)", + ldb_dn_get_linearized((*partition)->ctrl->dn)); + } + ret = ldb_next_start_trans((*partition)->module); + } + + return ret; +} + +/* Tell the rootDSE about the new partition */ +static int partition_register(struct ldb_context *ldb, struct dsdb_control_current_partition *ctrl) +{ + struct ldb_request *req; + int ret; + + req = talloc_zero(NULL, struct ldb_request); + if (req == NULL) { + return ldb_oom(ldb); + } + + req->operation = LDB_REQ_REGISTER_PARTITION; + req->op.reg_partition.dn = ctrl->dn; + req->callback = ldb_op_default_callback; + + ldb_set_timeout(ldb, req, 0); + + req->handle = ldb_handle_new(req, ldb); + if (req->handle == NULL) { + talloc_free(req); + return ldb_operr(ldb); + } + + ret = ldb_request(ldb, req); + if (ret == LDB_SUCCESS) { + ret = ldb_wait(req->handle, LDB_WAIT_ALL); + } + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, "partition: Unable to register partition with rootdse!\n"); + talloc_free(req); + return LDB_ERR_OTHER; + } + talloc_free(req); + + return LDB_SUCCESS; +} + +/* Add a newly found partition to the global data */ +static int add_partition_to_data(struct ldb_context *ldb, struct partition_private_data *data, + struct dsdb_partition *partition) +{ + unsigned int i; + int ret; + + /* Count the partitions */ + for (i=0; data->partitions && data->partitions[i]; i++) { /* noop */}; + + /* Add partition to list of partitions */ + data->partitions = talloc_realloc(data, data->partitions, struct dsdb_partition *, i + 2); + if (!data->partitions) { + return ldb_oom(ldb); + } + data->partitions[i] = talloc_steal(data->partitions, partition); + data->partitions[i+1] = NULL; + + /* Sort again (should use binary insert) */ + TYPESAFE_QSORT(data->partitions, i+1, partition_sort_compare); + + ret = partition_register(ldb, partition->ctrl); + if (ret != LDB_SUCCESS) { + return ret; + } + return LDB_SUCCESS; +} + +int partition_reload_if_required(struct ldb_module *module, + struct partition_private_data *data, + struct ldb_request *parent) +{ + uint64_t seq; + int ret; + unsigned int i; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_message *msg; + struct ldb_message_element *partition_attributes; + struct ldb_message_element *partial_replicas; + TALLOC_CTX *mem_ctx; + + if (!data) { + /* Not initialised yet */ + return LDB_SUCCESS; + } + + mem_ctx = talloc_new(data); + if (!mem_ctx) { + return ldb_oom(ldb); + } + + ret = partition_primary_sequence_number(module, mem_ctx, &seq, parent); + if (ret != LDB_SUCCESS) { + talloc_free(mem_ctx); + return ret; + } + if (seq == data->metadata_seq) { + talloc_free(mem_ctx); + return LDB_SUCCESS; + } + + /* This loads metadata tdb. If it's missing, creates it */ + ret = partition_metadata_init(module); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = partition_reload_metadata(module, data, mem_ctx, &msg, parent); + if (ret != LDB_SUCCESS) { + talloc_free(mem_ctx); + return ret; + } + + data->metadata_seq = seq; + + partition_attributes = ldb_msg_find_element(msg, "partition"); + partial_replicas = ldb_msg_find_element(msg, "partialReplica"); + data->backend_db_store + = talloc_strdup(data, ldb_msg_find_attr_as_string(msg, "backendStore", "tdb")); + + if (data->backend_db_store == NULL) { + talloc_free(mem_ctx); + return ldb_module_oom(module); + } + + for (i=0; partition_attributes && i < partition_attributes->num_values; i++) { + unsigned int j; + bool new_partition = true; + const char *filename = NULL; + DATA_BLOB dn_blob; + struct ldb_dn *dn; + struct dsdb_partition *partition; + struct ldb_result *dn_res; + const char *no_attrs[] = { NULL }; + + for (j=0; data->partitions && data->partitions[j]; j++) { + if (data_blob_cmp(&data->partitions[j]->orig_record, &partition_attributes->values[i]) == 0) { + new_partition = false; + break; + } + } + if (new_partition == false) { + continue; + } + + dn_blob = partition_attributes->values[i]; + + if (dn_blob.length > 4 && + (strncmp((const char *)&dn_blob.data[dn_blob.length-4], ".ldb", 4) == 0)) { + + /* Look for DN:filename.ldb */ + char *p = strrchr((const char *)dn_blob.data, ':'); + if (!p) { + ldb_asprintf_errstring(ldb, + "partition_init: invalid DN in attempting to parse partition record: %s", (const char *)dn_blob.data); + talloc_free(mem_ctx); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + filename = p+1; + + /* Now trim off the filename */ + dn_blob.length = ((uint8_t *)p - dn_blob.data); + } + + dn = ldb_dn_from_ldb_val(mem_ctx, ldb, &dn_blob); + if (!dn) { + ldb_asprintf_errstring(ldb, + "partition_init: invalid DN in partition record: %s", (const char *)dn_blob.data); + talloc_free(mem_ctx); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + /* Now do a slow check with the DN compare */ + for (j=0; data->partitions && data->partitions[j]; j++) { + if (ldb_dn_compare(dn, data->partitions[j]->ctrl->dn) == 0) { + new_partition = false; + break; + } + } + if (new_partition == false) { + continue; + } + + if (!filename) { + char *base64_dn = NULL; + const char *p; + for (p = ldb_dn_get_linearized(dn); *p; p++) { + /* We have such a strict check because I don't want shell metacharacters in the file name, nor ../ */ + if (!(isalnum(*p) || *p == ' ' || *p == '=' || *p == ',')) { + break; + } + } + if (*p) { + base64_dn = ldb_base64_encode(data, ldb_dn_get_linearized(dn), strlen(ldb_dn_get_linearized(dn))); + filename = talloc_asprintf(mem_ctx, "%s.ldb", base64_dn); + } else { + filename = talloc_asprintf(mem_ctx, "%s.ldb", ldb_dn_get_linearized(dn)); + } + } + + /* We call ldb_dn_get_linearized() because the DN in + * partition_attributes is already casefolded + * correctly. We don't want to mess that up as the + * schema isn't loaded yet */ + ret = new_partition_from_dn(ldb, data, data->partitions, dn, + filename, data->backend_db_store, + &partition); + if (ret != LDB_SUCCESS) { + talloc_free(mem_ctx); + return ret; + } + + talloc_steal(partition, partition_attributes->values[i].data); + partition->orig_record = partition_attributes->values[i]; + + /* Get the 'correct' case of the partition DNs from the database */ + ret = dsdb_module_search_dn(partition->module, data, &dn_res, + dn, no_attrs, + DSDB_FLAG_NEXT_MODULE, parent); + if (ret == LDB_SUCCESS) { + talloc_free(partition->ctrl->dn); + partition->ctrl->dn = talloc_steal(partition->ctrl, dn_res->msgs[0]->dn); + talloc_free(dn_res); + } else if (ret != LDB_ERR_NO_SUCH_OBJECT) { + ldb_asprintf_errstring(ldb, + "Failed to search for partition base %s in new partition at %s: %s", + ldb_dn_get_linearized(dn), + partition->backend_url, + ldb_errstring(ldb)); + talloc_free(mem_ctx); + return ret; + } + + /* see if it is a partial replica */ + for (j=0; partial_replicas && j<partial_replicas->num_values; j++) { + struct ldb_dn *pa_dn = ldb_dn_from_ldb_val(mem_ctx, ldb, &partial_replicas->values[j]); + if (pa_dn != NULL && ldb_dn_compare(pa_dn, partition->ctrl->dn) == 0) { + partition->partial_replica = true; + } + talloc_free(pa_dn); + } + + ret = add_partition_to_data(ldb, data, partition); + if (ret != LDB_SUCCESS) { + talloc_free(mem_ctx); + return ret; + } + } + + talloc_free(mem_ctx); + return LDB_SUCCESS; +} + +/* Copy the metadata (@OPTIONS etc) for the new partition into the partition */ + +static int new_partition_set_replicated_metadata(struct ldb_context *ldb, + struct ldb_module *module, struct ldb_request *last_req, + struct partition_private_data *data, + struct dsdb_partition *partition) +{ + unsigned int i; + int ret; + /* for each replicate, copy from main partition. If we get an error, we report it up the chain */ + for (i=0; data->replicate && data->replicate[i]; i++) { + struct ldb_result *replicate_res; + struct ldb_request *add_req; + ret = dsdb_module_search_dn(module, last_req, &replicate_res, + data->replicate[i], + NULL, + DSDB_FLAG_NEXT_MODULE, NULL); + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + continue; + } + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "Failed to search for %s from " DSDB_PARTITION_DN + " replicateEntries for new partition at %s on %s: %s", + ldb_dn_get_linearized(data->replicate[i]), + partition->backend_url, + ldb_dn_get_linearized(partition->ctrl->dn), + ldb_errstring(ldb)); + return ret; + } + + /* Build add request */ + ret = ldb_build_add_req(&add_req, ldb, replicate_res, + replicate_res->msgs[0], NULL, NULL, + ldb_op_default_callback, last_req); + LDB_REQ_SET_LOCATION(add_req); + last_req = add_req; + if (ret != LDB_SUCCESS) { + /* return directly, this is a very unlikely error */ + return ret; + } + /* do request */ + ret = ldb_next_request(partition->module, add_req); + /* wait */ + if (ret == LDB_SUCCESS) { + ret = ldb_wait(add_req->handle, LDB_WAIT_ALL); + } + + switch (ret) { + case LDB_SUCCESS: + break; + + case LDB_ERR_ENTRY_ALREADY_EXISTS: + /* Handle this case specially - if the + * metadata already exists, replace it */ + { + struct ldb_request *del_req; + + /* Don't leave a confusing string in the ldb_errstring() */ + ldb_reset_err_string(ldb); + /* Build del request */ + ret = ldb_build_del_req(&del_req, ldb, replicate_res, replicate_res->msgs[0]->dn, NULL, NULL, + ldb_op_default_callback, last_req); + LDB_REQ_SET_LOCATION(del_req); + last_req = del_req; + if (ret != LDB_SUCCESS) { + /* return directly, this is a very unlikely error */ + return ret; + } + /* do request */ + ret = ldb_next_request(partition->module, del_req); + + /* wait */ + if (ret == LDB_SUCCESS) { + ret = ldb_wait(del_req->handle, LDB_WAIT_ALL); + } + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "Failed to delete (for re-add) %s from " DSDB_PARTITION_DN + " replicateEntries in new partition at %s on %s: %s", + ldb_dn_get_linearized(data->replicate[i]), + partition->backend_url, + ldb_dn_get_linearized(partition->ctrl->dn), + ldb_errstring(ldb)); + return ret; + } + + /* Build add request */ + ret = ldb_build_add_req(&add_req, ldb, replicate_res, replicate_res->msgs[0], NULL, NULL, + ldb_op_default_callback, last_req); + LDB_REQ_SET_LOCATION(add_req); + last_req = add_req; + if (ret != LDB_SUCCESS) { + /* return directly, this is a very unlikely error */ + return ret; + } + + /* do the add again */ + ret = ldb_next_request(partition->module, add_req); + + /* wait */ + if (ret == LDB_SUCCESS) { + ret = ldb_wait(add_req->handle, LDB_WAIT_ALL); + } + + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "Failed to add (after delete) %s from " DSDB_PARTITION_DN + " replicateEntries to new partition at %s on %s: %s", + ldb_dn_get_linearized(data->replicate[i]), + partition->backend_url, + ldb_dn_get_linearized(partition->ctrl->dn), + ldb_errstring(ldb)); + return ret; + } + break; + } + default: + { + ldb_asprintf_errstring(ldb, + "Failed to add %s from " DSDB_PARTITION_DN + " replicateEntries to new partition at %s on %s: %s", + ldb_dn_get_linearized(data->replicate[i]), + partition->backend_url, + ldb_dn_get_linearized(partition->ctrl->dn), + ldb_errstring(ldb)); + return ret; + } + } + + /* And around again, for the next thing we must merge */ + } + return LDB_SUCCESS; +} + +/* Extended operation to create a new partition, called when + * 'new_partition' detects that one is being added based on it's + * instanceType */ +int partition_create(struct ldb_module *module, struct ldb_request *req) +{ + unsigned int i; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_request *mod_req, *last_req = req; + struct ldb_message *mod_msg; + struct partition_private_data *data; + struct dsdb_partition *partition = NULL; + const char *casefold_dn; + bool new_partition = false; + + /* Check if this is already a partition */ + + struct dsdb_create_partition_exop *ex_op = talloc_get_type(req->op.extended.data, struct dsdb_create_partition_exop); + struct ldb_dn *dn = ex_op->new_dn; + + data = talloc_get_type(ldb_module_get_private(module), struct partition_private_data); + if (!data) { + /* We are not going to create a partition before we are even set up */ + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* see if we are still up-to-date */ + ret = partition_reload_if_required(module, data, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + for (i=0; data->partitions && data->partitions[i]; i++) { + if (ldb_dn_compare(data->partitions[i]->ctrl->dn, dn) == 0) { + partition = data->partitions[i]; + } + } + + if (!partition) { + char *filename; + char *partition_record; + new_partition = true; + mod_msg = ldb_msg_new(req); + if (!mod_msg) { + return ldb_oom(ldb); + } + + mod_msg->dn = ldb_dn_new(mod_msg, ldb, DSDB_PARTITION_DN); + + casefold_dn = ldb_dn_get_casefold(dn); + + { + char *escaped; + const char *p, *sam_name; + sam_name = strrchr((const char *)ldb_get_opaque(ldb, "ldb_url"), '/'); + if (!sam_name) { + return ldb_operr(ldb); + } + sam_name++; + + for (p = casefold_dn; *p; p++) { + /* We have such a strict check because + * I don't want shell metacharacters + * in the file name, nor ../, but I do + * want it to be easily typed if SAFE + * to do so */ + if (!(isalnum(*p) || *p == ' ' || *p == '=' || *p == ',')) { + break; + } + } + if (*p) { + escaped = rfc1738_escape_part(mod_msg, casefold_dn); + if (!escaped) { + return ldb_oom(ldb); + } + filename = talloc_asprintf(mod_msg, "%s.d/%s.ldb", sam_name, escaped); + talloc_free(escaped); + } else { + filename = talloc_asprintf(mod_msg, "%s.d/%s.ldb", sam_name, casefold_dn); + } + + if (!filename) { + return ldb_oom(ldb); + } + } + partition_record = talloc_asprintf(mod_msg, "%s:%s", casefold_dn, filename); + + ret = ldb_msg_append_steal_string(mod_msg, DSDB_PARTITION_ATTR, partition_record, + LDB_FLAG_MOD_ADD); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (ldb_request_get_control(req, DSDB_CONTROL_PARTIAL_REPLICA)) { + /* this new partition is a partial replica */ + ret = ldb_msg_append_fmt(mod_msg, LDB_FLAG_MOD_ADD, + "partialReplica", "%s", ldb_dn_get_linearized(dn)); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* Perform modify on @PARTITION record */ + ret = ldb_build_mod_req(&mod_req, ldb, req, mod_msg, NULL, NULL, + ldb_op_default_callback, req); + LDB_REQ_SET_LOCATION(mod_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + last_req = mod_req; + + ret = ldb_next_request(module, mod_req); + if (ret == LDB_SUCCESS) { + ret = ldb_wait(mod_req->handle, LDB_WAIT_ALL); + } + + if (ret != LDB_SUCCESS) { + return ret; + } + + /* Make a partition structure for this new partition, so we can copy in the template structure */ + ret = new_partition_from_dn(ldb, data, req, ldb_dn_copy(req, dn), + filename, data->backend_db_store, + &partition); + if (ret != LDB_SUCCESS) { + return ret; + } + talloc_steal(partition, partition_record); + partition->orig_record = data_blob_string_const(partition_record); + } + + ret = new_partition_set_replicated_metadata(ldb, module, last_req, data, partition); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (new_partition) { + ret = add_partition_to_data(ldb, data, partition); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* send request done */ + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS); +} + + +int partition_init(struct ldb_module *module) +{ + int ret; + TALLOC_CTX *mem_ctx = talloc_new(module); + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct partition_private_data *data; + + if (!mem_ctx) { + return ldb_operr(ldb); + } + + /* We actually got this during the read_lock call */ + data = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + + /* This loads the partitions */ + ret = partition_reload_if_required(module, data, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + ldb_module_set_private(module, talloc_steal(module, data)); + talloc_free(mem_ctx); + + ret = ldb_mod_register_control(module, LDB_CONTROL_DOMAIN_SCOPE_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "partition: Unable to register control with rootdse!\n"); + return ldb_operr(ldb); + } + + ret = ldb_mod_register_control(module, LDB_CONTROL_SEARCH_OPTIONS_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "partition: Unable to register control with rootdse!\n"); + return ldb_operr(ldb); + } + + return ldb_next_init(module); +} diff --git a/source4/dsdb/samdb/ldb_modules/partition_metadata.c b/source4/dsdb/samdb/ldb_modules/partition_metadata.c new file mode 100644 index 0000000..7763e53 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/partition_metadata.c @@ -0,0 +1,605 @@ +/* + Partitions ldb module - management of metadata.tdb for sequence number + + Copyright (C) Amitay Isaacs <amitay@samba.org> 2011 + + 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/>. +*/ + +#include "dsdb/samdb/ldb_modules/partition.h" +#include "lib/ldb-samba/ldb_wrap.h" +#include "system/filesys.h" +#include "lib/util/smb_strtox.h" + +#define LDB_METADATA_SEQ_NUM "SEQ_NUM" + + +/* + * Read a key with uint64 value + */ +static int partition_metadata_get_uint64(struct ldb_module *module, + const char *key, uint64_t *value, + uint64_t default_value) +{ + struct partition_private_data *data; + struct tdb_context *tdb; + TDB_DATA tdb_key, tdb_data; + char *value_str; + TALLOC_CTX *tmp_ctx; + int error = 0; + + data = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + + if (!data || !data->metadata || !data->metadata->db) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: metadata tdb not initialized"); + } + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + return ldb_module_oom(module); + } + + tdb = data->metadata->db->tdb; + + tdb_key.dptr = (uint8_t *)discard_const_p(char, key); + tdb_key.dsize = strlen(key); + + tdb_data = tdb_fetch(tdb, tdb_key); + if (!tdb_data.dptr) { + if (tdb_error(tdb) == TDB_ERR_NOEXIST) { + *value = default_value; + return LDB_SUCCESS; + } else { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + tdb_errorstr(tdb)); + } + } + + value_str = talloc_strndup(tmp_ctx, (char *)tdb_data.dptr, tdb_data.dsize); + if (value_str == NULL) { + SAFE_FREE(tdb_data.dptr); + talloc_free(tmp_ctx); + return ldb_module_oom(module); + } + + *value = smb_strtoull(value_str, NULL, 10, &error, SMB_STR_STANDARD); + if (error != 0) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: converision failed"); + } + + SAFE_FREE(tdb_data.dptr); + talloc_free(tmp_ctx); + + return LDB_SUCCESS; +} + + +/* + * Write a key with uin64 value + */ +static int partition_metadata_set_uint64(struct ldb_module *module, + const char *key, uint64_t value, + bool insert) +{ + struct partition_private_data *data; + struct tdb_context *tdb; + TDB_DATA tdb_key, tdb_data; + int tdb_flag; + char *value_str; + TALLOC_CTX *tmp_ctx; + + data = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + + if (!data || !data->metadata || !data->metadata->db) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: metadata tdb not initialized"); + } + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + return ldb_module_oom(module); + } + + tdb = data->metadata->db->tdb; + + value_str = talloc_asprintf(tmp_ctx, "%llu", (unsigned long long)value); + if (value_str == NULL) { + talloc_free(tmp_ctx); + return ldb_module_oom(module); + } + + tdb_key.dptr = (uint8_t *)discard_const_p(char, key); + tdb_key.dsize = strlen(key); + + tdb_data.dptr = (uint8_t *)value_str; + tdb_data.dsize = strlen(value_str); + + if (insert) { + tdb_flag = TDB_INSERT; + } else { + tdb_flag = TDB_MODIFY; + } + + if (tdb_store(tdb, tdb_key, tdb_data, tdb_flag) != 0) { + int ret; + char *error_string = talloc_asprintf(tmp_ctx, "%s: tdb_store of key %s failed: %s", + tdb_name(tdb), key, tdb_errorstr(tdb)); + ret = ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + error_string); + talloc_free(tmp_ctx); + return ret; + } + + talloc_free(tmp_ctx); + + return LDB_SUCCESS; +} + +int partition_metadata_inc_schema_sequence(struct ldb_module *module) +{ + struct partition_private_data *data; + int ret; + uint64_t value = 0; + + data = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + if (!data || !data->metadata) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: metadata not initialized"); + } + + if (data->metadata->in_transaction == 0) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: increment sequence number without transaction"); + } + ret = partition_metadata_get_uint64(module, DSDB_METADATA_SCHEMA_SEQ_NUM, &value, 0); + if (ret != LDB_SUCCESS) { + return ret; + } + + value++; + ret = partition_metadata_set_uint64(module, DSDB_METADATA_SCHEMA_SEQ_NUM, value, false); + if (ret == LDB_ERR_OPERATIONS_ERROR) { + /* Modify failed, let's try the add */ + ret = partition_metadata_set_uint64(module, DSDB_METADATA_SCHEMA_SEQ_NUM, value, true); + } + return ret; +} + + + +/* + * Open sam.ldb.d/metadata.tdb. + */ +static int partition_metadata_open(struct ldb_module *module, bool create) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + TALLOC_CTX *tmp_ctx; + struct partition_private_data *data; + struct loadparm_context *lp_ctx; + char *filename, *dirname; + int open_flags, tdb_flags, ldb_flags; + struct stat statbuf; + + data = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + if (!data || !data->metadata) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: metadata not initialized"); + } + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + return ldb_module_oom(module); + } + + filename = ldb_relative_path(ldb, + tmp_ctx, + "sam.ldb.d/metadata.tdb"); + + if (!filename) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + + open_flags = O_RDWR; + if (create) { + open_flags |= O_CREAT; + + /* While provisioning, sam.ldb.d directory may not exist, + * so create it. Ignore errors, if it already exists. */ + dirname = ldb_relative_path(ldb, + tmp_ctx, + "sam.ldb.d"); + if (!dirname) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + + mkdir(dirname, 0700); + talloc_free(dirname); + } else { + if (stat(filename, &statbuf) != 0) { + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + } + + lp_ctx = talloc_get_type_abort(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + + tdb_flags = lpcfg_tdb_flags(lp_ctx, TDB_DEFAULT|TDB_SEQNUM); + + ldb_flags = ldb_module_flags(ldb); + + if (ldb_flags & LDB_FLG_NOSYNC) { + tdb_flags |= TDB_NOSYNC; + } + + data->metadata->db = tdb_wrap_open( + data->metadata, filename, 10, + tdb_flags, open_flags, 0660); + if (data->metadata->db == NULL) { + talloc_free(tmp_ctx); + if (create) { + ldb_asprintf_errstring(ldb, "partition_metadata: Unable to create %s: %s", + filename, strerror(errno)); + } else { + ldb_asprintf_errstring(ldb, "partition_metadata: Unable to open %s: %s", + filename, strerror(errno)); + } + if (errno == EACCES || errno == EPERM) { + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + return LDB_ERR_OPERATIONS_ERROR; + } + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + + +/* + * Set the sequence number calculated from older logic (sum of primary sequence + * numbers for each partition) as LDB_METADATA_SEQ_NUM key. + */ +static int partition_metadata_set_sequence_number(struct ldb_module *module) +{ + int ret; + uint64_t seq_number; + + ret = partition_sequence_number_from_partitions(module, &seq_number); + if (ret != LDB_SUCCESS) { + return ret; + } + + return partition_metadata_set_uint64(module, LDB_METADATA_SEQ_NUM, seq_number, true); +} + + +/* + * Initialize metadata. Load metadata.tdb. + * If missing, create it and fill in sequence number + */ +int partition_metadata_init(struct ldb_module *module) +{ + struct partition_private_data *data; + int ret; + + data = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + + if (data->metadata != NULL && data->metadata->db != NULL) { + return LDB_SUCCESS; + } + + data->metadata = talloc_zero(data, struct partition_metadata); + if (data->metadata == NULL) { + return ldb_module_oom(module); + } + + ret = partition_metadata_open(module, false); + if (ret == LDB_SUCCESS) { + /* Great, we got the DB open */ + return LDB_SUCCESS; + } + + /* metadata.tdb does not exist, create it */ + DEBUG(2, ("partition_metadata: Migrating partition metadata: " + "open of metadata.tdb gave: %s\n", + ldb_errstring(ldb_module_get_ctx(module)))); + ret = partition_metadata_open(module, true); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "partition_metadata: " + "Migrating partition metadata: " + "create of metadata.tdb gave: %s\n", + ldb_errstring(ldb_module_get_ctx(module))); + TALLOC_FREE(data->metadata); + return ret; + } + + return ret; +} + + +/* + * Read the sequence number, default to 0 if LDB_METADATA_SEQ_NUM key is missing + */ +int partition_metadata_sequence_number(struct ldb_module *module, uint64_t *value) +{ + + /* We have to lock all the databases as otherwise we can + * return a sequence number that is higher than the DB values + * that we can see, as those transactions close after the + * metadata.tdb transaction closes */ + int ret = partition_read_lock(module); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + * This means we will give a 0 until the first write + * transaction, which is actually pretty reasonable. + * + * All modern databases will have the metadata.tdb from + * the time of the first transaction in provision anyway. + */ + ret = partition_metadata_get_uint64(module, + LDB_METADATA_SEQ_NUM, + value, + 0); + if (ret == LDB_SUCCESS) { + ret = partition_read_unlock(module); + } else { + /* Don't overwrite the error code */ + partition_read_unlock(module); + } + return ret; + +} + + +/* + * Increment the sequence number, returning the new sequence number + */ +int partition_metadata_sequence_number_increment(struct ldb_module *module, uint64_t *value) +{ + struct partition_private_data *data; + int ret; + + data = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + if (!data || !data->metadata) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: metadata not initialized"); + } + + if (data->metadata->in_transaction == 0) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: increment sequence number without transaction"); + } + + ret = partition_metadata_get_uint64(module, LDB_METADATA_SEQ_NUM, value, 0); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (*value == 0) { + /* + * We are in a transaction now, so we can get the + * sequence number from the partitions. + */ + ret = partition_metadata_set_sequence_number(module); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = partition_metadata_get_uint64(module, + LDB_METADATA_SEQ_NUM, + value, 0); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + (*value)++; + ret = partition_metadata_set_uint64(module, LDB_METADATA_SEQ_NUM, *value, false); + return ret; +} +/* + lock the database for read - use by partition_lock_read +*/ +int partition_metadata_read_lock(struct ldb_module *module) +{ + struct partition_private_data *data + = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + struct tdb_context *tdb = NULL; + int tdb_ret = 0; + int ret; + + if (!data || !data->metadata || !data->metadata->db) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: metadata not initialized"); + } + tdb = data->metadata->db->tdb; + + if (tdb_transaction_active(tdb) == false && + data->metadata->read_lock_count == 0) { + tdb_ret = tdb_lockall_read(tdb); + } + if (tdb_ret == 0) { + data->metadata->read_lock_count++; + return LDB_SUCCESS; + } else { + /* Sadly we can't call ltdb_err_map(tdb_error(tdb)); */ + ret = LDB_ERR_BUSY; + } + ldb_debug_set(ldb_module_get_ctx(module), + LDB_DEBUG_FATAL, + "Failure during partition_metadata_read_lock(): %s", + tdb_errorstr(tdb)); + return ret; +} + +/* + unlock the database after a partition_metadata_lock_read() +*/ +int partition_metadata_read_unlock(struct ldb_module *module) +{ + struct partition_private_data *data + = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + struct tdb_context *tdb = NULL; + + data = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + if (!data || !data->metadata || !data->metadata->db) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: metadata not initialized"); + } + tdb = data->metadata->db->tdb; + + if (!tdb_transaction_active(tdb) && + data->metadata->read_lock_count == 1) { + tdb_unlockall_read(tdb); + data->metadata->read_lock_count--; + return 0; + } + data->metadata->read_lock_count--; + return 0; +} + + +/* + * Transaction start + */ +int partition_metadata_start_trans(struct ldb_module *module) +{ + struct partition_private_data *data; + struct tdb_context *tdb; + + data = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + if (!data || !data->metadata || !data->metadata->db) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: metadata not initialized"); + } + tdb = data->metadata->db->tdb; + + if (tdb_transaction_start(tdb) != 0) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + tdb_errorstr(tdb)); + } + + data->metadata->in_transaction++; + + return LDB_SUCCESS; +} + + +/* + * Transaction prepare commit + */ +int partition_metadata_prepare_commit(struct ldb_module *module) +{ + struct partition_private_data *data; + struct tdb_context *tdb; + + data = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + if (!data || !data->metadata || !data->metadata->db) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: metadata not initialized"); + } + tdb = data->metadata->db->tdb; + + if (data->metadata->in_transaction == 0) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: not in transaction"); + } + + if (tdb_transaction_prepare_commit(tdb) != 0) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + tdb_errorstr(tdb)); + } + + return LDB_SUCCESS; +} + + +/* + * Transaction end + */ +int partition_metadata_end_trans(struct ldb_module *module) +{ + struct partition_private_data *data; + struct tdb_context *tdb; + + data = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + if (!data || !data->metadata || !data->metadata->db) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: metadata not initialized"); + } + tdb = data->metadata->db->tdb; + + if (data->metadata->in_transaction == 0) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: not in transaction"); + } + + data->metadata->in_transaction--; + + if (tdb_transaction_commit(tdb) != 0) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + tdb_errorstr(tdb)); + } + + return LDB_SUCCESS; +} + + +/* + * Transaction delete + */ +int partition_metadata_del_trans(struct ldb_module *module) +{ + struct partition_private_data *data; + struct tdb_context *tdb; + + data = talloc_get_type_abort(ldb_module_get_private(module), + struct partition_private_data); + if (!data || !data->metadata || !data->metadata->db) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: metadata not initialized"); + } + tdb = data->metadata->db->tdb; + + if (data->metadata->in_transaction == 0) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "partition_metadata: not in transaction"); + } + + data->metadata->in_transaction--; + + tdb_transaction_cancel(tdb); + + return LDB_SUCCESS; +} diff --git a/source4/dsdb/samdb/ldb_modules/password_hash.c b/source4/dsdb/samdb/ldb_modules/password_hash.c new file mode 100644 index 0000000..6a713b8 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/password_hash.c @@ -0,0 +1,5134 @@ +/* + ldb database module + + Copyright (C) Simo Sorce 2004-2008 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2005-2006 + Copyright (C) Andrew Tridgell 2004 + Copyright (C) Stefan Metzmacher 2007-2010 + Copyright (C) Matthias Dieter Wallnöfer 2009-2010 + + 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 password_hash module + * + * Description: correctly handle AD password changes fields + * + * Author: Andrew Bartlett + * Author: Stefan Metzmacher + */ + +#include "includes.h" +#include "ldb_module.h" +#include "libcli/auth/libcli_auth.h" +#include "libcli/security/dom_sid.h" +#include "system/kerberos.h" +#include "auth/kerberos/kerberos.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "dsdb/samdb/ldb_modules/password_modules.h" +#include "librpc/gen_ndr/ndr_drsblobs.h" +#include "lib/crypto/md4.h" +#include "param/param.h" +#include "lib/krb5_wrap/krb5_samba.h" +#include "auth/auth_sam.h" +#include "auth/common_auth.h" +#include "lib/messaging/messaging.h" +#include "lib/param/loadparm.h" + +#include "lib/crypto/gnutls_helpers.h" +#include <gnutls/crypto.h> + +#include "kdc/db-glue.h" + +#ifdef ENABLE_GPGME +#undef class +#include <gpgme.h> + +/* + * 1.2.0 is what dpkg-shlibdeps generates, based on used symbols and + * libgpgme11.symbols + * https://salsa.debian.org/debian/gpgme/blob/debian/master/debian/libgpgme11.symbols + */ + +#define MINIMUM_GPGME_VERSION "1.2.0" +#endif + +#undef strncasecmp +#undef strcasecmp + +/* If we have decided there is a reason to work on this request, then + * setup all the password hash types correctly. + * + * If we haven't the hashes yet but the password given as plain-text (attributes + * 'unicodePwd', 'userPassword' and 'clearTextPassword') we have to check for + * the constraints. Once this is done, we calculate the password hashes. + * + * Notice: unlike the real AD which only supports the UTF16 special based + * 'unicodePwd' and the UTF8 based 'userPassword' plaintext attribute we + * understand also a UTF16 based 'clearTextPassword' one. + * The latter is also accessible through LDAP so it can also be set by external + * tools and scripts. But be aware that this isn't portable on non SAMBA 4 ADs! + * + * Also when the module receives only the password hashes (possible through + * specifying an internal LDB control - for security reasons) some checks are + * performed depending on the operation mode (see below) (e.g. if the password + * has been in use before if the password memory policy was activated). + * + * Attention: There is a difference between "modify" and "reset" operations + * (see MS-ADTS 3.1.1.3.1.5). If the client sends a "add" and "remove" + * operation for a password attribute we thread this as a "modify"; if it sends + * only a "replace" one we have an (administrative) reset. + * + * Finally, if the administrator has requested that a password history + * be maintained, then this should also be written out. + * + */ + +/* TODO: [consider always MS-ADTS 3.1.1.3.1.5] + * - Check for right connection encryption + */ + +/* Notice: Definition of "dsdb_control_password_change_status" moved into + * "samdb.h" */ + +struct ph_context { + struct ldb_module *module; + struct ldb_request *req; + + struct ldb_request *dom_req; + struct ldb_reply *dom_res; + + struct ldb_reply *pso_res; + + struct ldb_reply *search_res; + + struct ldb_message *update_msg; + + struct dsdb_control_password_change_status *status; + struct dsdb_control_password_change *change; + + const char **gpg_key_ids; + + bool pwd_reset; + bool change_status; + bool hash_values; + bool userPassword; + bool update_password; + bool update_lastset; + bool pwd_last_set_bypass; + bool pwd_last_set_default; + bool smartcard_reset; + const char **userPassword_schemes; +}; + + +struct setup_password_fields_io { + struct ph_context *ac; + + struct smb_krb5_context *smb_krb5_context; + + /* info about the user account */ + struct { + uint32_t userAccountControl; + NTTIME pwdLastSet; + const char *sAMAccountName; + const char *user_principal_name; + const char *displayName; /* full name */ + bool is_krbtgt; + uint32_t restrictions; + struct dom_sid *account_sid; + bool store_nt_hash; + } u; + + /* new credentials and old given credentials */ + struct setup_password_fields_given { + const struct ldb_val *cleartext_utf8; + const struct ldb_val *cleartext_utf16; + + struct samr_Password *nt_hash; + + /* + * The AES256 kerberos key to confirm the previous password was + * not reused (for n) and to prove the old password was known + * (for og). + * + * We don't have any old salts, so we won't catch password reuse + * if said password was used prior to an account rename and + * another password change. + */ + DATA_BLOB aes_256; + } n, og; + + /* old credentials */ + struct { + struct samr_Password *nt_hash; + uint32_t nt_history_len; + struct samr_Password *nt_history; + const struct ldb_val *supplemental; + struct supplementalCredentialsBlob scb; + + /* + * The AES256 kerberos key as stored in the DB. + * Used to confirm the given password was correct + * and in case the previous password was reused. + */ + DATA_BLOB aes_256; + DATA_BLOB salt; + uint32_t kvno; + } o; + + /* generated credentials */ + struct { + struct samr_Password *nt_hash; + uint32_t nt_history_len; + struct samr_Password *nt_history; + const char *salt; + DATA_BLOB aes_256; + DATA_BLOB aes_128; + DATA_BLOB des_md5; + DATA_BLOB des_crc; + struct ldb_val supplemental; + NTTIME last_set; + } g; +}; + +static int msg_find_old_and_new_pwd_val(const struct ldb_message *msg, + const char *name, + enum ldb_request_type operation, + const struct ldb_val **new_val, + const struct ldb_val **old_val); + +static int password_hash_bypass(struct ldb_module *module, struct ldb_request *request) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + const struct ldb_message *msg; + struct ldb_message_element *nte; + struct ldb_message_element *lme; + struct ldb_message_element *nthe; + struct ldb_message_element *lmhe; + struct ldb_message_element *sce; + int ret; + + switch (request->operation) { + case LDB_ADD: + msg = request->op.add.message; + break; + case LDB_MODIFY: + msg = request->op.mod.message; + break; + default: + return ldb_next_request(module, request); + } + + /* nobody must touch password histories and 'supplementalCredentials' */ + +#define GET_VALUES(el, attr) do { \ + ret = dsdb_get_expected_new_values(request, \ + msg, \ + attr, \ + &el, \ + request->operation); \ + \ + if (ret != LDB_SUCCESS) { \ + return ret; \ + } \ +} while(0) + + GET_VALUES(nte, "unicodePwd"); + + /* + * Even as Samba contiuues to ignore the LM hash, and reset it + * when practical, we keep the constraint that it must be a 16 + * byte value if specified. + */ + GET_VALUES(lme, "dBCSPwd"); + GET_VALUES(nthe, "ntPwdHistory"); + GET_VALUES(lmhe, "lmPwdHistory"); + GET_VALUES(sce, "supplementalCredentials"); + +#undef GET_VALUES +#define CHECK_HASH_ELEMENT(e, min, max) do {\ + if (e && e->num_values) { \ + unsigned int _count; \ + if (e->num_values != 1) { \ + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, \ + "num_values != 1"); \ + } \ + if ((e->values[0].length % 16) != 0) { \ + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, \ + "length % 16 != 0"); \ + } \ + _count = e->values[0].length / 16; \ + if (_count < min) { \ + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, \ + "count < min"); \ + } \ + if (_count > max) { \ + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, \ + "count > max"); \ + } \ + } \ +} while (0) + + CHECK_HASH_ELEMENT(nte, 1, 1); + CHECK_HASH_ELEMENT(lme, 1, 1); + CHECK_HASH_ELEMENT(nthe, 1, INT32_MAX); + CHECK_HASH_ELEMENT(lmhe, 1, INT32_MAX); + + if (sce && sce->num_values) { + enum ndr_err_code ndr_err; + struct supplementalCredentialsBlob *scb; + struct supplementalCredentialsPackage *scpp = NULL; + struct supplementalCredentialsPackage *scpk = NULL; + struct supplementalCredentialsPackage *scpkn = NULL; + struct supplementalCredentialsPackage *scpct = NULL; + DATA_BLOB scpbp = data_blob_null; + DATA_BLOB scpbk = data_blob_null; + DATA_BLOB scpbkn = data_blob_null; + DATA_BLOB scpbct = data_blob_null; + DATA_BLOB blob; + uint32_t i; + + if (sce->num_values != 1) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "num_values != 1"); + } + + scb = talloc_zero(request, struct supplementalCredentialsBlob); + if (!scb) { + return ldb_module_oom(module); + } + + ndr_err = ndr_pull_struct_blob_all(&sce->values[0], scb, scb, + (ndr_pull_flags_fn_t)ndr_pull_supplementalCredentialsBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "ndr_pull_struct_blob_all"); + } + + if (scb->sub.num_packages < 2) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "num_packages < 2"); + } + + for (i=0; i < scb->sub.num_packages; i++) { + DATA_BLOB subblob; + + subblob = strhex_to_data_blob(scb, scb->sub.packages[i].data); + if (subblob.data == NULL) { + return ldb_module_oom(module); + } + + if (strcmp(scb->sub.packages[i].name, "Packages") == 0) { + if (scpp) { + return ldb_error(ldb, + LDB_ERR_CONSTRAINT_VIOLATION, + "Packages twice"); + } + scpp = &scb->sub.packages[i]; + scpbp = subblob; + continue; + } + if (strcmp(scb->sub.packages[i].name, "Primary:Kerberos") == 0) { + if (scpk) { + return ldb_error(ldb, + LDB_ERR_CONSTRAINT_VIOLATION, + "Primary:Kerberos twice"); + } + scpk = &scb->sub.packages[i]; + scpbk = subblob; + continue; + } + if (strcmp(scb->sub.packages[i].name, "Primary:Kerberos-Newer-Keys") == 0) { + if (scpkn) { + return ldb_error(ldb, + LDB_ERR_CONSTRAINT_VIOLATION, + "Primary:Kerberos-Newer-Keys twice"); + } + scpkn = &scb->sub.packages[i]; + scpbkn = subblob; + continue; + } + if (strcmp(scb->sub.packages[i].name, "Primary:CLEARTEXT") == 0) { + if (scpct) { + return ldb_error(ldb, + LDB_ERR_CONSTRAINT_VIOLATION, + "Primary:CLEARTEXT twice"); + } + scpct = &scb->sub.packages[i]; + scpbct = subblob; + continue; + } + + data_blob_free(&subblob); + } + + if (scpp == NULL) { + return ldb_error(ldb, + LDB_ERR_CONSTRAINT_VIOLATION, + "Primary:Packages missing"); + } + + if (scpk == NULL) { + /* + * If Primary:Kerberos is missing w2k8r2 reboots + * when a password is changed. + */ + return ldb_error(ldb, + LDB_ERR_CONSTRAINT_VIOLATION, + "Primary:Kerberos missing"); + } + + if (scpp) { + struct package_PackagesBlob *p; + uint32_t n; + + p = talloc_zero(scb, struct package_PackagesBlob); + if (p == NULL) { + return ldb_module_oom(module); + } + + ndr_err = ndr_pull_struct_blob(&scpbp, p, p, + (ndr_pull_flags_fn_t)ndr_pull_package_PackagesBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "ndr_pull_struct_blob Packages"); + } + + if (p->names == NULL) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "Packages names == NULL"); + } + + for (n = 0; p->names[n]; n++) { + /* noop */ + } + + if (scb->sub.num_packages != (n + 1)) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "Packages num_packages != num_names + 1"); + } + + talloc_free(p); + } + + if (scpk) { + struct package_PrimaryKerberosBlob *k; + + k = talloc_zero(scb, struct package_PrimaryKerberosBlob); + if (k == NULL) { + return ldb_module_oom(module); + } + + ndr_err = ndr_pull_struct_blob(&scpbk, k, k, + (ndr_pull_flags_fn_t)ndr_pull_package_PrimaryKerberosBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "ndr_pull_struct_blob PrimaryKerberos"); + } + + if (k->version != 3) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "PrimaryKerberos version != 3"); + } + + if (k->ctr.ctr3.salt.string == NULL) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "PrimaryKerberos salt == NULL"); + } + + if (strlen(k->ctr.ctr3.salt.string) == 0) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "PrimaryKerberos strlen(salt) == 0"); + } + + if (k->ctr.ctr3.num_keys != 2) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "PrimaryKerberos num_keys != 2"); + } + + if (k->ctr.ctr3.num_old_keys > k->ctr.ctr3.num_keys) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "PrimaryKerberos num_old_keys > num_keys"); + } + + if (k->ctr.ctr3.keys[0].keytype != ENCTYPE_DES_CBC_MD5) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "PrimaryKerberos key[0] != DES_CBC_MD5"); + } + if (k->ctr.ctr3.keys[1].keytype != ENCTYPE_DES_CBC_CRC) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "PrimaryKerberos key[1] != DES_CBC_CRC"); + } + + if (k->ctr.ctr3.keys[0].value_len != 8) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "PrimaryKerberos key[0] value_len != 8"); + } + if (k->ctr.ctr3.keys[1].value_len != 8) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "PrimaryKerberos key[1] value_len != 8"); + } + + for (i = 0; i < k->ctr.ctr3.num_old_keys; i++) { + if (k->ctr.ctr3.old_keys[i].keytype == + k->ctr.ctr3.keys[i].keytype && + k->ctr.ctr3.old_keys[i].value_len == + k->ctr.ctr3.keys[i].value_len) { + continue; + } + + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "PrimaryKerberos old_keys type/value_len doesn't match"); + } + + talloc_free(k); + } + + if (scpkn) { + struct package_PrimaryKerberosBlob *k; + + k = talloc_zero(scb, struct package_PrimaryKerberosBlob); + if (k == NULL) { + return ldb_module_oom(module); + } + + ndr_err = ndr_pull_struct_blob(&scpbkn, k, k, + (ndr_pull_flags_fn_t)ndr_pull_package_PrimaryKerberosBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "ndr_pull_struct_blob PrimaryKerberosNeverKeys"); + } + + if (k->version != 4) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNerverKeys version != 4"); + } + + if (k->ctr.ctr4.salt.string == NULL) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNewerKeys salt == NULL"); + } + + if (strlen(k->ctr.ctr4.salt.string) == 0) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNewerKeys strlen(salt) == 0"); + } + + if (k->ctr.ctr4.num_keys != 4) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNewerKeys num_keys != 2"); + } + + if (k->ctr.ctr4.num_old_keys > k->ctr.ctr4.num_keys) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNewerKeys num_old_keys > num_keys"); + } + + if (k->ctr.ctr4.num_older_keys > k->ctr.ctr4.num_old_keys) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNewerKeys num_older_keys > num_old_keys"); + } + + if (k->ctr.ctr4.keys[0].keytype != ENCTYPE_AES256_CTS_HMAC_SHA1_96) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNewerKeys key[0] != AES256"); + } + if (k->ctr.ctr4.keys[1].keytype != ENCTYPE_AES128_CTS_HMAC_SHA1_96) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNewerKeys key[1] != AES128"); + } + if (k->ctr.ctr4.keys[2].keytype != ENCTYPE_DES_CBC_MD5) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNewerKeys key[2] != DES_CBC_MD5"); + } + if (k->ctr.ctr4.keys[3].keytype != ENCTYPE_DES_CBC_CRC) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNewerKeys key[3] != DES_CBC_CRC"); + } + + if (k->ctr.ctr4.keys[0].value_len != 32) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNewerKeys key[0] value_len != 32"); + } + if (k->ctr.ctr4.keys[1].value_len != 16) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNewerKeys key[1] value_len != 16"); + } + if (k->ctr.ctr4.keys[2].value_len != 8) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNewerKeys key[2] value_len != 8"); + } + if (k->ctr.ctr4.keys[3].value_len != 8) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "KerberosNewerKeys key[3] value_len != 8"); + } + + /* + * TODO: + * Maybe we can check old and older keys here. + * But we need to do some tests, if the old keys + * can be taken from the PrimaryKerberos blob + * (with only des keys), when the domain was upgraded + * from w2k3 to w2k8. + */ + + talloc_free(k); + } + + if (scpct) { + struct package_PrimaryCLEARTEXTBlob *ct; + + ct = talloc_zero(scb, struct package_PrimaryCLEARTEXTBlob); + if (ct == NULL) { + return ldb_module_oom(module); + } + + ndr_err = ndr_pull_struct_blob(&scpbct, ct, ct, + (ndr_pull_flags_fn_t)ndr_pull_package_PrimaryCLEARTEXTBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "ndr_pull_struct_blob PrimaryCLEARTEXT"); + } + + if ((ct->cleartext.length % 2) != 0) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "PrimaryCLEARTEXT length % 2 != 0"); + } + + talloc_free(ct); + } + + ndr_err = ndr_push_struct_blob(&blob, scb, scb, + (ndr_push_flags_fn_t)ndr_push_supplementalCredentialsBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "ndr_pull_struct_blob_all"); + } + + if (sce->values[0].length != blob.length) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "supplementalCredentialsBlob length differ"); + } + + if (!mem_equal_const_time(sce->values[0].data, blob.data, blob.length)) { + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, + "supplementalCredentialsBlob memcmp differ"); + } + + talloc_free(scb); + } + + ldb_debug(ldb, LDB_DEBUG_TRACE, "password_hash_bypass - validated\n"); + return ldb_next_request(module, request); +} + +/* Get the NT hash, and fill it in as an entry in the password history, + and specify it into io->g.nt_hash */ + +static int setup_nt_fields(struct setup_password_fields_io *io) +{ + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); + uint32_t i; + if (io->u.store_nt_hash) { + io->g.nt_hash = io->n.nt_hash; + } + + if (io->ac->status->domain_data.pwdHistoryLength == 0) { + return LDB_SUCCESS; + } + + /* We might not have an old NT password */ + + if (io->g.nt_hash == NULL) { + /* + * If there was not an NT hash specified, then don't + * store the NT password history. + * + * While the NTLM code on a Windows DC will cope with + * a missing unicodePwd, if it finds a last password + * in the ntPwdHistory, even if the bytes are zero , + * it will (quite reasonably) treat it as a valid NT + * hash. NTLM logins with the previous password are + * allowed for a short time after the password is + * changed to allow for password propagation delays. + */ + return LDB_SUCCESS; + } + + io->g.nt_history = talloc_array(io->ac, + struct samr_Password, + io->ac->status->domain_data.pwdHistoryLength); + if (!io->g.nt_history) { + return ldb_oom(ldb); + } + + for (i = 0; i < MIN(io->ac->status->domain_data.pwdHistoryLength-1, + io->o.nt_history_len); i++) { + io->g.nt_history[i+1] = io->o.nt_history[i]; + } + io->g.nt_history_len = i + 1; + + io->g.nt_history[0] = *io->g.nt_hash; + + return LDB_SUCCESS; +} + +static int setup_kerberos_keys(struct setup_password_fields_io *io) +{ + struct ldb_context *ldb; + krb5_error_code krb5_ret; + krb5_principal salt_principal = NULL; + krb5_data salt_data; + krb5_data salt; + krb5_keyblock key; + krb5_data cleartext_data; + uint32_t uac_flags = 0; + + ldb = ldb_module_get_ctx(io->ac->module); + cleartext_data.data = (char *)io->n.cleartext_utf8->data; + cleartext_data.length = io->n.cleartext_utf8->length; + + uac_flags = io->u.userAccountControl & UF_ACCOUNT_TYPE_MASK; + krb5_ret = smb_krb5_salt_principal(io->smb_krb5_context->krb5_context, + io->ac->status->domain_data.realm, + io->u.sAMAccountName, + io->u.user_principal_name, + uac_flags, + &salt_principal); + if (krb5_ret) { + ldb_asprintf_errstring(ldb, + "setup_kerberos_keys: " + "generation of a salting principal failed: %s", + smb_get_krb5_error_message(io->smb_krb5_context->krb5_context, + krb5_ret, io->ac)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * create salt from salt_principal + */ + krb5_ret = smb_krb5_get_pw_salt(io->smb_krb5_context->krb5_context, + salt_principal, &salt_data); + + krb5_free_principal(io->smb_krb5_context->krb5_context, salt_principal); + if (krb5_ret) { + ldb_asprintf_errstring(ldb, + "setup_kerberos_keys: " + "generation of krb5_salt failed: %s", + smb_get_krb5_error_message(io->smb_krb5_context->krb5_context, + krb5_ret, io->ac)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* now use the talloced copy of the salt */ + salt.data = talloc_strndup(io->ac, + (char *)salt_data.data, + salt_data.length); + io->g.salt = salt.data; + salt.length = strlen(io->g.salt); + + smb_krb5_free_data_contents(io->smb_krb5_context->krb5_context, + &salt_data); + + /* + * create ENCTYPE_AES256_CTS_HMAC_SHA1_96 key out of + * the salt and the cleartext password + */ + krb5_ret = smb_krb5_create_key_from_string(io->smb_krb5_context->krb5_context, + NULL, + &salt, + &cleartext_data, + ENCTYPE_AES256_CTS_HMAC_SHA1_96, + &key); + if (krb5_ret) { + ldb_asprintf_errstring(ldb, + "setup_kerberos_keys: " + "generation of a aes256-cts-hmac-sha1-96 key failed: %s", + smb_get_krb5_error_message(io->smb_krb5_context->krb5_context, + krb5_ret, io->ac)); + return LDB_ERR_OPERATIONS_ERROR; + } + io->g.aes_256 = data_blob_talloc(io->ac, + KRB5_KEY_DATA(&key), + KRB5_KEY_LENGTH(&key)); + krb5_free_keyblock_contents(io->smb_krb5_context->krb5_context, &key); + if (!io->g.aes_256.data) { + return ldb_oom(ldb); + } + + /* + * create ENCTYPE_AES128_CTS_HMAC_SHA1_96 key out of + * the salt and the cleartext password + */ + krb5_ret = smb_krb5_create_key_from_string(io->smb_krb5_context->krb5_context, + NULL, + &salt, + &cleartext_data, + ENCTYPE_AES128_CTS_HMAC_SHA1_96, + &key); + if (krb5_ret) { + ldb_asprintf_errstring(ldb, + "setup_kerberos_keys: " + "generation of a aes128-cts-hmac-sha1-96 key failed: %s", + smb_get_krb5_error_message(io->smb_krb5_context->krb5_context, + krb5_ret, io->ac)); + return LDB_ERR_OPERATIONS_ERROR; + } + io->g.aes_128 = data_blob_talloc(io->ac, + KRB5_KEY_DATA(&key), + KRB5_KEY_LENGTH(&key)); + krb5_free_keyblock_contents(io->smb_krb5_context->krb5_context, &key); + if (!io->g.aes_128.data) { + return ldb_oom(ldb); + } + + /* + * As per RFC-6649 single DES encryption types are no longer considered + * secure to be used in Kerberos, we store random keys instead of the + * ENCTYPE_DES_CBC_MD5 and ENCTYPE_DES_CBC_CRC keys. + */ + io->g.des_md5 = data_blob_talloc(io->ac, NULL, 8); + if (!io->g.des_md5.data) { + return ldb_oom(ldb); + } + generate_secret_buffer(io->g.des_md5.data, 8); + + io->g.des_crc = data_blob_talloc(io->ac, NULL, 8); + if (!io->g.des_crc.data) { + return ldb_oom(ldb); + } + generate_secret_buffer(io->g.des_crc.data, 8); + + return LDB_SUCCESS; +} + +static int setup_kerberos_key_hash(struct setup_password_fields_io *io, + struct setup_password_fields_given *g) +{ + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); + krb5_error_code krb5_ret; + krb5_data salt; + krb5_keyblock key; + krb5_data cleartext_data; + + if (io->ac->search_res == NULL) { + /* No old data so nothing to do */ + return LDB_SUCCESS; + } + + if (io->o.salt.data == NULL) { + /* We didn't fetch the salt in setup_io(), so nothing to do */ + return LDB_SUCCESS; + } + + salt.data = (char *)io->o.salt.data; + salt.length = io->o.salt.length; + + cleartext_data.data = (char *)g->cleartext_utf8->data; + cleartext_data.length = g->cleartext_utf8->length; + + /* + * create ENCTYPE_AES256_CTS_HMAC_SHA1_96 key out of the salt + * and the cleartext password + */ + krb5_ret = smb_krb5_create_key_from_string(io->smb_krb5_context->krb5_context, + NULL, + &salt, + &cleartext_data, + ENCTYPE_AES256_CTS_HMAC_SHA1_96, + &key); + if (krb5_ret) { + ldb_asprintf_errstring(ldb, + "setup_kerberos_key_hash: " + "generation of a aes256-cts-hmac-sha1-96 key failed: %s", + smb_get_krb5_error_message(io->smb_krb5_context->krb5_context, + krb5_ret, io->ac)); + return LDB_ERR_OPERATIONS_ERROR; + } + + g->aes_256 = data_blob_talloc(io->ac, + KRB5_KEY_DATA(&key), + KRB5_KEY_LENGTH(&key)); + krb5_free_keyblock_contents(io->smb_krb5_context->krb5_context, &key); + if (g->aes_256.data == NULL) { + return ldb_oom(ldb); + } + + talloc_keep_secret(g->aes_256.data); + + return LDB_SUCCESS; +} + +static int setup_primary_kerberos(struct setup_password_fields_io *io, + const struct supplementalCredentialsBlob *old_scb, + struct package_PrimaryKerberosBlob *pkb) +{ + struct ldb_context *ldb; + struct package_PrimaryKerberosCtr3 *pkb3 = &pkb->ctr.ctr3; + struct supplementalCredentialsPackage *old_scp = NULL; + struct package_PrimaryKerberosBlob _old_pkb; + struct package_PrimaryKerberosCtr3 *old_pkb3 = NULL; + uint32_t i; + enum ndr_err_code ndr_err; + + ldb = ldb_module_get_ctx(io->ac->module); + + /* + * prepare generation of keys + * + * ENCTYPE_DES_CBC_MD5 + * ENCTYPE_DES_CBC_CRC + */ + pkb->version = 3; + pkb3->salt.string = io->g.salt; + pkb3->num_keys = 2; + pkb3->keys = talloc_array(io->ac, + struct package_PrimaryKerberosKey3, + pkb3->num_keys); + if (!pkb3->keys) { + return ldb_oom(ldb); + } + + pkb3->keys[0].keytype = ENCTYPE_DES_CBC_MD5; + pkb3->keys[0].value = &io->g.des_md5; + pkb3->keys[1].keytype = ENCTYPE_DES_CBC_CRC; + pkb3->keys[1].value = &io->g.des_crc; + + /* initialize the old keys to zero */ + pkb3->num_old_keys = 0; + pkb3->old_keys = NULL; + + /* if there're no old keys, then we're done */ + if (!old_scb) { + return LDB_SUCCESS; + } + + for (i=0; i < old_scb->sub.num_packages; i++) { + if (strcmp("Primary:Kerberos", old_scb->sub.packages[i].name) != 0) { + continue; + } + + if (!old_scb->sub.packages[i].data || !old_scb->sub.packages[i].data[0]) { + continue; + } + + old_scp = &old_scb->sub.packages[i]; + break; + } + /* Primary:Kerberos element of supplementalCredentials */ + if (old_scp) { + DATA_BLOB blob; + + blob = strhex_to_data_blob(io->ac, old_scp->data); + if (!blob.data) { + return ldb_oom(ldb); + } + + /* TODO: use ndr_pull_struct_blob_all(), when the ndr layer handles it correct with relative pointers */ + ndr_err = ndr_pull_struct_blob(&blob, io->ac, &_old_pkb, + (ndr_pull_flags_fn_t)ndr_pull_package_PrimaryKerberosBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS status = ndr_map_error2ntstatus(ndr_err); + ldb_asprintf_errstring(ldb, + "setup_primary_kerberos: " + "failed to pull old package_PrimaryKerberosBlob: %s", + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + + if (_old_pkb.version != 3) { + ldb_asprintf_errstring(ldb, + "setup_primary_kerberos: " + "package_PrimaryKerberosBlob version[%u] expected[3]", + _old_pkb.version); + return LDB_ERR_OPERATIONS_ERROR; + } + + old_pkb3 = &_old_pkb.ctr.ctr3; + } + + /* if we didn't found the old keys we're done */ + if (!old_pkb3) { + return LDB_SUCCESS; + } + + /* fill in the old keys */ + pkb3->num_old_keys = old_pkb3->num_keys; + pkb3->old_keys = old_pkb3->keys; + + return LDB_SUCCESS; +} + +static int setup_primary_kerberos_newer(struct setup_password_fields_io *io, + const struct supplementalCredentialsBlob *old_scb, + struct package_PrimaryKerberosBlob *pkb) +{ + struct ldb_context *ldb; + struct package_PrimaryKerberosCtr4 *pkb4 = &pkb->ctr.ctr4; + struct supplementalCredentialsPackage *old_scp = NULL; + struct package_PrimaryKerberosBlob _old_pkb; + struct package_PrimaryKerberosCtr4 *old_pkb4 = NULL; + uint32_t i; + enum ndr_err_code ndr_err; + + ldb = ldb_module_get_ctx(io->ac->module); + + /* + * prepare generation of keys + * + * ENCTYPE_AES256_CTS_HMAC_SHA1_96 + * ENCTYPE_AES128_CTS_HMAC_SHA1_96 + * ENCTYPE_DES_CBC_MD5 + * ENCTYPE_DES_CBC_CRC + */ + pkb->version = 4; + pkb4->salt.string = io->g.salt; + pkb4->default_iteration_count = 4096; + pkb4->num_keys = 4; + + pkb4->keys = talloc_array(io->ac, + struct package_PrimaryKerberosKey4, + pkb4->num_keys); + if (!pkb4->keys) { + return ldb_oom(ldb); + } + + pkb4->keys[0].iteration_count = 4096; + pkb4->keys[0].keytype = ENCTYPE_AES256_CTS_HMAC_SHA1_96; + pkb4->keys[0].value = &io->g.aes_256; + pkb4->keys[1].iteration_count = 4096; + pkb4->keys[1].keytype = ENCTYPE_AES128_CTS_HMAC_SHA1_96; + pkb4->keys[1].value = &io->g.aes_128; + pkb4->keys[2].iteration_count = 4096; + pkb4->keys[2].keytype = ENCTYPE_DES_CBC_MD5; + pkb4->keys[2].value = &io->g.des_md5; + pkb4->keys[3].iteration_count = 4096; + pkb4->keys[3].keytype = ENCTYPE_DES_CBC_CRC; + pkb4->keys[3].value = &io->g.des_crc; + + /* initialize the old keys to zero */ + pkb4->num_old_keys = 0; + pkb4->old_keys = NULL; + pkb4->num_older_keys = 0; + pkb4->older_keys = NULL; + + /* if there're no old keys, then we're done */ + if (!old_scb) { + return LDB_SUCCESS; + } + + for (i=0; i < old_scb->sub.num_packages; i++) { + if (strcmp("Primary:Kerberos-Newer-Keys", old_scb->sub.packages[i].name) != 0) { + continue; + } + + if (!old_scb->sub.packages[i].data || !old_scb->sub.packages[i].data[0]) { + continue; + } + + old_scp = &old_scb->sub.packages[i]; + break; + } + /* Primary:Kerberos-Newer-Keys element of supplementalCredentials */ + if (old_scp) { + DATA_BLOB blob; + + blob = strhex_to_data_blob(io->ac, old_scp->data); + if (!blob.data) { + return ldb_oom(ldb); + } + + /* TODO: use ndr_pull_struct_blob_all(), when the ndr layer handles it correct with relative pointers */ + ndr_err = ndr_pull_struct_blob(&blob, io->ac, + &_old_pkb, + (ndr_pull_flags_fn_t)ndr_pull_package_PrimaryKerberosBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS status = ndr_map_error2ntstatus(ndr_err); + ldb_asprintf_errstring(ldb, + "setup_primary_kerberos_newer: " + "failed to pull old package_PrimaryKerberosBlob: %s", + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + + if (_old_pkb.version != 4) { + ldb_asprintf_errstring(ldb, + "setup_primary_kerberos_newer: " + "package_PrimaryKerberosBlob version[%u] expected[4]", + _old_pkb.version); + return LDB_ERR_OPERATIONS_ERROR; + } + + old_pkb4 = &_old_pkb.ctr.ctr4; + } + + /* if we didn't found the old keys we're done */ + if (!old_pkb4) { + return LDB_SUCCESS; + } + + /* fill in the old keys */ + pkb4->num_old_keys = old_pkb4->num_keys; + pkb4->old_keys = old_pkb4->keys; + pkb4->num_older_keys = old_pkb4->num_old_keys; + pkb4->older_keys = old_pkb4->old_keys; + + return LDB_SUCCESS; +} + +static int setup_primary_wdigest(struct setup_password_fields_io *io, + const struct supplementalCredentialsBlob *old_scb, + struct package_PrimaryWDigestBlob *pdb) +{ + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); + DATA_BLOB sAMAccountName; + DATA_BLOB sAMAccountName_l; + DATA_BLOB sAMAccountName_u; + const char *user_principal_name = io->u.user_principal_name; + DATA_BLOB userPrincipalName; + DATA_BLOB userPrincipalName_l; + DATA_BLOB userPrincipalName_u; + DATA_BLOB netbios_domain; + DATA_BLOB netbios_domain_l; + DATA_BLOB netbios_domain_u; + DATA_BLOB dns_domain; + DATA_BLOB dns_domain_l; + DATA_BLOB dns_domain_u; + DATA_BLOB digest; + DATA_BLOB delim; + DATA_BLOB backslash; + uint8_t i; + struct { + DATA_BLOB *user; + DATA_BLOB *realm; + DATA_BLOB *nt4dom; + } wdigest[] = { + /* + * See 3.1.1.8.11.3.1 WDIGEST_CREDENTIALS Construction + * https://msdn.microsoft.com/en-us/library/cc245680.aspx + * for what precalculated hashes are supposed to be stored... + * + * I can't reproduce all values which should contain "Digest" as realm, + * am I doing something wrong or is w2k3 just broken...? + * + * W2K3 fills in following for a user: + * + * dn: CN=NewUser,OU=newtop,DC=sub1,DC=w2k3,DC=vmnet1,DC=vm,DC=base + * sAMAccountName: NewUser2Sam + * userPrincipalName: NewUser2Princ@sub1.w2k3.vmnet1.vm.base + * + * 4279815024bda54fc074a5f8bd0a6e6f => NewUser2Sam:SUB1:TestPwd2007 + * b7ec9da91062199aee7d121e6710fe23 => newuser2sam:sub1:TestPwd2007 + * 17d290bc5c9f463fac54c37a8cea134d => NEWUSER2SAM:SUB1:TestPwd2007 + * 4279815024bda54fc074a5f8bd0a6e6f => NewUser2Sam:SUB1:TestPwd2007 + * 5d57e7823938348127322e08cd81bcb5 => NewUser2Sam:sub1:TestPwd2007 + * 07dd701bf8a011ece585de3d47237140 => NEWUSER2SAM:sub1:TestPwd2007 + * e14fb0eb401498d2cb33c9aae1cc7f37 => newuser2sam:SUB1:TestPwd2007 + * 8dadc90250f873d8b883f79d890bef82 => NewUser2Sam:sub1.w2k3.vmnet1.vm.base:TestPwd2007 + * f52da1266a6bdd290ffd48b2c823dda7 => newuser2sam:sub1.w2k3.vmnet1.vm.base:TestPwd2007 + * d2b42f171248cec37a3c5c6b55404062 => NEWUSER2SAM:SUB1.W2K3.VMNET1.VM.BASE:TestPwd2007 + * fff8d790ff6c152aaeb6ebe17b4021de => NewUser2Sam:SUB1.W2K3.VMNET1.VM.BASE:TestPwd2007 + * 8dadc90250f873d8b883f79d890bef82 => NewUser2Sam:sub1.w2k3.vmnet1.vm.base:TestPwd2007 + * 2a7563c3715bc418d626dabef378c008 => NEWUSER2SAM:sub1.w2k3.vmnet1.vm.base:TestPwd2007 + * c8e9557a87cd4200fda0c11d2fa03f96 => newuser2sam:SUB1.W2K3.VMNET1.VM.BASE:TestPwd2007 + * 221c55284451ae9b3aacaa2a3c86f10f => NewUser2Princ@sub1.w2k3.vmnet1.vm.base::TestPwd2007 + * 74e1be668853d4324d38c07e2acfb8ea => (w2k3 has a bug here!) newuser2princ@sub1.w2k3.vmnet1.vm.base::TestPwd2007 + * e1e244ab7f098e3ae1761be7f9229bbb => NEWUSER2PRINC@SUB1.W2K3.VMNET1.VM.BASE::TestPwd2007 + * 86db637df42513039920e605499c3af6 => SUB1\NewUser2Sam::TestPwd2007 + * f5e43474dfaf067fee8197a253debaa2 => sub1\newuser2sam::TestPwd2007 + * 2ecaa8382e2518e4b77a52422b279467 => SUB1\NEWUSER2SAM::TestPwd2007 + * 31dc704d3640335b2123d4ee28aa1f11 => ??? changes with NewUser2Sam => NewUser1Sam + * 36349f5cecd07320fb3bb0e119230c43 => ??? changes with NewUser2Sam => NewUser1Sam + * 12adf019d037fb535c01fd0608e78d9d => ??? changes with NewUser2Sam => NewUser1Sam + * 6feecf8e724906f3ee1105819c5105a1 => ??? changes with NewUser2Princ => NewUser1Princ + * 6c6911f3de6333422640221b9c51ff1f => ??? changes with NewUser2Princ => NewUser1Princ + * 4b279877e742895f9348ac67a8de2f69 => ??? changes with NewUser2Princ => NewUser1Princ + * db0c6bff069513e3ebb9870d29b57490 => ??? changes with NewUser2Sam => NewUser1Sam + * 45072621e56b1c113a4e04a8ff68cd0e => ??? changes with NewUser2Sam => NewUser1Sam + * 11d1220abc44a9c10cf91ef4a9c1de02 => ??? changes with NewUser2Sam => NewUser1Sam + * + * dn: CN=NewUser,OU=newtop,DC=sub1,DC=w2k3,DC=vmnet1,DC=vm,DC=base + * sAMAccountName: NewUser2Sam + * + * 4279815024bda54fc074a5f8bd0a6e6f => NewUser2Sam:SUB1:TestPwd2007 + * b7ec9da91062199aee7d121e6710fe23 => newuser2sam:sub1:TestPwd2007 + * 17d290bc5c9f463fac54c37a8cea134d => NEWUSER2SAM:SUB1:TestPwd2007 + * 4279815024bda54fc074a5f8bd0a6e6f => NewUser2Sam:SUB1:TestPwd2007 + * 5d57e7823938348127322e08cd81bcb5 => NewUser2Sam:sub1:TestPwd2007 + * 07dd701bf8a011ece585de3d47237140 => NEWUSER2SAM:sub1:TestPwd2007 + * e14fb0eb401498d2cb33c9aae1cc7f37 => newuser2sam:SUB1:TestPwd2007 + * 8dadc90250f873d8b883f79d890bef82 => NewUser2Sam:sub1.w2k3.vmnet1.vm.base:TestPwd2007 + * f52da1266a6bdd290ffd48b2c823dda7 => newuser2sam:sub1.w2k3.vmnet1.vm.base:TestPwd2007 + * d2b42f171248cec37a3c5c6b55404062 => NEWUSER2SAM:SUB1.W2K3.VMNET1.VM.BASE:TestPwd2007 + * fff8d790ff6c152aaeb6ebe17b4021de => NewUser2Sam:SUB1.W2K3.VMNET1.VM.BASE:TestPwd2007 + * 8dadc90250f873d8b883f79d890bef82 => NewUser2Sam:sub1.w2k3.vmnet1.vm.base:TestPwd2007 + * 2a7563c3715bc418d626dabef378c008 => NEWUSER2SAM:sub1.w2k3.vmnet1.vm.base:TestPwd2007 + * c8e9557a87cd4200fda0c11d2fa03f96 => newuser2sam:SUB1.W2K3.VMNET1.VM.BASE:TestPwd2007 + * 8a140d30b6f0a5912735dc1e3bc993b4 => NewUser2Sam@sub1.w2k3.vmnet1.vm.base::TestPwd2007 + * 86d95b2faae6cae4ec261e7fbaccf093 => (here w2k3 is correct) newuser2sam@sub1.w2k3.vmnet1.vm.base::TestPwd2007 + * dfeff1493110220efcdfc6362e5f5450 => NEWUSER2SAM@SUB1.W2K3.VMNET1.VM.BASE::TestPwd2007 + * 86db637df42513039920e605499c3af6 => SUB1\NewUser2Sam::TestPwd2007 + * f5e43474dfaf067fee8197a253debaa2 => sub1\newuser2sam::TestPwd2007 + * 2ecaa8382e2518e4b77a52422b279467 => SUB1\NEWUSER2SAM::TestPwd2007 + * 31dc704d3640335b2123d4ee28aa1f11 => ???M1 changes with NewUser2Sam => NewUser1Sam + * 36349f5cecd07320fb3bb0e119230c43 => ???M1.L changes with newuser2sam => newuser1sam + * 12adf019d037fb535c01fd0608e78d9d => ???M1.U changes with NEWUSER2SAM => NEWUSER1SAM + * 569b4533f2d9e580211dd040e5e360a8 => ???M2 changes with NewUser2Princ => NewUser1Princ + * 52528bddf310a587c5d7e6a9ae2cbb20 => ???M2.L changes with newuser2princ => newuser1princ + * 4f629a4f0361289ca4255ab0f658fcd5 => ???M3 changes with NewUser2Princ => NewUser1Princ (doesn't depend on case of userPrincipal ) + * db0c6bff069513e3ebb9870d29b57490 => ???M4 changes with NewUser2Sam => NewUser1Sam + * 45072621e56b1c113a4e04a8ff68cd0e => ???M5 changes with NewUser2Sam => NewUser1Sam (doesn't depend on case of sAMAccountName) + * 11d1220abc44a9c10cf91ef4a9c1de02 => ???M4.U changes with NEWUSER2SAM => NEWUSER1SAM + */ + + /* + * sAMAccountName, netbios_domain + */ + { + .user = &sAMAccountName, + .realm = &netbios_domain, + }, + { + .user = &sAMAccountName_l, + .realm = &netbios_domain_l, + }, + { + .user = &sAMAccountName_u, + .realm = &netbios_domain_u, + }, + { + .user = &sAMAccountName, + .realm = &netbios_domain_u, + }, + { + .user = &sAMAccountName, + .realm = &netbios_domain_l, + }, + { + .user = &sAMAccountName_u, + .realm = &netbios_domain_l, + }, + { + .user = &sAMAccountName_l, + .realm = &netbios_domain_u, + }, + /* + * sAMAccountName, dns_domain + * + * TODO: + * Windows preserves the case of the DNS domain, + * Samba lower cases the domain at provision time + * This means that for mixed case Domains, the WDigest08 hash + * calculated by Samba differs from that calculated by Windows. + * Until we get a real world use case this will remain a known + * bug, as changing the case could have unforeseen impacts. + * + */ + { + .user = &sAMAccountName, + .realm = &dns_domain, + }, + { + .user = &sAMAccountName_l, + .realm = &dns_domain_l, + }, + { + .user = &sAMAccountName_u, + .realm = &dns_domain_u, + }, + { + .user = &sAMAccountName, + .realm = &dns_domain_u, + }, + { + .user = &sAMAccountName, + .realm = &dns_domain_l, + }, + { + .user = &sAMAccountName_u, + .realm = &dns_domain_l, + }, + { + .user = &sAMAccountName_l, + .realm = &dns_domain_u, + }, + /* + * userPrincipalName, no realm + */ + { + .user = &userPrincipalName, + }, + { + /* + * NOTE: w2k3 messes this up, if the user has a real userPrincipalName, + * the fallback to the sAMAccountName based userPrincipalName is correct + */ + .user = &userPrincipalName_l, + }, + { + .user = &userPrincipalName_u, + }, + /* + * nt4dom\sAMAccountName, no realm + */ + { + .user = &sAMAccountName, + .nt4dom = &netbios_domain + }, + { + .user = &sAMAccountName_l, + .nt4dom = &netbios_domain_l + }, + { + .user = &sAMAccountName_u, + .nt4dom = &netbios_domain_u + }, + + /* + * the following ones are guessed depending on the technet2 article + * but not reproducable on a w2k3 server + */ + /* sAMAccountName with "Digest" realm */ + { + .user = &sAMAccountName, + .realm = &digest + }, + { + .user = &sAMAccountName_l, + .realm = &digest + }, + { + .user = &sAMAccountName_u, + .realm = &digest + }, + /* userPrincipalName with "Digest" realm */ + { + .user = &userPrincipalName, + .realm = &digest + }, + { + .user = &userPrincipalName_l, + .realm = &digest + }, + { + .user = &userPrincipalName_u, + .realm = &digest + }, + /* nt4dom\\sAMAccountName with "Digest" realm */ + { + .user = &sAMAccountName, + .nt4dom = &netbios_domain, + .realm = &digest + }, + { + .user = &sAMAccountName_l, + .nt4dom = &netbios_domain_l, + .realm = &digest + }, + { + .user = &sAMAccountName_u, + .nt4dom = &netbios_domain_u, + .realm = &digest + }, + }; + int rc = LDB_ERR_OTHER; + + /* prepare DATA_BLOB's used in the combinations array */ + sAMAccountName = data_blob_string_const(io->u.sAMAccountName); + sAMAccountName_l = data_blob_string_const(strlower_talloc(io->ac, io->u.sAMAccountName)); + if (!sAMAccountName_l.data) { + return ldb_oom(ldb); + } + sAMAccountName_u = data_blob_string_const(strupper_talloc(io->ac, io->u.sAMAccountName)); + if (!sAMAccountName_u.data) { + return ldb_oom(ldb); + } + + /* if the user doesn't have a userPrincipalName, create one (with lower case realm) */ + if (!user_principal_name) { + user_principal_name = talloc_asprintf(io->ac, "%s@%s", + io->u.sAMAccountName, + io->ac->status->domain_data.dns_domain); + if (!user_principal_name) { + return ldb_oom(ldb); + } + } + userPrincipalName = data_blob_string_const(user_principal_name); + userPrincipalName_l = data_blob_string_const(strlower_talloc(io->ac, user_principal_name)); + if (!userPrincipalName_l.data) { + return ldb_oom(ldb); + } + userPrincipalName_u = data_blob_string_const(strupper_talloc(io->ac, user_principal_name)); + if (!userPrincipalName_u.data) { + return ldb_oom(ldb); + } + + netbios_domain = data_blob_string_const(io->ac->status->domain_data.netbios_domain); + netbios_domain_l = data_blob_string_const(strlower_talloc(io->ac, + io->ac->status->domain_data.netbios_domain)); + if (!netbios_domain_l.data) { + return ldb_oom(ldb); + } + netbios_domain_u = data_blob_string_const(strupper_talloc(io->ac, + io->ac->status->domain_data.netbios_domain)); + if (!netbios_domain_u.data) { + return ldb_oom(ldb); + } + + dns_domain = data_blob_string_const(io->ac->status->domain_data.dns_domain); + dns_domain_l = data_blob_string_const(io->ac->status->domain_data.dns_domain); + dns_domain_u = data_blob_string_const(io->ac->status->domain_data.realm); + + digest = data_blob_string_const("Digest"); + + delim = data_blob_string_const(":"); + backslash = data_blob_string_const("\\"); + + pdb->num_hashes = ARRAY_SIZE(wdigest); + pdb->hashes = talloc_array(io->ac, struct package_PrimaryWDigestHash, + pdb->num_hashes); + if (!pdb->hashes) { + return ldb_oom(ldb); + } + + for (i=0; i < ARRAY_SIZE(wdigest); i++) { + gnutls_hash_hd_t hash_hnd = NULL; + + rc = gnutls_hash_init(&hash_hnd, GNUTLS_DIG_MD5); + if (rc < 0) { + rc = ldb_oom(ldb); + goto out; + } + + if (wdigest[i].nt4dom) { + rc = gnutls_hash(hash_hnd, + wdigest[i].nt4dom->data, + wdigest[i].nt4dom->length); + if (rc < 0) { + gnutls_hash_deinit(hash_hnd, NULL); + rc = LDB_ERR_UNWILLING_TO_PERFORM; + goto out; + } + rc = gnutls_hash(hash_hnd, + backslash.data, + backslash.length); + if (rc < 0) { + gnutls_hash_deinit(hash_hnd, NULL); + rc = LDB_ERR_UNWILLING_TO_PERFORM; + goto out; + } + } + rc = gnutls_hash(hash_hnd, + wdigest[i].user->data, + wdigest[i].user->length); + if (rc < 0) { + gnutls_hash_deinit(hash_hnd, NULL); + rc = LDB_ERR_UNWILLING_TO_PERFORM; + goto out; + } + rc = gnutls_hash(hash_hnd, delim.data, delim.length); + if (rc < 0) { + gnutls_hash_deinit(hash_hnd, NULL); + rc = LDB_ERR_UNWILLING_TO_PERFORM; + goto out; + } + if (wdigest[i].realm) { + rc = gnutls_hash(hash_hnd, + wdigest[i].realm->data, + wdigest[i].realm->length); + if (rc < 0) { + gnutls_hash_deinit(hash_hnd, NULL); + rc = LDB_ERR_UNWILLING_TO_PERFORM; + goto out; + } + } + rc = gnutls_hash(hash_hnd, delim.data, delim.length); + if (rc < 0) { + gnutls_hash_deinit(hash_hnd, NULL); + rc = LDB_ERR_UNWILLING_TO_PERFORM; + goto out; + } + rc = gnutls_hash(hash_hnd, + io->n.cleartext_utf8->data, + io->n.cleartext_utf8->length); + if (rc < 0) { + gnutls_hash_deinit(hash_hnd, NULL); + rc = LDB_ERR_UNWILLING_TO_PERFORM; + goto out; + } + + gnutls_hash_deinit(hash_hnd, pdb->hashes[i].hash); + } + + rc = LDB_SUCCESS; +out: + return rc; +} + +#define SHA_SALT_PERMITTED_CHARS "abcdefghijklmnopqrstuvwxyz" \ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ + "0123456789./" +#define SHA_SALT_SIZE 16 +#define SHA_256_SCHEME "CryptSHA256" +#define SHA_512_SCHEME "CryptSHA512" +#define CRYPT "{CRYPT}" +#define SHA_ID_LEN 3 +#define SHA_256_ALGORITHM_ID 5 +#define SHA_512_ALGORITHM_ID 6 +#define ROUNDS_PARAMETER "rounds=" + +/* + * Extract the crypt (3) algorithm number and number of hash rounds from the + * supplied scheme string + */ +static bool parse_scheme(const char *scheme, int *algorithm, int *rounds) { + + const char *rp = NULL; /* Pointer to the 'rounds=' option */ + char digits[21]; /* digits extracted from the rounds option */ + int i = 0; /* loop index variable */ + + if (strncasecmp(SHA_256_SCHEME, scheme, strlen(SHA_256_SCHEME)) == 0) { + *algorithm = SHA_256_ALGORITHM_ID; + } else if (strncasecmp(SHA_512_SCHEME, scheme, strlen(SHA_256_SCHEME)) + == 0) { + *algorithm = SHA_512_ALGORITHM_ID; + } else { + return false; + } + + rp = strcasestr(scheme, ROUNDS_PARAMETER); + if (rp == NULL) { + /* No options specified, use crypt default number of rounds */ + *rounds = 0; + return true; + } + rp += strlen(ROUNDS_PARAMETER); + for (i = 0; isdigit(rp[i]) && i < (sizeof(digits) - 1); i++) { + digits[i] = rp[i]; + } + digits[i] = '\0'; + *rounds = atoi(digits); + return true; +} + +/* + * Calculate the password hash specified by scheme, and return it in + * hash_value + */ +static int setup_primary_userPassword_hash( + TALLOC_CTX *ctx, + struct setup_password_fields_io *io, + const char* scheme, + struct package_PrimaryUserPasswordValue *hash_value) +{ + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); + const char *salt = NULL; /* Randomly generated salt */ + const char *cmd = NULL; /* command passed to crypt */ + const char *hash = NULL; /* password hash generated by crypt */ + int algorithm = 0; /* crypt hash algorithm number */ + int rounds = 0; /* The number of hash rounds */ + DATA_BLOB *hash_blob = NULL; + TALLOC_CTX *frame = talloc_stackframe(); +#if defined(HAVE_CRYPT_R) || defined(HAVE_CRYPT_RN) + struct crypt_data crypt_data = { + .initialized = 0 /* working storage used by crypt */ + }; +#endif + + /* Genrate a random password salt */ + salt = generate_random_str_list(frame, + SHA_SALT_SIZE, + SHA_SALT_PERMITTED_CHARS); + if (salt == NULL) { + TALLOC_FREE(frame); + return ldb_oom(ldb); + } + + /* determine the hashing algoritm and number of rounds*/ + if (!parse_scheme(scheme, &algorithm, &rounds)) { + ldb_asprintf_errstring( + ldb, + "setup_primary_userPassword: Invalid scheme of [%s] " + "specified for 'password hash userPassword schemes' in " + "samba.conf", + scheme); + TALLOC_FREE(frame); + return LDB_ERR_OPERATIONS_ERROR; + } + hash_value->scheme = talloc_strdup(ctx, CRYPT); + hash_value->scheme_len = strlen(CRYPT) + 1; + + /* generate the id/salt parameter used by crypt */ + if (rounds) { + cmd = talloc_asprintf(frame, + "$%d$rounds=%d$%s", + algorithm, + rounds, + salt); + } else { + cmd = talloc_asprintf(frame, "$%d$%s", algorithm, salt); + } + + /* + * Relies on the assertion that cleartext_utf8->data is a zero + * terminated UTF-8 string + */ + + /* + * crypt_r() and crypt() may return a null pointer upon error + * depending on how libcrypt was configured, so we prefer + * crypt_rn() from libcrypt / libxcrypt which always returns + * NULL on error. + * + * POSIX specifies returning a null pointer and setting + * errno. + * + * RHEL 7 (which does not use libcrypt / libxcrypt) returns a + * non-NULL pointer from crypt_r() on success but (always?) + * sets errno during internal processing in the NSS crypto + * subsystem. + * + * By preferring crypt_rn we avoid the 'return non-NULL but + * set-errno' that we otherwise cannot tell apart from the + * RHEL 7 behaviour. + */ + errno = 0; + +#ifdef HAVE_CRYPT_RN + hash = crypt_rn((char *)io->n.cleartext_utf8->data, + cmd, + &crypt_data, + sizeof(crypt_data)); +#elif HAVE_CRYPT_R + hash = crypt_r((char *)io->n.cleartext_utf8->data, cmd, &crypt_data); +#else + /* + * No crypt_r falling back to crypt, which is NOT thread safe + * Thread safety MT-Unsafe race:crypt + */ + hash = crypt((char *)io->n.cleartext_utf8->data, cmd); +#endif + /* + * On error, crypt() and crypt_r() may return a null pointer, + * or a pointer to an invalid hash beginning with a '*'. + */ + if (hash == NULL || hash[0] == '*') { + char buf[1024]; + const char *reason = NULL; + if (errno == ERANGE) { + reason = "Password exceeds maximum length allowed for crypt() hashing"; + } else { + int err = strerror_r(errno, buf, sizeof(buf)); + if (err == 0) { + reason = buf; + } else { + reason = "Unknown error"; + } + } + ldb_asprintf_errstring( + ldb, + "setup_primary_userPassword: generation of a %s " + "password hash failed: (%s)", + scheme, + reason); + TALLOC_FREE(frame); + return LDB_ERR_OPERATIONS_ERROR; + } + + hash_blob = talloc_zero(ctx, DATA_BLOB); + + if (hash_blob == NULL) { + TALLOC_FREE(frame); + return ldb_oom(ldb); + } + + *hash_blob = data_blob_talloc(hash_blob, + (const uint8_t *)hash, + strlen(hash)); + if (hash_blob->data == NULL) { + TALLOC_FREE(frame); + return ldb_oom(ldb); + } + hash_value->value = hash_blob; + TALLOC_FREE(frame); + return LDB_SUCCESS; +} + +/* + * Calculate the desired extra password hashes + */ +static int setup_primary_userPassword( + struct setup_password_fields_io *io, + const struct supplementalCredentialsBlob *old_scb, + struct package_PrimaryUserPasswordBlob *p_userPassword_b) +{ + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); + TALLOC_CTX *frame = talloc_stackframe(); + int i; + int ret; + + /* + * Save the current nt_hash, use this to determine if the password + * has been changed by windows. Which will invalidate the userPassword + * hash. Note once NTLM-Strong-NOWTF becomes available it should be + * used in preference to the NT password hash + */ + if (io->g.nt_hash == NULL) { + /* + * When the NT hash is not available, we use this field to store + * the first 16 bytes of the AES256 key instead. This allows + * 'samba-tool user' to verify that the user's password is in + * sync with the userPassword package. + */ + uint8_t hash_len = MIN(16, io->g.aes_256.length); + + ZERO_STRUCT(p_userPassword_b->current_nt_hash); + memcpy(p_userPassword_b->current_nt_hash.hash, + io->g.aes_256.data, + hash_len); + } else { + p_userPassword_b->current_nt_hash = *io->g.nt_hash; + } + + /* + * Determine the number of hashes + * Note: that currently there is no limit on the number of hashes + * no checking is done on the number of schemes specified + * or for uniqueness. + */ + p_userPassword_b->num_hashes = 0; + for (i = 0; io->ac->userPassword_schemes[i]; i++) { + p_userPassword_b->num_hashes++; + } + + p_userPassword_b->hashes + = talloc_array(io->ac, + struct package_PrimaryUserPasswordValue, + p_userPassword_b->num_hashes); + if (p_userPassword_b->hashes == NULL) { + TALLOC_FREE(frame); + return ldb_oom(ldb); + } + + for (i = 0; io->ac->userPassword_schemes[i]; i++) { + ret = setup_primary_userPassword_hash( + p_userPassword_b->hashes, + io, + io->ac->userPassword_schemes[i], + &p_userPassword_b->hashes[i]); + if (ret != LDB_SUCCESS) { + TALLOC_FREE(frame); + return ret; + } + } + return LDB_SUCCESS; +} + + +static int setup_primary_samba_gpg(struct setup_password_fields_io *io, + struct package_PrimarySambaGPGBlob *pgb) +{ + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); +#ifdef ENABLE_GPGME + gpgme_error_t gret; + gpgme_ctx_t ctx = NULL; + size_t num_keys = str_list_length(io->ac->gpg_key_ids); + gpgme_key_t keys[num_keys+1]; + size_t ki = 0; + size_t kr = 0; + gpgme_data_t plain_data = NULL; + gpgme_data_t crypt_data = NULL; + size_t crypt_length = 0; + char *crypt_mem = NULL; + + gret = gpgme_new(&ctx); + if (gret != GPG_ERR_NO_ERROR) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "%s:%s: gret[%u] %s\n", + __location__, __func__, + gret, gpgme_strerror(gret)); + return ldb_module_operr(io->ac->module); + } + + gpgme_set_armor(ctx, 1); + + gret = gpgme_data_new_from_mem(&plain_data, + (const char *)io->n.cleartext_utf16->data, + io->n.cleartext_utf16->length, + 0 /* no copy */); + if (gret != GPG_ERR_NO_ERROR) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "%s:%s: gret[%u] %s\n", + __location__, __func__, + gret, gpgme_strerror(gret)); + gpgme_release(ctx); + return ldb_module_operr(io->ac->module); + } + gret = gpgme_data_new(&crypt_data); + if (gret != GPG_ERR_NO_ERROR) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "%s:%s: gret[%u] %s\n", + __location__, __func__, + gret, gpgme_strerror(gret)); + gpgme_data_release(plain_data); + gpgme_release(ctx); + return ldb_module_operr(io->ac->module); + } + + for (ki = 0; ki < num_keys; ki++) { + const char *key_id = io->ac->gpg_key_ids[ki]; + size_t len = strlen(key_id); + + keys[ki] = NULL; + + if (len < 16) { + ldb_debug(ldb, LDB_DEBUG_FATAL, + "%s:%s: ki[%zu] key_id[%s] strlen < 16, " + "please specify at least the 64bit key id\n", + __location__, __func__, + ki, key_id); + for (kr = 0; keys[kr] != NULL; kr++) { + gpgme_key_release(keys[kr]); + } + gpgme_data_release(crypt_data); + gpgme_data_release(plain_data); + gpgme_release(ctx); + return ldb_module_operr(io->ac->module); + } + + gret = gpgme_get_key(ctx, key_id, &keys[ki], 0 /* public key */); + if (gret != GPG_ERR_NO_ERROR) { + keys[ki] = NULL; + if (gpg_err_source(gret) == GPG_ERR_SOURCE_GPGME + && gpg_err_code(gret) == GPG_ERR_EOF) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "Invalid " + "'password hash gpg key ids': " + "Public Key ID [%s] " + "not found in keyring\n", + key_id); + + } else { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "%s:%s: ki[%zu] key_id[%s] " + "gret[%u] %s\n", + __location__, __func__, + ki, key_id, + gret, gpgme_strerror(gret)); + } + for (kr = 0; keys[kr] != NULL; kr++) { + gpgme_key_release(keys[kr]); + } + gpgme_data_release(crypt_data); + gpgme_data_release(plain_data); + gpgme_release(ctx); + return ldb_module_operr(io->ac->module); + } + } + keys[ki] = NULL; + + gret = gpgme_op_encrypt(ctx, keys, + GPGME_ENCRYPT_ALWAYS_TRUST, + plain_data, crypt_data); + gpgme_data_release(plain_data); + plain_data = NULL; + for (kr = 0; keys[kr] != NULL; kr++) { + gpgme_key_release(keys[kr]); + keys[kr] = NULL; + } + gpgme_release(ctx); + ctx = NULL; + if (gret != GPG_ERR_NO_ERROR) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "%s:%s: gret[%u] %s\n", + __location__, __func__, + gret, gpgme_strerror(gret)); + gpgme_data_release(crypt_data); + return ldb_module_operr(io->ac->module); + } + + crypt_mem = gpgme_data_release_and_get_mem(crypt_data, &crypt_length); + crypt_data = NULL; + if (crypt_mem == NULL) { + return ldb_module_oom(io->ac->module); + } + + pgb->gpg_blob = data_blob_talloc(io->ac, + (const uint8_t *)crypt_mem, + crypt_length); + gpgme_free(crypt_mem); + crypt_mem = NULL; + crypt_length = 0; + if (pgb->gpg_blob.data == NULL) { + return ldb_module_oom(io->ac->module); + } + + return LDB_SUCCESS; +#else /* ENABLE_GPGME */ + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "You configured 'password hash gpg key ids', " + "but GPGME support is missing. (%s:%d)", + __FILE__, __LINE__); + return LDB_ERR_UNWILLING_TO_PERFORM; +#endif /* else ENABLE_GPGME */ +} + +#define NUM_PACKAGES 6 +static int setup_supplemental_field(struct setup_password_fields_io *io) +{ + struct ldb_context *ldb; + struct supplementalCredentialsBlob scb; + struct supplementalCredentialsBlob *old_scb = NULL; + /* + * Packages + + * ( Kerberos-Newer-Keys, Kerberos, + * WDigest, CLEARTEXT, userPassword, SambaGPG) + */ + uint32_t num_names = 0; + const char *names[1+NUM_PACKAGES]; + uint32_t num_packages = 0; + struct supplementalCredentialsPackage packages[1+NUM_PACKAGES]; + struct supplementalCredentialsPackage *pp = packages; + int ret; + enum ndr_err_code ndr_err; + bool do_newer_keys = false; + bool do_cleartext = false; + bool do_samba_gpg = false; + struct loadparm_context *lp_ctx = NULL; + + ZERO_STRUCT(names); + ZERO_STRUCT(packages); + + ldb = ldb_module_get_ctx(io->ac->module); + lp_ctx = talloc_get_type(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + + if (!io->n.cleartext_utf8) { + /* + * when we don't have a cleartext password + * we can't setup a supplementalCredential value + */ + return LDB_SUCCESS; + } + + /* if there's an old supplementaCredentials blob then use it */ + if (io->o.supplemental) { + if (io->o.scb.sub.signature == SUPPLEMENTAL_CREDENTIALS_SIGNATURE) { + old_scb = &io->o.scb; + } else { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "setup_supplemental_field: " + "supplementalCredentialsBlob " + "signature[0x%04X] expected[0x%04X]", + io->o.scb.sub.signature, + SUPPLEMENTAL_CREDENTIALS_SIGNATURE); + } + } + /* Per MS-SAMR 3.1.1.8.11.6 we create AES keys if our domain functionality level is 2008 or higher */ + + + + /* + * The ordering is this + * + * Primary:Kerberos-Newer-Keys (optional) + * Primary:Kerberos + * Primary:WDigest + * Primary:CLEARTEXT (optional) + * Primary:userPassword + * Primary:SambaGPG (optional) + * + * And the 'Packages' package is insert before the last + * other package. + * + * Note: it's important that Primary:SambaGPG is added as + * the last element. This is the indication that it matches + * the current password. When a password change happens on + * a Windows DC, it will keep the old Primary:SambaGPG value, + * but as the first element. + */ + do_newer_keys = (dsdb_functional_level(ldb) >= DS_DOMAIN_FUNCTION_2008); + if (do_newer_keys) { + struct package_PrimaryKerberosBlob pknb; + DATA_BLOB pknb_blob; + char *pknb_hexstr; + /* + * setup 'Primary:Kerberos-Newer-Keys' element + */ + names[num_names++] = "Kerberos-Newer-Keys"; + + ret = setup_primary_kerberos_newer(io, old_scb, &pknb); + if (ret != LDB_SUCCESS) { + return ret; + } + + ndr_err = ndr_push_struct_blob( + &pknb_blob, io->ac, + &pknb, + (ndr_push_flags_fn_t)ndr_push_package_PrimaryKerberosBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS status = ndr_map_error2ntstatus(ndr_err); + ldb_asprintf_errstring( + ldb, + "setup_supplemental_field: " + "failed to push " + "package_PrimaryKerberosNeverBlob: %s", + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + pknb_hexstr = data_blob_hex_string_upper(io->ac, &pknb_blob); + if (!pknb_hexstr) { + return ldb_oom(ldb); + } + pp->name = "Primary:Kerberos-Newer-Keys"; + pp->reserved = 1; + pp->data = pknb_hexstr; + pp++; + num_packages++; + } + + { + /* + * setup 'Primary:Kerberos' element + */ + /* Primary:Kerberos */ + struct package_PrimaryKerberosBlob pkb; + DATA_BLOB pkb_blob; + char *pkb_hexstr; + + names[num_names++] = "Kerberos"; + + ret = setup_primary_kerberos(io, old_scb, &pkb); + if (ret != LDB_SUCCESS) { + return ret; + } + + ndr_err = ndr_push_struct_blob( + &pkb_blob, io->ac, + &pkb, + (ndr_push_flags_fn_t)ndr_push_package_PrimaryKerberosBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS status = ndr_map_error2ntstatus(ndr_err); + ldb_asprintf_errstring( + ldb, + "setup_supplemental_field: " + "failed to push package_PrimaryKerberosBlob: %s", + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + pkb_hexstr = data_blob_hex_string_upper(io->ac, &pkb_blob); + if (!pkb_hexstr) { + return ldb_oom(ldb); + } + pp->name = "Primary:Kerberos"; + pp->reserved = 1; + pp->data = pkb_hexstr; + pp++; + num_packages++; + } + + if (lpcfg_weak_crypto(lp_ctx) == SAMBA_WEAK_CRYPTO_ALLOWED) { + /* + * setup 'Primary:WDigest' element + */ + struct package_PrimaryWDigestBlob pdb; + DATA_BLOB pdb_blob; + char *pdb_hexstr; + + names[num_names++] = "WDigest"; + + ret = setup_primary_wdigest(io, old_scb, &pdb); + if (ret != LDB_SUCCESS) { + return ret; + } + + ndr_err = ndr_push_struct_blob( + &pdb_blob, io->ac, + &pdb, + (ndr_push_flags_fn_t)ndr_push_package_PrimaryWDigestBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS status = ndr_map_error2ntstatus(ndr_err); + ldb_asprintf_errstring( + ldb, + "setup_supplemental_field: " + "failed to push package_PrimaryWDigestBlob: %s", + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + pdb_hexstr = data_blob_hex_string_upper(io->ac, &pdb_blob); + if (!pdb_hexstr) { + return ldb_oom(ldb); + } + pp->name = "Primary:WDigest"; + pp->reserved = 1; + pp->data = pdb_hexstr; + pp++; + num_packages++; + } + + /* + * setup 'Primary:CLEARTEXT' element + */ + if (io->ac->status->domain_data.store_cleartext && + (io->u.userAccountControl & UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED)) { + do_cleartext = true; + } + if (do_cleartext) { + struct package_PrimaryCLEARTEXTBlob pcb; + DATA_BLOB pcb_blob; + char *pcb_hexstr; + + names[num_names++] = "CLEARTEXT"; + + pcb.cleartext = *io->n.cleartext_utf16; + + ndr_err = ndr_push_struct_blob( + &pcb_blob, io->ac, + &pcb, + (ndr_push_flags_fn_t)ndr_push_package_PrimaryCLEARTEXTBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS status = ndr_map_error2ntstatus(ndr_err); + ldb_asprintf_errstring( + ldb, + "setup_supplemental_field: " + "failed to push package_PrimaryCLEARTEXTBlob: %s", + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + pcb_hexstr = data_blob_hex_string_upper(io->ac, &pcb_blob); + if (!pcb_hexstr) { + return ldb_oom(ldb); + } + pp->name = "Primary:CLEARTEXT"; + pp->reserved = 1; + pp->data = pcb_hexstr; + pp++; + num_packages++; + } + + /* + * Don't generate crypt() or similar password for the krbtgt account. + * It's unnecessary, and the length of the cleartext in UTF-8 form + * exceeds the maximum (CRYPT_MAX_PASSPHRASE_SIZE) allowed by crypt(). + */ + if (io->ac->userPassword_schemes && !io->u.is_krbtgt) { + /* + * setup 'Primary:userPassword' element + */ + struct package_PrimaryUserPasswordBlob + p_userPassword_b; + DATA_BLOB p_userPassword_b_blob; + char *p_userPassword_b_hexstr; + + names[num_names++] = "userPassword"; + + ret = setup_primary_userPassword(io, + old_scb, + &p_userPassword_b); + if (ret != LDB_SUCCESS) { + return ret; + } + + ndr_err = ndr_push_struct_blob( + &p_userPassword_b_blob, + io->ac, + &p_userPassword_b, + (ndr_push_flags_fn_t) + ndr_push_package_PrimaryUserPasswordBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS status = ndr_map_error2ntstatus(ndr_err); + ldb_asprintf_errstring( + ldb, + "setup_supplemental_field: failed to push " + "package_PrimaryUserPasswordBlob: %s", + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + p_userPassword_b_hexstr + = data_blob_hex_string_upper( + io->ac, + &p_userPassword_b_blob); + if (!p_userPassword_b_hexstr) { + return ldb_oom(ldb); + } + pp->name = "Primary:userPassword"; + pp->reserved = 1; + pp->data = p_userPassword_b_hexstr; + pp++; + num_packages++; + } + + /* + * setup 'Primary:SambaGPG' element + */ + if (io->ac->gpg_key_ids != NULL) { + do_samba_gpg = true; + } + if (do_samba_gpg) { + struct package_PrimarySambaGPGBlob pgb; + DATA_BLOB pgb_blob; + char *pgb_hexstr; + + names[num_names++] = "SambaGPG"; + + ret = setup_primary_samba_gpg(io, &pgb); + if (ret != LDB_SUCCESS) { + return ret; + } + + ndr_err = ndr_push_struct_blob(&pgb_blob, io->ac, &pgb, + (ndr_push_flags_fn_t)ndr_push_package_PrimarySambaGPGBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS status = ndr_map_error2ntstatus(ndr_err); + ldb_asprintf_errstring(ldb, + "setup_supplemental_field: failed to " + "push package_PrimarySambaGPGBlob: %s", + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + pgb_hexstr = data_blob_hex_string_upper(io->ac, &pgb_blob); + if (!pgb_hexstr) { + return ldb_oom(ldb); + } + pp->name = "Primary:SambaGPG"; + pp->reserved = 1; + pp->data = pgb_hexstr; + pp++; + num_packages++; + } + + /* + * setup 'Packages' element + */ + { + struct package_PackagesBlob pb; + DATA_BLOB pb_blob; + char *pb_hexstr; + + pb.names = names; + ndr_err = ndr_push_struct_blob( + &pb_blob, io->ac, + &pb, + (ndr_push_flags_fn_t)ndr_push_package_PackagesBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS status = ndr_map_error2ntstatus(ndr_err); + ldb_asprintf_errstring( + ldb, + "setup_supplemental_field: " + "failed to push package_PackagesBlob: %s", + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + pb_hexstr = data_blob_hex_string_upper(io->ac, &pb_blob); + if (!pb_hexstr) { + return ldb_oom(ldb); + } + pp->name = "Packages"; + pp->reserved = 2; + pp->data = pb_hexstr; + num_packages++; + /* + * We don't increment pp so it's pointing to the last package + */ + } + + /* + * setup 'supplementalCredentials' value + */ + { + /* + * The 'Packages' element needs to be the second last element + * in supplementalCredentials + */ + struct supplementalCredentialsPackage temp; + struct supplementalCredentialsPackage *prev; + + prev = pp-1; + temp = *prev; + *prev = *pp; + *pp = temp; + + ZERO_STRUCT(scb); + scb.sub.signature = SUPPLEMENTAL_CREDENTIALS_SIGNATURE; + scb.sub.num_packages = num_packages; + scb.sub.packages = packages; + + ndr_err = ndr_push_struct_blob( + &io->g.supplemental, io->ac, + &scb, + (ndr_push_flags_fn_t)ndr_push_supplementalCredentialsBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS status = ndr_map_error2ntstatus(ndr_err); + ldb_asprintf_errstring( + ldb, + "setup_supplemental_field: " + "failed to push supplementalCredentialsBlob: %s", + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + } + + return LDB_SUCCESS; +} + +static int setup_last_set_field(struct setup_password_fields_io *io) +{ + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); + const struct ldb_message *msg = NULL; + struct timeval tv = { .tv_sec = 0 }; + const struct ldb_val *old_val = NULL; + const struct ldb_val *new_val = NULL; + int ret; + + switch (io->ac->req->operation) { + case LDB_ADD: + msg = io->ac->req->op.add.message; + break; + case LDB_MODIFY: + msg = io->ac->req->op.mod.message; + break; + default: + return LDB_ERR_OPERATIONS_ERROR; + break; + } + + if (io->ac->pwd_last_set_bypass) { + struct ldb_message_element *el = NULL; + size_t i; + size_t count = 0; + /* + * This is a message from pdb_samba_dsdb_replace_by_sam() + * + * We want to ensure there is only one pwdLastSet element, and + * it isn't deleting. + */ + if (msg == NULL) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + for (i = 0; i < msg->num_elements; i++) { + if (ldb_attr_cmp(msg->elements[i].name, + "pwdLastSet") == 0) { + count++; + el = &msg->elements[i]; + } + } + if (count != 1) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + if (LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_DELETE) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + io->g.last_set = samdb_result_nttime(msg, "pwdLastSet", 0); + return LDB_SUCCESS; + } + + ret = msg_find_old_and_new_pwd_val(msg, "pwdLastSet", + io->ac->req->operation, + &new_val, &old_val); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (old_val != NULL && new_val == NULL) { + ldb_set_errstring(ldb, + "'pwdLastSet' deletion is not allowed!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + io->g.last_set = UINT64_MAX; + if (new_val != NULL) { + struct ldb_message *tmp_msg = NULL; + + tmp_msg = ldb_msg_new(io->ac); + if (tmp_msg == NULL) { + return ldb_module_oom(io->ac->module); + } + + if (old_val != NULL) { + NTTIME old_last_set = 0; + + ret = ldb_msg_add_value(tmp_msg, "oldval", + old_val, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + old_last_set = samdb_result_nttime(tmp_msg, + "oldval", + 1); + if (io->u.pwdLastSet != old_last_set) { + return dsdb_module_werror(io->ac->module, + LDB_ERR_NO_SUCH_ATTRIBUTE, + WERR_DS_CANT_REM_MISSING_ATT_VAL, + "setup_last_set_field: old pwdLastSet " + "value not found!"); + } + } + + ret = ldb_msg_add_value(tmp_msg, "newval", + new_val, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + io->g.last_set = samdb_result_nttime(tmp_msg, + "newval", + 1); + } else if (ldb_msg_find_element(msg, "pwdLastSet")) { + ldb_set_errstring(ldb, + "'pwdLastSet' deletion is not allowed!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } else if (io->ac->smartcard_reset) { + /* + * adding UF_SMARTCARD_REQUIRED doesn't update + * pwdLastSet implicitly. + */ + io->ac->update_lastset = false; + } + + /* only 0 or -1 (0xFFFFFFFFFFFFFFFF) are allowed */ + switch (io->g.last_set) { + case 0: + if (!io->ac->pwd_last_set_default) { + break; + } + if (!io->ac->update_password) { + break; + } + FALL_THROUGH; + case UINT64_MAX: + if (!io->ac->update_password && + io->u.pwdLastSet != 0 && + io->u.pwdLastSet != UINT64_MAX) + { + /* + * Just setting pwdLastSet to -1, while not changing + * any password field has no effect if pwdLastSet + * is already non-zero. + */ + io->ac->update_lastset = false; + break; + } + /* -1 means set it as now */ + GetTimeOfDay(&tv); + io->g.last_set = timeval_to_nttime(&tv); + break; + default: + return dsdb_module_werror(io->ac->module, + LDB_ERR_OTHER, + WERR_INVALID_PARAMETER, + "setup_last_set_field: " + "pwdLastSet must be 0 or -1 only!"); + } + + if (io->ac->req->operation == LDB_ADD) { + /* + * We always need to store the value on add + * operations. + */ + return LDB_SUCCESS; + } + + if (io->g.last_set == io->u.pwdLastSet) { + /* + * Just setting pwdLastSet to 0, is no-op if it's already 0. + */ + io->ac->update_lastset = false; + } + + return LDB_SUCCESS; +} + +static int setup_given_passwords(struct setup_password_fields_io *io, + struct setup_password_fields_given *g) +{ + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); + + if (g->cleartext_utf8) { + struct ldb_val *cleartext_utf16_blob; + + cleartext_utf16_blob = talloc(io->ac, struct ldb_val); + if (!cleartext_utf16_blob) { + return ldb_oom(ldb); + } + if (!convert_string_talloc(io->ac, + CH_UTF8, CH_UTF16, + g->cleartext_utf8->data, + g->cleartext_utf8->length, + (void *)&cleartext_utf16_blob->data, + &cleartext_utf16_blob->length)) { + if (g->cleartext_utf8->length != 0) { + talloc_free(cleartext_utf16_blob); + ldb_asprintf_errstring(ldb, + "setup_password_fields: " + "failed to generate UTF16 password from cleartext UTF8 one for user '%s'!", + io->u.sAMAccountName); + return LDB_ERR_CONSTRAINT_VIOLATION; + } else { + /* passwords with length "0" are valid! */ + cleartext_utf16_blob->data = NULL; + cleartext_utf16_blob->length = 0; + } + } + g->cleartext_utf16 = cleartext_utf16_blob; + } else if (g->cleartext_utf16) { + struct ldb_val *cleartext_utf8_blob; + + cleartext_utf8_blob = talloc(io->ac, struct ldb_val); + if (!cleartext_utf8_blob) { + return ldb_oom(ldb); + } + if (!convert_string_talloc(io->ac, + CH_UTF16MUNGED, CH_UTF8, + g->cleartext_utf16->data, + g->cleartext_utf16->length, + (void *)&cleartext_utf8_blob->data, + &cleartext_utf8_blob->length)) { + if (g->cleartext_utf16->length != 0) { + /* We must bail out here, the input wasn't even + * a multiple of 2 bytes */ + talloc_free(cleartext_utf8_blob); + ldb_asprintf_errstring(ldb, + "setup_password_fields: " + "failed to generate UTF8 password from cleartext UTF 16 one for user '%s' - the latter had odd length (length must be a multiple of 2)!", + io->u.sAMAccountName); + return LDB_ERR_CONSTRAINT_VIOLATION; + } else { + /* passwords with length "0" are valid! */ + cleartext_utf8_blob->data = NULL; + cleartext_utf8_blob->length = 0; + } + } + g->cleartext_utf8 = cleartext_utf8_blob; + } + + if (g->cleartext_utf16) { + struct samr_Password *nt_hash; + + nt_hash = talloc(io->ac, struct samr_Password); + if (!nt_hash) { + return ldb_oom(ldb); + } + g->nt_hash = nt_hash; + + /* compute the new nt hash */ + mdfour(nt_hash->hash, + g->cleartext_utf16->data, + g->cleartext_utf16->length); + } + + /* + * We need to build one more hash, so we can compare with what might + * have been stored in the old password (for the LDAP password change) + * + * We don't have any old salts, so we won't catch password reuse if said + * password was used prior to an account rename and another password + * change. + * + * We don't have to store the 'opaque' (string2key iterations) + * as Heimdal doesn't allow that to be changed. + */ + if (g->cleartext_utf8 != NULL) { + int ret = setup_kerberos_key_hash(io, g); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + return LDB_SUCCESS; +} + +static int setup_password_fields(struct setup_password_fields_io *io) +{ + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); + int ret; + + ret = setup_last_set_field(io); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (!io->ac->update_password) { + return LDB_SUCCESS; + } + + if (io->u.is_krbtgt) { + size_t min = 196; + size_t max = 255; + size_t diff = max - min; + size_t len = max; + struct ldb_val *krbtgt_utf16 = NULL; + + if (!io->ac->pwd_reset) { + return dsdb_module_werror(io->ac->module, + LDB_ERR_ATTRIBUTE_OR_VALUE_EXISTS, + WERR_DS_ATT_ALREADY_EXISTS, + "Password change on krbtgt not permitted!"); + } + + if (io->n.cleartext_utf16 == NULL) { + return dsdb_module_werror(io->ac->module, + LDB_ERR_UNWILLING_TO_PERFORM, + WERR_DS_INVALID_ATTRIBUTE_SYNTAX, + "Password reset on krbtgt requires UTF16!"); + } + + /* + * Instead of taking the callers value, + * we just generate a new random value here. + * + * Include null termination in the array. + */ + if (diff > 0) { + size_t tmp; + + generate_random_buffer((uint8_t *)&tmp, sizeof(tmp)); + + tmp %= diff; + + len = min + tmp; + } + + krbtgt_utf16 = talloc_zero(io->ac, struct ldb_val); + if (krbtgt_utf16 == NULL) { + return ldb_oom(ldb); + } + + *krbtgt_utf16 = data_blob_talloc_zero(krbtgt_utf16, + (len+1)*2); + if (krbtgt_utf16->data == NULL) { + return ldb_oom(ldb); + } + krbtgt_utf16->length = len * 2; + generate_secret_buffer(krbtgt_utf16->data, + krbtgt_utf16->length); + io->n.cleartext_utf16 = krbtgt_utf16; + } + + /* transform the old password (for password changes) */ + ret = setup_given_passwords(io, &io->og); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* transform the new password */ + ret = setup_given_passwords(io, &io->n); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (io->n.cleartext_utf8) { + ret = setup_kerberos_keys(io); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* + * This relies on setup_kerberos_keys to make a NT-hash-like + * value for password history purposes + */ + + ret = setup_nt_fields(io); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = setup_supplemental_field(io); + if (ret != LDB_SUCCESS) { + return ret; + } + + return LDB_SUCCESS; +} + +static int setup_smartcard_reset(struct setup_password_fields_io *io) +{ + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); + struct supplementalCredentialsBlob scb = { .__ndr_size = 0 }; + enum ndr_err_code ndr_err; + + if (!io->ac->smartcard_reset) { + return LDB_SUCCESS; + } + + io->g.nt_hash = talloc(io->ac, struct samr_Password); + if (io->g.nt_hash == NULL) { + return ldb_module_oom(io->ac->module); + } + generate_secret_buffer(io->g.nt_hash->hash, + sizeof(io->g.nt_hash->hash)); + io->g.nt_history_len = 0; + + /* + * We take the "old" value and store it + * with num_packages = 0. + * + * On "add" we have scb.sub.signature == 0, which + * results in: + * + * [0000] 00 00 00 00 00 00 00 00 00 00 00 00 00 + * + * On modify it's likely to be scb.sub.signature == + * SUPPLEMENTAL_CREDENTIALS_SIGNATURE (0x0050), which results in + * something like: + * + * [0000] 00 00 00 00 62 00 00 00 00 00 00 00 20 00 20 00 + * [0010] 20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 + * [0020] 20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 + * [0030] 20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 + * [0040] 20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 + * [0050] 20 00 20 00 20 00 20 00 20 00 20 00 20 00 20 00 + * [0060] 20 00 20 00 20 00 20 00 20 00 20 00 50 00 00 + * + * See https://bugzilla.samba.org/show_bug.cgi?id=11441 + * and ndr_{push,pull}_supplementalCredentialsSubBlob(). + */ + scb = io->o.scb; + scb.sub.num_packages = 0; + + /* + * setup 'supplementalCredentials' value without packages + */ + ndr_err = ndr_push_struct_blob(&io->g.supplemental, io->ac, + &scb, + (ndr_push_flags_fn_t)ndr_push_supplementalCredentialsBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS status = ndr_map_error2ntstatus(ndr_err); + ldb_asprintf_errstring(ldb, + "setup_smartcard_reset: " + "failed to push supplementalCredentialsBlob: %s", + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + + io->ac->update_password = true; + return LDB_SUCCESS; +} + +static int make_error_and_update_badPwdCount(struct setup_password_fields_io *io, WERROR *werror) +{ + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); + struct ldb_message *mod_msg = NULL; + struct ldb_message *pso_msg = NULL; + struct ldb_message *current = NULL; + NTSTATUS status = NT_STATUS_OK; + int ret; /* The errors we will actually return */ + int dbg_ret; /* The errors we can only complain about in logs */ + + /* + * OK, horrible semantics ahead. + * + * - We need to abort any existing transaction + * - create a transaction arround the badPwdCount update + * - re-open the transaction so the upper layer + * doesn't know what happened. + * + * This is needed because returning an error to the upper + * layer will cancel the transaction and undo the badPwdCount + * update. + */ + + /* + * Checking errors here is a bit pointless. + * What can we do if we can't end the transaction? + */ + dbg_ret = ldb_next_del_trans(io->ac->module); + if (dbg_ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_FATAL, + "Failed to abort transaction prior to update of badPwdCount of %s: %s", + ldb_dn_get_linearized(io->ac->search_res->message->dn), + ldb_errstring(ldb)); + /* + * just return the original error + */ + goto done; + } + + /* Likewise, what should we do if we can't open a new transaction? */ + dbg_ret = ldb_next_start_trans(io->ac->module); + if (dbg_ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "Failed to open transaction to update badPwdCount of %s: %s", + ldb_dn_get_linearized(io->ac->search_res->message->dn), + ldb_errstring(ldb)); + /* + * just return the original error + */ + goto done; + } + + /* + * Re-read the account details, using the GUID in case the DN + * is being changed. + */ + status = authsam_reread_user_logon_data( + ldb, io->ac, + io->ac->search_res->message, + ¤t); + if (!NT_STATUS_IS_OK(status)) { + /* The re-read can return account locked out, as well + * as an internal error + */ + goto end_transaction; + } + + /* PSO search result is optional (NULL if no PSO applies) */ + if (io->ac->pso_res != NULL) { + pso_msg = io->ac->pso_res->message; + } + + status = dsdb_update_bad_pwd_count(io->ac, ldb, + current, + io->ac->dom_res->message, + pso_msg, + &mod_msg); + if (!NT_STATUS_IS_OK(status)) { + goto end_transaction; + } + + if (mod_msg == NULL) { + goto end_transaction; + } + + dbg_ret = dsdb_module_modify(io->ac->module, mod_msg, + DSDB_FLAG_NEXT_MODULE, + io->ac->req); + if (dbg_ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "Failed to update badPwdCount of %s: %s", + ldb_dn_get_linearized(io->ac->search_res->message->dn), + ldb_errstring(ldb)); + /* + * We can only ignore this... + */ + } + +end_transaction: + dbg_ret = ldb_next_end_trans(io->ac->module); + if (dbg_ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "Failed to close transaction to update badPwdCount of %s: %s", + ldb_dn_get_linearized(io->ac->search_res->message->dn), + ldb_errstring(ldb)); + /* + * We can only ignore this... + */ + } + + dbg_ret = ldb_next_start_trans(io->ac->module); + if (dbg_ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "Failed to open transaction after update of badPwdCount of %s: %s", + ldb_dn_get_linearized(io->ac->search_res->message->dn), + ldb_errstring(ldb)); + /* + * We can only ignore this... + */ + } + +done: + ret = LDB_ERR_CONSTRAINT_VIOLATION; + if (NT_STATUS_EQUAL(status, NT_STATUS_ACCOUNT_LOCKED_OUT)) { + *werror = WERR_ACCOUNT_LOCKED_OUT; + } else { + *werror = WERR_INVALID_PASSWORD; + } + ldb_asprintf_errstring(ldb, + "%08X: %s - check_password_restrictions: " + "The old password specified doesn't match!", + W_ERROR_V(*werror), + ldb_strerror(ret)); + return ret; +} + +static int check_password_restrictions(struct setup_password_fields_io *io, WERROR *werror) +{ + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); + int ret; + uint32_t i; + struct loadparm_context *lp_ctx = + talloc_get_type(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + + *werror = WERR_INVALID_PARAMETER; + + if (!io->ac->update_password) { + return LDB_SUCCESS; + } + + /* + * First check the old password is correct, for password + * changes when this hasn't already been checked by a + * trustwrothy layer above + */ + if (!io->ac->pwd_reset && !(io->ac->change + && io->ac->change->old_password_checked == DSDB_PASSWORD_CHECKED_AND_CORRECT)) { + bool hash_checked = false; + /* + * we need the old nt hash given by the client (this + * is for the plaintext over LDAP password change, + * Kpasswd and SAMR supply the control) + */ + if (io->og.nt_hash == NULL && io->og.aes_256.length == 0) { + ldb_asprintf_errstring(ldb, + "check_password_restrictions: " + "You need to provide the old password in order " + "to change it!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* + * First compare the ENCTYPE_AES256_CTS_HMAC_SHA1_96 password and see if we have a match + */ + + if (io->og.aes_256.length > 0 && io->o.aes_256.length) { + hash_checked = data_blob_equal_const_time(&io->og.aes_256, &io->o.aes_256); + } + + /* The password modify through the NT hash is encouraged and + has no problems at all */ + if (!hash_checked && io->og.nt_hash && io->o.nt_hash) { + hash_checked = mem_equal_const_time(io->og.nt_hash->hash, io->o.nt_hash->hash, 16); + } + + if (!hash_checked) { + return make_error_and_update_badPwdCount(io, werror); + } + } + + if (io->u.restrictions == 0) { + /* FIXME: Is this right? */ + return LDB_SUCCESS; + } + + /* Password minimum age: yes, this is a minus. The ages are in negative 100nsec units! */ + if ((io->u.pwdLastSet - io->ac->status->domain_data.minPwdAge > io->g.last_set) && + !io->ac->pwd_reset) + { + ret = LDB_ERR_CONSTRAINT_VIOLATION; + *werror = WERR_PASSWORD_RESTRICTION; + ldb_asprintf_errstring(ldb, + "%08X: %s - check_password_restrictions: " + "password is too young to change!", + W_ERROR_V(*werror), + ldb_strerror(ret)); + return ret; + } + + /* + * Fundamental password checks done by the call + * "samdb_check_password". + * It is also in use by "dcesrv_samr_ValidatePassword". + */ + if (io->n.cleartext_utf8 != NULL) { + enum samr_ValidationStatus vstat; + vstat = samdb_check_password(io->ac, lp_ctx, + io->u.sAMAccountName, + io->u.user_principal_name, + io->u.displayName, + io->n.cleartext_utf8, + io->ac->status->domain_data.pwdProperties, + io->ac->status->domain_data.minPwdLength); + switch (vstat) { + case SAMR_VALIDATION_STATUS_SUCCESS: + /* perfect -> proceed! */ + break; + + case SAMR_VALIDATION_STATUS_PWD_TOO_SHORT: + ret = LDB_ERR_CONSTRAINT_VIOLATION; + *werror = WERR_PASSWORD_RESTRICTION; + ldb_asprintf_errstring(ldb, + "%08X: %s - check_password_restrictions: " + "the password is too short. It should be equal or longer than %u characters!", + W_ERROR_V(*werror), + ldb_strerror(ret), + io->ac->status->domain_data.minPwdLength); + io->ac->status->reject_reason = SAM_PWD_CHANGE_PASSWORD_TOO_SHORT; + return ret; + + case SAMR_VALIDATION_STATUS_NOT_COMPLEX_ENOUGH: + ret = LDB_ERR_CONSTRAINT_VIOLATION; + *werror = WERR_PASSWORD_RESTRICTION; + ldb_asprintf_errstring(ldb, + "%08X: %s - check_password_restrictions: " + "the password does not meet the complexity criteria!", + W_ERROR_V(*werror), + ldb_strerror(ret)); + io->ac->status->reject_reason = SAM_PWD_CHANGE_NOT_COMPLEX; + return ret; + + default: + ret = LDB_ERR_CONSTRAINT_VIOLATION; + *werror = WERR_PASSWORD_RESTRICTION; + ldb_asprintf_errstring(ldb, + "%08X: %s - check_password_restrictions: " + "the password doesn't fit due to a miscellaneous restriction!", + W_ERROR_V(*werror), + ldb_strerror(ret)); + return ret; + } + } + + if (io->ac->pwd_reset) { + *werror = WERR_OK; + return LDB_SUCCESS; + } + + /* + * This check works by using the current Kerberos password to + * make up a password history. We already did the salted hash + * creation to pass the password change check. + * + * We check the pwdHistoryLength to ensure we honour the + * policy on if the history should be checked + */ + if (io->ac->status->domain_data.pwdHistoryLength > 0 + && io->g.aes_256.length && io->o.aes_256.length) + { + bool equal = data_blob_equal_const_time(&io->g.aes_256, + &io->o.aes_256); + if (equal) { + ret = LDB_ERR_CONSTRAINT_VIOLATION; + *werror = WERR_PASSWORD_RESTRICTION; + ldb_asprintf_errstring(ldb, + "%08X: %s - check_password_restrictions: " + "the password was already used (previous password)!", + W_ERROR_V(*werror), + ldb_strerror(ret)); + io->ac->status->reject_reason = SAM_PWD_CHANGE_PWD_IN_HISTORY; + return ret; + } + } + + if (io->n.nt_hash) { + /* + * checks the NT hash password history, against the + * generated NT hash + */ + for (i = 0; i < io->o.nt_history_len; i++) { + bool pw_cmp = mem_equal_const_time(io->n.nt_hash, io->o.nt_history[i].hash, 16); + if (pw_cmp) { + ret = LDB_ERR_CONSTRAINT_VIOLATION; + *werror = WERR_PASSWORD_RESTRICTION; + ldb_asprintf_errstring(ldb, + "%08X: %s - check_password_restrictions: " + "the password was already used (in history)!", + W_ERROR_V(*werror), + ldb_strerror(ret)); + io->ac->status->reject_reason = SAM_PWD_CHANGE_PWD_IN_HISTORY; + return ret; + } + } + } + + /* + * This check works by using the old Kerberos passwords + * (old and older) to make up a password history. + * + * We check the pwdHistoryLength to ensure we honour the + * policy on if the history should be checked + */ + for (i = 1; + i <= io->o.kvno && i < MIN(3, io->ac->status->domain_data.pwdHistoryLength); + i++) + { + krb5_error_code krb5_ret; + const uint32_t request_kvno = io->o.kvno - i; + DATA_BLOB db_key_blob; + bool pw_equal; + + if (io->n.cleartext_utf8 == NULL) { + /* + * No point checking history if we don't have + * a cleartext password. + */ + break; + } + + if (io->ac->search_res == NULL) { + /* + * This is an ADD, no existing history to check + */ + break; + } + + /* + * If this account requires a smartcard for login, we don't + * attempt a comparison with the old password. + */ + if (io->u.userAccountControl & UF_SMARTCARD_REQUIRED) { + break; + } + + /* + * Extract the old ENCTYPE_AES256_CTS_HMAC_SHA1_96 value from + * the supplementalCredentials. + */ + krb5_ret = dsdb_extract_aes_256_key(io->smb_krb5_context->krb5_context, + io->ac, + io->ac->search_res->message, + io->u.userAccountControl, + &request_kvno, /* kvno */ + NULL, /* kvno_out */ + &db_key_blob, + NULL); /* salt */ + if (krb5_ret == ENOENT) { + /* + * If there is no old AES hash (perhaps an imported DB with + * just unicodePwd) then we just wont have an old + * password to compare to if there is no NT hash + */ + break; + } else if (krb5_ret) { + ldb_asprintf_errstring(ldb, + "check_password_restrictions: " + "extraction of old[%u - %d = %d] aes256-cts-hmac-sha1-96 key failed: %s", + io->o.kvno, i, io->o.kvno - i, + smb_get_krb5_error_message(io->smb_krb5_context->krb5_context, + krb5_ret, io->ac)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* This is the actual history check */ + pw_equal = data_blob_equal_const_time(&io->n.aes_256, + &db_key_blob); + if (pw_equal) { + ret = LDB_ERR_CONSTRAINT_VIOLATION; + *werror = WERR_PASSWORD_RESTRICTION; + ldb_asprintf_errstring(ldb, + "%08X: %s - check_password_restrictions: " + "the password was already used (in history)!", + W_ERROR_V(*werror), + ldb_strerror(ret)); + io->ac->status->reject_reason = SAM_PWD_CHANGE_PWD_IN_HISTORY; + return ret; + } + } + + /* are all password changes disallowed? */ + if (io->ac->status->domain_data.pwdProperties & DOMAIN_REFUSE_PASSWORD_CHANGE) { + ret = LDB_ERR_CONSTRAINT_VIOLATION; + *werror = WERR_PASSWORD_RESTRICTION; + ldb_asprintf_errstring(ldb, + "%08X: %s - check_password_restrictions: " + "password changes disabled!", + W_ERROR_V(*werror), + ldb_strerror(ret)); + return ret; + } + + /* can this user change the password? */ + if (io->u.userAccountControl & UF_PASSWD_CANT_CHANGE) { + ret = LDB_ERR_CONSTRAINT_VIOLATION; + *werror = WERR_PASSWORD_RESTRICTION; + ldb_asprintf_errstring(ldb, + "%08X: %s - check_password_restrictions: " + "password can't be changed on this account!", + W_ERROR_V(*werror), + ldb_strerror(ret)); + return ret; + } + + return LDB_SUCCESS; +} + +static int check_password_restrictions_and_log(struct setup_password_fields_io *io) +{ + WERROR werror; + int ret = check_password_restrictions(io, &werror); + struct ph_context *ac = io->ac; + /* + * Password resets are not authentication events, and if the + * upper layer checked the password and supplied the hash + * values as proof, then this is also not an authentication + * even at this layer (already logged). This is to log LDAP + * password changes. + */ + + /* Do not record a failure in the auth log below in the success case */ + if (ret == LDB_SUCCESS) { + werror = WERR_OK; + } + + if (ac->pwd_reset == false && ac->change == NULL) { + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + struct imessaging_context *msg_ctx; + struct loadparm_context *lp_ctx + = talloc_get_type_abort(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + NTSTATUS status = werror_to_ntstatus(werror); + const char *domain_name = lpcfg_sam_name(lp_ctx); + void *opaque_remote_address = NULL; + /* + * Forcing this via the NTLM auth structure is not ideal, but + * it is the most practical option right now, and ensures the + * logs are consistent, even if some elements are always NULL. + */ + struct auth_usersupplied_info ui = { + .was_mapped = true, + .client = { + .account_name = io->u.sAMAccountName, + .domain_name = domain_name, + }, + .mapped = { + .account_name = io->u.sAMAccountName, + .domain_name = domain_name, + }, + .service_description = "LDAP Password Change", + .auth_description = "LDAP Modify", + .password_type = "plaintext" + }; + + opaque_remote_address = ldb_get_opaque(ldb, + "remoteAddress"); + if (opaque_remote_address == NULL) { + ldb_asprintf_errstring(ldb, + "Failed to obtain remote address for " + "the LDAP client while changing the " + "password"); + return LDB_ERR_OPERATIONS_ERROR; + } + ui.remote_host = talloc_get_type(opaque_remote_address, + struct tsocket_address); + + msg_ctx = imessaging_client_init(ac, lp_ctx, + ldb_get_event_context(ldb)); + if (!msg_ctx) { + ldb_asprintf_errstring(ldb, + "Failed to generate client messaging context in %s", + lpcfg_imessaging_path(ac, lp_ctx)); + return LDB_ERR_OPERATIONS_ERROR; + } + log_authentication_event(msg_ctx, + lp_ctx, + NULL, + &ui, + status, + domain_name, + io->u.sAMAccountName, + io->u.account_sid); + + } + return ret; +} + +static int update_final_msg(struct setup_password_fields_io *io) +{ + struct ldb_context *ldb = ldb_module_get_ctx(io->ac->module); + int ret; + int el_flags = 0; + bool update_password = io->ac->update_password; + bool update_scb = io->ac->update_password; + + /* + * If we add a user without initial password, + * we need to add replication meta data for + * following attributes: + * - unicodePwd + * - dBCSPwd + * - ntPwdHistory + * - lmPwdHistory + * + * If we add a user with initial password or a + * password is changed of an existing user, + * we need to replace the following attributes + * with a forced meta data update, e.g. also + * when updating an empty attribute with an empty value: + * - unicodePwd + * - dBCSPwd + * - ntPwdHistory + * - lmPwdHistory + * - supplementalCredentials + */ + + switch (io->ac->req->operation) { + case LDB_ADD: + update_password = true; + el_flags |= DSDB_FLAG_INTERNAL_FORCE_META_DATA; + break; + case LDB_MODIFY: + el_flags |= LDB_FLAG_MOD_REPLACE; + el_flags |= DSDB_FLAG_INTERNAL_FORCE_META_DATA; + break; + default: + return ldb_module_operr(io->ac->module); + } + + if (update_password) { + ret = ldb_msg_add_empty(io->ac->update_msg, + "unicodePwd", + el_flags, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + * This wipes any old LM password after any password + * update operation. + * + * This is the same as the previous default behaviour + * of 'lanman auth = no' + */ + ret = ldb_msg_add_empty(io->ac->update_msg, + "dBCSPwd", + el_flags, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + ret = ldb_msg_add_empty(io->ac->update_msg, + "ntPwdHistory", + el_flags, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + /* + * This wipes any LM password history after any password + * update operation. + * + * This is the same as the previous default behaviour + * of 'lanman auth = no' + */ + ret = ldb_msg_add_empty(io->ac->update_msg, + "lmPwdHistory", + el_flags, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + } + if (update_scb) { + ret = ldb_msg_add_empty(io->ac->update_msg, + "supplementalCredentials", + el_flags, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + } + if (io->ac->update_lastset) { + ret = ldb_msg_add_empty(io->ac->update_msg, + "pwdLastSet", + el_flags, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + if (io->g.nt_hash != NULL) { + ret = samdb_msg_add_hash(ldb, io->ac, + io->ac->update_msg, + "unicodePwd", + io->g.nt_hash); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + if (io->g.nt_history_len > 0) { + ret = samdb_msg_add_hashes(ldb, io->ac, + io->ac->update_msg, + "ntPwdHistory", + io->g.nt_history, + io->g.nt_history_len); + if (ret != LDB_SUCCESS) { + return ret; + } + } + if (io->g.supplemental.length > 0) { + ret = ldb_msg_add_value(io->ac->update_msg, + "supplementalCredentials", + &io->g.supplemental, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + } + if (io->ac->update_lastset) { + ret = samdb_msg_add_uint64(ldb, io->ac, + io->ac->update_msg, + "pwdLastSet", + io->g.last_set); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + return LDB_SUCCESS; +} + +/* + * This is intended for use by the "password_hash" module since there + * password changes can be specified through one message element with the + * new password (to set) and another one with the old password (to unset). + * + * The first which sets a password (new value) can have flags + * (LDB_FLAG_MOD_ADD, LDB_FLAG_MOD_REPLACE) but also none (on "add" operations + * for entries). The latter (old value) has always specified + * LDB_FLAG_MOD_DELETE. + * + * Returns LDB_ERR_CONSTRAINT_VIOLATION and LDB_ERR_UNWILLING_TO_PERFORM if + * matching message elements are malformed in respect to the set/change rules. + * Otherwise it returns LDB_SUCCESS. + */ +static int msg_find_old_and_new_pwd_val(const struct ldb_message *msg, + const char *name, + enum ldb_request_type operation, + const struct ldb_val **new_val, + const struct ldb_val **old_val) +{ + unsigned int i; + + *new_val = NULL; + *old_val = NULL; + + if (msg == NULL) { + return LDB_SUCCESS; + } + + for (i = 0; i < msg->num_elements; i++) { + if (ldb_attr_cmp(msg->elements[i].name, name) != 0) { + continue; + } + + if ((operation == LDB_MODIFY) && + (LDB_FLAG_MOD_TYPE(msg->elements[i].flags) == LDB_FLAG_MOD_DELETE)) { + /* 0 values are allowed */ + if (msg->elements[i].num_values == 1) { + *old_val = &msg->elements[i].values[0]; + } else if (msg->elements[i].num_values > 1) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } else if ((operation == LDB_MODIFY) && + (LDB_FLAG_MOD_TYPE(msg->elements[i].flags) == LDB_FLAG_MOD_REPLACE)) { + if (msg->elements[i].num_values > 0) { + *new_val = &msg->elements[i].values[msg->elements[i].num_values - 1]; + } else { + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } else { + /* Add operations and LDB_FLAG_MOD_ADD */ + if (msg->elements[i].num_values > 0) { + *new_val = &msg->elements[i].values[msg->elements[i].num_values - 1]; + } else { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } + } + + return LDB_SUCCESS; +} + +static int setup_io(struct ph_context *ac, + const struct ldb_message *client_msg, + const struct ldb_message *existing_msg, + struct setup_password_fields_io *io) +{ + const struct ldb_val *quoted_utf16, *old_quoted_utf16, *lm_hash, *old_lm_hash; + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + struct loadparm_context *lp_ctx = talloc_get_type( + ldb_get_opaque(ldb, "loadparm"), struct loadparm_context); + enum store_nt_hash store_hash_setting = + lpcfg_nt_hash_store(lp_ctx); + int ret; + const struct ldb_message *info_msg = NULL; + struct dom_sid *account_sid = NULL; + int rodc_krbtgt = 0; + + ZERO_STRUCTP(io); + + /* Some operations below require kerberos contexts */ + + if (existing_msg != NULL) { + /* + * This is a modify operation + */ + info_msg = existing_msg; + } else { + /* + * This is an add operation + */ + info_msg = client_msg; + } + + ret = smb_krb5_init_context(ac, + (struct loadparm_context *)ldb_get_opaque(ldb, "loadparm"), + &io->smb_krb5_context); + + if (ret != 0) { + /* + * In the special case of mit krb5.conf vs heimdal, the includedir + * statement causes ret == 22 (KRB5_CONFIG_BADFORMAT) to be returned. + * We look for this case so that we can give a more instructional + * message to the administrator. + */ + if (ret == KRB5_CONFIG_BADFORMAT || ret == EINVAL) { + ldb_asprintf_errstring(ldb, "Failed to setup krb5_context: %s - " + "This could be due to an invalid krb5 configuration. " + "Please check your system's krb5 configuration is correct.", + error_message(ret)); + } else { + ldb_asprintf_errstring(ldb, "Failed to setup krb5_context: %s", + error_message(ret)); + } + return LDB_ERR_OPERATIONS_ERROR; + } + + io->ac = ac; + + io->u.userAccountControl = ldb_msg_find_attr_as_uint(info_msg, + "userAccountControl", 0); + if (info_msg == existing_msg) { + /* + * We only take pwdLastSet from the existing object + * otherwise we leave it as 0. + * + * If no attribute is available, e.g. on deleted objects + * we remember that as UINT64_MAX. + */ + io->u.pwdLastSet = samdb_result_nttime(info_msg, "pwdLastSet", + UINT64_MAX); + } + io->u.sAMAccountName = ldb_msg_find_attr_as_string(info_msg, + "sAMAccountName", NULL); + io->u.user_principal_name = ldb_msg_find_attr_as_string(info_msg, + "userPrincipalName", NULL); + io->u.displayName = ldb_msg_find_attr_as_string(info_msg, + "displayName", NULL); + + /* Ensure it has an objectSID too */ + io->u.account_sid = samdb_result_dom_sid(ac, info_msg, "objectSid"); + if (io->u.account_sid != NULL) { + NTSTATUS status; + uint32_t rid = 0; + + status = dom_sid_split_rid(account_sid, io->u.account_sid, NULL, &rid); + if (NT_STATUS_IS_OK(status)) { + if (rid == DOMAIN_RID_KRBTGT) { + io->u.is_krbtgt = true; + } + } + } + + rodc_krbtgt = ldb_msg_find_attr_as_int(info_msg, + "msDS-SecondaryKrbTgtNumber", 0); + if (rodc_krbtgt != 0) { + io->u.is_krbtgt = true; + } + + if (io->u.sAMAccountName == NULL) { + ldb_asprintf_errstring(ldb, + "setup_io: sAMAccountName attribute is missing on %s for attempted password set/change", + ldb_dn_get_linearized(info_msg->dn)); + + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + if (io->u.userAccountControl & UF_INTERDOMAIN_TRUST_ACCOUNT) { + struct ldb_control *permit_trust = ldb_request_get_control(ac->req, + DSDB_CONTROL_PERMIT_INTERDOMAIN_TRUST_UAC_OID); + + if (permit_trust == NULL) { + ret = LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + ldb_asprintf_errstring(ldb, + "%08X: %s - setup_io: changing the interdomain trust password " + "on %s not allowed via LDAP. Use LSA or NETLOGON", + W_ERROR_V(WERR_ACCESS_DENIED), + ldb_strerror(ret), + ldb_dn_get_linearized(info_msg->dn)); + return ret; + } + } + + /* Only non-trust accounts have restrictions (possibly this test is the + * wrong way around, but we like to be restrictive if possible */ + io->u.restrictions = !(io->u.userAccountControl & UF_TRUST_ACCOUNT_MASK); + + if (io->u.is_krbtgt) { + io->u.restrictions = 0; + io->ac->status->domain_data.pwdHistoryLength = + MAX(io->ac->status->domain_data.pwdHistoryLength, 3); + } + + /* + * Machine accounts need the NT hash to operate the NETLOGON + * ServerAuthenticate{,2,3} logic + */ + if (!(io->u.userAccountControl & UF_NORMAL_ACCOUNT)) { + store_hash_setting = NT_HASH_STORE_ALWAYS; + } + + switch (store_hash_setting) { + case NT_HASH_STORE_ALWAYS: + io->u.store_nt_hash = true; + break; + case NT_HASH_STORE_NEVER: + io->u.store_nt_hash = false; + break; + case NT_HASH_STORE_AUTO: + if (lpcfg_ntlm_auth(lp_ctx) == NTLM_AUTH_DISABLED) { + io->u.store_nt_hash = false; + break; + } + io->u.store_nt_hash = true; + break; + } + + if (ac->userPassword) { + ret = msg_find_old_and_new_pwd_val(client_msg, "userPassword", + ac->req->operation, + &io->n.cleartext_utf8, + &io->og.cleartext_utf8); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "setup_io: " + "it's only allowed to set the old password once!"); + return ret; + } + } + + if (io->n.cleartext_utf8 != NULL) { + struct ldb_val *cleartext_utf8_blob; + char *p; + + cleartext_utf8_blob = talloc(io->ac, struct ldb_val); + if (!cleartext_utf8_blob) { + return ldb_oom(ldb); + } + + *cleartext_utf8_blob = *io->n.cleartext_utf8; + + /* make sure we have a null terminated string */ + p = talloc_strndup(cleartext_utf8_blob, + (const char *)io->n.cleartext_utf8->data, + io->n.cleartext_utf8->length); + if ((p == NULL) && (io->n.cleartext_utf8->length > 0)) { + return ldb_oom(ldb); + } + cleartext_utf8_blob->data = (uint8_t *)p; + + io->n.cleartext_utf8 = cleartext_utf8_blob; + } + + ret = msg_find_old_and_new_pwd_val(client_msg, "clearTextPassword", + ac->req->operation, + &io->n.cleartext_utf16, + &io->og.cleartext_utf16); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "setup_io: " + "it's only allowed to set the old password once!"); + return ret; + } + + /* this rather strange looking piece of code is there to + handle a ldap client setting a password remotely using the + unicodePwd ldap field. The syntax is that the password is + in UTF-16LE, with a " at either end. Unfortunately the + unicodePwd field is also used to store the nt hashes + internally in Samba, and is used in the nt hash format on + the wire in DRS replication, so we have a single name for + two distinct values. The code below leaves us with a small + chance (less than 1 in 2^32) of a mixup, if someone manages + to create a MD4 hash which starts and ends in 0x22 0x00, as + that would then be treated as a UTF16 password rather than + a nthash */ + + ret = msg_find_old_and_new_pwd_val(client_msg, "unicodePwd", + ac->req->operation, + "ed_utf16, + &old_quoted_utf16); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "setup_io: " + "it's only allowed to set the old password once!"); + return ret; + } + + /* Checks and converts the actual "unicodePwd" attribute */ + if (!ac->hash_values && + quoted_utf16 && + quoted_utf16->length >= 4 && + quoted_utf16->data[0] == '"' && + quoted_utf16->data[1] == 0 && + quoted_utf16->data[quoted_utf16->length-2] == '"' && + quoted_utf16->data[quoted_utf16->length-1] == 0) { + struct ldb_val *quoted_utf16_2; + + if (io->n.cleartext_utf16) { + /* refuse the change if someone wants to change with + with both UTF16 possibilities at the same time... */ + ldb_asprintf_errstring(ldb, + "setup_io: " + "it's only allowed to set the cleartext password as 'unicodePwd' or as 'clearTextPassword'"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* + * adapt the quoted UTF16 string to be a real + * cleartext one + */ + quoted_utf16_2 = talloc(io->ac, struct ldb_val); + if (quoted_utf16_2 == NULL) { + return ldb_oom(ldb); + } + + quoted_utf16_2->data = quoted_utf16->data + 2; + quoted_utf16_2->length = quoted_utf16->length-4; + io->n.cleartext_utf16 = quoted_utf16_2; + io->n.nt_hash = NULL; + + } else if (quoted_utf16) { + /* We have only the hash available -> so no plaintext here */ + if (!ac->hash_values) { + /* refuse the change if someone wants to change + the hash without control specified... */ + ldb_asprintf_errstring(ldb, + "setup_io: " + "it's not allowed to set the NT hash password directly'"); + /* this looks odd but this is what Windows does: + returns "UNWILLING_TO_PERFORM" on wrong + password sets and "CONSTRAINT_VIOLATION" on + wrong password changes. */ + if (old_quoted_utf16 == NULL) { + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + io->n.nt_hash = talloc(io->ac, struct samr_Password); + memcpy(io->n.nt_hash->hash, quoted_utf16->data, + MIN(quoted_utf16->length, sizeof(io->n.nt_hash->hash))); + } + + /* Checks and converts the previous "unicodePwd" attribute */ + if (!ac->hash_values && + old_quoted_utf16 && + old_quoted_utf16->length >= 4 && + old_quoted_utf16->data[0] == '"' && + old_quoted_utf16->data[1] == 0 && + old_quoted_utf16->data[old_quoted_utf16->length-2] == '"' && + old_quoted_utf16->data[old_quoted_utf16->length-1] == 0) { + struct ldb_val *old_quoted_utf16_2; + + if (io->og.cleartext_utf16) { + /* refuse the change if someone wants to change with + both UTF16 possibilities at the same time... */ + ldb_asprintf_errstring(ldb, + "setup_io: " + "it's only allowed to set the cleartext password as 'unicodePwd' or as 'clearTextPassword'"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* + * adapt the quoted UTF16 string to be a real + * cleartext one + */ + old_quoted_utf16_2 = talloc(io->ac, struct ldb_val); + if (old_quoted_utf16_2 == NULL) { + return ldb_oom(ldb); + } + + old_quoted_utf16_2->data = old_quoted_utf16->data + 2; + old_quoted_utf16_2->length = old_quoted_utf16->length-4; + + io->og.cleartext_utf16 = old_quoted_utf16_2; + io->og.nt_hash = NULL; + } else if (old_quoted_utf16) { + /* We have only the hash available -> so no plaintext here */ + if (!ac->hash_values) { + /* refuse the change if someone wants to change + the hash without control specified... */ + ldb_asprintf_errstring(ldb, + "setup_io: " + "it's not allowed to set the NT hash password directly'"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + io->og.nt_hash = talloc(io->ac, struct samr_Password); + memcpy(io->og.nt_hash->hash, old_quoted_utf16->data, + MIN(old_quoted_utf16->length, sizeof(io->og.nt_hash->hash))); + } + + /* Handles the "dBCSPwd" attribute (LM hash) */ + ret = msg_find_old_and_new_pwd_val(client_msg, "dBCSPwd", + ac->req->operation, + &lm_hash, &old_lm_hash); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "setup_io: " + "it's only allowed to set the old password once!"); + return ret; + } + + if (((lm_hash != NULL) || (old_lm_hash != NULL))) { + /* refuse the change if someone wants to change the LM hash */ + ldb_asprintf_errstring(ldb, + "setup_io: " + "it's not allowed to set the LM hash password (dBCSPwd)'"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* + * Handles the password change control if it's specified. It has the + * precedance and overrides already specified old password values of + * change requests (but that shouldn't happen since the control is + * fully internal and only used in conjunction with replace requests!). + */ + if (ac->change != NULL) { + io->og.nt_hash = NULL; + } + + /* refuse the change if someone wants to change the clear- + text and supply his own hashes at the same time... */ + if ((io->n.cleartext_utf8 || io->n.cleartext_utf16) + && (io->n.nt_hash)) { + ldb_asprintf_errstring(ldb, + "setup_io: " + "it's only allowed to set the password in form of cleartext attributes or as hashes"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* refuse the change if someone wants to change the password + using both plaintext methods (UTF8 and UTF16) at the same time... */ + if (io->n.cleartext_utf8 && io->n.cleartext_utf16) { + ldb_asprintf_errstring(ldb, + "setup_io: " + "it's only allowed to set the cleartext password as 'unicodePwd' or as 'userPassword' or as 'clearTextPassword'"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* refuse the change if someone tries to set/change the password by + * any method that would leave us without a password! */ + if (io->ac->update_password + && (!io->n.cleartext_utf8) && (!io->n.cleartext_utf16) + && (!io->n.nt_hash)) { + ldb_asprintf_errstring(ldb, + "setup_io: " + "It's not possible to delete the password (changes using the LAN Manager hash alone could be deactivated)!"); + /* on "userPassword" and "clearTextPassword" we've to return + * something different, since these are virtual attributes */ + if ((ldb_msg_find_element(client_msg, "userPassword") != NULL) || + (ldb_msg_find_element(client_msg, "clearTextPassword") != NULL)) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* + * refuse the change if someone wants to compare against a + * plaintext or dsdb_control_password_change at the same time + * for a "password modify" operation... + */ + if ((io->og.cleartext_utf8 || io->og.cleartext_utf16) + && ac->change) { + ldb_asprintf_errstring(ldb, + "setup_io: " + "it's only allowed to provide the old password in form of cleartext attributes or as the dsdb_control_password_change"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* refuse the change if someone wants to compare against both + * plaintexts at the same time for a "password modify" operation... */ + if (io->og.cleartext_utf8 && io->og.cleartext_utf16) { + ldb_asprintf_errstring(ldb, + "setup_io: " + "it's only allowed to provide the old cleartext password as 'unicodePwd' or as 'userPassword' or as 'clearTextPassword'"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* Decides if we have a password modify or password reset operation */ + if (ac->req->operation == LDB_ADD) { + /* On "add" we have only "password reset" */ + ac->pwd_reset = true; + } else if (ac->req->operation == LDB_MODIFY) { + struct ldb_control *pav_ctrl = NULL; + struct dsdb_control_password_acl_validation *pav = NULL; + + pav_ctrl = ldb_request_get_control(ac->req, + DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID); + if (pav_ctrl != NULL) { + pav = talloc_get_type_abort(pav_ctrl->data, + struct dsdb_control_password_acl_validation); + } + + if (pav == NULL && ac->update_password) { + bool ok; + + /* + * If the DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID + * control is missing, we require system access! + */ + ok = dsdb_module_am_system(ac->module); + if (!ok) { + return ldb_module_operr(ac->module); + } + } + + if (pav != NULL) { + /* + * We assume what the acl module has validated. + */ + ac->pwd_reset = pav->pwd_reset; + } else if (io->og.cleartext_utf8 || io->og.cleartext_utf16 + || ac->change) { + /* + * If we have an old password specified or the + * dsdb_control_password_change then for sure + * it is a user "password change" + */ + ac->pwd_reset = false; + } else { + /* Otherwise we have also here a "password reset" */ + ac->pwd_reset = true; + } + } else { + /* this shouldn't happen */ + return ldb_operr(ldb); + } + + if (existing_msg != NULL) { + NTSTATUS status; + krb5_error_code krb5_ret; + DATA_BLOB key_blob; + DATA_BLOB salt_blob; + uint32_t kvno; + + if (ac->pwd_reset) { + /* Get the old password from the database */ + status = samdb_result_passwords_no_lockout(ac, + lp_ctx, + existing_msg, + &io->o.nt_hash); + } else { + /* Get the old password from the database */ + status = samdb_result_passwords(ac, + lp_ctx, + existing_msg, + &io->o.nt_hash); + } + + if (NT_STATUS_EQUAL(status, NT_STATUS_ACCOUNT_LOCKED_OUT)) { + return dsdb_module_werror(ac->module, + LDB_ERR_CONSTRAINT_VIOLATION, + WERR_ACCOUNT_LOCKED_OUT, + "Password change not permitted," + " account locked out!"); + } + + if (!NT_STATUS_IS_OK(status)) { + /* + * This only happens if the database has gone weird, + * not if we are just missing the passwords + */ + return ldb_operr(ldb); + } + + io->o.nt_history_len = samdb_result_hashes(ac, existing_msg, + "ntPwdHistory", + &io->o.nt_history); + io->o.supplemental = ldb_msg_find_ldb_val(existing_msg, + "supplementalCredentials"); + + if (io->o.supplemental != NULL) { + enum ndr_err_code ndr_err; + + ndr_err = ndr_pull_struct_blob_all(io->o.supplemental, io->ac, + &io->o.scb, + (ndr_pull_flags_fn_t)ndr_pull_supplementalCredentialsBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + status = ndr_map_error2ntstatus(ndr_err); + ldb_asprintf_errstring(ldb, + "setup_io: failed to pull " + "old supplementalCredentialsBlob: %s", + nt_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + } + + /* + * If this account requires a smartcard for login, we don't + * attempt a comparison with the old password. + */ + if (io->u.userAccountControl & UF_SMARTCARD_REQUIRED) { + return LDB_SUCCESS; + } + + /* + * Extract the old ENCTYPE_AES256_CTS_HMAC_SHA1_96 + * value from the supplementalCredentials. + */ + krb5_ret = dsdb_extract_aes_256_key(io->smb_krb5_context->krb5_context, + io->ac, + existing_msg, + io->u.userAccountControl, + NULL, /* kvno */ + &kvno, /* kvno_out */ + &key_blob, + &salt_blob); + if (krb5_ret == ENOENT) { + /* + * If there is no old AES hash (perhaps an imported DB with + * just unicodePwd) then we just wont have an old + * password to compare to if there is no NT hash + */ + return LDB_SUCCESS; + } + if (krb5_ret) { + ldb_asprintf_errstring(ldb, + "setup_io: " + "extraction of salt for old aes256-cts-hmac-sha1-96 key failed: %s", + smb_get_krb5_error_message(io->smb_krb5_context->krb5_context, + krb5_ret, io->ac)); + return LDB_ERR_OPERATIONS_ERROR; + } + + io->o.salt = salt_blob; + io->o.aes_256 = key_blob; + io->o.kvno = kvno; + } + + return LDB_SUCCESS; +} + +static struct ph_context *ph_init_context(struct ldb_module *module, + struct ldb_request *req, + bool userPassword, + bool update_password) +{ + struct ldb_context *ldb; + struct ph_context *ac; + struct loadparm_context *lp_ctx = NULL; + + ldb = ldb_module_get_ctx(module); + + ac = talloc_zero(req, struct ph_context); + if (ac == NULL) { + ldb_set_errstring(ldb, "Out of Memory"); + return NULL; + } + + ac->module = module; + ac->req = req; + ac->userPassword = userPassword; + ac->update_password = update_password; + ac->update_lastset = true; + + lp_ctx = talloc_get_type_abort(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + ac->gpg_key_ids = lpcfg_password_hash_gpg_key_ids(lp_ctx); + ac->userPassword_schemes + = lpcfg_password_hash_userpassword_schemes(lp_ctx); + return ac; +} + +static void ph_apply_controls(struct ph_context *ac) +{ + struct ldb_control *ctrl; + + ac->change_status = false; + ctrl = ldb_request_get_control(ac->req, + DSDB_CONTROL_PASSWORD_CHANGE_STATUS_OID); + if (ctrl != NULL) { + ac->change_status = true; + + /* Mark the "change status" control as uncritical (done) */ + ctrl->critical = false; + } + + ac->hash_values = false; + ctrl = ldb_request_get_control(ac->req, + DSDB_CONTROL_PASSWORD_HASH_VALUES_OID); + if (ctrl != NULL) { + ac->hash_values = true; + + /* Mark the "hash values" control as uncritical (done) */ + ctrl->critical = false; + } + + ctrl = ldb_request_get_control(ac->req, + DSDB_CONTROL_PASSWORD_CHANGE_OLD_PW_CHECKED_OID); + if (ctrl != NULL) { + ac->change = talloc_get_type_abort(ctrl->data, struct dsdb_control_password_change); + + /* Mark the "change" control as uncritical (done) */ + ctrl->critical = false; + } + + ac->pwd_last_set_bypass = false; + ctrl = ldb_request_get_control(ac->req, + DSDB_CONTROL_PASSWORD_BYPASS_LAST_SET_OID); + if (ctrl != NULL) { + ac->pwd_last_set_bypass = true; + + /* Mark the "bypass pwdLastSet" control as uncritical (done) */ + ctrl->critical = false; + } + + ac->pwd_last_set_default = false; + ctrl = ldb_request_get_control(ac->req, + DSDB_CONTROL_PASSWORD_DEFAULT_LAST_SET_OID); + if (ctrl != NULL) { + ac->pwd_last_set_default = true; + + /* Mark the "bypass pwdLastSet" control as uncritical (done) */ + ctrl->critical = false; + } + + ac->smartcard_reset = false; + ctrl = ldb_request_get_control(ac->req, + DSDB_CONTROL_PASSWORD_USER_ACCOUNT_CONTROL_OID); + if (ctrl != NULL) { + struct dsdb_control_password_user_account_control *uac = NULL; + uint32_t added_flags = 0; + + uac = talloc_get_type_abort(ctrl->data, + struct dsdb_control_password_user_account_control); + + added_flags = uac->new_flags & ~uac->old_flags; + + if (added_flags & UF_SMARTCARD_REQUIRED) { + ac->smartcard_reset = true; + } + + /* Mark the "smartcard required" control as uncritical (done) */ + ctrl->critical = false; + } +} + +static int ph_op_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct ph_context *ac; + + ac = talloc_get_type(req->context, struct ph_context); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + if (ares->type == LDB_REPLY_REFERRAL) { + return ldb_module_send_referral(ac->req, ares->referral); + } + + if ((ares->error != LDB_ERR_OPERATIONS_ERROR) && (ac->change_status)) { + /* On success and trivial errors a status control is being + * added (used for example by the "samdb_set_password" call) */ + ldb_reply_add_control(ares, + DSDB_CONTROL_PASSWORD_CHANGE_STATUS_OID, + false, + ac->status); + } + + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + if (ares->type != LDB_REPLY_DONE) { + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); +} + +static int password_hash_add_do_add(struct ph_context *ac); +static int ph_modify_callback(struct ldb_request *req, struct ldb_reply *ares); +static int password_hash_mod_search_self(struct ph_context *ac); +static int ph_mod_search_callback(struct ldb_request *req, struct ldb_reply *ares); +static int password_hash_mod_do_mod(struct ph_context *ac); + +/* + * LDB callback handler for searching for a user's PSO. Once we have all the + * Password Settings that apply to the user, we can continue with the modify + * operation + */ +static int get_pso_data_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct ldb_context *ldb = NULL; + struct ph_context *ac = NULL; + bool domain_complexity = true; + bool pso_complexity = true; + struct dsdb_user_pwd_settings *settings = NULL; + int ret = LDB_SUCCESS; + + ac = talloc_get_type(req->context, struct ph_context); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + ret = LDB_ERR_OPERATIONS_ERROR; + goto done; + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + + /* check status was initialized by the domain query */ + if (ac->status == NULL) { + talloc_free(ares); + ldb_set_errstring(ldb, "Uninitialized status"); + ret = LDB_ERR_OPERATIONS_ERROR; + goto done; + } + + /* + * use the PSO's values instead of the domain defaults (the PSO + * attributes should always exist, but use the domain default + * values as a fallback). + */ + settings = &ac->status->domain_data; + settings->store_cleartext = + ldb_msg_find_attr_as_bool(ares->message, + "msDS-PasswordReversibleEncryptionEnabled", + settings->store_cleartext); + + settings->pwdHistoryLength = + ldb_msg_find_attr_as_uint(ares->message, + "msDS-PasswordHistoryLength", + settings->pwdHistoryLength); + settings->maxPwdAge = + ldb_msg_find_attr_as_int64(ares->message, + "msDS-MaximumPasswordAge", + settings->maxPwdAge); + settings->minPwdAge = + ldb_msg_find_attr_as_int64(ares->message, + "msDS-MinimumPasswordAge", + settings->minPwdAge); + settings->minPwdLength = + ldb_msg_find_attr_as_uint(ares->message, + "msDS-MinimumPasswordLength", + settings->minPwdLength); + domain_complexity = + (settings->pwdProperties & DOMAIN_PASSWORD_COMPLEX); + pso_complexity = + ldb_msg_find_attr_as_bool(ares->message, + "msDS-PasswordComplexityEnabled", + domain_complexity); + + /* set or clear the complexity bit if required */ + if (pso_complexity && !domain_complexity) { + settings->pwdProperties |= DOMAIN_PASSWORD_COMPLEX; + } else if (domain_complexity && !pso_complexity) { + settings->pwdProperties &= ~DOMAIN_PASSWORD_COMPLEX; + } + + if (ac->pso_res != NULL) { + DBG_ERR("Too many PSO results for %s", + ldb_dn_get_linearized(ac->search_res->message->dn)); + talloc_free(ac->pso_res); + } + + /* store the PSO result (we may need its lockout settings) */ + ac->pso_res = talloc_steal(ac, ares); + ret = LDB_SUCCESS; + break; + + case LDB_REPLY_REFERRAL: + /* ignore */ + talloc_free(ares); + ret = LDB_SUCCESS; + break; + + case LDB_REPLY_DONE: + talloc_free(ares); + + /* + * perform the next step of the modify operation (this code + * shouldn't get called in the 'user add' case) + */ + if (ac->req->operation == LDB_MODIFY) { + ret = password_hash_mod_do_mod(ac); + } else { + ret = LDB_ERR_OPERATIONS_ERROR; + } + break; + } + +done: + if (ret != LDB_SUCCESS) { + struct ldb_reply *new_ares; + + new_ares = talloc_zero(ac->req, struct ldb_reply); + if (new_ares == NULL) { + ldb_oom(ldb); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + new_ares->error = ret; + if ((ret != LDB_ERR_OPERATIONS_ERROR) && (ac->change_status)) { + /* On success and trivial errors a status control is being + * added (used for example by the "samdb_set_password" call) */ + ldb_reply_add_control(new_ares, + DSDB_CONTROL_PASSWORD_CHANGE_STATUS_OID, + false, + ac->status); + } + + return ldb_module_done(ac->req, new_ares->controls, + new_ares->response, new_ares->error); + } + + return LDB_SUCCESS; +} + +/* + * Builds and returns a search request to lookup up the PSO that applies to + * the user in question. Returns NULL if no PSO applies, or could not be found + */ +static struct ldb_request * build_pso_data_request(struct ph_context *ac) +{ + /* attrs[] is returned from this function in + pso_req->op.search.attrs, so it must be static, as + otherwise the compiler can put it on the stack */ + static const char * const attrs[] = { "msDS-PasswordComplexityEnabled", + "msDS-PasswordReversibleEncryptionEnabled", + "msDS-PasswordHistoryLength", + "msDS-MaximumPasswordAge", + "msDS-MinimumPasswordAge", + "msDS-MinimumPasswordLength", + "msDS-LockoutThreshold", + "msDS-LockoutObservationWindow", + NULL }; + struct ldb_context *ldb = NULL; + struct ldb_request *pso_req = NULL; + struct ldb_dn *pso_dn = NULL; + TALLOC_CTX *mem_ctx = ac; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + /* if a PSO applies to the user, we need to lookup the PSO as well */ + pso_dn = ldb_msg_find_attr_as_dn(ldb, mem_ctx, ac->search_res->message, + "msDS-ResultantPSO"); + if (pso_dn == NULL) { + return NULL; + } + + ret = ldb_build_search_req(&pso_req, ldb, mem_ctx, pso_dn, + LDB_SCOPE_BASE, NULL, attrs, NULL, + ac, get_pso_data_callback, + ac->dom_req); + + /* log errors, but continue with the default domain settings */ + if (ret != LDB_SUCCESS) { + DBG_ERR("Error %d constructing PSO query for user %s", ret, + ldb_dn_get_linearized(ac->search_res->message->dn)); + } + LDB_REQ_SET_LOCATION(pso_req); + return pso_req; +} + + +static int get_domain_data_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct ph_context *ac; + struct loadparm_context *lp_ctx; + struct ldb_request *pso_req = NULL; + int ret = LDB_SUCCESS; + + ac = talloc_get_type(req->context, struct ph_context); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + ret = LDB_ERR_OPERATIONS_ERROR; + goto done; + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + if (ac->status != NULL) { + talloc_free(ares); + + ldb_set_errstring(ldb, "Too many results"); + ret = LDB_ERR_OPERATIONS_ERROR; + goto done; + } + + /* Setup the "status" structure (used as control later) */ + ac->status = talloc_zero(ac->req, + struct dsdb_control_password_change_status); + if (ac->status == NULL) { + talloc_free(ares); + + ldb_oom(ldb); + ret = LDB_ERR_OPERATIONS_ERROR; + goto done; + } + + /* Setup the "domain data" structure */ + ac->status->domain_data.pwdProperties = + ldb_msg_find_attr_as_uint(ares->message, "pwdProperties", -1); + ac->status->domain_data.pwdHistoryLength = + ldb_msg_find_attr_as_uint(ares->message, "pwdHistoryLength", -1); + ac->status->domain_data.maxPwdAge = + ldb_msg_find_attr_as_int64(ares->message, "maxPwdAge", -1); + ac->status->domain_data.minPwdAge = + ldb_msg_find_attr_as_int64(ares->message, "minPwdAge", -1); + ac->status->domain_data.minPwdLength = + ldb_msg_find_attr_as_uint(ares->message, "minPwdLength", -1); + ac->status->domain_data.store_cleartext = + ac->status->domain_data.pwdProperties & DOMAIN_PASSWORD_STORE_CLEARTEXT; + + /* For a domain DN, this puts things in dotted notation */ + /* For builtin domains, this will give details for the host, + * but that doesn't really matter, as it's just used for salt + * and kerberos principals, which don't exist here */ + + lp_ctx = talloc_get_type(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + + ac->status->domain_data.dns_domain = lpcfg_dnsdomain(lp_ctx); + ac->status->domain_data.realm = lpcfg_realm(lp_ctx); + ac->status->domain_data.netbios_domain = lpcfg_sam_name(lp_ctx); + + ac->status->reject_reason = SAM_PWD_CHANGE_NO_ERROR; + + if (ac->dom_res != NULL) { + talloc_free(ares); + + ldb_set_errstring(ldb, "Too many results"); + ret = LDB_ERR_OPERATIONS_ERROR; + goto done; + } + + ac->dom_res = talloc_steal(ac, ares); + ret = LDB_SUCCESS; + break; + + case LDB_REPLY_REFERRAL: + /* ignore */ + talloc_free(ares); + ret = LDB_SUCCESS; + break; + + case LDB_REPLY_DONE: + talloc_free(ares); + /* call the next step */ + switch (ac->req->operation) { + case LDB_ADD: + ret = password_hash_add_do_add(ac); + break; + + case LDB_MODIFY: + + /* + * The user may have an optional PSO applied. If so, + * query the PSO to get the Fine-Grained Password Policy + * for the user, before we perform the modify + */ + pso_req = build_pso_data_request(ac); + if (pso_req != NULL) { + ret = ldb_next_request(ac->module, pso_req); + } else { + + /* no PSO, so we can perform the modify now */ + ret = password_hash_mod_do_mod(ac); + } + break; + + default: + ret = LDB_ERR_OPERATIONS_ERROR; + break; + } + break; + } + +done: + if (ret != LDB_SUCCESS) { + struct ldb_reply *new_ares; + + new_ares = talloc_zero(ac->req, struct ldb_reply); + if (new_ares == NULL) { + ldb_oom(ldb); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + new_ares->error = ret; + if ((ret != LDB_ERR_OPERATIONS_ERROR) && (ac->change_status)) { + /* On success and trivial errors a status control is being + * added (used for example by the "samdb_set_password" call) */ + ldb_reply_add_control(new_ares, + DSDB_CONTROL_PASSWORD_CHANGE_STATUS_OID, + false, + ac->status); + } + + return ldb_module_done(ac->req, new_ares->controls, + new_ares->response, new_ares->error); + } + + return LDB_SUCCESS; +} + +static int build_domain_data_request(struct ph_context *ac) +{ + /* attrs[] is returned from this function in + ac->dom_req->op.search.attrs, so it must be static, as + otherwise the compiler can put it on the stack */ + struct ldb_context *ldb; + static const char * const attrs[] = { "pwdProperties", + "pwdHistoryLength", + "maxPwdAge", + "minPwdAge", + "minPwdLength", + "lockoutThreshold", + "lockOutObservationWindow", + NULL }; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + ret = ldb_build_search_req(&ac->dom_req, ldb, ac, + ldb_get_default_basedn(ldb), + LDB_SCOPE_BASE, + NULL, attrs, + NULL, + ac, get_domain_data_callback, + ac->req); + LDB_REQ_SET_LOCATION(ac->dom_req); + return ret; +} + +static int password_hash_needed(struct ldb_module *module, + struct ldb_request *req, + struct ph_context **_ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + const char *operation = NULL; + const struct ldb_message *msg = NULL; + struct ph_context *ac = NULL; + const char *passwordAttrs[] = { + DSDB_PASSWORD_ATTRIBUTES, + NULL + }; + const char **a = NULL; + unsigned int attr_cnt = 0; + struct ldb_control *bypass = NULL; + struct ldb_control *uac_ctrl = NULL; + bool userPassword = dsdb_user_password_support(module, req, req); + bool update_password = false; + bool processing_needed = false; + + *_ac = NULL; + + ldb_debug(ldb, LDB_DEBUG_TRACE, "password_hash_needed\n"); + + switch (req->operation) { + case LDB_ADD: + operation = "add"; + msg = req->op.add.message; + break; + case LDB_MODIFY: + operation = "modify"; + msg = req->op.mod.message; + break; + default: + return ldb_next_request(module, req); + } + + if (ldb_dn_is_special(msg->dn)) { /* do not manipulate our control entries */ + return ldb_next_request(module, req); + } + + bypass = ldb_request_get_control(req, + DSDB_CONTROL_BYPASS_PASSWORD_HASH_OID); + if (bypass != NULL) { + /* Mark the "bypass" control as uncritical (done) */ + bypass->critical = false; + ldb_debug(ldb, LDB_DEBUG_TRACE, + "password_hash_needed(%s) (bypassing)\n", + operation); + return password_hash_bypass(module, req); + } + + /* nobody must touch password histories and 'supplementalCredentials' */ + if (ldb_msg_find_element(msg, "ntPwdHistory")) { + return LDB_ERR_UNWILLING_TO_PERFORM; + } + if (ldb_msg_find_element(msg, "lmPwdHistory")) { + return LDB_ERR_UNWILLING_TO_PERFORM; + } + if (ldb_msg_find_element(msg, "supplementalCredentials")) { + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* + * If no part of this touches the 'userPassword' OR 'clearTextPassword' + * OR 'unicodePwd' OR 'dBCSPwd' we don't need to make any changes. + * For password changes/set there should be a 'delete' or a 'modify' + * on these attributes. + */ + for (a = passwordAttrs; *a != NULL; a++) { + if ((!userPassword) && (ldb_attr_cmp(*a, "userPassword") == 0)) { + continue; + } + + if (ldb_msg_find_element(msg, *a) != NULL) { + /* MS-ADTS 3.1.1.3.1.5.2 */ + if ((ldb_attr_cmp(*a, "userPassword") == 0) && + (dsdb_functional_level(ldb) < DS_DOMAIN_FUNCTION_2003)) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + ++attr_cnt; + } + } + + if (attr_cnt > 0) { + update_password = true; + processing_needed = true; + } + + if (ldb_msg_find_element(msg, "pwdLastSet")) { + processing_needed = true; + } + + uac_ctrl = ldb_request_get_control(req, + DSDB_CONTROL_PASSWORD_USER_ACCOUNT_CONTROL_OID); + if (uac_ctrl != NULL) { + struct dsdb_control_password_user_account_control *uac = NULL; + uint32_t added_flags = 0; + + uac = talloc_get_type_abort(uac_ctrl->data, + struct dsdb_control_password_user_account_control); + + added_flags = uac->new_flags & ~uac->old_flags; + + if (added_flags & UF_SMARTCARD_REQUIRED) { + processing_needed = true; + } + } + + if (!processing_needed) { + return ldb_next_request(module, req); + } + + ac = ph_init_context(module, req, userPassword, update_password); + if (!ac) { + DEBUG(0,(__location__ ": %s\n", ldb_errstring(ldb))); + return ldb_operr(ldb); + } + ph_apply_controls(ac); + + /* + * Make a copy in order to apply our modifications + * to the final update + */ + ac->update_msg = ldb_msg_copy_shallow(ac, msg); + if (ac->update_msg == NULL) { + return ldb_oom(ldb); + } + + /* + * Remove all password related attributes. + */ + if (ac->userPassword) { + ldb_msg_remove_attr(ac->update_msg, "userPassword"); + } + ldb_msg_remove_attr(ac->update_msg, "clearTextPassword"); + ldb_msg_remove_attr(ac->update_msg, "unicodePwd"); + ldb_msg_remove_attr(ac->update_msg, "ntPwdHistory"); + ldb_msg_remove_attr(ac->update_msg, "dBCSPwd"); + ldb_msg_remove_attr(ac->update_msg, "lmPwdHistory"); + ldb_msg_remove_attr(ac->update_msg, "supplementalCredentials"); + ldb_msg_remove_attr(ac->update_msg, "pwdLastSet"); + + *_ac = ac; + return LDB_SUCCESS; +} + +static int password_hash_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ph_context *ac = NULL; + int ret; + + ldb_debug(ldb, LDB_DEBUG_TRACE, "password_hash_add\n"); + + ret = password_hash_needed(module, req, &ac); + if (ret != LDB_SUCCESS) { + return ret; + } + if (ac == NULL) { + return ret; + } + + /* Make sure we are performing the password set action on a (for us) + * valid object. Those are instances of either "user" and/or + * "inetOrgPerson". Otherwise continue with the submodules. */ + if ((!ldb_msg_check_string_attribute(req->op.add.message, "objectClass", "user")) + && (!ldb_msg_check_string_attribute(req->op.add.message, "objectClass", "inetOrgPerson"))) { + + TALLOC_FREE(ac); + + if (ldb_msg_find_element(req->op.add.message, "clearTextPassword") != NULL) { + ldb_set_errstring(ldb, + "'clearTextPassword' is only allowed on objects of class 'user' and/or 'inetOrgPerson'!"); + return LDB_ERR_NO_SUCH_ATTRIBUTE; + } + + return ldb_next_request(module, req); + } + + /* get user domain data */ + ret = build_domain_data_request(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, ac->dom_req); +} + +static int password_hash_add_do_add(struct ph_context *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + struct ldb_request *down_req; + struct setup_password_fields_io io; + int ret; + + /* Prepare the internal data structure containing the passwords */ + ret = setup_io(ac, ac->req->op.add.message, NULL, &io); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = setup_password_fields(&io); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = check_password_restrictions_and_log(&io); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = setup_smartcard_reset(&io); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = update_final_msg(&io); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_build_add_req(&down_req, ldb, ac, + ac->update_msg, + ac->req->controls, + ac, ph_op_callback, + ac->req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(ac->module, down_req); +} + +static int password_hash_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ph_context *ac = NULL; + const char *passwordAttrs[] = {DSDB_PASSWORD_ATTRIBUTES, NULL}, **l; + unsigned int del_attr_cnt, add_attr_cnt, rep_attr_cnt; + struct ldb_message_element *passwordAttr; + struct ldb_message *msg; + struct ldb_request *down_req; + struct ldb_control *restore = NULL; + int ret; + unsigned int i = 0; + + ldb_debug(ldb, LDB_DEBUG_TRACE, "password_hash_modify\n"); + + ret = password_hash_needed(module, req, &ac); + if (ret != LDB_SUCCESS) { + return ret; + } + if (ac == NULL) { + return ret; + } + + /* use a new message structure so that we can modify it */ + msg = ldb_msg_copy_shallow(ac, req->op.mod.message); + if (msg == NULL) { + return ldb_oom(ldb); + } + + /* - check for single-valued password attributes + * (if not return "CONSTRAINT_VIOLATION") + * - check that for a password change operation one add and one delete + * operation exists + * (if not return "CONSTRAINT_VIOLATION" or "UNWILLING_TO_PERFORM") + * - check that a password change and a password set operation cannot + * be mixed + * (if not return "UNWILLING_TO_PERFORM") + * - remove all password attributes modifications from the first change + * operation (anything without the passwords) - we will make the real + * modification later */ + del_attr_cnt = 0; + add_attr_cnt = 0; + rep_attr_cnt = 0; + for (l = passwordAttrs; *l != NULL; l++) { + if ((!ac->userPassword) && + (ldb_attr_cmp(*l, "userPassword") == 0)) { + continue; + } + + while ((passwordAttr = ldb_msg_find_element(msg, *l)) != NULL) { + unsigned int mtype = LDB_FLAG_MOD_TYPE(passwordAttr->flags); + unsigned int nvalues = passwordAttr->num_values; + + if (mtype == LDB_FLAG_MOD_DELETE) { + ++del_attr_cnt; + } + if (mtype == LDB_FLAG_MOD_ADD) { + ++add_attr_cnt; + } + if (mtype == LDB_FLAG_MOD_REPLACE) { + ++rep_attr_cnt; + } + if ((nvalues != 1) && (mtype == LDB_FLAG_MOD_ADD)) { + talloc_free(ac); + ldb_asprintf_errstring(ldb, + "'%s' attribute must have exactly one value on add operations!", + *l); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + if ((nvalues > 1) && (mtype == LDB_FLAG_MOD_DELETE)) { + talloc_free(ac); + ldb_asprintf_errstring(ldb, + "'%s' attribute must have zero or one value(s) on delete operations!", + *l); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + ldb_msg_remove_element(msg, passwordAttr); + } + } + if ((del_attr_cnt == 0) && (add_attr_cnt > 0)) { + talloc_free(ac); + ldb_set_errstring(ldb, + "Only the add action for a password change specified!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + if ((del_attr_cnt > 1) || (add_attr_cnt > 1)) { + talloc_free(ac); + ldb_set_errstring(ldb, + "Only one delete and one add action for a password change allowed!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + if ((rep_attr_cnt > 0) && ((del_attr_cnt > 0) || (add_attr_cnt > 0))) { + talloc_free(ac); + ldb_set_errstring(ldb, + "Either a password change or a password set operation is allowed!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + restore = ldb_request_get_control(req, + DSDB_CONTROL_RESTORE_TOMBSTONE_OID); + if (restore == NULL) { + /* + * A tomstone reanimation generates a double update + * of pwdLastSet. + * + * So we only remove it without the + * DSDB_CONTROL_RESTORE_TOMBSTONE_OID control. + */ + ldb_msg_remove_attr(msg, "pwdLastSet"); + } + + + /* if there was nothing else to be modified skip to next step */ + if (msg->num_elements == 0) { + return password_hash_mod_search_self(ac); + } + + /* + * Now we apply all changes remaining in msg + * and remove them from our final update_msg + */ + + for (i = 0; i < msg->num_elements; i++) { + ldb_msg_remove_attr(ac->update_msg, + msg->elements[i].name); + } + + ret = ldb_build_mod_req(&down_req, ldb, ac, + msg, + req->controls, + ac, ph_modify_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, down_req); +} + +static int ph_modify_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct ph_context *ac; + + ac = talloc_get_type(req->context, struct ph_context); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + if (ares->type == LDB_REPLY_REFERRAL) { + return ldb_module_send_referral(ac->req, ares->referral); + } + + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + if (ares->type != LDB_REPLY_DONE) { + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + talloc_free(ares); + + return password_hash_mod_search_self(ac); +} + +static int ph_mod_search_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct ph_context *ac; + int ret = LDB_SUCCESS; + + ac = talloc_get_type(req->context, struct ph_context); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + ret = LDB_ERR_OPERATIONS_ERROR; + goto done; + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + /* we are interested only in the single reply (base search) */ + switch (ares->type) { + case LDB_REPLY_ENTRY: + /* Make sure we are performing the password change action on a + * (for us) valid object. Those are instances of either "user" + * and/or "inetOrgPerson". Otherwise continue with the + * submodules. */ + if ((!ldb_msg_check_string_attribute(ares->message, "objectClass", "user")) + && (!ldb_msg_check_string_attribute(ares->message, "objectClass", "inetOrgPerson"))) { + talloc_free(ares); + + if (ldb_msg_find_element(ac->req->op.mod.message, "clearTextPassword") != NULL) { + ldb_set_errstring(ldb, + "'clearTextPassword' is only allowed on objects of class 'user' and/or 'inetOrgPerson'!"); + ret = LDB_ERR_NO_SUCH_ATTRIBUTE; + goto done; + } + + ret = ldb_next_request(ac->module, ac->req); + goto done; + } + + if (ac->search_res != NULL) { + talloc_free(ares); + + ldb_set_errstring(ldb, "Too many results"); + ret = LDB_ERR_OPERATIONS_ERROR; + goto done; + } + + ac->search_res = talloc_steal(ac, ares); + ret = LDB_SUCCESS; + break; + + case LDB_REPLY_REFERRAL: + /* ignore anything else for now */ + talloc_free(ares); + ret = LDB_SUCCESS; + break; + + case LDB_REPLY_DONE: + talloc_free(ares); + + /* get user domain data */ + ret = build_domain_data_request(ac); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + ret = ldb_next_request(ac->module, ac->dom_req); + break; + } + +done: + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + return LDB_SUCCESS; +} + +static int password_hash_mod_search_self(struct ph_context *ac) +{ + struct ldb_context *ldb; + static const char * const attrs[] = { "objectClass", + "userAccountControl", + "msDS-ResultantPSO", + "msDS-User-Account-Control-Computed", + "pwdLastSet", + "sAMAccountName", + "objectSid", + "userPrincipalName", + "displayName", + "supplementalCredentials", + "lmPwdHistory", + "ntPwdHistory", + "dBCSPwd", + "unicodePwd", + "badPasswordTime", + "badPwdCount", + "lockoutTime", + "msDS-KeyVersionNumber", + "msDS-SecondaryKrbTgtNumber", + NULL }; + struct ldb_request *search_req; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + ret = ldb_build_search_req(&search_req, ldb, ac, + ac->req->op.mod.message->dn, + LDB_SCOPE_BASE, + "(objectclass=*)", + attrs, + NULL, + ac, ph_mod_search_callback, + ac->req); + LDB_REQ_SET_LOCATION(search_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(ac->module, search_req); +} + +static int password_hash_mod_do_mod(struct ph_context *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + struct ldb_request *mod_req; + struct setup_password_fields_io io; + int ret; + + /* Prepare the internal data structure containing the passwords */ + ret = setup_io(ac, ac->req->op.mod.message, + ac->search_res->message, &io); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = setup_password_fields(&io); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = check_password_restrictions_and_log(&io); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = setup_smartcard_reset(&io); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = update_final_msg(&io); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_build_mod_req(&mod_req, ldb, ac, + ac->update_msg, + ac->req->controls, + ac, ph_op_callback, + ac->req); + LDB_REQ_SET_LOCATION(mod_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(ac->module, mod_req); +} + +static const struct ldb_module_ops ldb_password_hash_module_ops = { + .name = "password_hash", + .add = password_hash_add, + .modify = password_hash_modify +}; + +int ldb_password_hash_module_init(const char *version) +{ +#ifdef ENABLE_GPGME + const char *gversion = NULL; +#endif /* ENABLE_GPGME */ + + LDB_MODULE_CHECK_VERSION(version); + +#ifdef ENABLE_GPGME + /* + * Note: this sets a SIGPIPE handler + * if none is active already. See: + * https://www.gnupg.org/documentation/manuals/gpgme/Signal-Handling.html#Signal-Handling + */ + gversion = gpgme_check_version(MINIMUM_GPGME_VERSION); + if (gversion == NULL) { + fprintf(stderr, "%s() in %s version[%s]: " + "gpgme_check_version(%s) not available, " + "gpgme_check_version(NULL) => '%s'\n", + __func__, __FILE__, version, + MINIMUM_GPGME_VERSION, gpgme_check_version(NULL)); + return LDB_ERR_UNAVAILABLE; + } +#endif /* ENABLE_GPGME */ + + return ldb_register_module(&ldb_password_hash_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/password_modules.h b/source4/dsdb/samdb/ldb_modules/password_modules.h new file mode 100644 index 0000000..40d0144 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/password_modules.h @@ -0,0 +1,3 @@ +/* We store these passwords under this base DN: */ + +#define LOCAL_BASE "cn=Passwords" diff --git a/source4/dsdb/samdb/ldb_modules/proxy.c b/source4/dsdb/samdb/ldb_modules/proxy.c new file mode 100644 index 0000000..e363534 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/proxy.c @@ -0,0 +1,415 @@ +/* + samdb proxy module + + Copyright (C) Andrew Tridgell 2005 + + 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/>. +*/ + +/* + ldb proxy module. At startup this looks for a record like this: + + dn=@PROXYINFO + url=destination url + olddn = basedn to proxy in upstream server + newdn = basedn in local server + username = username to connect to upstream + password = password for upstream + + NOTE: this module is a complete hack at this stage. I am committing it just + so others can know how I am investigating mmc support + + */ + +#include "includes.h" +#include "ldb_module.h" +#include "auth/credentials/credentials.h" + +struct proxy_data { + struct ldb_context *upstream; + struct ldb_dn *olddn; + struct ldb_dn *newdn; + const char **oldstr; + const char **newstr; +}; + +struct proxy_ctx { + struct ldb_module *module; + struct ldb_request *req; + +#ifdef DEBUG_PROXY + int count; +#endif +}; + +/* + load the @PROXYINFO record +*/ +static int load_proxy_info(struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct proxy_data *proxy = talloc_get_type(ldb_module_get_private(module), struct proxy_data); + struct ldb_dn *dn; + struct ldb_result *res = NULL; + int ret; + const char *olddn, *newdn, *url, *username, *password, *oldstr, *newstr; + struct cli_credentials *creds; + bool ok; + + /* see if we have already loaded it */ + if (proxy->upstream != NULL) { + return LDB_SUCCESS; + } + + dn = ldb_dn_new(proxy, ldb, "@PROXYINFO"); + if (dn == NULL) { + goto failed; + } + ret = ldb_search(ldb, proxy, &res, dn, LDB_SCOPE_BASE, NULL, NULL); + talloc_free(dn); + if (ret != LDB_SUCCESS || res->count != 1) { + ldb_debug(ldb, LDB_DEBUG_FATAL, "Can't find @PROXYINFO\n"); + goto failed; + } + + url = ldb_msg_find_attr_as_string(res->msgs[0], "url", NULL); + olddn = ldb_msg_find_attr_as_string(res->msgs[0], "olddn", NULL); + newdn = ldb_msg_find_attr_as_string(res->msgs[0], "newdn", NULL); + username = ldb_msg_find_attr_as_string(res->msgs[0], "username", NULL); + password = ldb_msg_find_attr_as_string(res->msgs[0], "password", NULL); + oldstr = ldb_msg_find_attr_as_string(res->msgs[0], "oldstr", NULL); + newstr = ldb_msg_find_attr_as_string(res->msgs[0], "newstr", NULL); + + if (url == NULL || olddn == NULL || newdn == NULL || username == NULL || password == NULL) { + ldb_debug(ldb, LDB_DEBUG_FATAL, "Need url, olddn, newdn, oldstr, newstr, username and password in @PROXYINFO\n"); + goto failed; + } + + proxy->olddn = ldb_dn_new(proxy, ldb, olddn); + if (proxy->olddn == NULL) { + ldb_debug(ldb, LDB_DEBUG_FATAL, "Failed to explode olddn '%s'\n", olddn); + goto failed; + } + + proxy->newdn = ldb_dn_new(proxy, ldb, newdn); + if (proxy->newdn == NULL) { + ldb_debug(ldb, LDB_DEBUG_FATAL, "Failed to explode newdn '%s'\n", newdn); + goto failed; + } + + proxy->upstream = ldb_init(proxy, ldb_get_event_context(ldb)); + if (proxy->upstream == NULL) { + ldb_oom(ldb); + goto failed; + } + + proxy->oldstr = str_list_make(proxy, oldstr, ", "); + if (proxy->oldstr == NULL) { + ldb_oom(ldb); + goto failed; + } + + proxy->newstr = str_list_make(proxy, newstr, ", "); + if (proxy->newstr == NULL) { + ldb_oom(ldb); + goto failed; + } + + /* setup credentials for connection */ + creds = cli_credentials_init(proxy->upstream); + if (creds == NULL) { + ldb_oom(ldb); + goto failed; + } + ok = cli_credentials_guess(creds, ldb_get_opaque(ldb, "loadparm")); + if (!ok) { + ldb_oom(ldb); + goto failed; + } + + cli_credentials_set_username(creds, username, CRED_SPECIFIED); + cli_credentials_set_password(creds, password, CRED_SPECIFIED); + + ldb_set_opaque(proxy->upstream, "credentials", creds); + + ret = ldb_connect(proxy->upstream, url, 0, NULL); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_FATAL, "proxy failed to connect to %s\n", url); + goto failed; + } + + ldb_debug(ldb, LDB_DEBUG_TRACE, "proxy connected to %s\n", url); + + talloc_free(res); + + return LDB_SUCCESS; + +failed: + talloc_free(res); + talloc_free(proxy->olddn); + talloc_free(proxy->newdn); + talloc_free(proxy->upstream); + proxy->upstream = NULL; + return ldb_operr(ldb); +} + + +/* + convert a binary blob +*/ +static void proxy_convert_blob(TALLOC_CTX *mem_ctx, struct ldb_val *v, + const char *oldstr, const char *newstr) +{ + size_t len1, len2, len3; + uint8_t *olddata = v->data; + char *p = strcasestr((char *)v->data, oldstr); + + len1 = (p - (char *)v->data); + len2 = strlen(newstr); + len3 = v->length - (p+strlen(oldstr) - (char *)v->data); + v->length = len1+len2+len3; + v->data = talloc_size(mem_ctx, v->length); + memcpy(v->data, olddata, len1); + memcpy(v->data+len1, newstr, len2); + memcpy(v->data+len1+len2, olddata + len1 + strlen(oldstr), len3); +} + +/* + convert a returned value +*/ +static void proxy_convert_value(struct proxy_data *proxy, struct ldb_message *msg, struct ldb_val *v) +{ + size_t i; + + for (i=0;proxy->oldstr[i];i++) { + char *p = strcasestr((char *)v->data, proxy->oldstr[i]); + if (p == NULL) continue; + proxy_convert_blob(msg, v, proxy->oldstr[i], proxy->newstr[i]); + } +} + + +/* + convert a returned value +*/ +static struct ldb_parse_tree *proxy_convert_tree(TALLOC_CTX *mem_ctx, + struct proxy_data *proxy, + struct ldb_parse_tree *tree) +{ + size_t i; + char *expression = ldb_filter_from_tree(mem_ctx, tree); + + for (i=0;proxy->newstr[i];i++) { + struct ldb_val v; + char *p = strcasestr(expression, proxy->newstr[i]); + if (p == NULL) continue; + v.data = (uint8_t *)expression; + v.length = strlen(expression)+1; + proxy_convert_blob(mem_ctx, &v, proxy->newstr[i], proxy->oldstr[i]); + return ldb_parse_tree(mem_ctx, (const char *)v.data); + } + return tree; +} + + + +/* + convert a returned record +*/ +static void proxy_convert_record(struct ldb_context *ldb, + struct proxy_data *proxy, + struct ldb_message *msg) +{ + unsigned int attr, v; + + /* fix the message DN */ + if (ldb_dn_compare_base(proxy->olddn, msg->dn) == 0) { + ldb_dn_remove_base_components(msg->dn, ldb_dn_get_comp_num(proxy->olddn)); + ldb_dn_add_base(msg->dn, proxy->newdn); + } + + /* fix any attributes */ + for (attr=0;attr<msg->num_elements;attr++) { + for (v=0;v<msg->elements[attr].num_values;v++) { + proxy_convert_value(proxy, msg, &msg->elements[attr].values[v]); + } + } + + /* fix any DN components */ + for (attr=0;attr<msg->num_elements;attr++) { + for (v=0;v<msg->elements[attr].num_values;v++) { + proxy_convert_value(proxy, msg, &msg->elements[attr].values[v]); + } + } +} + +static int proxy_search_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct proxy_data *proxy; + struct proxy_ctx *ac; + int ret; + + ac = talloc_get_type(req->context, struct proxy_ctx); + ldb = ldb_module_get_ctx(ac->module); + proxy = talloc_get_type(ldb_module_get_private(module), struct proxy_data); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(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: + +#ifdef DEBUG_PROXY + ac->count++; +#endif + proxy_convert_record(ldb, proxy, ares->message); + ret = ldb_module_send_entry(ac->req, ares->message, ares->controls); + break; + + case LDB_REPLY_REFERRAL: + + /* ignore remote referrals */ + break; + + case LDB_REPLY_DONE: + +#ifdef DEBUG_PROXY + printf("# record %d\n", ac->count+1); +#endif + + return ldb_module_done(ac->req, NULL, NULL, LDB_SUCCESS); + } + + talloc_free(ares); + return ret; +} + +/* search */ +static int proxy_search_bytree(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct proxy_ctx *ac; + struct ldb_parse_tree *newtree; + struct proxy_data *proxy = talloc_get_type(ldb_module_get_private(module), struct proxy_data); + struct ldb_request *newreq; + struct ldb_dn *base; + unsigned int i; + int ret; + + ldb = ldb_module_get_ctx(module); + + if (req->op.search.base == NULL || + (req->op.search.base->comp_num == 1 && + req->op.search.base->components[0].name[0] == '@')) { + goto passthru; + } + + if (load_proxy_info(module) != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + /* see if the dn is within olddn */ + if (ldb_dn_compare_base(proxy->newdn, req->op.search.base) != 0) { + goto passthru; + } + + ac = talloc(req, struct proxy_ctx); + if (ac == NULL) { + return ldb_oom(ldb); + } + + ac->module = module; + ac->req = req; +#ifdef DEBUG_PROXY + ac->count = 0; +#endif + + newtree = proxy_convert_tree(ac, proxy, req->op.search.tree); + if (newtree == NULL) { + goto failed; + } + + /* convert the basedn of this search */ + base = ldb_dn_copy(ac, req->op.search.base); + if (base == NULL) { + goto failed; + } + ldb_dn_remove_base_components(base, ldb_dn_get_comp_num(proxy->newdn)); + ldb_dn_add_base(base, proxy->olddn); + + ldb_debug(ldb, LDB_DEBUG_FATAL, "proxying: '%s' with dn '%s' \n", + ldb_filter_from_tree(ac, newreq->op.search.tree), ldb_dn_get_linearized(newreq->op.search.base)); + for (i = 0; req->op.search.attrs && req->op.search.attrs[i]; i++) { + ldb_debug(ldb, LDB_DEBUG_FATAL, "attr: '%s'\n", req->op.search.attrs[i]); + } + + ret = ldb_build_search_req_ex(&newreq, ldb, ac, + base, req->op.search.scope, + newtree, req->op.search.attrs, + req->controls, + ac, proxy_search_callback, + req); + LDB_REQ_SET_LOCATION(newreq); + /* FIXME: warning, need a real event system hooked up for this to work properly, + * for now this makes the module *not* ASYNC */ + ret = ldb_request(proxy->upstream, newreq); + if (ret != LDB_SUCCESS) { + ldb_set_errstring(ldb, ldb_errstring(proxy->upstream)); + } + ret = ldb_wait(newreq->handle, LDB_WAIT_ALL); + if (ret != LDB_SUCCESS) { + ldb_set_errstring(ldb, ldb_errstring(proxy->upstream)); + } + return ret; + +failed: + ldb_debug(ldb, LDB_DEBUG_TRACE, "proxy failed for %s\n", + ldb_dn_get_linearized(req->op.search.base)); + +passthru: + return ldb_next_request(module, req); +} + +static int proxy_request(struct ldb_module *module, struct ldb_request *req) +{ + switch (req->operation) { + + case LDB_REQ_SEARCH: + return proxy_search_bytree(module, req); + + default: + return ldb_next_request(module, req); + + } +} + +static const struct ldb_module_ops ldb_proxy_module_ops = { + .name = "proxy", + .request = proxy_request +}; + +int ldb_proxy_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_proxy_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/ranged_results.c b/source4/dsdb/samdb/ldb_modules/ranged_results.c new file mode 100644 index 0000000..b010abb --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/ranged_results.c @@ -0,0 +1,295 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett 2007 + + 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 ranged results module + * + * Description: munge AD-style 'ranged results' requests into + * requests for all values in an attribute, then return the range to + * the client. + * + * Author: Andrew Bartlett + */ + +#include "includes.h" +#include "ldb_module.h" + +#undef strncasecmp + +struct rr_context { + struct ldb_module *module; + struct ldb_request *req; + bool dirsync_in_use; +}; + +static struct rr_context *rr_init_context(struct ldb_module *module, + struct ldb_request *req) +{ + struct ldb_control *dirsync_control = NULL; + struct rr_context *ac = talloc_zero(req, struct rr_context); + if (ac == NULL) { + ldb_set_errstring(ldb_module_get_ctx(module), "Out of Memory"); + return NULL; + } + + ac->module = module; + ac->req = req; + + /* + * check if there's a dirsync control (as there is an + * interaction between these modules) + */ + dirsync_control = ldb_request_get_control(req, + LDB_CONTROL_DIRSYNC_OID); + if (dirsync_control != NULL) { + ac->dirsync_in_use = true; + } + + return ac; +} + +static int rr_search_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct rr_context *ac; + unsigned int i, j; + TALLOC_CTX *temp_ctx; + + ac = talloc_get_type(req->context, struct rr_context); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + if (ares->type == LDB_REPLY_REFERRAL) { + return ldb_module_send_referral(ac->req, ares->referral); + } + + if (ares->type == LDB_REPLY_DONE) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + if (ac->dirsync_in_use) { + /* + * We return full attribute values when mixed with + * dirsync + */ + return ldb_module_send_entry(ac->req, + ares->message, + ares->controls); + } + /* LDB_REPLY_ENTRY */ + + temp_ctx = talloc_new(ac->req); + if (!temp_ctx) { + ldb_module_oom(ac->module); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + /* Find those that are range requests from the attribute list */ + for (i = 0; ac->req->op.search.attrs[i]; i++) { + char *p, *new_attr; + const char *end_str; + unsigned int start, end; + struct ldb_message_element *el; + struct ldb_val *orig_values; + + p = strchr(ac->req->op.search.attrs[i], ';'); + if (!p) { + continue; + } + if (strncasecmp(p, ";range=", strlen(";range=")) != 0) { + continue; + } + if (sscanf(p, ";range=%u-%u", &start, &end) != 2) { + if (sscanf(p, ";range=%u-*", &start) == 1) { + end = (unsigned int)-1; + } else { + continue; + } + } + new_attr = talloc_strndup(temp_ctx, + ac->req->op.search.attrs[i], + (size_t)(p - ac->req->op.search.attrs[i])); + + if (!new_attr) { + ldb_oom(ldb); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + el = ldb_msg_find_element(ares->message, new_attr); + talloc_free(new_attr); + if (!el) { + continue; + } + if (end >= (el->num_values - 1)) { + /* Need to leave the requested attribute in + * there (so add an empty one to match) */ + end_str = "*"; + end = el->num_values - 1; + } else { + end_str = talloc_asprintf(temp_ctx, "%u", end); + if (!end_str) { + ldb_oom(ldb); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + } + /* If start is greater then where we are find the end to be */ + if (start > end) { + el->num_values = 0; + el->values = NULL; + } else { + orig_values = el->values; + + if ((start + end < start) || (start + end < end)) { + ldb_asprintf_errstring(ldb, + "range request error: start or end would overflow!"); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_UNWILLING_TO_PERFORM); + } + + el->num_values = 0; + + el->values = talloc_array(ares->message->elements, + struct ldb_val, + (end - start) + 1); + if (!el->values) { + ldb_oom(ldb); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + for (j=start; j <= end; j++) { + el->values[el->num_values] = orig_values[j]; + el->num_values++; + } + } + el->name = talloc_asprintf(ares->message->elements, + "%s;range=%u-%s", el->name, start, + end_str); + if (!el->name) { + ldb_oom(ldb); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + } + + talloc_free(temp_ctx); + + return ldb_module_send_entry(ac->req, ares->message, ares->controls); +} + +/* search */ +static int rr_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + unsigned int i; + unsigned int start, end; + const char **new_attrs = NULL; + bool found_rr = false; + struct ldb_request *down_req; + struct rr_context *ac; + int ret; + + ldb = ldb_module_get_ctx(module); + + /* Strip the range request from the attribute */ + for (i = 0; req->op.search.attrs && req->op.search.attrs[i]; i++) { + char *p; + size_t range_len = strlen(";range="); + + new_attrs = talloc_realloc(req, new_attrs, const char *, i+2); + new_attrs[i] = req->op.search.attrs[i]; + new_attrs[i+1] = NULL; + p = strchr(new_attrs[i], ';'); + if (!p) { + continue; + } + if (strncasecmp(p, ";range=", range_len) != 0) { + continue; + } + end = (unsigned int)-1; + if (sscanf(p + range_len, "%u-*", &start) != 1) { + if (sscanf(p + range_len, "%u-%u", &start, &end) != 2) { + ldb_asprintf_errstring(ldb, + "range request error: " + "range request malformed"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + if (start > end) { + ldb_asprintf_errstring(ldb, "range request error: start must not be greater than end"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + found_rr = true; + new_attrs[i] = talloc_strndup(new_attrs, new_attrs[i], + (size_t)(p - new_attrs[i])); + + if (!new_attrs[i]) { + return ldb_oom(ldb); + } + } + + if (found_rr) { + ac = rr_init_context(module, req); + if (!ac) { + return ldb_operr(ldb); + } + + ret = ldb_build_search_req_ex(&down_req, ldb, ac, + req->op.search.base, + req->op.search.scope, + req->op.search.tree, + new_attrs, + req->controls, + ac, rr_search_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(module, down_req); + } + + /* No change, just run the original request as if we were never here */ + talloc_free(new_attrs); + return ldb_next_request(module, req); +} + +static const struct ldb_module_ops ldb_ranged_results_module_ops = { + .name = "ranged_results", + .search = rr_search, +}; + +int ldb_ranged_results_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_ranged_results_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/repl_meta_data.c b/source4/dsdb/samdb/ldb_modules/repl_meta_data.c new file mode 100644 index 0000000..175a02d --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/repl_meta_data.c @@ -0,0 +1,8658 @@ +/* + ldb database library + + Copyright (C) Simo Sorce 2004-2008 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2005-2013 + Copyright (C) Andrew Tridgell 2005-2009 + Copyright (C) Stefan Metzmacher <metze@samba.org> 2007 + Copyright (C) Matthieu Patou <mat@samba.org> 2010-2011 + + 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 repl_meta_data module + * + * Description: - add a unique objectGUID onto every new record, + * - handle whenCreated, whenChanged timestamps + * - handle uSNCreated, uSNChanged numbers + * - handle replPropertyMetaData attribute + * + * Author: Simo Sorce + * Author: Stefan Metzmacher + */ + +#include "includes.h" +#include "ldb_module.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/common/proto.h" +#include "dsdb/common/util.h" +#include "../libds/common/flags.h" +#include "librpc/gen_ndr/irpc.h" +#include "librpc/gen_ndr/ndr_misc.h" +#include "librpc/gen_ndr/ndr_drsuapi.h" +#include "librpc/gen_ndr/ndr_drsblobs.h" +#include "param/param.h" +#include "libcli/security/security.h" +#include "lib/util/dlinklist.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "lib/util/tsort.h" +#include "lib/util/binsearch.h" + +#undef strcasecmp + +#undef DBGC_CLASS +#define DBGC_CLASS DBGC_DRS_REPL + +/* the RMD_VERSION for linked attributes starts from 1 */ +#define RMD_VERSION_INITIAL 1 + +/* + * It's 29/12/9999 at 23:59:59 UTC as specified in MS-ADTS 7.1.1.4.2 + * Deleted Objects Container + */ +static const NTTIME DELETED_OBJECT_CONTAINER_CHANGE_TIME = 2650466015990000000ULL; + +struct replmd_private { + TALLOC_CTX *la_ctx; + struct la_group *la_list; + struct nc_entry { + struct nc_entry *prev, *next; + struct ldb_dn *dn; + uint64_t mod_usn; + uint64_t mod_usn_urgent; + } *ncs; + struct ldb_dn *schema_dn; + bool originating_updates; + bool sorted_links; + uint32_t total_links; + uint32_t num_processed; + bool recyclebin_enabled; + bool recyclebin_state_known; +}; + +/* + * groups link attributes together by source-object and attribute-ID, + * to improve processing efficiency (i.e. for 'member' attribute, which + * could have 100s or 1000s of links). + * Note this grouping is best effort - the same source object could still + * correspond to several la_groups (a lot depends on the order DRS sends + * the links in). The groups currently don't span replication chunks (which + * caps the size to ~1500 links by default). + */ +struct la_group { + struct la_group *next, *prev; + struct la_entry *la_entries; +}; + +struct la_entry { + struct la_entry *next, *prev; + struct drsuapi_DsReplicaLinkedAttribute *la; + uint32_t dsdb_repl_flags; +}; + +struct replmd_replicated_request { + struct ldb_module *module; + struct ldb_request *req; + + const struct dsdb_schema *schema; + struct GUID our_invocation_id; + + /* the controls we pass down */ + struct ldb_control **controls; + + /* + * Backlinks for the replmd_add() case (we want to create + * backlinks after creating the user, but before the end of + * the ADD request) + */ + struct la_backlink *la_backlinks; + + /* details for the mode where we apply a bunch of inbound replication meessages */ + bool apply_mode; + uint32_t index_current; + struct dsdb_extended_replicated_objects *objs; + + struct ldb_message *search_msg; + struct GUID local_parent_guid; + + uint64_t seq_num; + bool is_urgent; + + bool isDeleted; + + bool fix_link_sid; +}; + +/* + * the result of replmd_process_linked_attribute(): either there was no change + * (update was ignored), a new link was added (either inactive or active), or + * an existing link was modified (active/inactive status may have changed). + */ +typedef enum { + LINK_CHANGE_NONE, + LINK_CHANGE_ADDED, + LINK_CHANGE_MODIFIED, +} replmd_link_changed; + +static int replmd_replicated_apply_merge(struct replmd_replicated_request *ar); +static int replmd_delete_internals(struct ldb_module *module, struct ldb_request *req, bool re_delete); +static int replmd_check_upgrade_links(struct ldb_context *ldb, + struct parsed_dn *dns, uint32_t count, + struct ldb_message_element *el, + const char *ldap_oid); +static int replmd_verify_link_target(struct replmd_replicated_request *ar, + TALLOC_CTX *mem_ctx, + struct la_entry *la_entry, + struct ldb_dn *src_dn, + const struct dsdb_attribute *attr); +static int replmd_get_la_entry_source(struct ldb_module *module, + struct la_entry *la_entry, + TALLOC_CTX *mem_ctx, + const struct dsdb_attribute **ret_attr, + struct ldb_message **source_msg); +static int replmd_set_la_val(TALLOC_CTX *mem_ctx, struct ldb_val *v, struct dsdb_dn *dsdb_dn, + struct dsdb_dn *old_dsdb_dn, const struct GUID *invocation_id, + uint64_t usn, uint64_t local_usn, NTTIME nttime, + uint32_t version, bool deleted); + +static int replmd_make_deleted_child_dn(TALLOC_CTX *tmp_ctx, + struct ldb_context *ldb, + struct ldb_dn *dn, + const char *rdn_name, + const struct ldb_val *rdn_value, + struct GUID guid); + +enum urgent_situation { + REPL_URGENT_ON_CREATE = 1, + REPL_URGENT_ON_UPDATE = 2, + REPL_URGENT_ON_DELETE = 4 +}; + +enum deletion_state { + OBJECT_NOT_DELETED=1, + OBJECT_DELETED=2, + OBJECT_RECYCLED=3, + OBJECT_TOMBSTONE=4, + OBJECT_REMOVED=5 +}; + +static bool replmd_recyclebin_enabled(struct ldb_module *module) +{ + bool enabled = false; + struct replmd_private *replmd_private = + talloc_get_type_abort(ldb_module_get_private(module), + struct replmd_private); + + /* + * only lookup the recycle-bin state once per replication, then cache + * the result. This can save us 1000s of DB searches + */ + if (!replmd_private->recyclebin_state_known) { + int ret = dsdb_recyclebin_enabled(module, &enabled); + if (ret != LDB_SUCCESS) { + return false; + } + + replmd_private->recyclebin_enabled = enabled; + replmd_private->recyclebin_state_known = true; + } + + return replmd_private->recyclebin_enabled; +} + +static void replmd_deletion_state(struct ldb_module *module, + const struct ldb_message *msg, + enum deletion_state *current_state, + enum deletion_state *next_state) +{ + bool enabled = false; + + if (msg == NULL) { + *current_state = OBJECT_REMOVED; + if (next_state != NULL) { + *next_state = OBJECT_REMOVED; + } + return; + } + + enabled = replmd_recyclebin_enabled(module); + + if (ldb_msg_check_string_attribute(msg, "isDeleted", "TRUE")) { + if (!enabled) { + *current_state = OBJECT_TOMBSTONE; + if (next_state != NULL) { + *next_state = OBJECT_REMOVED; + } + return; + } + + if (ldb_msg_check_string_attribute(msg, "isRecycled", "TRUE")) { + *current_state = OBJECT_RECYCLED; + if (next_state != NULL) { + *next_state = OBJECT_REMOVED; + } + return; + } + + *current_state = OBJECT_DELETED; + if (next_state != NULL) { + *next_state = OBJECT_RECYCLED; + } + return; + } + + *current_state = OBJECT_NOT_DELETED; + if (next_state == NULL) { + return; + } + + if (enabled) { + *next_state = OBJECT_DELETED; + } else { + *next_state = OBJECT_TOMBSTONE; + } +} + +static const struct { + const char *update_name; + enum urgent_situation repl_situation; +} urgent_objects[] = { + {"nTDSDSA", (REPL_URGENT_ON_CREATE | REPL_URGENT_ON_DELETE)}, + {"crossRef", (REPL_URGENT_ON_CREATE | REPL_URGENT_ON_DELETE)}, + {"attributeSchema", (REPL_URGENT_ON_CREATE | REPL_URGENT_ON_UPDATE)}, + {"classSchema", (REPL_URGENT_ON_CREATE | REPL_URGENT_ON_UPDATE)}, + {"secret", (REPL_URGENT_ON_CREATE | REPL_URGENT_ON_UPDATE)}, + {"rIDManager", (REPL_URGENT_ON_CREATE | REPL_URGENT_ON_UPDATE)}, + {NULL, 0} +}; + +/* Attributes looked for when updating or deleting, to check for a urgent replication needed */ +static const char *urgent_attrs[] = { + "lockoutTime", + "pwdLastSet", + "userAccountControl", + NULL +}; + + +static bool replmd_check_urgent_objectclass(const struct ldb_message_element *objectclass_el, + enum urgent_situation situation) +{ + unsigned int i, j; + for (i=0; urgent_objects[i].update_name; i++) { + + if ((situation & urgent_objects[i].repl_situation) == 0) { + continue; + } + + for (j=0; j<objectclass_el->num_values; j++) { + const struct ldb_val *v = &objectclass_el->values[j]; + if (ldb_attr_cmp((const char *)v->data, urgent_objects[i].update_name) == 0) { + return true; + } + } + } + return false; +} + +static bool replmd_check_urgent_attribute(const struct ldb_message_element *el) +{ + if (ldb_attr_in_list(urgent_attrs, el->name)) { + return true; + } + return false; +} + +static int replmd_replicated_apply_isDeleted(struct replmd_replicated_request *ar); + +/* + initialise the module + allocate the private structure and build the list + of partition DNs for use by replmd_notify() + */ +static int replmd_init(struct ldb_module *module) +{ + struct replmd_private *replmd_private; + struct ldb_context *ldb = ldb_module_get_ctx(module); + int ret; + + replmd_private = talloc_zero(module, struct replmd_private); + if (replmd_private == NULL) { + ldb_oom(ldb); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = dsdb_check_samba_compatible_feature(module, + SAMBA_SORTED_LINKS_FEATURE, + &replmd_private->sorted_links); + if (ret != LDB_SUCCESS) { + talloc_free(replmd_private); + return ret; + } + + replmd_private->schema_dn = ldb_get_schema_basedn(ldb); + ldb_module_set_private(module, replmd_private); + return ldb_next_init(module); +} + +/* + cleanup our per-transaction contexts + */ +static void replmd_txn_cleanup(struct replmd_private *replmd_private) +{ + talloc_free(replmd_private->la_ctx); + replmd_private->la_list = NULL; + replmd_private->la_ctx = NULL; + replmd_private->recyclebin_state_known = false; +} + + +struct la_backlink { + struct la_backlink *next, *prev; + const char *attr_name; + struct ldb_dn *forward_dn; + struct GUID target_guid; + bool active; +}; + +/* + a ldb_modify request operating on modules below the + current module + */ +static int linked_attr_modify(struct ldb_module *module, + const struct ldb_message *message, + struct ldb_request *parent) +{ + struct ldb_request *mod_req; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + TALLOC_CTX *tmp_ctx = talloc_new(module); + struct ldb_result *res; + + res = talloc_zero(tmp_ctx, struct ldb_result); + if (!res) { + talloc_free(tmp_ctx); + return ldb_oom(ldb_module_get_ctx(module)); + } + + ret = ldb_build_mod_req(&mod_req, ldb, tmp_ctx, + message, + NULL, + res, + ldb_modify_default_callback, + parent); + LDB_REQ_SET_LOCATION(mod_req); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = ldb_request_add_control(mod_req, DSDB_CONTROL_REPLICATED_UPDATE_OID, + false, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* Run the new request */ + ret = ldb_next_request(module, mod_req); + + if (ret == LDB_SUCCESS) { + ret = ldb_wait(mod_req->handle, LDB_WAIT_ALL); + } + + talloc_free(tmp_ctx); + return ret; +} + +/* + process a backlinks we accumulated during a transaction, adding and + deleting the backlinks from the target objects + */ +static int replmd_process_backlink(struct ldb_module *module, struct la_backlink *bl, struct ldb_request *parent) +{ + struct ldb_dn *target_dn, *source_dn; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_message *msg; + TALLOC_CTX *frame = talloc_stackframe(); + char *dn_string; + + /* + - find DN of target + - find DN of source + - construct ldb_message + - either an add or a delete + */ + ret = dsdb_module_dn_by_guid(module, frame, &bl->target_guid, &target_dn, parent); + if (ret != LDB_SUCCESS) { + struct GUID_txt_buf guid_str; + DBG_WARNING("Failed to find target DN for linked attribute with GUID %s\n", + GUID_buf_string(&bl->target_guid, &guid_str)); + DBG_WARNING("Please run 'samba-tool dbcheck' to resolve any missing backlinks.\n"); + talloc_free(frame); + return LDB_SUCCESS; + } + + msg = ldb_msg_new(frame); + if (msg == NULL) { + ldb_module_oom(module); + talloc_free(frame); + return LDB_ERR_OPERATIONS_ERROR; + } + + source_dn = ldb_dn_copy(frame, bl->forward_dn); + if (!source_dn) { + ldb_module_oom(module); + talloc_free(frame); + return LDB_ERR_OPERATIONS_ERROR; + } else { + /* Filter down to the attributes we want in the backlink */ + const char *accept[] = { "GUID", "SID", NULL }; + ldb_dn_extended_filter(source_dn, accept); + } + + /* construct a ldb_message for adding/deleting the backlink */ + msg->dn = target_dn; + dn_string = ldb_dn_get_extended_linearized(frame, bl->forward_dn, 1); + if (!dn_string) { + ldb_module_oom(module); + talloc_free(frame); + return LDB_ERR_OPERATIONS_ERROR; + } + ret = ldb_msg_add_steal_string(msg, bl->attr_name, dn_string); + if (ret != LDB_SUCCESS) { + talloc_free(frame); + return ret; + } + msg->elements[0].flags = bl->active?LDB_FLAG_MOD_ADD:LDB_FLAG_MOD_DELETE; + + /* a backlink should never be single valued. Unfortunately the + exchange schema has a attribute + msExchBridgeheadedLocalConnectorsDNBL which is single + valued and a backlink. We need to cope with that by + ignoring the single value flag */ + msg->elements[0].flags |= LDB_FLAG_INTERNAL_DISABLE_SINGLE_VALUE_CHECK; + + ret = dsdb_module_modify(module, msg, DSDB_FLAG_NEXT_MODULE, parent); + if (ret == LDB_ERR_NO_SUCH_ATTRIBUTE && !bl->active) { + /* we allow LDB_ERR_NO_SUCH_ATTRIBUTE as success to + cope with possible corruption where the backlink has + already been removed */ + DEBUG(3,("WARNING: backlink from %s already removed from %s - %s\n", + ldb_dn_get_linearized(target_dn), + ldb_dn_get_linearized(source_dn), + ldb_errstring(ldb))); + ret = LDB_SUCCESS; + } else if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to %s backlink from %s to %s - %s", + bl->active?"add":"remove", + ldb_dn_get_linearized(source_dn), + ldb_dn_get_linearized(target_dn), + ldb_errstring(ldb)); + talloc_free(frame); + return ret; + } + talloc_free(frame); + return ret; +} + +/* + add a backlink to the list of backlinks to add/delete in the prepare + commit + + forward_dn is stolen onto the defereed context + */ +static int replmd_defer_add_backlink(struct ldb_module *module, + struct replmd_private *replmd_private, + const struct dsdb_schema *schema, + struct replmd_replicated_request *ac, + struct ldb_dn *forward_dn, + struct GUID *target_guid, bool active, + const struct dsdb_attribute *schema_attr, + struct ldb_request *parent) +{ + const struct dsdb_attribute *target_attr; + struct la_backlink *bl; + + bl = talloc(ac, struct la_backlink); + if (bl == NULL) { + ldb_module_oom(module); + return LDB_ERR_OPERATIONS_ERROR; + } + + target_attr = dsdb_attribute_by_linkID(schema, schema_attr->linkID ^ 1); + if (!target_attr) { + /* + * windows 2003 has a broken schema where the + * definition of msDS-IsDomainFor is missing (which is + * supposed to be the backlink of the + * msDS-HasDomainNCs attribute + */ + return LDB_SUCCESS; + } + + bl->attr_name = target_attr->lDAPDisplayName; + bl->forward_dn = talloc_steal(bl, forward_dn); + bl->target_guid = *target_guid; + bl->active = active; + + DLIST_ADD(ac->la_backlinks, bl); + + return LDB_SUCCESS; +} + +/* + add a backlink to the list of backlinks to add/delete in the prepare + commit + */ +static int replmd_add_backlink(struct ldb_module *module, + struct replmd_private *replmd_private, + const struct dsdb_schema *schema, + struct ldb_dn *forward_dn, + struct GUID *target_guid, bool active, + const struct dsdb_attribute *schema_attr, + struct ldb_request *parent) +{ + const struct dsdb_attribute *target_attr; + struct la_backlink bl; + int ret; + + target_attr = dsdb_attribute_by_linkID(schema, schema_attr->linkID ^ 1); + if (!target_attr) { + /* + * windows 2003 has a broken schema where the + * definition of msDS-IsDomainFor is missing (which is + * supposed to be the backlink of the + * msDS-HasDomainNCs attribute + */ + return LDB_SUCCESS; + } + + bl.attr_name = target_attr->lDAPDisplayName; + bl.forward_dn = forward_dn; + bl.target_guid = *target_guid; + bl.active = active; + + ret = replmd_process_backlink(module, &bl, parent); + return ret; +} + + +/* + * Callback for most write operations in this module: + * + * notify the repl task that a object has changed. The notifies are + * gathered up in the replmd_private structure then written to the + * @REPLCHANGED object in each partition during the prepare_commit + */ +static int replmd_op_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + int ret; + struct replmd_replicated_request *ac = + talloc_get_type_abort(req->context, struct replmd_replicated_request); + struct replmd_private *replmd_private = + talloc_get_type_abort(ldb_module_get_private(ac->module), struct replmd_private); + struct nc_entry *modified_partition; + struct ldb_control *partition_ctrl; + const struct dsdb_control_current_partition *partition; + + struct ldb_control **controls; + + partition_ctrl = ldb_reply_get_control(ares, DSDB_CONTROL_CURRENT_PARTITION_OID); + + controls = ares->controls; + if (ldb_request_get_control(ac->req, + DSDB_CONTROL_CURRENT_PARTITION_OID) == NULL) { + /* + * Remove the current partition control from what we pass up + * the chain if it hasn't been requested manually. + */ + controls = ldb_controls_except_specified(ares->controls, ares, + partition_ctrl); + } + + if (ares->error != LDB_SUCCESS) { + struct GUID_txt_buf guid_txt; + struct ldb_message *msg = NULL; + char *s = NULL; + + if (ac->apply_mode == false) { + DBG_NOTICE("Originating update failure. Error is: %s\n", + ldb_strerror(ares->error)); + return ldb_module_done(ac->req, controls, + ares->response, ares->error); + } + + msg = ac->objs->objects[ac->index_current].msg; + /* + * Set at DBG_NOTICE as once these start to happe, they + * will happen a lot until resolved, due to repeated + * replication. The caller will probably print the + * ldb error string anyway. + */ + DBG_NOTICE("DRS replication apply failure for %s. Error is: %s\n", + ldb_dn_get_linearized(msg->dn), + ldb_strerror(ares->error)); + + s = ldb_ldif_message_redacted_string(ldb_module_get_ctx(ac->module), + ac, + LDB_CHANGETYPE_ADD, + msg); + + DBG_INFO("Failing DRS %s replication message was %s:\n%s\n", + ac->search_msg == NULL ? "ADD" : "MODIFY", + GUID_buf_string(&ac->objs->objects[ac->index_current].object_guid, + &guid_txt), + s); + talloc_free(s); + return ldb_module_done(ac->req, controls, + ares->response, ares->error); + } + + if (ares->type != LDB_REPLY_DONE) { + ldb_set_errstring(ldb_module_get_ctx(ac->module), "Invalid reply type for notify\n!"); + return ldb_module_done(ac->req, NULL, + NULL, LDB_ERR_OPERATIONS_ERROR); + } + + if (ac->apply_mode == false) { + struct la_backlink *bl; + /* + * process our backlink list after an replmd_add(), + * creating and deleting backlinks as necessary (this + * code is sync). The other cases are handled inline + * with the modify. + */ + for (bl=ac->la_backlinks; bl; bl=bl->next) { + ret = replmd_process_backlink(ac->module, bl, ac->req); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, + NULL, ret); + } + } + } + + if (!partition_ctrl) { + ldb_set_errstring(ldb_module_get_ctx(ac->module),"No partition control on reply"); + return ldb_module_done(ac->req, NULL, + NULL, LDB_ERR_OPERATIONS_ERROR); + } + + partition = talloc_get_type_abort(partition_ctrl->data, + struct dsdb_control_current_partition); + + if (ac->seq_num > 0) { + for (modified_partition = replmd_private->ncs; modified_partition; + modified_partition = modified_partition->next) { + if (ldb_dn_compare(modified_partition->dn, partition->dn) == 0) { + break; + } + } + + if (modified_partition == NULL) { + modified_partition = talloc_zero(replmd_private, struct nc_entry); + if (!modified_partition) { + ldb_oom(ldb_module_get_ctx(ac->module)); + return ldb_module_done(ac->req, NULL, + NULL, LDB_ERR_OPERATIONS_ERROR); + } + modified_partition->dn = ldb_dn_copy(modified_partition, partition->dn); + if (!modified_partition->dn) { + ldb_oom(ldb_module_get_ctx(ac->module)); + return ldb_module_done(ac->req, NULL, + NULL, LDB_ERR_OPERATIONS_ERROR); + } + DLIST_ADD(replmd_private->ncs, modified_partition); + } + + if (ac->seq_num > modified_partition->mod_usn) { + modified_partition->mod_usn = ac->seq_num; + if (ac->is_urgent) { + modified_partition->mod_usn_urgent = ac->seq_num; + } + } + if (!ac->apply_mode) { + replmd_private->originating_updates = true; + } + } + + if (ac->apply_mode) { + ret = replmd_replicated_apply_isDeleted(ac); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + return ret; + } else { + /* free the partition control container here, for the + * common path. Other cases will have it cleaned up + * eventually with the ares */ + talloc_free(partition_ctrl); + return ldb_module_done(ac->req, controls, + ares->response, LDB_SUCCESS); + } +} + + +/* + * update a @REPLCHANGED record in each partition if there have been + * any writes of replicated data in the partition + */ +static int replmd_notify_store(struct ldb_module *module, struct ldb_request *parent) +{ + struct replmd_private *replmd_private = + talloc_get_type(ldb_module_get_private(module), struct replmd_private); + + while (replmd_private->ncs) { + int ret; + struct nc_entry *modified_partition = replmd_private->ncs; + + ret = dsdb_module_save_partition_usn(module, modified_partition->dn, + modified_partition->mod_usn, + modified_partition->mod_usn_urgent, parent); + if (ret != LDB_SUCCESS) { + DEBUG(0,(__location__ ": Failed to save partition uSN for %s\n", + ldb_dn_get_linearized(modified_partition->dn))); + return ret; + } + + if (ldb_dn_compare(modified_partition->dn, + replmd_private->schema_dn) == 0) { + struct ldb_result *ext_res; + ret = dsdb_module_extended(module, + replmd_private->schema_dn, + &ext_res, + DSDB_EXTENDED_SCHEMA_UPDATE_NOW_OID, + ext_res, + DSDB_FLAG_NEXT_MODULE, + parent); + if (ret != LDB_SUCCESS) { + return ret; + } + talloc_free(ext_res); + } + + DLIST_REMOVE(replmd_private->ncs, modified_partition); + talloc_free(modified_partition); + } + + return LDB_SUCCESS; +} + + +/* + created a replmd_replicated_request context + */ +static struct replmd_replicated_request *replmd_ctx_init(struct ldb_module *module, + struct ldb_request *req) +{ + struct ldb_context *ldb; + struct replmd_replicated_request *ac; + const struct GUID *our_invocation_id; + + ldb = ldb_module_get_ctx(module); + + ac = talloc_zero(req, struct replmd_replicated_request); + if (ac == NULL) { + ldb_oom(ldb); + return NULL; + } + + ac->module = module; + ac->req = req; + + ac->schema = dsdb_get_schema(ldb, ac); + if (!ac->schema) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "replmd_modify: no dsdb_schema loaded"); + DEBUG(0,(__location__ ": %s\n", ldb_errstring(ldb))); + talloc_free(ac); + return NULL; + } + + /* get our invocationId */ + our_invocation_id = samdb_ntds_invocation_id(ldb); + if (!our_invocation_id) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "replmd_add: unable to find invocationId\n"); + talloc_free(ac); + return NULL; + } + ac->our_invocation_id = *our_invocation_id; + + return ac; +} + +/* + add a time element to a record +*/ +static int add_time_element(struct ldb_message *msg, const char *attr, time_t t) +{ + struct ldb_message_element *el; + char *s; + int ret; + + if (ldb_msg_find_element(msg, attr) != NULL) { + return LDB_SUCCESS; + } + + s = ldb_timestring(msg, t); + if (s == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = ldb_msg_add_string(msg, attr, s); + if (ret != LDB_SUCCESS) { + return ret; + } + + el = ldb_msg_find_element(msg, attr); + /* always set as replace. This works because on add ops, the flag + is ignored */ + el->flags = LDB_FLAG_MOD_REPLACE; + + return LDB_SUCCESS; +} + +/* + add a uint64_t element to a record +*/ +static int add_uint64_element(struct ldb_context *ldb, struct ldb_message *msg, + const char *attr, uint64_t v) +{ + struct ldb_message_element *el; + int ret; + + if (ldb_msg_find_element(msg, attr) != NULL) { + return LDB_SUCCESS; + } + + ret = samdb_msg_add_uint64(ldb, msg, msg, attr, v); + if (ret != LDB_SUCCESS) { + return ret; + } + + el = ldb_msg_find_element(msg, attr); + /* always set as replace. This works because on add ops, the flag + is ignored */ + el->flags = LDB_FLAG_MOD_REPLACE; + + return LDB_SUCCESS; +} + +static int replmd_replPropertyMetaData1_attid_sort(const struct replPropertyMetaData1 *m1, + const struct replPropertyMetaData1 *m2) +{ + /* + * This assignment seems inoccous, but it is critical for the + * system, as we need to do the comparisons as a unsigned + * quantity, not signed (enums are signed integers) + */ + uint32_t attid_1 = m1->attid; + uint32_t attid_2 = m2->attid; + + if (attid_1 == attid_2) { + return 0; + } + + /* + * See above regarding this being an unsigned comparison. + * Otherwise when the high bit is set on non-standard + * attributes, they would end up first, before objectClass + * (0). + */ + return attid_1 > attid_2 ? 1 : -1; +} + +static int replmd_replPropertyMetaDataCtr1_verify(struct ldb_context *ldb, + struct replPropertyMetaDataCtr1 *ctr1, + struct ldb_dn *dn) +{ + if (ctr1->count == 0) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "No elements found in replPropertyMetaData for %s!\n", + ldb_dn_get_linearized(dn)); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + /* the objectClass attribute is value 0x00000000, so must be first */ + if (ctr1->array[0].attid != DRSUAPI_ATTID_objectClass) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "No objectClass found in replPropertyMetaData for %s!\n", + ldb_dn_get_linearized(dn)); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + + return LDB_SUCCESS; +} + +static int replmd_replPropertyMetaDataCtr1_sort_and_verify(struct ldb_context *ldb, + struct replPropertyMetaDataCtr1 *ctr1, + struct ldb_dn *dn) +{ + /* Note this is O(n^2) for the almost-sorted case, which this is */ + TYPESAFE_QSORT(ctr1->array, ctr1->count, + replmd_replPropertyMetaData1_attid_sort); + return replmd_replPropertyMetaDataCtr1_verify(ldb, ctr1, dn); +} + +static int replmd_ldb_message_element_attid_sort(const struct ldb_message_element *e1, + const struct ldb_message_element *e2, + const struct dsdb_schema *schema) +{ + const struct dsdb_attribute *a1; + const struct dsdb_attribute *a2; + + /* + * TODO: make this faster by caching the dsdb_attribute pointer + * on the ldb_messag_element + */ + + a1 = dsdb_attribute_by_lDAPDisplayName(schema, e1->name); + a2 = dsdb_attribute_by_lDAPDisplayName(schema, e2->name); + + /* + * TODO: remove this check, we should rely on e1 and e2 having valid attribute names + * in the schema + */ + if (!a1 || !a2) { + return strcasecmp(e1->name, e2->name); + } + if (a1->attributeID_id == a2->attributeID_id) { + return 0; + } + return a1->attributeID_id > a2->attributeID_id ? 1 : -1; +} + +static void replmd_ldb_message_sort(struct ldb_message *msg, + const struct dsdb_schema *schema) +{ + LDB_TYPESAFE_QSORT(msg->elements, msg->num_elements, schema, replmd_ldb_message_element_attid_sort); +} + +static int replmd_build_la_val(TALLOC_CTX *mem_ctx, struct ldb_val *v, struct dsdb_dn *dsdb_dn, + const struct GUID *invocation_id, + uint64_t local_usn, NTTIME nttime); + +static int parsed_dn_compare(struct parsed_dn *pdn1, struct parsed_dn *pdn2); + +static int get_parsed_dns(struct ldb_module *module, TALLOC_CTX *mem_ctx, + struct ldb_message_element *el, struct parsed_dn **pdn, + const char *ldap_oid, struct ldb_request *parent); + +static int check_parsed_dn_duplicates(struct ldb_module *module, + struct ldb_message_element *el, + struct parsed_dn *pdn); + +/* + fix up linked attributes in replmd_add. + This involves setting up the right meta-data in extended DN + components, and creating backlinks to the object + */ +static int replmd_add_fix_la(struct ldb_module *module, TALLOC_CTX *mem_ctx, + struct replmd_private *replmd_private, + struct ldb_message_element *el, + struct replmd_replicated_request *ac, + NTTIME now, + struct ldb_dn *forward_dn, + const struct dsdb_attribute *sa, + struct ldb_request *parent) +{ + unsigned int i; + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct parsed_dn *pdn; + /* We will take a reference to the schema in replmd_add_backlink */ + const struct dsdb_schema *schema = dsdb_get_schema(ldb, NULL); + struct ldb_val *new_values = NULL; + int ret; + + if (dsdb_check_single_valued_link(sa, el) == LDB_SUCCESS) { + el->flags |= LDB_FLAG_INTERNAL_DISABLE_SINGLE_VALUE_CHECK; + } else { + ldb_asprintf_errstring(ldb, + "Attribute %s is single valued but " + "more than one value has been supplied", + el->name); + talloc_free(tmp_ctx); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + /* + * At the successful end of these functions el->values is + * overwritten with new_values. However get_parsed_dns() + * points p->v at the supplied el and it effectively gets used + * as a working area by replmd_build_la_val(). So we must + * duplicate it because our caller only called + * ldb_msg_copy_shallow(). + */ + + el->values = talloc_memdup(tmp_ctx, + el->values, + sizeof(el->values[0]) * el->num_values); + if (el->values == NULL) { + ldb_module_oom(module); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = get_parsed_dns(module, tmp_ctx, el, &pdn, + sa->syntax->ldap_oid, parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = check_parsed_dn_duplicates(module, el, pdn); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + new_values = talloc_array(tmp_ctx, struct ldb_val, el->num_values); + if (new_values == NULL) { + ldb_module_oom(module); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + for (i = 0; i < el->num_values; i++) { + struct parsed_dn *p = &pdn[i]; + ret = replmd_build_la_val(new_values, p->v, p->dsdb_dn, + &ac->our_invocation_id, + ac->seq_num, now); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = replmd_defer_add_backlink(module, replmd_private, + schema, ac, + forward_dn, &p->guid, true, sa, + parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + new_values[i] = *p->v; + } + el->values = talloc_steal(mem_ctx, new_values); + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + +static int replmd_add_make_extended_dn(struct ldb_request *req, + const DATA_BLOB *guid_blob, + struct ldb_dn **_extended_dn) +{ + int ret; + const DATA_BLOB *sid_blob; + /* Calculate an extended DN for any linked attributes */ + struct ldb_dn *extended_dn = ldb_dn_copy(req, req->op.add.message->dn); + if (!extended_dn) { + return LDB_ERR_OPERATIONS_ERROR; + } + ret = ldb_dn_set_extended_component(extended_dn, "GUID", guid_blob); + if (ret != LDB_SUCCESS) { + return ret; + } + + sid_blob = ldb_msg_find_ldb_val(req->op.add.message, "objectSID"); + if (sid_blob != NULL) { + ret = ldb_dn_set_extended_component(extended_dn, "SID", sid_blob); + if (ret != LDB_SUCCESS) { + return ret; + } + } + *_extended_dn = extended_dn; + return LDB_SUCCESS; +} + +/* + intercept add requests + */ +static int replmd_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct ldb_control *control; + struct replmd_replicated_request *ac; + enum ndr_err_code ndr_err; + struct ldb_request *down_req; + struct ldb_message *msg; + const DATA_BLOB *guid_blob; + DATA_BLOB guid_blob_stack; + struct GUID guid; + uint8_t guid_data[16]; + struct replPropertyMetaDataBlob nmd; + struct ldb_val nmd_value; + struct ldb_dn *extended_dn = NULL; + + /* + * The use of a time_t here seems odd, but as the NTTIME + * elements are actually declared as NTTIME_1sec in the IDL, + * getting a higher resolution timestamp is not required. + */ + time_t t = time(NULL); + NTTIME now; + char *time_str; + int ret; + unsigned int i; + unsigned int functional_level; + uint32_t ni=0; + bool allow_add_guid = false; + bool remove_current_guid = false; + bool is_urgent = false; + bool is_schema_nc = false; + struct ldb_message_element *objectclass_el; + struct replmd_private *replmd_private = + talloc_get_type_abort(ldb_module_get_private(module), struct replmd_private); + + /* check if there's a show relax control (used by provision to say 'I know what I'm doing') */ + control = ldb_request_get_control(req, LDB_CONTROL_RELAX_OID); + if (control) { + allow_add_guid = true; + } + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.add.message->dn)) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + ldb_debug(ldb, LDB_DEBUG_TRACE, "replmd_add\n"); + + guid_blob = ldb_msg_find_ldb_val(req->op.add.message, "objectGUID"); + if (guid_blob != NULL) { + if (!allow_add_guid) { + ldb_set_errstring(ldb, + "replmd_add: it's not allowed to add an object with objectGUID!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } else { + NTSTATUS status = GUID_from_data_blob(guid_blob,&guid); + if (!NT_STATUS_IS_OK(status)) { + ldb_set_errstring(ldb, + "replmd_add: Unable to parse the 'objectGUID' as a GUID!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + /* we remove this attribute as it can be a string and + * will not be treated correctly and then we will re-add + * it later on in the good format */ + remove_current_guid = true; + } + } else { + /* a new GUID */ + guid = GUID_random(); + + guid_blob_stack = data_blob_const(guid_data, sizeof(guid_data)); + + /* This can't fail */ + ndr_push_struct_into_fixed_blob(&guid_blob_stack, &guid, + (ndr_push_flags_fn_t)ndr_push_GUID); + guid_blob = &guid_blob_stack; + } + + ac = replmd_ctx_init(module, req); + if (ac == NULL) { + return ldb_module_oom(module); + } + + functional_level = dsdb_functional_level(ldb); + + /* Get a sequence number from the backend */ + ret = ldb_sequence_number(ldb, LDB_SEQ_NEXT, &ac->seq_num); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + + /* we have to copy the message as the caller might have it as a const */ + msg = ldb_msg_copy_shallow(ac, req->op.add.message); + if (msg == NULL) { + ldb_oom(ldb); + talloc_free(ac); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* generated times */ + unix_to_nt_time(&now, t); + time_str = ldb_timestring(msg, t); + if (!time_str) { + ldb_oom(ldb); + talloc_free(ac); + return LDB_ERR_OPERATIONS_ERROR; + } + if (remove_current_guid) { + ldb_msg_remove_attr(msg,"objectGUID"); + } + + /* + * remove autogenerated attributes + */ + ldb_msg_remove_attr(msg, "whenCreated"); + ldb_msg_remove_attr(msg, "whenChanged"); + ldb_msg_remove_attr(msg, "uSNCreated"); + ldb_msg_remove_attr(msg, "uSNChanged"); + ldb_msg_remove_attr(msg, "replPropertyMetaData"); + + /* + * readd replicated attributes + */ + ret = ldb_msg_add_string(msg, "whenCreated", time_str); + if (ret != LDB_SUCCESS) { + ldb_oom(ldb); + talloc_free(ac); + return ret; + } + + /* build the replication meta_data */ + ZERO_STRUCT(nmd); + nmd.version = 1; + nmd.ctr.ctr1.count = msg->num_elements; + nmd.ctr.ctr1.array = talloc_array(msg, + struct replPropertyMetaData1, + nmd.ctr.ctr1.count); + if (!nmd.ctr.ctr1.array) { + ldb_oom(ldb); + talloc_free(ac); + return LDB_ERR_OPERATIONS_ERROR; + } + + is_schema_nc = ldb_dn_compare_base(replmd_private->schema_dn, msg->dn) == 0; + + for (i=0; i < msg->num_elements;) { + struct ldb_message_element *e = &msg->elements[i]; + struct replPropertyMetaData1 *m = &nmd.ctr.ctr1.array[ni]; + const struct dsdb_attribute *sa; + + if (e->name[0] == '@') { + i++; + continue; + } + + sa = dsdb_attribute_by_lDAPDisplayName(ac->schema, e->name); + if (!sa) { + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + "replmd_add: attribute '%s' not defined in schema\n", + e->name); + talloc_free(ac); + return LDB_ERR_NO_SUCH_ATTRIBUTE; + } + + if ((sa->systemFlags & DS_FLAG_ATTR_NOT_REPLICATED) || (sa->systemFlags & DS_FLAG_ATTR_IS_CONSTRUCTED)) { + /* if the attribute is not replicated (0x00000001) + * or constructed (0x00000004) it has no metadata + */ + i++; + continue; + } + + if (sa->linkID != 0 && functional_level > DS_DOMAIN_FUNCTION_2000) { + if (extended_dn == NULL) { + ret = replmd_add_make_extended_dn(req, + guid_blob, + &extended_dn); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + } + + /* + * Prepare the context for the backlinks and + * create metadata for the forward links. The + * backlinks are created in + * replmd_op_callback() after the successful + * ADD of the object. + */ + ret = replmd_add_fix_la(module, msg->elements, + replmd_private, e, + ac, now, + extended_dn, + sa, req); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + /* linked attributes are not stored in + replPropertyMetaData in FL above w2k */ + i++; + continue; + } + + m->attid = dsdb_attribute_get_attid(sa, is_schema_nc); + m->version = 1; + if (m->attid == DRSUAPI_ATTID_isDeleted) { + const struct ldb_val *rdn_val = ldb_dn_get_rdn_val(msg->dn); + const char* rdn; + + if (rdn_val == NULL) { + ldb_oom(ldb); + talloc_free(ac); + return LDB_ERR_OPERATIONS_ERROR; + } + + rdn = (const char*)rdn_val->data; + if (strcmp(rdn, "Deleted Objects") == 0) { + /* + * Set the originating_change_time to 29/12/9999 at 23:59:59 + * as specified in MS-ADTS 7.1.1.4.2 Deleted Objects Container + */ + m->originating_change_time = DELETED_OBJECT_CONTAINER_CHANGE_TIME; + } else { + m->originating_change_time = now; + } + } else { + m->originating_change_time = now; + } + m->originating_invocation_id = ac->our_invocation_id; + m->originating_usn = ac->seq_num; + m->local_usn = ac->seq_num; + ni++; + + if (!(e->flags & DSDB_FLAG_INTERNAL_FORCE_META_DATA)) { + i++; + continue; + } + + e->flags &= ~DSDB_FLAG_INTERNAL_FORCE_META_DATA; + + if (e->num_values != 0) { + i++; + continue; + } + + ldb_msg_remove_element(msg, e); + } + + /* fix meta data count */ + nmd.ctr.ctr1.count = ni; + + /* + * sort meta data array + */ + ret = replmd_replPropertyMetaDataCtr1_sort_and_verify(ldb, &nmd.ctr.ctr1, msg->dn); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "%s: error during direct ADD: %s", __func__, ldb_errstring(ldb)); + talloc_free(ac); + return ret; + } + + /* generated NDR encoded values */ + ndr_err = ndr_push_struct_blob(&nmd_value, msg, + &nmd, + (ndr_push_flags_fn_t)ndr_push_replPropertyMetaDataBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + ldb_oom(ldb); + talloc_free(ac); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * add the autogenerated values + */ + ret = dsdb_msg_add_guid(msg, &guid, "objectGUID"); + if (ret != LDB_SUCCESS) { + ldb_oom(ldb); + talloc_free(ac); + return ret; + } + ret = ldb_msg_add_string(msg, "whenChanged", time_str); + if (ret != LDB_SUCCESS) { + ldb_oom(ldb); + talloc_free(ac); + return ret; + } + ret = samdb_msg_add_uint64(ldb, msg, msg, "uSNCreated", ac->seq_num); + if (ret != LDB_SUCCESS) { + ldb_oom(ldb); + talloc_free(ac); + return ret; + } + ret = samdb_msg_add_uint64(ldb, msg, msg, "uSNChanged", ac->seq_num); + if (ret != LDB_SUCCESS) { + ldb_oom(ldb); + talloc_free(ac); + return ret; + } + ret = ldb_msg_add_value(msg, "replPropertyMetaData", &nmd_value, NULL); + if (ret != LDB_SUCCESS) { + ldb_oom(ldb); + talloc_free(ac); + return ret; + } + + /* + * sort the attributes by attid before storing the object + */ + replmd_ldb_message_sort(msg, ac->schema); + + /* + * Assert that we do have an objectClass + */ + objectclass_el = ldb_msg_find_element(msg, "objectClass"); + if (objectclass_el == NULL) { + ldb_asprintf_errstring(ldb, __location__ + ": objectClass missing on %s\n", + ldb_dn_get_linearized(msg->dn)); + talloc_free(ac); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + is_urgent = replmd_check_urgent_objectclass(objectclass_el, + REPL_URGENT_ON_CREATE); + + ac->is_urgent = is_urgent; + ret = ldb_build_add_req(&down_req, ldb, ac, + msg, + req->controls, + ac, replmd_op_callback, + req); + + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + + /* current partition control is needed by "replmd_op_callback" */ + if (ldb_request_get_control(req, DSDB_CONTROL_CURRENT_PARTITION_OID) == NULL) { + ret = ldb_request_add_control(down_req, + DSDB_CONTROL_CURRENT_PARTITION_OID, + false, NULL); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + } + + if (functional_level == DS_DOMAIN_FUNCTION_2000) { + ret = ldb_request_add_control(down_req, DSDB_CONTROL_APPLY_LINKS, false, NULL); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + } + + /* mark the relax control done */ + if (control) { + control->critical = 0; + } + /* go on with the call chain */ + return ldb_next_request(module, down_req); +} + + +/* + * update the replPropertyMetaData for one element + */ +static int replmd_update_rpmd_element(struct ldb_context *ldb, + struct ldb_message *msg, + struct ldb_message_element *el, + struct ldb_message_element *old_el, + struct replPropertyMetaDataBlob *omd, + const struct dsdb_schema *schema, + uint64_t *seq_num, + const struct GUID *our_invocation_id, + NTTIME now, + bool is_schema_nc, + bool is_forced_rodc, + struct ldb_request *req) +{ + uint32_t i; + const struct dsdb_attribute *a; + struct replPropertyMetaData1 *md1; + bool may_skip = false; + uint32_t attid; + + a = dsdb_attribute_by_lDAPDisplayName(schema, el->name); + if (a == NULL) { + if (ldb_request_get_control(req, LDB_CONTROL_RELAX_OID)) { + /* allow this to make it possible for dbcheck + to remove bad attributes */ + return LDB_SUCCESS; + } + + DEBUG(0,(__location__ ": Unable to find attribute %s in schema\n", + el->name)); + return LDB_ERR_OPERATIONS_ERROR; + } + + attid = dsdb_attribute_get_attid(a, is_schema_nc); + + if ((a->systemFlags & DS_FLAG_ATTR_NOT_REPLICATED) || (a->systemFlags & DS_FLAG_ATTR_IS_CONSTRUCTED)) { + return LDB_SUCCESS; + } + + /* + * if the attribute's value haven't changed, and this isn't + * just a delete of everything then return LDB_SUCCESS Unless + * we have the provision control or if the attribute is + * interSiteTopologyGenerator as this page explain: + * http://support.microsoft.com/kb/224815 this attribute is + * periodicaly written by the DC responsible for the intersite + * generation in a given site + * + * Unchanged could be deleting or replacing an already-gone + * thing with an unconstrained delete/empty replace or a + * replace with the same value, but not an add with the same + * value because that could be about adding a duplicate (which + * is for someone else to error out on). + */ + if (old_el != NULL && ldb_msg_element_equal_ordered(el, old_el)) { + if (LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_REPLACE) { + may_skip = true; + } + } else if (old_el == NULL && el->num_values == 0) { + if (LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_REPLACE) { + may_skip = true; + } else if (LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_DELETE) { + may_skip = true; + } + } else if (a->linkID != 0 && LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_DELETE && + ldb_request_get_control(req, DSDB_CONTROL_REPLMD_VANISH_LINKS) != NULL) { + /* + * We intentionally skip the version bump when attempting to + * vanish links. + * + * The control is set by dbcheck and expunge-tombstones which + * both attempt to be non-replicating. Otherwise, making an + * alteration to the replication state would trigger a + * broadcast of all expunged objects. + */ + may_skip = true; + } + + if (el->flags & DSDB_FLAG_INTERNAL_FORCE_META_DATA) { + may_skip = false; + el->flags &= ~DSDB_FLAG_INTERNAL_FORCE_META_DATA; + } + + if (may_skip) { + if (strcmp(el->name, "interSiteTopologyGenerator") != 0 && + !ldb_request_get_control(req, LDB_CONTROL_PROVISION_OID)) { + /* + * allow this to make it possible for dbcheck + * to rebuild broken metadata + */ + return LDB_SUCCESS; + } + } + + for (i=0; i<omd->ctr.ctr1.count; i++) { + /* + * First check if we find it under the msDS-IntID, + * then check if we find it under the OID and + * prefixMap ID. + * + * This allows the administrator to simply re-write + * the attributes and so restore replication, which is + * likely what they will try to do. + */ + if (attid == omd->ctr.ctr1.array[i].attid) { + break; + } + + if (a->attributeID_id == omd->ctr.ctr1.array[i].attid) { + break; + } + } + + if (a->linkID != 0 && dsdb_functional_level(ldb) > DS_DOMAIN_FUNCTION_2000) { + /* linked attributes are not stored in + replPropertyMetaData in FL above w2k, but we do + raise the seqnum for the object */ + if (*seq_num == 0 && + ldb_sequence_number(ldb, LDB_SEQ_NEXT, seq_num) != LDB_SUCCESS) { + return LDB_ERR_OPERATIONS_ERROR; + } + return LDB_SUCCESS; + } + + if (i == omd->ctr.ctr1.count) { + /* we need to add a new one */ + omd->ctr.ctr1.array = talloc_realloc(msg, omd->ctr.ctr1.array, + struct replPropertyMetaData1, omd->ctr.ctr1.count+1); + if (omd->ctr.ctr1.array == NULL) { + ldb_oom(ldb); + return LDB_ERR_OPERATIONS_ERROR; + } + omd->ctr.ctr1.count++; + ZERO_STRUCT(omd->ctr.ctr1.array[i]); + } + + /* Get a new sequence number from the backend. We only do this + * if we have a change that requires a new + * replPropertyMetaData element + */ + if (*seq_num == 0) { + int ret = ldb_sequence_number(ldb, LDB_SEQ_NEXT, seq_num); + if (ret != LDB_SUCCESS) { + return LDB_ERR_OPERATIONS_ERROR; + } + } + + md1 = &omd->ctr.ctr1.array[i]; + md1->version++; + md1->attid = attid; + + if (md1->attid == DRSUAPI_ATTID_isDeleted) { + const struct ldb_val *rdn_val = ldb_dn_get_rdn_val(msg->dn); + const char* rdn; + + if (rdn_val == NULL) { + ldb_oom(ldb); + return LDB_ERR_OPERATIONS_ERROR; + } + + rdn = (const char*)rdn_val->data; + if (strcmp(rdn, "Deleted Objects") == 0) { + /* + * Set the originating_change_time to 29/12/9999 at 23:59:59 + * as specified in MS-ADTS 7.1.1.4.2 Deleted Objects Container + */ + md1->originating_change_time = DELETED_OBJECT_CONTAINER_CHANGE_TIME; + } else { + md1->originating_change_time = now; + } + } else { + md1->originating_change_time = now; + } + md1->originating_invocation_id = *our_invocation_id; + md1->originating_usn = *seq_num; + md1->local_usn = *seq_num; + + if (is_forced_rodc) { + /* Force version to 0 to be overridden later via replication */ + md1->version = 0; + } + + return LDB_SUCCESS; +} + +/* + * Bump the replPropertyMetaData version on an attribute, and if it + * has changed (or forced by leaving rdn_old NULL), update the value + * in the entry. + * + * This is important, as calling a modify operation may not change the + * version number if the values appear unchanged, but a rename between + * parents bumps this value. + * + */ +static int replmd_update_rpmd_rdn_attr(struct ldb_context *ldb, + struct ldb_message *msg, + const struct ldb_val *rdn_new, + const struct ldb_val *rdn_old, + struct replPropertyMetaDataBlob *omd, + struct replmd_replicated_request *ar, + NTTIME now, + bool is_schema_nc, + bool is_forced_rodc) +{ + const char *rdn_name = ldb_dn_get_rdn_name(msg->dn); + const struct dsdb_attribute *rdn_attr = + dsdb_attribute_by_lDAPDisplayName(ar->schema, rdn_name); + const char *attr_name = rdn_attr != NULL ? + rdn_attr->lDAPDisplayName : + rdn_name; + struct ldb_message_element new_el = { + .flags = LDB_FLAG_MOD_REPLACE, + .name = attr_name, + .num_values = 1, + .values = discard_const_p(struct ldb_val, rdn_new) + }; + struct ldb_message_element old_el = { + .flags = LDB_FLAG_MOD_REPLACE, + .name = attr_name, + .num_values = rdn_old ? 1 : 0, + .values = discard_const_p(struct ldb_val, rdn_old) + }; + + if (ldb_msg_element_equal_ordered(&new_el, &old_el) == false) { + int ret = ldb_msg_add(msg, &new_el, LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ldb_oom(ldb); + } + } + + return replmd_update_rpmd_element(ldb, msg, &new_el, NULL, + omd, ar->schema, &ar->seq_num, + &ar->our_invocation_id, + now, is_schema_nc, is_forced_rodc, + ar->req); + +} + +static uint64_t find_max_local_usn(struct replPropertyMetaDataBlob omd) +{ + uint32_t count = omd.ctr.ctr1.count; + uint64_t max = 0; + uint32_t i; + for (i=0; i < count; i++) { + struct replPropertyMetaData1 m = omd.ctr.ctr1.array[i]; + if (max < m.local_usn) { + max = m.local_usn; + } + } + return max; +} + +/* + * update the replPropertyMetaData object each time we modify an + * object. This is needed for DRS replication, as the merge on the + * client is based on this object + */ +static int replmd_update_rpmd(struct ldb_module *module, + const struct dsdb_schema *schema, + struct ldb_request *req, + const char * const *rename_attrs, + struct ldb_message *msg, uint64_t *seq_num, + time_t t, bool is_schema_nc, + bool *is_urgent, bool *rodc) +{ + const struct ldb_val *omd_value; + enum ndr_err_code ndr_err; + struct replPropertyMetaDataBlob omd; + unsigned int i; + NTTIME now; + const struct GUID *our_invocation_id; + int ret; + const char * const *attrs = NULL; + const char * const attrs2[] = { "uSNChanged", "objectClass", "instanceType", NULL }; + struct ldb_result *res; + struct ldb_context *ldb; + struct ldb_message_element *objectclass_el; + enum urgent_situation situation; + bool rmd_is_provided; + bool rmd_is_just_resorted = false; + const char *not_rename_attrs[4 + msg->num_elements]; + bool is_forced_rodc = false; + + if (rename_attrs) { + attrs = rename_attrs; + } else { + for (i = 0; i < msg->num_elements; i++) { + not_rename_attrs[i] = msg->elements[i].name; + } + not_rename_attrs[i] = "replPropertyMetaData"; + not_rename_attrs[i+1] = "objectClass"; + not_rename_attrs[i+2] = "instanceType"; + not_rename_attrs[i+3] = NULL; + attrs = not_rename_attrs; + } + + ldb = ldb_module_get_ctx(module); + + ret = samdb_rodc(ldb, rodc); + if (ret != LDB_SUCCESS) { + DEBUG(4, (__location__ ": unable to tell if we are an RODC\n")); + *rodc = false; + } + + if (*rodc && + ldb_request_get_control(req, DSDB_CONTROL_FORCE_RODC_LOCAL_CHANGE)) { + is_forced_rodc = true; + } + + our_invocation_id = samdb_ntds_invocation_id(ldb); + if (!our_invocation_id) { + /* this happens during an initial vampire while + updating the schema */ + DEBUG(5,("No invocationID - skipping replPropertyMetaData update\n")); + return LDB_SUCCESS; + } + + unix_to_nt_time(&now, t); + + if (ldb_request_get_control(req, DSDB_CONTROL_CHANGEREPLMETADATA_OID)) { + rmd_is_provided = true; + if (ldb_request_get_control(req, DSDB_CONTROL_CHANGEREPLMETADATA_RESORT_OID)) { + rmd_is_just_resorted = true; + } + } else { + rmd_is_provided = false; + } + + /* if isDeleted is present and is TRUE, then we consider we are deleting, + * otherwise we consider we are updating */ + if (ldb_msg_check_string_attribute(msg, "isDeleted", "TRUE")) { + situation = REPL_URGENT_ON_DELETE; + } else if (rename_attrs) { + situation = REPL_URGENT_ON_CREATE | REPL_URGENT_ON_DELETE; + } else { + situation = REPL_URGENT_ON_UPDATE; + } + + if (rmd_is_provided) { + /* In this case the change_replmetadata control was supplied */ + /* We check that it's the only attribute that is provided + * (it's a rare case so it's better to keep the code simplier) + * We also check that the highest local_usn is bigger or the same as + * uSNChanged. */ + uint64_t db_seq; + if( msg->num_elements != 1 || + strncmp(msg->elements[0].name, + "replPropertyMetaData", 20) ) { + DEBUG(0,(__location__ ": changereplmetada control called without "\ + "a specified replPropertyMetaData attribute or with others\n")); + return LDB_ERR_OPERATIONS_ERROR; + } + if (situation != REPL_URGENT_ON_UPDATE) { + DEBUG(0,(__location__ ": changereplmetada control can't be called when deleting an object\n")); + return LDB_ERR_OPERATIONS_ERROR; + } + omd_value = ldb_msg_find_ldb_val(msg, "replPropertyMetaData"); + if (!omd_value) { + DEBUG(0,(__location__ ": replPropertyMetaData was not specified for Object %s\n", + ldb_dn_get_linearized(msg->dn))); + return LDB_ERR_OPERATIONS_ERROR; + } + ndr_err = ndr_pull_struct_blob(omd_value, msg, &omd, + (ndr_pull_flags_fn_t)ndr_pull_replPropertyMetaDataBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + DEBUG(0,(__location__ ": Failed to parse replPropertyMetaData for %s\n", + ldb_dn_get_linearized(msg->dn))); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = dsdb_module_search_dn(module, msg, &res, msg->dn, attrs2, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_SHOW_EXTENDED_DN | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT | + DSDB_SEARCH_REVEAL_INTERNALS, req); + + if (ret != LDB_SUCCESS) { + return ret; + } + + if (rmd_is_just_resorted == false) { + *seq_num = find_max_local_usn(omd); + + db_seq = ldb_msg_find_attr_as_uint64(res->msgs[0], "uSNChanged", 0); + + /* + * The test here now allows for a new + * replPropertyMetaData with no change, if was + * just dbcheck re-sorting the values. + */ + if (*seq_num <= db_seq) { + DEBUG(0,(__location__ ": changereplmetada control provided but max(local_usn)" \ + " is less than uSNChanged (max = %lld uSNChanged = %lld)\n", + (long long)*seq_num, (long long)db_seq)); + return LDB_ERR_OPERATIONS_ERROR; + } + } + + } else { + /* search for the existing replPropertyMetaDataBlob. We need + * to use REVEAL and ask for DNs in storage format to support + * the check for values being the same in + * replmd_update_rpmd_element() + */ + ret = dsdb_module_search_dn(module, msg, &res, msg->dn, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_SHOW_EXTENDED_DN | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT | + DSDB_SEARCH_REVEAL_INTERNALS, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + omd_value = ldb_msg_find_ldb_val(res->msgs[0], "replPropertyMetaData"); + if (!omd_value) { + DEBUG(0,(__location__ ": Object %s does not have a replPropertyMetaData attribute\n", + ldb_dn_get_linearized(msg->dn))); + return LDB_ERR_OPERATIONS_ERROR; + } + + ndr_err = ndr_pull_struct_blob(omd_value, msg, &omd, + (ndr_pull_flags_fn_t)ndr_pull_replPropertyMetaDataBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + DEBUG(0,(__location__ ": Failed to parse replPropertyMetaData for %s\n", + ldb_dn_get_linearized(msg->dn))); + return LDB_ERR_OPERATIONS_ERROR; + } + + if (omd.version != 1) { + DEBUG(0,(__location__ ": bad version %u in replPropertyMetaData for %s\n", + omd.version, ldb_dn_get_linearized(msg->dn))); + return LDB_ERR_OPERATIONS_ERROR; + } + + for (i=0; i<msg->num_elements;) { + struct ldb_message_element *el = &msg->elements[i]; + struct ldb_message_element *old_el; + + old_el = ldb_msg_find_element(res->msgs[0], el->name); + ret = replmd_update_rpmd_element(ldb, msg, el, old_el, + &omd, schema, seq_num, + our_invocation_id, + now, is_schema_nc, + is_forced_rodc, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (!*is_urgent && (situation == REPL_URGENT_ON_UPDATE)) { + *is_urgent = replmd_check_urgent_attribute(el); + } + + if (!(el->flags & DSDB_FLAG_INTERNAL_FORCE_META_DATA)) { + i++; + continue; + } + + el->flags &= ~DSDB_FLAG_INTERNAL_FORCE_META_DATA; + + if (el->num_values != 0) { + i++; + continue; + } + + ldb_msg_remove_element(msg, el); + } + } + + /* + * Assert that we have an objectClass attribute - this is major + * corruption if we don't have this! + */ + objectclass_el = ldb_msg_find_element(res->msgs[0], "objectClass"); + if (objectclass_el != NULL) { + /* + * Now check if this objectClass means we need to do urgent replication + */ + if (!*is_urgent && replmd_check_urgent_objectclass(objectclass_el, + situation)) { + *is_urgent = true; + } + } else if (!ldb_request_get_control(req, DSDB_CONTROL_DBCHECK)) { + ldb_asprintf_errstring(ldb, __location__ + ": objectClass missing on %s\n", + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + + /* + * replmd_update_rpmd_element has done an update if the + * seq_num is set + */ + if (*seq_num != 0 || rmd_is_just_resorted == true) { + struct ldb_val *md_value; + struct ldb_message_element *el; + + /*if we are RODC and this is a DRSR update then its ok*/ + if (!ldb_request_get_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID) + && !ldb_request_get_control(req, DSDB_CONTROL_DBCHECK_MODIFY_RO_REPLICA) + && !is_forced_rodc) { + unsigned instanceType; + + if (*rodc) { + ldb_set_errstring(ldb, "RODC modify is forbidden!"); + return LDB_ERR_REFERRAL; + } + + instanceType = ldb_msg_find_attr_as_uint(res->msgs[0], "instanceType", INSTANCE_TYPE_WRITE); + if (!(instanceType & INSTANCE_TYPE_WRITE)) { + return ldb_error(ldb, LDB_ERR_UNWILLING_TO_PERFORM, + "cannot change replicated attribute on partial replica"); + } + } + + md_value = talloc(msg, struct ldb_val); + if (md_value == NULL) { + ldb_oom(ldb); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = replmd_replPropertyMetaDataCtr1_sort_and_verify(ldb, &omd.ctr.ctr1, msg->dn); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "%s: %s", __func__, ldb_errstring(ldb)); + return ret; + } + + ndr_err = ndr_push_struct_blob(md_value, msg, &omd, + (ndr_push_flags_fn_t)ndr_push_replPropertyMetaDataBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + DEBUG(0,(__location__ ": Failed to marshall replPropertyMetaData for %s\n", + ldb_dn_get_linearized(msg->dn))); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = ldb_msg_add_empty(msg, "replPropertyMetaData", LDB_FLAG_MOD_REPLACE, &el); + if (ret != LDB_SUCCESS) { + DEBUG(0,(__location__ ": Failed to add updated replPropertyMetaData %s\n", + ldb_dn_get_linearized(msg->dn))); + return ret; + } + + el->num_values = 1; + el->values = md_value; + } + + return LDB_SUCCESS; +} + +static int parsed_dn_compare(struct parsed_dn *pdn1, struct parsed_dn *pdn2) +{ + int ret = ndr_guid_compare(&pdn1->guid, &pdn2->guid); + if (ret == 0) { + return data_blob_cmp(&pdn1->dsdb_dn->extra_part, + &pdn2->dsdb_dn->extra_part); + } + return ret; +} + +/* + get a series of message element values as an array of DNs and GUIDs + the result is sorted by GUID + */ +static int get_parsed_dns(struct ldb_module *module, TALLOC_CTX *mem_ctx, + struct ldb_message_element *el, struct parsed_dn **pdn, + const char *ldap_oid, struct ldb_request *parent) +{ + unsigned int i; + bool values_are_sorted = true; + struct ldb_context *ldb = ldb_module_get_ctx(module); + + if (el == NULL) { + *pdn = NULL; + return LDB_SUCCESS; + } + + (*pdn) = talloc_array(mem_ctx, struct parsed_dn, el->num_values); + if (!*pdn) { + ldb_module_oom(module); + return LDB_ERR_OPERATIONS_ERROR; + } + + for (i=0; i<el->num_values; i++) { + struct ldb_val *v = &el->values[i]; + NTSTATUS status; + struct ldb_dn *dn; + struct parsed_dn *p; + + p = &(*pdn)[i]; + + p->dsdb_dn = dsdb_dn_parse(*pdn, ldb, v, ldap_oid); + if (p->dsdb_dn == NULL) { + return LDB_ERR_INVALID_DN_SYNTAX; + } + + dn = p->dsdb_dn->dn; + + status = dsdb_get_extended_dn_guid(dn, &p->guid, "GUID"); + if (NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND) || + unlikely(GUID_all_zero(&p->guid))) { + /* we got a DN without a GUID - go find the GUID */ + int ret = dsdb_module_guid_by_dn(module, dn, &p->guid, parent); + if (ret != LDB_SUCCESS) { + char *dn_str = NULL; + dn_str = ldb_dn_get_extended_linearized(mem_ctx, + (dn), 1); + ldb_asprintf_errstring(ldb, + "Unable to find GUID for DN %s\n", + dn_str); + if (ret == LDB_ERR_NO_SUCH_OBJECT && + LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_DELETE && + ldb_attr_cmp(el->name, "member") == 0) { + return LDB_ERR_UNWILLING_TO_PERFORM; + } + return ret; + } + ret = dsdb_set_extended_dn_guid(dn, &p->guid, "GUID"); + if (ret != LDB_SUCCESS) { + return ret; + } + } else if (!NT_STATUS_IS_OK(status)) { + return LDB_ERR_OPERATIONS_ERROR; + } + if (i > 0 && values_are_sorted) { + int cmp = parsed_dn_compare(p, &(*pdn)[i - 1]); + if (cmp < 0) { + values_are_sorted = false; + } + } + /* keep a pointer to the original ldb_val */ + p->v = v; + } + if (! values_are_sorted) { + TYPESAFE_QSORT(*pdn, el->num_values, parsed_dn_compare); + } + return LDB_SUCCESS; +} + +/* + * Get a series of trusted message element values. The result is sorted by + * GUID, even though the GUIDs might not be known. That works because we trust + * the database to give us the elements like that if the + * replmd_private->sorted_links flag is set. + * + * We also ensure that the links are in the Functional Level 2003 + * linked attributes format. + */ +static int get_parsed_dns_trusted_fallback(struct ldb_module *module, + struct replmd_private *replmd_private, + TALLOC_CTX *mem_ctx, + struct ldb_message_element *el, + struct parsed_dn **pdn, + const char *ldap_oid, + struct ldb_request *parent) +{ + int ret; + if (el == NULL) { + *pdn = NULL; + return LDB_SUCCESS; + } + + if (!replmd_private->sorted_links) { + /* We need to sort the list. This is the slow old path we want + to avoid. + */ + ret = get_parsed_dns(module, mem_ctx, el, pdn, ldap_oid, + parent); + if (ret != LDB_SUCCESS) { + return ret; + } + } else { + ret = get_parsed_dns_trusted(mem_ctx, el, pdn); + if (ret != LDB_SUCCESS) { + ldb_module_oom(module); + return LDB_ERR_OPERATIONS_ERROR; + } + } + + /* + * This upgrades links to FL2003 style, and sorts the result + * if that was needed. + * + * TODO: Add a database feature that asserts we have no FL2000 + * style links to avoid this check or add a feature that + * uses a similar check to find sorted/unsorted links + * for an on-the-fly upgrade. + */ + + ret = replmd_check_upgrade_links(ldb_module_get_ctx(module), + *pdn, el->num_values, + el, + ldap_oid); + if (ret != LDB_SUCCESS) { + return ret; + } + + return LDB_SUCCESS; +} + +/* + Return LDB_SUCCESS if a parsed_dn list contains no duplicate values, + otherwise an error code. For compatibility the error code differs depending + on whether or not the attribute is "member". + + As always, the parsed_dn list is assumed to be sorted. + */ +static int check_parsed_dn_duplicates(struct ldb_module *module, + struct ldb_message_element *el, + struct parsed_dn *pdn) +{ + unsigned int i; + struct ldb_context *ldb = ldb_module_get_ctx(module); + + for (i = 1; i < el->num_values; i++) { + struct parsed_dn *p = &pdn[i]; + if (parsed_dn_compare(p, &pdn[i - 1]) == 0) { + ldb_asprintf_errstring(ldb, + "Linked attribute %s has " + "multiple identical values", + el->name); + if (ldb_attr_cmp(el->name, "member") == 0) { + return LDB_ERR_ENTRY_ALREADY_EXISTS; + } else { + return LDB_ERR_ATTRIBUTE_OR_VALUE_EXISTS; + } + } + } + return LDB_SUCCESS; +} + +/* + build a new extended DN, including all meta data fields + + RMD_FLAGS = DSDB_RMD_FLAG_* bits + RMD_ADDTIME = originating_add_time + RMD_INVOCID = originating_invocation_id + RMD_CHANGETIME = originating_change_time + RMD_ORIGINATING_USN = originating_usn + RMD_LOCAL_USN = local_usn + RMD_VERSION = version + */ +static int replmd_build_la_val(TALLOC_CTX *mem_ctx, struct ldb_val *v, + struct dsdb_dn *dsdb_dn, + const struct GUID *invocation_id, + uint64_t local_usn, NTTIME nttime) +{ + return replmd_set_la_val(mem_ctx, v, dsdb_dn, NULL, invocation_id, + local_usn, local_usn, nttime, + RMD_VERSION_INITIAL, false); +} + +static int replmd_update_la_val(TALLOC_CTX *mem_ctx, struct ldb_val *v, struct dsdb_dn *dsdb_dn, + struct dsdb_dn *old_dsdb_dn, const struct GUID *invocation_id, + uint64_t seq_num, uint64_t local_usn, NTTIME nttime, + bool deleted); + +/* + check if any links need upgrading from w2k format + */ +static int replmd_check_upgrade_links(struct ldb_context *ldb, + struct parsed_dn *dns, uint32_t count, + struct ldb_message_element *el, + const char *ldap_oid) +{ + uint32_t i; + const struct GUID *invocation_id = NULL; + for (i=0; i<count; i++) { + NTSTATUS status; + uint32_t version; + int ret; + if (dns[i].dsdb_dn == NULL) { + ret = really_parse_trusted_dn(dns, ldb, &dns[i], + ldap_oid); + if (ret != LDB_SUCCESS) { + return LDB_ERR_INVALID_DN_SYNTAX; + } + } + + status = dsdb_get_extended_dn_uint32(dns[i].dsdb_dn->dn, + &version, "RMD_VERSION"); + if (!NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) { + /* + * We optimistically assume they are all the same; if + * the first one is fixed, they are all fixed. + * + * If the first one was *not* fixed and we find a + * later one that is, that is an occasion to shout + * with DEBUG(0). + */ + if (i == 0) { + return LDB_SUCCESS; + } + DEBUG(0, ("Mixed w2k and fixed format " + "linked attributes\n")); + continue; + } + + if (invocation_id == NULL) { + invocation_id = samdb_ntds_invocation_id(ldb); + if (invocation_id == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + } + + + /* it's an old one that needs upgrading */ + ret = replmd_update_la_val(el->values, dns[i].v, + dns[i].dsdb_dn, dns[i].dsdb_dn, + invocation_id, 1, 1, 0, false); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* + * This sort() is critical for the operation of + * get_parsed_dns_trusted_fallback() because callers of this function + * expect a sorted list, and FL2000 style links are not + * sorted. In particular, as well as the upgrade case, + * get_parsed_dns_trusted_fallback() is called from + * replmd_delete_remove_link() even in FL2000 mode + * + * We do not normally pay the cost of the qsort() due to the + * early return in the RMD_VERSION found case. + */ + TYPESAFE_QSORT(dns, count, parsed_dn_compare); + return LDB_SUCCESS; +} + +/* + Sets the value for a linked attribute, including all meta data fields + + see replmd_build_la_val for value names + */ +static int replmd_set_la_val(TALLOC_CTX *mem_ctx, struct ldb_val *v, struct dsdb_dn *dsdb_dn, + struct dsdb_dn *old_dsdb_dn, const struct GUID *invocation_id, + uint64_t usn, uint64_t local_usn, NTTIME nttime, + uint32_t version, bool deleted) +{ + struct ldb_dn *dn = dsdb_dn->dn; + const char *tstring, *usn_string, *flags_string; + struct ldb_val tval; + struct ldb_val iid; + struct ldb_val usnv, local_usnv; + struct ldb_val vers, flagsv; + const struct ldb_val *old_addtime = NULL; + NTSTATUS status; + int ret; + const char *dnstring; + char *vstring; + uint32_t rmd_flags = deleted?DSDB_RMD_FLAG_DELETED:0; + + tstring = talloc_asprintf(mem_ctx, "%llu", (unsigned long long)nttime); + if (!tstring) { + return LDB_ERR_OPERATIONS_ERROR; + } + tval = data_blob_string_const(tstring); + + usn_string = talloc_asprintf(mem_ctx, "%llu", (unsigned long long)usn); + if (!usn_string) { + return LDB_ERR_OPERATIONS_ERROR; + } + usnv = data_blob_string_const(usn_string); + + usn_string = talloc_asprintf(mem_ctx, "%llu", (unsigned long long)local_usn); + if (!usn_string) { + return LDB_ERR_OPERATIONS_ERROR; + } + local_usnv = data_blob_string_const(usn_string); + + status = GUID_to_ndr_blob(invocation_id, dn, &iid); + if (!NT_STATUS_IS_OK(status)) { + return LDB_ERR_OPERATIONS_ERROR; + } + + flags_string = talloc_asprintf(mem_ctx, "%u", rmd_flags); + if (!flags_string) { + return LDB_ERR_OPERATIONS_ERROR; + } + flagsv = data_blob_string_const(flags_string); + + ret = ldb_dn_set_extended_component(dn, "RMD_FLAGS", &flagsv); + if (ret != LDB_SUCCESS) return ret; + + /* get the ADDTIME from the original */ + if (old_dsdb_dn != NULL) { + old_addtime = ldb_dn_get_extended_component(old_dsdb_dn->dn, + "RMD_ADDTIME"); + } + if (old_addtime == NULL) { + old_addtime = &tval; + } + if (dsdb_dn != old_dsdb_dn || + ldb_dn_get_extended_component(dn, "RMD_ADDTIME") == NULL) { + ret = ldb_dn_set_extended_component(dn, "RMD_ADDTIME", old_addtime); + if (ret != LDB_SUCCESS) return ret; + } + + /* use our invocation id */ + ret = ldb_dn_set_extended_component(dn, "RMD_INVOCID", &iid); + if (ret != LDB_SUCCESS) return ret; + + /* changetime is the current time */ + ret = ldb_dn_set_extended_component(dn, "RMD_CHANGETIME", &tval); + if (ret != LDB_SUCCESS) return ret; + + /* update the USN */ + ret = ldb_dn_set_extended_component(dn, "RMD_ORIGINATING_USN", &usnv); + if (ret != LDB_SUCCESS) return ret; + + ret = ldb_dn_set_extended_component(dn, "RMD_LOCAL_USN", &local_usnv); + if (ret != LDB_SUCCESS) return ret; + + vstring = talloc_asprintf(mem_ctx, "%lu", (unsigned long)version); + vers = data_blob_string_const(vstring); + ret = ldb_dn_set_extended_component(dn, "RMD_VERSION", &vers); + if (ret != LDB_SUCCESS) return ret; + + dnstring = dsdb_dn_get_extended_linearized(mem_ctx, dsdb_dn, 1); + if (dnstring == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + *v = data_blob_string_const(dnstring); + + return LDB_SUCCESS; +} + +/** + * Updates the value for a linked attribute, including all meta data fields + */ +static int replmd_update_la_val(TALLOC_CTX *mem_ctx, struct ldb_val *v, struct dsdb_dn *dsdb_dn, + struct dsdb_dn *old_dsdb_dn, const struct GUID *invocation_id, + uint64_t usn, uint64_t local_usn, NTTIME nttime, + bool deleted) +{ + uint32_t old_version; + uint32_t version = RMD_VERSION_INITIAL; + NTSTATUS status; + + /* + * We're updating the linked attribute locally, so increase the version + * by 1 so that other DCs will see the change when it gets replicated out + */ + status = dsdb_get_extended_dn_uint32(old_dsdb_dn->dn, &old_version, + "RMD_VERSION"); + + if (NT_STATUS_IS_OK(status)) { + version = old_version + 1; + } + + return replmd_set_la_val(mem_ctx, v, dsdb_dn, old_dsdb_dn, invocation_id, + usn, local_usn, nttime, version, deleted); +} + +/* + handle adding a linked attribute + */ +static int replmd_modify_la_add(struct ldb_module *module, + struct replmd_private *replmd_private, + struct replmd_replicated_request *ac, + struct ldb_message *msg, + struct ldb_message_element *el, + struct ldb_message_element *old_el, + const struct dsdb_attribute *schema_attr, + time_t t, + struct ldb_dn *msg_dn, + struct ldb_request *parent) +{ + unsigned int i, j; + struct parsed_dn *dns, *old_dns; + TALLOC_CTX *tmp_ctx = talloc_new(msg); + int ret; + struct ldb_val *new_values = NULL; + unsigned old_num_values = old_el ? old_el->num_values : 0; + unsigned num_values = 0; + unsigned max_num_values; + struct ldb_context *ldb = ldb_module_get_ctx(module); + NTTIME now; + unix_to_nt_time(&now, t); + + /* get the DNs to be added, fully parsed. + * + * We need full parsing because they came off the wire and we don't + * trust them, besides which we need their details to know where to put + * them. + */ + ret = get_parsed_dns(module, tmp_ctx, el, &dns, + schema_attr->syntax->ldap_oid, parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + /* get the existing DNs, lazily parsed */ + ret = get_parsed_dns_trusted_fallback(module, replmd_private, + tmp_ctx, old_el, &old_dns, + schema_attr->syntax->ldap_oid, + parent); + + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + max_num_values = old_num_values + el->num_values; + if (max_num_values < old_num_values) { + DEBUG(0, ("we seem to have overflow in replmd_modify_la_add. " + "old values: %u, new values: %u, sum: %u\n", + old_num_values, el->num_values, max_num_values)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + new_values = talloc_zero_array(tmp_ctx, struct ldb_val, max_num_values); + + if (new_values == NULL) { + ldb_module_oom(module); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * For each new value, find where it would go in the list. If there is + * a matching GUID there, we update the existing value; otherwise we + * put it in place. + */ + j = 0; + for (i = 0; i < el->num_values; i++) { + struct parsed_dn *exact; + struct parsed_dn *next; + unsigned offset; + int err = parsed_dn_find(ldb, old_dns, old_num_values, + &dns[i].guid, + dns[i].dsdb_dn->dn, + dns[i].dsdb_dn->extra_part, 0, + &exact, &next, + schema_attr->syntax->ldap_oid, + true); + if (err != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return err; + } + + if (ac->fix_link_sid) { + char *fixed_dnstring = NULL; + struct dom_sid tmp_sid = { 0, }; + DATA_BLOB sid_blob = data_blob_null; + enum ndr_err_code ndr_err; + NTSTATUS status; + int num; + + if (exact == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + if (dns[i].dsdb_dn->dn_format != DSDB_NORMAL_DN) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + /* + * Only "<GUID=...><SID=...>" is allowed. + * + * We get the GUID to just to find the old + * value and the SID in order to add it + * to the found value. + */ + + num = ldb_dn_get_comp_num(dns[i].dsdb_dn->dn); + if (num != 0) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + num = ldb_dn_get_extended_comp_num(dns[i].dsdb_dn->dn); + if (num != 2) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + status = dsdb_get_extended_dn_sid(exact->dsdb_dn->dn, + &tmp_sid, "SID"); + if (NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) { + /* this is what we expect */ + } else if (NT_STATUS_IS_OK(status)) { + struct GUID_txt_buf guid_str; + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "i[%u] SID NOT MISSING... Attribute %s already " + "exists for target GUID %s, SID %s, DN: %s", + i, el->name, + GUID_buf_string(&exact->guid, + &guid_str), + dom_sid_string(tmp_ctx, &tmp_sid), + dsdb_dn_get_extended_linearized(tmp_ctx, + exact->dsdb_dn, 1)); + talloc_free(tmp_ctx); + return LDB_ERR_ATTRIBUTE_OR_VALUE_EXISTS; + } else { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + status = dsdb_get_extended_dn_sid(dns[i].dsdb_dn->dn, + &tmp_sid, "SID"); + if (!NT_STATUS_IS_OK(status)) { + struct GUID_txt_buf guid_str; + ldb_asprintf_errstring(ldb, + "NO SID PROVIDED... Attribute %s already " + "exists for target GUID %s", + el->name, + GUID_buf_string(&exact->guid, + &guid_str)); + talloc_free(tmp_ctx); + return LDB_ERR_ATTRIBUTE_OR_VALUE_EXISTS; + } + + ndr_err = ndr_push_struct_blob(&sid_blob, tmp_ctx, &tmp_sid, + (ndr_push_flags_fn_t)ndr_push_dom_sid); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + ret = ldb_dn_set_extended_component(exact->dsdb_dn->dn, "SID", &sid_blob); + data_blob_free(&sid_blob); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + fixed_dnstring = dsdb_dn_get_extended_linearized( + new_values, exact->dsdb_dn, 1); + if (fixed_dnstring == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + /* + * We just replace the existing value... + */ + *exact->v = data_blob_string_const(fixed_dnstring); + + continue; + } + + if (exact != NULL) { + /* + * We are trying to add one that exists, which is only + * allowed if it was previously deleted. + * + * When we do undelete a link we change it in place. + * It will be copied across into the right spot in due + * course. + */ + uint32_t rmd_flags; + rmd_flags = dsdb_dn_rmd_flags(exact->dsdb_dn->dn); + + if (!(rmd_flags & DSDB_RMD_FLAG_DELETED)) { + struct GUID_txt_buf guid_str; + ldb_asprintf_errstring(ldb, + "Attribute %s already " + "exists for target GUID %s", + el->name, + GUID_buf_string(&exact->guid, + &guid_str)); + talloc_free(tmp_ctx); + /* error codes for 'member' need to be + special cased */ + if (ldb_attr_cmp(el->name, "member") == 0) { + return LDB_ERR_ENTRY_ALREADY_EXISTS; + } else { + return LDB_ERR_ATTRIBUTE_OR_VALUE_EXISTS; + } + } + + ret = replmd_update_la_val(new_values, exact->v, + dns[i].dsdb_dn, + exact->dsdb_dn, + &ac->our_invocation_id, + ac->seq_num, ac->seq_num, + now, false); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = replmd_add_backlink(module, replmd_private, + ac->schema, + msg_dn, + &dns[i].guid, + true, + schema_attr, + parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + continue; + } + /* + * Here we don't have an exact match. + * + * If next is NULL, this one goes beyond the end of the + * existing list, so we need to add all of those ones first. + * + * If next is not NULL, we need to add all the ones before + * next. + */ + if (next == NULL) { + offset = old_num_values; + } else { + /* next should have been parsed, but let's make sure */ + if (next->dsdb_dn == NULL) { + ret = really_parse_trusted_dn(tmp_ctx, ldb, next, + schema_attr->syntax->ldap_oid); + if (ret != LDB_SUCCESS) { + return ret; + } + } + offset = MIN(next - old_dns, old_num_values); + } + + /* put all the old ones before next on the list */ + for (; j < offset; j++) { + new_values[num_values] = *old_dns[j].v; + num_values++; + } + + ret = replmd_add_backlink(module, replmd_private, + ac->schema, msg_dn, + &dns[i].guid, + true, schema_attr, + parent); + /* Make the new linked attribute ldb_val. */ + ret = replmd_build_la_val(new_values, &new_values[num_values], + dns[i].dsdb_dn, &ac->our_invocation_id, + ac->seq_num, now); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + num_values++; + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + /* copy the rest of the old ones (if any) */ + for (; j < old_num_values; j++) { + new_values[num_values] = *old_dns[j].v; + num_values++; + } + + talloc_steal(msg->elements, new_values); + if (old_el != NULL) { + talloc_steal(msg->elements, old_el->values); + } + el->values = new_values; + el->num_values = num_values; + + talloc_free(tmp_ctx); + + /* we now tell the backend to replace all existing values + with the one we have constructed */ + el->flags = LDB_FLAG_MOD_REPLACE; + + return LDB_SUCCESS; +} + + +/* + handle deleting all active linked attributes + */ +static int replmd_modify_la_delete(struct ldb_module *module, + struct replmd_private *replmd_private, + struct replmd_replicated_request *ac, + struct ldb_message *msg, + struct ldb_message_element *el, + struct ldb_message_element *old_el, + const struct dsdb_attribute *schema_attr, + time_t t, + struct ldb_dn *msg_dn, + struct ldb_request *parent) +{ + unsigned int i; + struct parsed_dn *dns, *old_dns; + TALLOC_CTX *tmp_ctx = NULL; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_control *vanish_links_ctrl = NULL; + bool vanish_links = false; + unsigned int num_to_delete = el->num_values; + uint32_t rmd_flags; + NTTIME now; + + unix_to_nt_time(&now, t); + + if (old_el == NULL || old_el->num_values == 0) { + /* there is nothing to delete... */ + if (num_to_delete == 0) { + /* and we're deleting nothing, so that's OK */ + return LDB_SUCCESS; + } + return LDB_ERR_NO_SUCH_ATTRIBUTE; + } + + tmp_ctx = talloc_new(msg); + if (tmp_ctx == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = get_parsed_dns(module, tmp_ctx, el, &dns, + schema_attr->syntax->ldap_oid, parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = get_parsed_dns_trusted_fallback(module, replmd_private, + tmp_ctx, old_el, &old_dns, + schema_attr->syntax->ldap_oid, + parent); + + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + vanish_links_ctrl = ldb_request_get_control(parent, DSDB_CONTROL_REPLMD_VANISH_LINKS); + if (vanish_links_ctrl) { + vanish_links = true; + vanish_links_ctrl->critical = false; + } + + /* we empty out el->values here to avoid damage if we return early. */ + el->num_values = 0; + el->values = NULL; + + /* + * If vanish links is set, we are actually removing members of + * old_el->values; otherwise we are just marking them deleted. + * + * There is a special case when no values are given: we remove them + * all. When we have the vanish_links control we just have to remove + * the backlinks and change our element to replace the existing values + * with the empty list. + */ + + if (num_to_delete == 0) { + for (i = 0; i < old_el->num_values; i++) { + struct parsed_dn *p = &old_dns[i]; + if (p->dsdb_dn == NULL) { + ret = really_parse_trusted_dn(tmp_ctx, ldb, p, + schema_attr->syntax->ldap_oid); + if (ret != LDB_SUCCESS) { + return ret; + } + } + ret = replmd_add_backlink(module, replmd_private, + ac->schema, msg_dn, &p->guid, + false, schema_attr, + parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + if (vanish_links) { + continue; + } + + rmd_flags = dsdb_dn_rmd_flags(p->dsdb_dn->dn); + if (rmd_flags & DSDB_RMD_FLAG_DELETED) { + continue; + } + + ret = replmd_update_la_val(old_el->values, p->v, + p->dsdb_dn, p->dsdb_dn, + &ac->our_invocation_id, + ac->seq_num, ac->seq_num, + now, true); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + + if (vanish_links) { + el->flags = LDB_FLAG_MOD_REPLACE; + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + } + + + for (i = 0; i < num_to_delete; i++) { + struct parsed_dn *p = &dns[i]; + struct parsed_dn *exact = NULL; + struct parsed_dn *next = NULL; + ret = parsed_dn_find(ldb, old_dns, old_el->num_values, + &p->guid, + NULL, + p->dsdb_dn->extra_part, 0, + &exact, &next, + schema_attr->syntax->ldap_oid, + true); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + if (exact == NULL) { + struct GUID_txt_buf buf; + ldb_asprintf_errstring(ldb, "Attribute %s doesn't " + "exist for target GUID %s", + el->name, + GUID_buf_string(&p->guid, &buf)); + if (ldb_attr_cmp(el->name, "member") == 0) { + talloc_free(tmp_ctx); + return LDB_ERR_UNWILLING_TO_PERFORM; + } else { + talloc_free(tmp_ctx); + return LDB_ERR_NO_SUCH_ATTRIBUTE; + } + } + + if (vanish_links) { + if (CHECK_DEBUGLVL(5)) { + rmd_flags = dsdb_dn_rmd_flags(exact->dsdb_dn->dn); + if ((rmd_flags & DSDB_RMD_FLAG_DELETED)) { + struct GUID_txt_buf buf; + const char *guid_str = \ + GUID_buf_string(&p->guid, &buf); + DEBUG(5, ("Deleting deleted linked " + "attribute %s to %s, because " + "vanish_links control is set\n", + el->name, guid_str)); + } + } + + /* remove the backlink */ + ret = replmd_add_backlink(module, + replmd_private, + ac->schema, + msg_dn, + &p->guid, + false, schema_attr, + parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + /* We flag the deletion and tidy it up later. */ + exact->v = NULL; + continue; + } + + rmd_flags = dsdb_dn_rmd_flags(exact->dsdb_dn->dn); + + if (rmd_flags & DSDB_RMD_FLAG_DELETED) { + struct GUID_txt_buf buf; + const char *guid_str = GUID_buf_string(&p->guid, &buf); + ldb_asprintf_errstring(ldb, "Attribute %s already " + "deleted for target GUID %s", + el->name, guid_str); + if (ldb_attr_cmp(el->name, "member") == 0) { + talloc_free(tmp_ctx); + return LDB_ERR_UNWILLING_TO_PERFORM; + } else { + talloc_free(tmp_ctx); + return LDB_ERR_NO_SUCH_ATTRIBUTE; + } + } + + ret = replmd_update_la_val(old_el->values, exact->v, + exact->dsdb_dn, exact->dsdb_dn, + &ac->our_invocation_id, + ac->seq_num, ac->seq_num, + now, true); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + ret = replmd_add_backlink(module, replmd_private, + ac->schema, msg_dn, + &p->guid, + false, schema_attr, + parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + + if (vanish_links) { + unsigned j = 0; + struct ldb_val *tmp_vals = NULL; + + tmp_vals = talloc_array(tmp_ctx, struct ldb_val, + old_el->num_values); + if (tmp_vals == NULL) { + talloc_free(tmp_ctx); + return ldb_module_oom(module); + } + for (i = 0; i < old_el->num_values; i++) { + if (old_dns[i].v == NULL) { + continue; + } + tmp_vals[j] = *old_dns[i].v; + j++; + } + for (i = 0; i < j; i++) { + old_el->values[i] = tmp_vals[i]; + } + old_el->num_values = j; + } + + el->values = talloc_steal(msg->elements, old_el->values); + el->num_values = old_el->num_values; + + talloc_free(tmp_ctx); + + /* we now tell the backend to replace all existing values + with the one we have constructed */ + el->flags = LDB_FLAG_MOD_REPLACE; + + return LDB_SUCCESS; +} + +/* + handle replacing a linked attribute + */ +static int replmd_modify_la_replace(struct ldb_module *module, + struct replmd_private *replmd_private, + struct replmd_replicated_request *ac, + struct ldb_message *msg, + struct ldb_message_element *el, + struct ldb_message_element *old_el, + const struct dsdb_attribute *schema_attr, + time_t t, + struct ldb_dn *msg_dn, + struct ldb_request *parent) +{ + unsigned int i, old_i, new_i; + struct parsed_dn *dns, *old_dns; + TALLOC_CTX *tmp_ctx = talloc_new(msg); + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_val *new_values = NULL; + const char *ldap_oid = schema_attr->syntax->ldap_oid; + unsigned int old_num_values; + unsigned int repl_num_values; + unsigned int max_num_values; + NTTIME now; + + unix_to_nt_time(&now, t); + + /* + * The replace operation is unlike the replace and delete cases in that + * we need to look at every existing link to see whether it is being + * retained or deleted. In other words, we can't avoid parsing the GUIDs. + * + * As we are trying to combine two sorted lists, the algorithm we use + * is akin to the merge phase of a merge sort. We interleave the two + * lists, doing different things depending on which side the current + * item came from. + * + * There are three main cases, with some sub-cases. + * + * - a DN is in the old list but not the new one. It needs to be + * marked as deleted (but left in the list). + * - maybe it is already deleted, and we have less to do. + * + * - a DN is in both lists. The old data gets replaced by the new, + * and the list doesn't grow. The old link may have been marked as + * deleted, in which case we undelete it. + * + * - a DN is in the new list only. We add it in the right place. + */ + + old_num_values = old_el ? old_el->num_values : 0; + repl_num_values = el->num_values; + max_num_values = old_num_values + repl_num_values; + + if (max_num_values == 0) { + /* There is nothing to do! */ + return LDB_SUCCESS; + } + + /* + * At the successful end of these functions el->values is + * overwritten with new_values. However get_parsed_dns() + * points p->v at the supplied el and it effectively gets used + * as a working area by replmd_build_la_val(). So we must + * duplicate it because our caller only called + * ldb_msg_copy_shallow(). + */ + + el->values = talloc_memdup(tmp_ctx, + el->values, + sizeof(el->values[0]) * el->num_values); + if (el->values == NULL) { + ldb_module_oom(module); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = get_parsed_dns(module, tmp_ctx, el, &dns, ldap_oid, parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = check_parsed_dn_duplicates(module, el, dns); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = get_parsed_dns(module, tmp_ctx, old_el, &old_dns, + ldap_oid, parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = replmd_check_upgrade_links(ldb, old_dns, old_num_values, + old_el, ldap_oid); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + new_values = talloc_array(tmp_ctx, struct ldb_val, max_num_values); + if (new_values == NULL) { + ldb_module_oom(module); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + old_i = 0; + new_i = 0; + for (i = 0; i < max_num_values; i++) { + int cmp; + struct parsed_dn *old_p, *new_p; + if (old_i < old_num_values && new_i < repl_num_values) { + old_p = &old_dns[old_i]; + new_p = &dns[new_i]; + cmp = parsed_dn_compare(old_p, new_p); + } else if (old_i < old_num_values) { + /* the new list is empty, read the old list */ + old_p = &old_dns[old_i]; + new_p = NULL; + cmp = -1; + } else if (new_i < repl_num_values) { + /* the old list is empty, read new list */ + old_p = NULL; + new_p = &dns[new_i]; + cmp = 1; + } else { + break; + } + + if (cmp < 0) { + /* + * An old ones that come before the next replacement + * (if any). We mark it as deleted and add it to the + * final list. + */ + uint32_t rmd_flags = dsdb_dn_rmd_flags(old_p->dsdb_dn->dn); + if ((rmd_flags & DSDB_RMD_FLAG_DELETED) == 0) { + ret = replmd_update_la_val(new_values, old_p->v, + old_p->dsdb_dn, + old_p->dsdb_dn, + &ac->our_invocation_id, + ac->seq_num, ac->seq_num, + now, true); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = replmd_add_backlink(module, replmd_private, + ac->schema, + msg_dn, + &old_p->guid, false, + schema_attr, + parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + new_values[i] = *old_p->v; + old_i++; + } else if (cmp == 0) { + /* + * We are overwriting one. If it was previously + * deleted, we need to add a backlink. + * + * Note that if any RMD_FLAGs in an extended new DN + * will be ignored. + */ + uint32_t rmd_flags; + + ret = replmd_update_la_val(new_values, old_p->v, + new_p->dsdb_dn, + old_p->dsdb_dn, + &ac->our_invocation_id, + ac->seq_num, ac->seq_num, + now, false); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + rmd_flags = dsdb_dn_rmd_flags(old_p->dsdb_dn->dn); + if ((rmd_flags & DSDB_RMD_FLAG_DELETED) != 0) { + ret = replmd_add_backlink(module, replmd_private, + ac->schema, + msg_dn, + &new_p->guid, true, + schema_attr, + parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + + new_values[i] = *old_p->v; + old_i++; + new_i++; + } else { + /* + * Replacements that don't match an existing one. We + * just add them to the final list. + */ + ret = replmd_build_la_val(new_values, + new_p->v, + new_p->dsdb_dn, + &ac->our_invocation_id, + ac->seq_num, now); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + ret = replmd_add_backlink(module, replmd_private, + ac->schema, + msg_dn, + &new_p->guid, true, + schema_attr, + parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + new_values[i] = *new_p->v; + new_i++; + } + } + if (old_el != NULL) { + talloc_steal(msg->elements, old_el->values); + } + el->values = talloc_steal(msg->elements, new_values); + el->num_values = i; + talloc_free(tmp_ctx); + + el->flags = LDB_FLAG_MOD_REPLACE; + + return LDB_SUCCESS; +} + + +/* + handle linked attributes in modify requests + */ +static int replmd_modify_handle_linked_attribs(struct ldb_module *module, + struct replmd_private *replmd_private, + struct replmd_replicated_request *ac, + struct ldb_message *msg, + time_t t, + struct ldb_request *parent) +{ + struct ldb_result *res; + unsigned int i; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_message *old_msg; + + if (dsdb_functional_level(ldb) == DS_DOMAIN_FUNCTION_2000) { + /* + * Nothing special is required for modifying or vanishing links + * in fl2000 since they are just strings in a multi-valued + * attribute. + */ + struct ldb_control *ctrl = ldb_request_get_control(parent, + DSDB_CONTROL_REPLMD_VANISH_LINKS); + if (ctrl) { + ctrl->critical = false; + } + return LDB_SUCCESS; + } + + /* + * TODO: + * + * We should restrict this to the intersection of the list of + * linked attributes in the schema and the list of attributes + * being modified. + * + * This will help performance a little, as otherwise we have + * to allocate the entire object value-by-value. + */ + ret = dsdb_module_search_dn(module, msg, &res, msg->dn, NULL, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + parent); + if (ret != LDB_SUCCESS) { + return ret; + } + + old_msg = res->msgs[0]; + + for (i=0; i<msg->num_elements; i++) { + struct ldb_message_element *el = &msg->elements[i]; + struct ldb_message_element *old_el, *new_el; + unsigned int mod_type = LDB_FLAG_MOD_TYPE(el->flags); + const struct dsdb_attribute *schema_attr + = dsdb_attribute_by_lDAPDisplayName(ac->schema, el->name); + if (!schema_attr) { + ldb_asprintf_errstring(ldb, + "%s: attribute %s is not a valid attribute in schema", + __FUNCTION__, el->name); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + if (schema_attr->linkID == 0) { + continue; + } + if ((schema_attr->linkID & 1) == 1) { + struct ldb_control *ctrl; + + ctrl = ldb_request_get_control(parent, + DSDB_CONTROL_REPLMD_VANISH_LINKS); + if (ctrl != NULL) { + ctrl->critical = false; + continue; + } + ctrl = ldb_request_get_control(parent, + DSDB_CONTROL_DBCHECK); + if (ctrl != NULL) { + continue; + } + + /* Odd is for the target. Illegal to modify */ + ldb_asprintf_errstring(ldb, + "attribute %s must not be modified directly, it is a linked attribute", el->name); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + old_el = ldb_msg_find_element(old_msg, el->name); + switch (mod_type) { + case LDB_FLAG_MOD_REPLACE: + ret = replmd_modify_la_replace(module, replmd_private, + ac, msg, el, old_el, + schema_attr, t, + old_msg->dn, + parent); + break; + case LDB_FLAG_MOD_DELETE: + ret = replmd_modify_la_delete(module, replmd_private, + ac, msg, el, old_el, + schema_attr, t, + old_msg->dn, + parent); + break; + case LDB_FLAG_MOD_ADD: + ret = replmd_modify_la_add(module, replmd_private, + ac, msg, el, old_el, + schema_attr, t, + old_msg->dn, + parent); + break; + default: + ldb_asprintf_errstring(ldb, + "invalid flags 0x%x for %s linked attribute", + el->flags, el->name); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + if (ret != LDB_SUCCESS) { + return ret; + } + ret = dsdb_check_single_valued_link(schema_attr, el); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "Attribute %s is single valued but more than one value has been supplied", + el->name); + /* Return codes as found on Windows 2012r2 */ + if (mod_type == LDB_FLAG_MOD_REPLACE) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } else { + return LDB_ERR_ATTRIBUTE_OR_VALUE_EXISTS; + } + } else { + el->flags |= LDB_FLAG_INTERNAL_DISABLE_SINGLE_VALUE_CHECK; + } + + if (old_el) { + ldb_msg_remove_attr(old_msg, el->name); + } + ldb_msg_add_empty(old_msg, el->name, 0, &new_el); + new_el->num_values = el->num_values; + new_el->values = talloc_steal(msg->elements, el->values); + + /* TODO: this relises a bit too heavily on the exact + behaviour of ldb_msg_find_element and + ldb_msg_remove_element */ + old_el = ldb_msg_find_element(msg, el->name); + if (old_el != el) { + ldb_msg_remove_element(msg, old_el); + i--; + } + } + + talloc_free(res); + return ret; +} + + +static int send_rodc_referral(struct ldb_request *req, + struct ldb_context *ldb, + struct ldb_dn *dn) +{ + char *referral = NULL; + struct loadparm_context *lp_ctx = NULL; + struct ldb_dn *fsmo_role_dn = NULL; + struct ldb_dn *role_owner_dn = NULL; + const char *domain = NULL; + WERROR werr; + + lp_ctx = talloc_get_type(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + + werr = dsdb_get_fsmo_role_info(req, ldb, DREPL_PDC_MASTER, + &fsmo_role_dn, &role_owner_dn); + + if (W_ERROR_IS_OK(werr)) { + struct ldb_dn *server_dn = ldb_dn_copy(req, role_owner_dn); + if (server_dn != NULL) { + ldb_dn_remove_child_components(server_dn, 1); + domain = samdb_dn_to_dnshostname(ldb, req, + server_dn); + } + } + + if (domain == NULL) { + domain = lpcfg_dnsdomain(lp_ctx); + } + + referral = talloc_asprintf(req, "ldap://%s/%s", + domain, + ldb_dn_get_linearized(dn)); + if (referral == NULL) { + ldb_oom(ldb); + return LDB_ERR_OPERATIONS_ERROR; + } + + return ldb_module_send_referral(req, referral); +} + + +static int replmd_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct replmd_replicated_request *ac; + struct ldb_request *down_req; + struct ldb_message *msg; + time_t t = time(NULL); + int ret; + bool is_urgent = false, rodc = false; + bool is_schema_nc = false; + unsigned int functional_level; + const struct ldb_message_element *guid_el = NULL; + struct ldb_control *sd_propagation_control; + struct ldb_control *fix_links_control = NULL; + struct ldb_control *fix_dn_name_control = NULL; + struct ldb_control *fix_dn_sid_control = NULL; + struct replmd_private *replmd_private = + talloc_get_type(ldb_module_get_private(module), struct replmd_private); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.mod.message->dn)) { + return ldb_next_request(module, req); + } + + sd_propagation_control = ldb_request_get_control(req, + DSDB_CONTROL_SEC_DESC_PROPAGATION_OID); + if (sd_propagation_control != NULL) { + if (req->op.mod.message->num_elements != 1) { + return ldb_module_operr(module); + } + ret = strcmp(req->op.mod.message->elements[0].name, + "nTSecurityDescriptor"); + if (ret != 0) { + return ldb_module_operr(module); + } + + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + fix_links_control = ldb_request_get_control(req, + DSDB_CONTROL_DBCHECK_FIX_DUPLICATE_LINKS); + if (fix_links_control != NULL) { + struct dsdb_schema *schema = NULL; + const struct dsdb_attribute *sa = NULL; + + if (req->op.mod.message->num_elements != 1) { + return ldb_module_operr(module); + } + + if (LDB_FLAG_MOD_TYPE(req->op.mod.message->elements[0].flags) != LDB_FLAG_MOD_REPLACE) { + return ldb_module_operr(module); + } + + schema = dsdb_get_schema(ldb, req); + if (schema == NULL) { + return ldb_module_operr(module); + } + + sa = dsdb_attribute_by_lDAPDisplayName(schema, + req->op.mod.message->elements[0].name); + if (sa == NULL) { + return ldb_module_operr(module); + } + + if (sa->linkID == 0) { + return ldb_module_operr(module); + } + + fix_links_control->critical = false; + return ldb_next_request(module, req); + } + + fix_dn_name_control = ldb_request_get_control(req, + DSDB_CONTROL_DBCHECK_FIX_LINK_DN_NAME); + if (fix_dn_name_control != NULL) { + struct dsdb_schema *schema = NULL; + const struct dsdb_attribute *sa = NULL; + + if (req->op.mod.message->num_elements != 2) { + return ldb_module_operr(module); + } + + if (LDB_FLAG_MOD_TYPE(req->op.mod.message->elements[0].flags) != LDB_FLAG_MOD_DELETE) { + return ldb_module_operr(module); + } + + if (LDB_FLAG_MOD_TYPE(req->op.mod.message->elements[1].flags) != LDB_FLAG_MOD_ADD) { + return ldb_module_operr(module); + } + + if (req->op.mod.message->elements[0].num_values != 1) { + return ldb_module_operr(module); + } + + if (req->op.mod.message->elements[1].num_values != 1) { + return ldb_module_operr(module); + } + + schema = dsdb_get_schema(ldb, req); + if (schema == NULL) { + return ldb_module_operr(module); + } + + if (ldb_attr_cmp(req->op.mod.message->elements[0].name, + req->op.mod.message->elements[1].name) != 0) { + return ldb_module_operr(module); + } + + sa = dsdb_attribute_by_lDAPDisplayName(schema, + req->op.mod.message->elements[0].name); + if (sa == NULL) { + return ldb_module_operr(module); + } + + if (sa->dn_format == DSDB_INVALID_DN) { + return ldb_module_operr(module); + } + + if (sa->linkID != 0) { + return ldb_module_operr(module); + } + + /* + * If we are run from dbcheck and we are not updating + * a link (as these would need to be sorted and so + * can't go via such a simple update, then do not + * trigger replicated updates and a new USN from this + * change, it wasn't a real change, just a new + * (correct) string DN + */ + + fix_dn_name_control->critical = false; + return ldb_next_request(module, req); + } + + ldb_debug(ldb, LDB_DEBUG_TRACE, "replmd_modify\n"); + + guid_el = ldb_msg_find_element(req->op.mod.message, "objectGUID"); + if (guid_el != NULL) { + ldb_set_errstring(ldb, + "replmd_modify: it's not allowed to change the objectGUID!"); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + ac = replmd_ctx_init(module, req); + if (ac == NULL) { + return ldb_module_oom(module); + } + + functional_level = dsdb_functional_level(ldb); + + /* we have to copy the message as the caller might have it as a const */ + msg = ldb_msg_copy_shallow(ac, req->op.mod.message); + if (msg == NULL) { + ldb_oom(ldb); + talloc_free(ac); + return LDB_ERR_OPERATIONS_ERROR; + } + + fix_dn_sid_control = ldb_request_get_control(req, + DSDB_CONTROL_DBCHECK_FIX_LINK_DN_SID); + if (fix_dn_sid_control != NULL) { + const struct dsdb_attribute *sa = NULL; + + if (msg->num_elements != 1) { + talloc_free(ac); + return ldb_module_operr(module); + } + + if (LDB_FLAG_MOD_TYPE(msg->elements[0].flags) != LDB_FLAG_MOD_ADD) { + talloc_free(ac); + return ldb_module_operr(module); + } + + if (msg->elements[0].num_values != 1) { + talloc_free(ac); + return ldb_module_operr(module); + } + + sa = dsdb_attribute_by_lDAPDisplayName(ac->schema, + msg->elements[0].name); + if (sa == NULL) { + talloc_free(ac); + return ldb_module_operr(module); + } + + if (sa->dn_format != DSDB_NORMAL_DN) { + talloc_free(ac); + return ldb_module_operr(module); + } + + fix_dn_sid_control->critical = false; + ac->fix_link_sid = true; + + goto handle_linked_attribs; + } + + ldb_msg_remove_attr(msg, "whenChanged"); + ldb_msg_remove_attr(msg, "uSNChanged"); + + is_schema_nc = ldb_dn_compare_base(replmd_private->schema_dn, msg->dn) == 0; + + ret = replmd_update_rpmd(module, ac->schema, req, NULL, + msg, &ac->seq_num, t, is_schema_nc, + &is_urgent, &rodc); + if (rodc && (ret == LDB_ERR_REFERRAL)) { + ret = send_rodc_referral(req, ldb, msg->dn); + talloc_free(ac); + return ret; + + } + + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + + handle_linked_attribs: + ret = replmd_modify_handle_linked_attribs(module, replmd_private, + ac, msg, t, req); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + + /* TODO: + * - replace the old object with the newly constructed one + */ + + ac->is_urgent = is_urgent; + + ret = ldb_build_mod_req(&down_req, ldb, ac, + msg, + req->controls, + ac, replmd_op_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + + /* current partition control is needed by "replmd_op_callback" */ + if (ldb_request_get_control(req, DSDB_CONTROL_CURRENT_PARTITION_OID) == NULL) { + ret = ldb_request_add_control(down_req, + DSDB_CONTROL_CURRENT_PARTITION_OID, + false, NULL); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + } + + /* If we are in functional level 2000, then + * replmd_modify_handle_linked_attribs will have done + * nothing */ + if (functional_level == DS_DOMAIN_FUNCTION_2000) { + ret = ldb_request_add_control(down_req, DSDB_CONTROL_APPLY_LINKS, false, NULL); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + } + + talloc_steal(down_req, msg); + + /* we only change whenChanged and uSNChanged if the seq_num + has changed */ + if (ac->seq_num != 0) { + ret = add_time_element(msg, "whenChanged", t); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + ldb_operr(ldb); + return ret; + } + + ret = add_uint64_element(ldb, msg, "uSNChanged", ac->seq_num); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + ldb_operr(ldb); + return ret; + } + } + + /* go on with the call chain */ + return ldb_next_request(module, down_req); +} + +static int replmd_rename_callback(struct ldb_request *req, struct ldb_reply *ares); + +/* + handle a rename request + + On a rename we need to do an extra ldb_modify which sets the + whenChanged and uSNChanged attributes. We do this in a callback after the success. + */ +static int replmd_rename(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct ldb_control *fix_dn_name_control = NULL; + struct replmd_replicated_request *ac; + int ret; + struct ldb_request *down_req; + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.rename.olddn)) { + return ldb_next_request(module, req); + } + + fix_dn_name_control = ldb_request_get_control(req, + DSDB_CONTROL_DBCHECK_FIX_LINK_DN_NAME); + if (fix_dn_name_control != NULL) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + ldb_debug(ldb, LDB_DEBUG_TRACE, "replmd_rename\n"); + + ac = replmd_ctx_init(module, req); + if (ac == NULL) { + return ldb_module_oom(module); + } + + ret = ldb_build_rename_req(&down_req, ldb, ac, + ac->req->op.rename.olddn, + ac->req->op.rename.newdn, + ac->req->controls, + ac, replmd_rename_callback, + ac->req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + + /* go on with the call chain */ + return ldb_next_request(module, down_req); +} + +/* After the rename is compleated, update the whenchanged etc */ +static int replmd_rename_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct ldb_request *down_req; + struct ldb_message *msg; + const struct dsdb_attribute *rdn_attr; + const char *rdn_name; + const struct ldb_val *rdn_val; + const char *attrs[5] = { NULL, }; + time_t t = time(NULL); + int ret; + bool is_urgent = false, rodc = false; + bool is_schema_nc; + struct replmd_replicated_request *ac = + talloc_get_type(req->context, struct replmd_replicated_request); + struct replmd_private *replmd_private = + talloc_get_type(ldb_module_get_private(ac->module), + struct replmd_private); + + ldb = ldb_module_get_ctx(ac->module); + + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + if (ares->type != LDB_REPLY_DONE) { + ldb_set_errstring(ldb, + "invalid reply type in repl_meta_data rename callback"); + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + /* TODO: + * - replace the old object with the newly constructed one + */ + + msg = ldb_msg_new(ac); + if (msg == NULL) { + ldb_oom(ldb); + return LDB_ERR_OPERATIONS_ERROR; + } + + msg->dn = ac->req->op.rename.newdn; + + is_schema_nc = ldb_dn_compare_base(replmd_private->schema_dn, msg->dn) == 0; + + rdn_name = ldb_dn_get_rdn_name(msg->dn); + if (rdn_name == NULL) { + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + ldb_operr(ldb)); + } + + /* normalize the rdn attribute name */ + rdn_attr = dsdb_attribute_by_lDAPDisplayName(ac->schema, rdn_name); + if (rdn_attr == NULL) { + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + ldb_operr(ldb)); + } + rdn_name = rdn_attr->lDAPDisplayName; + + rdn_val = ldb_dn_get_rdn_val(msg->dn); + if (rdn_val == NULL) { + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + ldb_operr(ldb)); + } + + if (ldb_msg_append_value(msg, rdn_name, rdn_val, LDB_FLAG_MOD_REPLACE) != 0) { + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + ldb_oom(ldb)); + } + if (ldb_msg_append_value(msg, "name", rdn_val, LDB_FLAG_MOD_REPLACE) != 0) { + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + ldb_oom(ldb)); + } + + /* + * here we let replmd_update_rpmd() only search for + * the existing "replPropertyMetaData" and rdn_name attributes. + * + * We do not want the existing "name" attribute as + * the "name" attribute needs to get the version + * updated on rename even if the rdn value hasn't changed. + * + * This is the diff of the meta data, for a moved user + * on a w2k8r2 server: + * + * # record 1 + * -dn: CN=sdf df,CN=Users,DC=bla,DC=base + * +dn: CN=sdf df,OU=TestOU,DC=bla,DC=base + * replPropertyMetaData: NDR: struct replPropertyMetaDataBlob + * version : 0x00000001 (1) + * reserved : 0x00000000 (0) + * @@ -66,11 +66,11 @@ replPropertyMetaData: NDR: struct re + * local_usn : 0x00000000000037a5 (14245) + * array: struct replPropertyMetaData1 + * attid : DRSUAPI_ATTID_name (0x90001) + * - version : 0x00000001 (1) + * - originating_change_time : Wed Feb 9 17:20:49 2011 CET + * + version : 0x00000002 (2) + * + originating_change_time : Wed Apr 6 15:21:01 2011 CEST + * originating_invocation_id: 0d36ca05-5507-4e62-aca3-354bab0d39e1 + * - originating_usn : 0x00000000000037a5 (14245) + * - local_usn : 0x00000000000037a5 (14245) + * + originating_usn : 0x0000000000003834 (14388) + * + local_usn : 0x0000000000003834 (14388) + * array: struct replPropertyMetaData1 + * attid : DRSUAPI_ATTID_userAccountControl (0x90008) + * version : 0x00000004 (4) + */ + attrs[0] = "replPropertyMetaData"; + attrs[1] = "objectClass"; + attrs[2] = "instanceType"; + attrs[3] = rdn_name; + attrs[4] = NULL; + + ret = replmd_update_rpmd(ac->module, ac->schema, req, attrs, + msg, &ac->seq_num, t, + is_schema_nc, &is_urgent, &rodc); + if (rodc && (ret == LDB_ERR_REFERRAL)) { + ret = send_rodc_referral(req, ldb, ac->req->op.rename.olddn); + talloc_free(ares); + return ldb_module_done(req, NULL, NULL, ret); + } + + if (ret != LDB_SUCCESS) { + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + if (ac->seq_num == 0) { + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, + ldb_error(ldb, ret, + "internal error seq_num == 0")); + } + ac->is_urgent = is_urgent; + + ret = ldb_build_mod_req(&down_req, ldb, ac, + msg, + req->controls, + ac, replmd_op_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + + /* current partition control is needed by "replmd_op_callback" */ + if (ldb_request_get_control(req, DSDB_CONTROL_CURRENT_PARTITION_OID) == NULL) { + ret = ldb_request_add_control(down_req, + DSDB_CONTROL_CURRENT_PARTITION_OID, + false, NULL); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + } + + talloc_steal(down_req, msg); + + ret = add_time_element(msg, "whenChanged", t); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + ldb_operr(ldb); + return ret; + } + + ret = add_uint64_element(ldb, msg, "uSNChanged", ac->seq_num); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + ldb_operr(ldb); + return ret; + } + + /* go on with the call chain - do the modify after the rename */ + return ldb_next_request(ac->module, down_req); +} + +/* + * remove links from objects that point at this object when an object + * is deleted. We remove it from the NEXT module per MS-DRSR 5.160 + * RemoveObj which states that link removal due to the object being + * deleted is NOT an originating update - they just go away! + * + */ +static int replmd_delete_remove_link(struct ldb_module *module, + const struct dsdb_schema *schema, + struct replmd_private *replmd_private, + struct ldb_dn *dn, + struct GUID *guid, + struct ldb_message_element *el, + const struct dsdb_attribute *sa, + struct ldb_request *parent, + bool *caller_should_vanish) +{ + unsigned int i; + TALLOC_CTX *tmp_ctx = talloc_new(module); + struct ldb_context *ldb = ldb_module_get_ctx(module); + + for (i=0; i<el->num_values; i++) { + struct dsdb_dn *dsdb_dn; + int ret; + struct ldb_message *msg; + const struct dsdb_attribute *target_attr; + struct ldb_message_element *el2; + const char *dn_str; + struct ldb_val dn_val; + uint32_t dsdb_flags = 0; + const char *attrs[] = { NULL, NULL }; + struct ldb_result *link_res; + struct ldb_message *link_msg; + struct ldb_message_element *link_el; + struct parsed_dn *link_dns; + struct parsed_dn *p = NULL, *unused = NULL; + + if (dsdb_dn_is_deleted_val(&el->values[i])) { + continue; + } + + dsdb_dn = dsdb_dn_parse(tmp_ctx, ldb, &el->values[i], sa->syntax->ldap_oid); + if (!dsdb_dn) { + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* remove the link */ + msg = ldb_msg_new(tmp_ctx); + if (!msg) { + ldb_module_oom(module); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + msg->dn = dsdb_dn->dn; + + target_attr = dsdb_attribute_by_linkID(schema, sa->linkID ^ 1); + if (target_attr == NULL) { + continue; + } + attrs[0] = target_attr->lDAPDisplayName; + + ret = ldb_msg_add_empty(msg, target_attr->lDAPDisplayName, + LDB_FLAG_MOD_DELETE, &el2); + if (ret != LDB_SUCCESS) { + ldb_module_oom(module); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = dsdb_module_search_dn(module, tmp_ctx, &link_res, + msg->dn, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_EXTENDED_DN | + DSDB_SEARCH_SHOW_RECYCLED, + parent); + + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + DBG_WARNING("Failed to find forward link object %s " + "to remove backlink %s on %s", + ldb_dn_get_linearized(msg->dn), + sa->lDAPDisplayName, + ldb_dn_get_linearized(dn)); + *caller_should_vanish = true; + continue; + } + + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + link_msg = link_res->msgs[0]; + link_el = ldb_msg_find_element(link_msg, + target_attr->lDAPDisplayName); + if (link_el == NULL) { + DBG_WARNING("Failed to find forward link on %s " + "as %s to remove backlink %s on %s", + ldb_dn_get_linearized(msg->dn), + target_attr->lDAPDisplayName, + sa->lDAPDisplayName, + ldb_dn_get_linearized(dn)); + *caller_should_vanish = true; + continue; + } + + /* + * This call 'upgrades' the links in link_dns, but we + * do not commit the result back into the database, so + * this is safe to call in FL2000 or on databases that + * have been run at that level in the past. + */ + ret = get_parsed_dns_trusted_fallback(module, replmd_private, + tmp_ctx, + link_el, &link_dns, + target_attr->syntax->ldap_oid, + parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = parsed_dn_find(ldb, link_dns, link_el->num_values, + guid, dn, + data_blob_null, 0, + &p, &unused, + target_attr->syntax->ldap_oid, false); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + if (p == NULL) { + DBG_WARNING("Failed to find forward link on %s " + "as %s to remove backlink %s on %s", + ldb_dn_get_linearized(msg->dn), + target_attr->lDAPDisplayName, + sa->lDAPDisplayName, + ldb_dn_get_linearized(dn)); + *caller_should_vanish = true; + continue; + } + + /* + * If we find a backlink to ourself, we will delete + * the forward link before we get to process that + * properly, so just let the caller process this via + * the forward link. + * + * We do this once we are sure we have the forward + * link (to ourself) in case something is very wrong + * and they are out of sync. + */ + if (ldb_dn_compare(dsdb_dn->dn, dn) == 0) { + continue; + } + + /* This needs to get the Binary DN, by first searching */ + dn_str = dsdb_dn_get_linearized(tmp_ctx, + p->dsdb_dn); + + dn_val = data_blob_string_const(dn_str); + el2->values = &dn_val; + el2->num_values = 1; + + /* + * Ensure that we tell the modification to vanish any linked + * attributes (not simply mark them as isDeleted = TRUE) + */ + dsdb_flags |= DSDB_REPLMD_VANISH_LINKS; + + ret = dsdb_module_modify(module, msg, dsdb_flags|DSDB_FLAG_OWN_MODULE, parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + + +/* + handle update of replication meta data for deletion of objects + + This also handles the mapping of delete to a rename operation + to allow deletes to be replicated. + + It also handles the incoming deleted objects, to ensure they are + fully deleted here. In that case re_delete is true, and we do not + use this as a signal to change the deleted state, just reinforce it. + + */ +static int replmd_delete_internals(struct ldb_module *module, struct ldb_request *req, bool re_delete) +{ + int ret = LDB_ERR_OTHER; + bool retb, disallow_move_on_delete; + struct ldb_dn *old_dn = NULL, *new_dn = NULL; + const char *rdn_name; + const struct ldb_val *rdn_value, *new_rdn_value; + struct GUID guid; + struct ldb_context *ldb = ldb_module_get_ctx(module); + const struct dsdb_schema *schema; + struct ldb_message *msg, *old_msg; + struct ldb_message_element *el; + TALLOC_CTX *tmp_ctx; + struct ldb_result *res, *parent_res; + static const char * const preserved_attrs[] = { + /* + * This list MUST be kept in case-insensitive sorted order, + * as we use it in a binary search with ldb_attr_cmp(). + * + * We get this hard-coded list from + * MS-ADTS section 3.1.1.5.5.1.1 "Tombstone Requirements". + */ + "attributeID", + "attributeSyntax", + "distinguishedName", + "dNReferenceUpdate", + "dNSHostName", + "flatName", + "governsID", + "groupType", + "instanceType", + "isDeleted", + "isRecycled", + "lastKnownParent", + "lDAPDisplayName", + "legacyExchangeDN", + "mS-DS-CreatorSID", + "msDS-LastKnownRDN", + "msDS-PortLDAP", + "mSMQOwnerID", + "name", + "nCName", + "nTSecurityDescriptor", + "objectClass", + "objectGUID", + "objectSid", + "oMSyntax", + "proxiedObjectName", + "replPropertyMetaData", + "sAMAccountName", + "securityIdentifier", + "sIDHistory", + "subClassOf", + "systemFlags", + "trustAttributes", + "trustDirection", + "trustPartner", + "trustType", + "userAccountControl", + "uSNChanged", + "uSNCreated", + "whenChanged", + "whenCreated", + /* + * DO NOT JUST APPEND TO THIS LIST. + * + * In case you missed the note at the top, this list is kept + * in case-insensitive sorted order. In the unlikely event you + * need to add an attrbute, please add it in the RIGHT PLACE. + */ + }; + static const char * const all_attrs[] = { + DSDB_SECRET_ATTRIBUTES, + "*", + NULL + }; + static const struct ldb_val true_val = { + .data = discard_const_p(uint8_t, "TRUE"), + .length = 4 + }; + + unsigned int i; + uint32_t dsdb_flags = 0; + struct replmd_private *replmd_private; + enum deletion_state deletion_state, next_deletion_state; + + if (ldb_dn_is_special(req->op.del.dn)) { + return ldb_next_request(module, req); + } + + /* + * We have to allow dbcheck to remove an object that + * is beyond repair, and to do so totally. This could + * mean we we can get a partial object from the other + * DC, causing havoc, so dbcheck suggests + * re-replication first. dbcheck sets both DBCHECK + * and RELAX in this situation. + */ + if (ldb_request_get_control(req, LDB_CONTROL_RELAX_OID) + && ldb_request_get_control(req, DSDB_CONTROL_DBCHECK)) { + /* really, really remove it */ + return ldb_next_request(module, req); + } + + tmp_ctx = talloc_new(ldb); + if (!tmp_ctx) { + ldb_oom(ldb); + return LDB_ERR_OPERATIONS_ERROR; + } + + schema = dsdb_get_schema(ldb, tmp_ctx); + if (!schema) { + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + old_dn = ldb_dn_copy(tmp_ctx, req->op.del.dn); + + /* we need the complete msg off disk, so we can work out which + attributes need to be removed */ + ret = dsdb_module_search_dn(module, tmp_ctx, &res, old_dn, all_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, req); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "repmd_delete: Failed to %s %s, because we failed to find it: %s", + re_delete ? "re-delete" : "delete", + ldb_dn_get_linearized(old_dn), + ldb_errstring(ldb_module_get_ctx(module))); + talloc_free(tmp_ctx); + return ret; + } + old_msg = res->msgs[0]; + + replmd_deletion_state(module, old_msg, + &deletion_state, + &next_deletion_state); + + /* This supports us noticing an incoming isDeleted and acting on it */ + if (re_delete) { + SMB_ASSERT(deletion_state > OBJECT_NOT_DELETED); + next_deletion_state = deletion_state; + } + + if (next_deletion_state == OBJECT_REMOVED) { + /* + * We have to prevent objects being deleted, even if + * the administrator really wants them gone, as + * without the tombstone, we can get a partial object + * from the other DC, causing havoc. + * + * The only other valid case is when the 180 day + * timeout has expired, when relax is specified. + */ + if (ldb_request_get_control(req, LDB_CONTROL_RELAX_OID)) { + /* it is already deleted - really remove it this time */ + talloc_free(tmp_ctx); + return ldb_next_request(module, req); + } + + ldb_asprintf_errstring(ldb, "Refusing to delete tombstone object %s. " + "This check is to prevent corruption of the replicated state.", + ldb_dn_get_linearized(old_msg->dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + rdn_name = ldb_dn_get_rdn_name(old_dn); + rdn_value = ldb_dn_get_rdn_val(old_dn); + if ((rdn_name == NULL) || (rdn_value == NULL)) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + msg = ldb_msg_new(tmp_ctx); + if (msg == NULL) { + ldb_module_oom(module); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + msg->dn = old_dn; + + /* consider the SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE flag */ + disallow_move_on_delete = + (ldb_msg_find_attr_as_int(old_msg, "systemFlags", 0) + & SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE); + + /* work out where we will be renaming this object to */ + if (!disallow_move_on_delete) { + struct ldb_dn *deleted_objects_dn; + ret = dsdb_get_deleted_objects_dn(ldb, tmp_ctx, old_dn, + &deleted_objects_dn); + + /* + * We should not move objects if we can't find the + * deleted objects DN. Not moving (or otherwise + * harming) the Deleted Objects DN itself is handled + * in the caller. + */ + if (re_delete && (ret != LDB_SUCCESS)) { + new_dn = ldb_dn_get_parent(tmp_ctx, old_dn); + if (new_dn == NULL) { + ldb_module_oom(module); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + } else if (ret != LDB_SUCCESS) { + /* this is probably an attempted delete on a partition + * that doesn't allow delete operations, such as the + * schema partition */ + ldb_asprintf_errstring(ldb, "No Deleted Objects container for DN %s", + ldb_dn_get_linearized(old_dn)); + talloc_free(tmp_ctx); + return LDB_ERR_UNWILLING_TO_PERFORM; + } else { + new_dn = deleted_objects_dn; + } + } else { + new_dn = ldb_dn_get_parent(tmp_ctx, old_dn); + if (new_dn == NULL) { + ldb_module_oom(module); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + } + + /* get the objects GUID from the search we just did */ + guid = samdb_result_guid(old_msg, "objectGUID"); + + if (deletion_state == OBJECT_NOT_DELETED) { + struct ldb_message_element *is_deleted_el; + + ret = replmd_make_deleted_child_dn(tmp_ctx, + ldb, + new_dn, + rdn_name, rdn_value, + guid); + + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = ldb_msg_add_value(msg, "isDeleted", &true_val, + &is_deleted_el); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, __location__ + ": Failed to add isDeleted string to the msg"); + talloc_free(tmp_ctx); + return ret; + } + is_deleted_el->flags = LDB_FLAG_MOD_REPLACE; + } else { + /* + * No matter what has happened with other renames etc, try again to + * get this to be under the deleted DN. See MS-DRSR 5.160 RemoveObj + */ + + struct ldb_dn *rdn = ldb_dn_copy(tmp_ctx, old_dn); + retb = ldb_dn_remove_base_components(rdn, ldb_dn_get_comp_num(rdn) - 1); + if (!retb) { + ldb_asprintf_errstring(ldb, __location__ + ": Unable to add a prepare rdn of %s", + ldb_dn_get_linearized(rdn)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + SMB_ASSERT(ldb_dn_get_comp_num(rdn) == 1); + + retb = ldb_dn_add_child(new_dn, rdn); + if (!retb) { + ldb_asprintf_errstring(ldb, __location__ + ": Unable to add rdn %s to base dn: %s", + ldb_dn_get_linearized(rdn), + ldb_dn_get_linearized(new_dn)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + } + + /* + now we need to modify the object in the following ways: + + - add isDeleted=TRUE + - update rDN and name, with new rDN + - remove linked attributes + - remove objectCategory and sAMAccountType + - remove attribs not on the preserved list + - preserved if in above list, or is rDN + - remove all linked attribs from this object + - remove all links from other objects to this object + (note we use the backlinks to do this, so we won't find one-way + links that still point to this object, or deactivated two-way + links, i.e. 'member' after the user has been removed from the + group) + - add lastKnownParent + - update replPropertyMetaData? + + see MS-ADTS "Tombstone Requirements" section 3.1.1.5.5.1.1 + */ + + if (deletion_state == OBJECT_NOT_DELETED) { + struct ldb_dn *parent_dn = ldb_dn_get_parent(tmp_ctx, old_dn); + char *parent_dn_str = NULL; + struct ldb_message_element *p_el; + + /* we need the storage form of the parent GUID */ + ret = dsdb_module_search_dn(module, tmp_ctx, &parent_res, + parent_dn, NULL, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT | + DSDB_SEARCH_REVEAL_INTERNALS| + DSDB_SEARCH_SHOW_RECYCLED, req); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "repmd_delete: Failed to %s %s, " + "because we failed to find it's parent (%s): %s", + re_delete ? "re-delete" : "delete", + ldb_dn_get_linearized(old_dn), + ldb_dn_get_linearized(parent_dn), + ldb_errstring(ldb_module_get_ctx(module))); + talloc_free(tmp_ctx); + return ret; + } + + /* + * Now we can use the DB version, + * it will have the extended DN info in it + */ + parent_dn = parent_res->msgs[0]->dn; + parent_dn_str = ldb_dn_get_extended_linearized(tmp_ctx, + parent_dn, + 1); + if (parent_dn_str == NULL) { + talloc_free(tmp_ctx); + return ldb_module_oom(module); + } + + ret = ldb_msg_add_steal_string(msg, "lastKnownParent", + parent_dn_str); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, __location__ + ": Failed to add lastKnownParent " + "string when deleting %s", + ldb_dn_get_linearized(old_dn)); + talloc_free(tmp_ctx); + return ret; + } + p_el = ldb_msg_find_element(msg, + "lastKnownParent"); + if (p_el == NULL) { + talloc_free(tmp_ctx); + return ldb_module_operr(module); + } + p_el->flags = LDB_FLAG_MOD_REPLACE; + + if (next_deletion_state == OBJECT_DELETED) { + ret = ldb_msg_add_value(msg, "msDS-LastKnownRDN", rdn_value, NULL); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, __location__ + ": Failed to add msDS-LastKnownRDN " + "string when deleting %s", + ldb_dn_get_linearized(old_dn)); + talloc_free(tmp_ctx); + return ret; + } + p_el = ldb_msg_find_element(msg, + "msDS-LastKnownRDN"); + if (p_el == NULL) { + talloc_free(tmp_ctx); + return ldb_module_operr(module); + } + p_el->flags = LDB_FLAG_MOD_ADD; + } + } + + switch (next_deletion_state) { + + case OBJECT_RECYCLED: + case OBJECT_TOMBSTONE: + + /* + * MS-ADTS 3.1.1.5.5.1.1 Tombstone Requirements + * describes what must be removed from a tombstone + * object + * + * MS-ADTS 3.1.1.5.5.1.3 Recycled-Object Requirements + * describes what must be removed from a recycled + * object + * + */ + + /* + * we also mark it as recycled, meaning this object can't be + * recovered (we are stripping its attributes). + * This is done only if we have this schema object of course ... + * This behavior is identical to the one of Windows 2008R2 which + * always set the isRecycled attribute, even if the recycle-bin is + * not activated and what ever the forest level is. + */ + if (dsdb_attribute_by_lDAPDisplayName(schema, "isRecycled") != NULL) { + struct ldb_message_element *is_recycled_el; + + ret = ldb_msg_add_value(msg, "isRecycled", &true_val, + &is_recycled_el); + if (ret != LDB_SUCCESS) { + DEBUG(0,(__location__ ": Failed to add isRecycled string to the msg\n")); + ldb_module_oom(module); + talloc_free(tmp_ctx); + return ret; + } + is_recycled_el->flags = LDB_FLAG_MOD_REPLACE; + } + + replmd_private = talloc_get_type(ldb_module_get_private(module), + struct replmd_private); + /* work out which of the old attributes we will be removing */ + for (i=0; i<old_msg->num_elements; i++) { + const struct dsdb_attribute *sa; + el = &old_msg->elements[i]; + sa = dsdb_attribute_by_lDAPDisplayName(schema, el->name); + if (!sa) { + const char *old_dn_str + = ldb_dn_get_linearized(old_dn); + + ldb_asprintf_errstring(ldb, + __location__ + ": Attribute %s " + "not found in schema " + "when deleting %s. " + "Existing record is invalid", + el->name, + old_dn_str); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + if (ldb_attr_cmp(el->name, rdn_name) == 0) { + /* don't remove the rDN */ + continue; + } + + if (sa->linkID & 1) { + bool caller_should_vanish = false; + /* + * we have a backlink in this object + * that needs to be removed. We're not + * allowed to remove it directly + * however, so we instead setup a + * modify to delete the corresponding + * forward link + */ + ret = replmd_delete_remove_link(module, schema, + replmd_private, + old_dn, &guid, + el, sa, req, + &caller_should_vanish); + if (ret != LDB_SUCCESS) { + const char *old_dn_str + = ldb_dn_get_linearized(old_dn); + ldb_asprintf_errstring(ldb, + __location__ + ": Failed to remove backlink of " + "%s when deleting %s: %s", + el->name, + old_dn_str, + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + if (caller_should_vanish == false) { + /* + * now we continue, which means we + * won't remove this backlink + * directly + */ + continue; + } + + /* + * Otherwise vanish the link, we are + * out of sync and the controlling + * object does not have the source + * link any more + */ + + dsdb_flags |= DSDB_REPLMD_VANISH_LINKS; + + } else if (sa->linkID == 0) { + const char * const *attr = NULL; + if (sa->searchFlags & SEARCH_FLAG_PRESERVEONDELETE) { + continue; + } + BINARY_ARRAY_SEARCH_V(preserved_attrs, + ARRAY_SIZE(preserved_attrs), + el->name, + ldb_attr_cmp, + attr); + /* + * If we are preserving, do not do the + * ldb_msg_add_empty() below, continue + * to the next element + */ + if (attr != NULL) { + continue; + } + } else { + /* + * Ensure that we tell the modification to vanish any linked + * attributes (not simply mark them as isDeleted = TRUE) + */ + dsdb_flags |= DSDB_REPLMD_VANISH_LINKS; + } + ret = ldb_msg_add_empty(msg, el->name, LDB_FLAG_MOD_DELETE, &el); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + ldb_module_oom(module); + return ret; + } + } + + break; + + case OBJECT_DELETED: + /* + * MS-ADTS 3.1.1.5.5.1.2 Deleted-Object Requirements + * describes what must be removed from a deleted + * object + */ + + ret = ldb_msg_add_empty(msg, "objectCategory", LDB_FLAG_MOD_REPLACE, NULL); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + ldb_module_oom(module); + return ret; + } + + ret = ldb_msg_add_empty(msg, "sAMAccountType", LDB_FLAG_MOD_REPLACE, NULL); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + ldb_module_oom(module); + return ret; + } + + break; + + default: + break; + } + + if (deletion_state == OBJECT_NOT_DELETED) { + const struct dsdb_attribute *sa; + + /* work out what the new rdn value is, for updating the + rDN and name fields */ + new_rdn_value = ldb_dn_get_rdn_val(new_dn); + if (new_rdn_value == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + sa = dsdb_attribute_by_lDAPDisplayName(schema, rdn_name); + if (!sa) { + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = ldb_msg_add_value(msg, sa->lDAPDisplayName, new_rdn_value, + &el); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + el->flags = LDB_FLAG_MOD_REPLACE; + + el = ldb_msg_find_element(old_msg, "name"); + if (el) { + ret = ldb_msg_add_value(msg, "name", new_rdn_value, &el); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + el->flags = LDB_FLAG_MOD_REPLACE; + } + } + + /* + * TODO: Per MS-DRSR 5.160 RemoveObj we should remove links directly, not as an originating update! + * + */ + + /* + * No matter what has happned with other renames, try again to + * get this to be under the deleted DN. + */ + if (strcmp(ldb_dn_get_linearized(old_dn), ldb_dn_get_linearized(new_dn)) != 0) { + /* now rename onto the new DN */ + ret = dsdb_module_rename(module, old_dn, new_dn, DSDB_FLAG_NEXT_MODULE, req); + if (ret != LDB_SUCCESS){ + DEBUG(0,(__location__ ": Failed to rename object from '%s' to '%s' - %s\n", + ldb_dn_get_linearized(old_dn), + ldb_dn_get_linearized(new_dn), + ldb_errstring(ldb))); + talloc_free(tmp_ctx); + return ret; + } + msg->dn = new_dn; + } + + ret = dsdb_module_modify(module, msg, dsdb_flags|DSDB_FLAG_OWN_MODULE, req); + if (ret != LDB_SUCCESS) { + char *s = NULL; + /* + * This should not fail, so be quite verbose in the + * error handling if it fails + */ + if (strcmp(ldb_dn_get_linearized(old_dn), + ldb_dn_get_linearized(new_dn)) != 0) { + DBG_NOTICE("Failure to handle '%s' of object %s " + "after successful rename to %s. " + "Error during tombstone modificaton was: %s\n", + re_delete ? "re-delete" : "delete", + ldb_dn_get_linearized(new_dn), + ldb_dn_get_linearized(old_dn), + ldb_errstring(ldb)); + } else { + DBG_NOTICE("Failure to handle '%s' of object %s. " + "Error during tombstone modificaton was: %s\n", + re_delete ? "re-delete" : "delete", + ldb_dn_get_linearized(new_dn), + ldb_errstring(ldb)); + } + s = ldb_ldif_message_redacted_string(ldb_module_get_ctx(module), + tmp_ctx, + LDB_CHANGETYPE_MODIFY, + msg); + + DBG_INFO("Failed tombstone modify%s was:\n%s\n", + (dsdb_flags & DSDB_REPLMD_VANISH_LINKS) ? + " with VANISH_LINKS" : "", + s); + ldb_asprintf_errstring(ldb, + "replmd_delete: Failed to modify" + " object %s in '%s' - %s", + ldb_dn_get_linearized(old_dn), + re_delete ? "re-delete" : "delete", + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + talloc_free(tmp_ctx); + + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS); +} + +static int replmd_delete(struct ldb_module *module, struct ldb_request *req) +{ + return replmd_delete_internals(module, req, false); +} + + +static int replmd_replicated_request_error(struct replmd_replicated_request *ar, int ret) +{ + return ret; +} + +static int replmd_replicated_request_werror(struct replmd_replicated_request *ar, WERROR status) +{ + int ret = LDB_ERR_OTHER; + /* TODO: do some error mapping */ + + /* Let the caller know the full WERROR */ + ar->objs->error = status; + + return ret; +} + + +static struct replPropertyMetaData1 * +replmd_replPropertyMetaData1_find_attid(struct replPropertyMetaDataBlob *md_blob, + enum drsuapi_DsAttributeId attid) +{ + uint32_t i; + struct replPropertyMetaDataCtr1 *rpmd_ctr = &md_blob->ctr.ctr1; + + for (i = 0; i < rpmd_ctr->count; i++) { + if (rpmd_ctr->array[i].attid == attid) { + return &rpmd_ctr->array[i]; + } + } + return NULL; +} + + +/* + return true if an update is newer than an existing entry + see section 5.11 of MS-ADTS +*/ +static bool replmd_update_is_newer(const struct GUID *current_invocation_id, + const struct GUID *update_invocation_id, + uint32_t current_version, + uint32_t update_version, + NTTIME current_change_time, + NTTIME update_change_time) +{ + if (update_version != current_version) { + return update_version > current_version; + } + if (update_change_time != current_change_time) { + return update_change_time > current_change_time; + } + return GUID_compare(update_invocation_id, current_invocation_id) > 0; +} + +static bool replmd_replPropertyMetaData1_is_newer(struct replPropertyMetaData1 *cur_m, + struct replPropertyMetaData1 *new_m) +{ + return replmd_update_is_newer(&cur_m->originating_invocation_id, + &new_m->originating_invocation_id, + cur_m->version, + new_m->version, + cur_m->originating_change_time, + new_m->originating_change_time); +} + +static bool replmd_replPropertyMetaData1_new_should_be_taken(uint32_t dsdb_repl_flags, + struct replPropertyMetaData1 *cur_m, + struct replPropertyMetaData1 *new_m) +{ + bool cmp; + + /* + * If the new replPropertyMetaData entry for this attribute is + * not provided (this happens in the case where we look for + * ATTID_name, but the name was not changed), then the local + * state is clearly still current, as the remote + * server didn't send it due to being older the high watermark + * USN we sent. + */ + if (new_m == NULL) { + return false; + } + + if (dsdb_repl_flags & DSDB_REPL_FLAG_PRIORITISE_INCOMING) { + /* + * if we compare equal then do an + * update. This is used when a client + * asks for a FULL_SYNC, and can be + * used to recover a corrupt + * replica. + * + * This call is a bit tricky, what we + * are doing it turning the 'is_newer' + * call into a 'not is older' by + * swapping cur_m and new_m, and negating the + * outcome. + */ + cmp = !replmd_replPropertyMetaData1_is_newer(new_m, + cur_m); + } else { + cmp = replmd_replPropertyMetaData1_is_newer(cur_m, + new_m); + } + return cmp; +} + + +/* + form a DN for a deleted (DEL:) or conflict (CNF:) DN + */ +static int replmd_make_prefix_child_dn(TALLOC_CTX *tmp_ctx, + struct ldb_context *ldb, + struct ldb_dn *dn, + const char *four_char_prefix, + const char *rdn_name, + const struct ldb_val *rdn_value, + struct GUID guid) +{ + struct ldb_val deleted_child_rdn_val; + struct GUID_txt_buf guid_str; + int ret; + bool retb; + + GUID_buf_string(&guid, &guid_str); + + retb = ldb_dn_add_child_fmt(dn, "X=Y"); + if (!retb) { + ldb_asprintf_errstring(ldb, __location__ + ": Unable to add a formatted child to dn: %s", + ldb_dn_get_linearized(dn)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * TODO: Per MS-ADTS 3.1.1.5.5 Delete Operation + * we should truncate this value to ensure the RDN is not more than 255 chars. + * + * However we MS-ADTS 3.1.1.5.1.2 Naming Constraints indicates that: + * + * "Naming constraints are not enforced for replicated + * updates." so this is safe and we don't have to work out not + * splitting a UTF8 char right now. + */ + deleted_child_rdn_val = ldb_val_dup(tmp_ctx, rdn_value); + + /* + * sizeof(guid_str.buf) will always be longer than + * strlen(guid_str.buf) but we allocate using this and + * waste the trailing bytes to avoid scaring folks + * with memcpy() using strlen() below + */ + + deleted_child_rdn_val.data + = talloc_realloc(tmp_ctx, deleted_child_rdn_val.data, + uint8_t, + rdn_value->length + 5 + + sizeof(guid_str.buf)); + if (!deleted_child_rdn_val.data) { + ldb_asprintf_errstring(ldb, __location__ + ": Unable to add a formatted child to dn: %s", + ldb_dn_get_linearized(dn)); + return LDB_ERR_OPERATIONS_ERROR; + } + + deleted_child_rdn_val.length = + rdn_value->length + 5 + + strlen(guid_str.buf); + + SMB_ASSERT(deleted_child_rdn_val.length < + talloc_get_size(deleted_child_rdn_val.data)); + + /* + * talloc won't allocate more than 256MB so we can't + * overflow but just to be sure + */ + if (deleted_child_rdn_val.length < rdn_value->length) { + return LDB_ERR_OPERATIONS_ERROR; + } + + deleted_child_rdn_val.data[rdn_value->length] = 0x0a; + memcpy(&deleted_child_rdn_val.data[rdn_value->length + 1], + four_char_prefix, 4); + memcpy(&deleted_child_rdn_val.data[rdn_value->length + 5], + guid_str.buf, + sizeof(guid_str.buf)); + + /* Now set the value into the RDN, without parsing it */ + ret = ldb_dn_set_component( + dn, + 0, + rdn_name, + deleted_child_rdn_val); + + return ret; +} + + +/* + form a conflict DN + */ +static struct ldb_dn *replmd_conflict_dn(TALLOC_CTX *mem_ctx, + struct ldb_context *ldb, + struct ldb_dn *dn, + struct GUID *guid) +{ + const struct ldb_val *rdn_val; + const char *rdn_name; + struct ldb_dn *new_dn; + int ret; + + rdn_val = ldb_dn_get_rdn_val(dn); + rdn_name = ldb_dn_get_rdn_name(dn); + if (!rdn_val || !rdn_name) { + return NULL; + } + + new_dn = ldb_dn_get_parent(mem_ctx, dn); + if (!new_dn) { + return NULL; + } + + ret = replmd_make_prefix_child_dn(mem_ctx, + ldb, new_dn, + "CNF:", + rdn_name, + rdn_val, + *guid); + if (ret != LDB_SUCCESS) { + return NULL; + } + return new_dn; +} + +/* + form a deleted DN + */ +static int replmd_make_deleted_child_dn(TALLOC_CTX *tmp_ctx, + struct ldb_context *ldb, + struct ldb_dn *dn, + const char *rdn_name, + const struct ldb_val *rdn_value, + struct GUID guid) +{ + return replmd_make_prefix_child_dn(tmp_ctx, + ldb, dn, + "DEL:", + rdn_name, + rdn_value, + guid); +} + + +/* + perform a modify operation which sets the rDN and name attributes to + their current values. This has the effect of changing these + attributes to have been last updated by the current DC. This is + needed to ensure that renames performed as part of conflict + resolution are propagated to other DCs + */ +static int replmd_name_modify(struct replmd_replicated_request *ar, + struct ldb_request *req, struct ldb_dn *dn) +{ + struct ldb_message *msg; + const char *rdn_name; + const struct ldb_val *rdn_val; + const struct dsdb_attribute *rdn_attr; + int ret; + + msg = ldb_msg_new(req); + if (msg == NULL) { + goto failed; + } + msg->dn = dn; + + rdn_name = ldb_dn_get_rdn_name(dn); + if (rdn_name == NULL) { + goto failed; + } + + /* normalize the rdn attribute name */ + rdn_attr = dsdb_attribute_by_lDAPDisplayName(ar->schema, rdn_name); + if (rdn_attr == NULL) { + goto failed; + } + rdn_name = rdn_attr->lDAPDisplayName; + + rdn_val = ldb_dn_get_rdn_val(dn); + if (rdn_val == NULL) { + goto failed; + } + + if (ldb_msg_append_value(msg, rdn_name, rdn_val, LDB_FLAG_MOD_REPLACE) != 0) { + goto failed; + } + if (ldb_msg_append_value(msg, "name", rdn_val, LDB_FLAG_MOD_REPLACE) != 0) { + goto failed; + } + + /* + * We have to mark this as a replicated update otherwise + * schema_data may reject a rename in the schema partition + */ + + ret = dsdb_module_modify(ar->module, msg, + DSDB_FLAG_OWN_MODULE|DSDB_FLAG_REPLICATED_UPDATE, + req); + if (ret != LDB_SUCCESS) { + DEBUG(0,(__location__ ": Failed to modify rDN/name of DN being DRS renamed '%s' - %s", + ldb_dn_get_linearized(dn), + ldb_errstring(ldb_module_get_ctx(ar->module)))); + return ret; + } + + talloc_free(msg); + + return LDB_SUCCESS; + +failed: + talloc_free(msg); + DEBUG(0,(__location__ ": Failed to setup modify rDN/name of DN being DRS renamed '%s'", + ldb_dn_get_linearized(dn))); + return LDB_ERR_OPERATIONS_ERROR; +} + + +/* + callback for conflict DN handling where we have renamed the incoming + record. After renaming it, we need to ensure the change of name and + rDN for the incoming record is seen as an originating update by this DC. + + This also handles updating lastKnownParent for entries sent to lostAndFound + */ +static int replmd_op_name_modify_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct replmd_replicated_request *ar = + talloc_get_type_abort(req->context, struct replmd_replicated_request); + struct ldb_dn *conflict_dn = NULL; + int ret; + + if (ares->error != LDB_SUCCESS) { + /* call the normal callback for everything except success */ + return replmd_op_callback(req, ares); + } + + switch (req->operation) { + case LDB_ADD: + conflict_dn = req->op.add.message->dn; + break; + case LDB_MODIFY: + conflict_dn = req->op.mod.message->dn; + break; + default: + smb_panic("replmd_op_name_modify_callback called in unknown circumstances"); + } + + /* perform a modify of the rDN and name of the record */ + ret = replmd_name_modify(ar, req, conflict_dn); + if (ret != LDB_SUCCESS) { + ares->error = ret; + return replmd_op_callback(req, ares); + } + + if (ar->objs->objects[ar->index_current].last_known_parent) { + struct ldb_message *msg = ldb_msg_new(req); + if (msg == NULL) { + ldb_module_oom(ar->module); + return LDB_ERR_OPERATIONS_ERROR; + } + + msg->dn = req->op.add.message->dn; + + ret = ldb_msg_add_steal_string(msg, "lastKnownParent", + ldb_dn_get_extended_linearized(msg, ar->objs->objects[ar->index_current].last_known_parent, 1)); + if (ret != LDB_SUCCESS) { + DEBUG(0,(__location__ ": Failed to add lastKnownParent string to the msg\n")); + ldb_module_oom(ar->module); + return ret; + } + msg->elements[0].flags = LDB_FLAG_MOD_REPLACE; + + ret = dsdb_module_modify(ar->module, msg, DSDB_FLAG_OWN_MODULE, req); + if (ret != LDB_SUCCESS) { + DEBUG(0,(__location__ ": Failed to modify lastKnownParent of lostAndFound DN '%s' - %s", + ldb_dn_get_linearized(msg->dn), + ldb_errstring(ldb_module_get_ctx(ar->module)))); + return ret; + } + TALLOC_FREE(msg); + } + + return replmd_op_callback(req, ares); +} + + + +/* + * A helper for replmd_op_possible_conflict_callback() and + * replmd_replicated_handle_rename() + */ +static int incoming_dn_should_be_renamed(TALLOC_CTX *mem_ctx, + struct replmd_replicated_request *ar, + struct ldb_dn *conflict_dn, + struct ldb_result **res, + bool *rename_incoming_record) +{ + int ret; + bool rodc; + enum ndr_err_code ndr_err; + const struct ldb_val *omd_value = NULL; + struct replPropertyMetaDataBlob omd, *rmd = NULL; + struct ldb_context *ldb = ldb_module_get_ctx(ar->module); + const char *attrs[] = { "replPropertyMetaData", "objectGUID", NULL }; + struct replPropertyMetaData1 *omd_name = NULL; + struct replPropertyMetaData1 *rmd_name = NULL; + struct ldb_message *msg = NULL; + + ret = samdb_rodc(ldb, &rodc); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring( + ldb, + "Failed to determine if we are an RODC when attempting " + "to form conflict DN: %s", + ldb_errstring(ldb)); + return LDB_ERR_OPERATIONS_ERROR; + } + + if (rodc) { + /* + * We are on an RODC, or were a GC for this + * partition, so we have to fail this until + * someone who owns the partition sorts it + * out + */ + ldb_asprintf_errstring( + ldb, + "Conflict adding object '%s' from incoming replication " + "but we are read only for the partition. \n" + " - We must fail the operation until a master for this " + "partition resolves the conflict", + ldb_dn_get_linearized(conflict_dn)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * first we need the replPropertyMetaData attribute from the + * old record + */ + ret = dsdb_module_search_dn(ar->module, mem_ctx, res, conflict_dn, + attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_DELETED | + DSDB_SEARCH_SHOW_RECYCLED, ar->req); + if (ret != LDB_SUCCESS) { + DBG_ERR(__location__ + ": Unable to find object for conflicting record '%s'\n", + ldb_dn_get_linearized(conflict_dn)); + return LDB_ERR_OPERATIONS_ERROR; + } + + msg = (*res)->msgs[0]; + omd_value = ldb_msg_find_ldb_val(msg, "replPropertyMetaData"); + if (omd_value == NULL) { + DBG_ERR(__location__ + ": Unable to find replPropertyMetaData for conflicting " + "record '%s'\n", + ldb_dn_get_linearized(conflict_dn)); + return LDB_ERR_OPERATIONS_ERROR; + } + + ndr_err = ndr_pull_struct_blob( + omd_value, msg, &omd, + (ndr_pull_flags_fn_t)ndr_pull_replPropertyMetaDataBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + DBG_ERR(__location__ + ": Failed to parse old replPropertyMetaData for %s\n", + ldb_dn_get_linearized(conflict_dn)); + return LDB_ERR_OPERATIONS_ERROR; + } + + rmd = ar->objs->objects[ar->index_current].meta_data; + + /* + * we decide which is newer based on the RPMD on the name + * attribute. See [MS-DRSR] ResolveNameConflict. + * + * We expect omd_name to be present, as this is from a local + * search, but while rmd_name should have been given to us by + * the remote server, if it is missing we just prefer the + * local name in + * replmd_replPropertyMetaData1_new_should_be_taken() + */ + rmd_name = replmd_replPropertyMetaData1_find_attid(rmd, + DRSUAPI_ATTID_name); + omd_name = replmd_replPropertyMetaData1_find_attid(&omd, + DRSUAPI_ATTID_name); + if (!omd_name) { + DBG_ERR(__location__ + ": Failed to find name attribute in " + "local LDB replPropertyMetaData for %s\n", + ldb_dn_get_linearized(conflict_dn)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * Should we preserve the current record, and so rename the + * incoming record to be a conflict? + */ + *rename_incoming_record = + !replmd_replPropertyMetaData1_new_should_be_taken( + (ar->objs->dsdb_repl_flags & + DSDB_REPL_FLAG_PRIORITISE_INCOMING), + omd_name, rmd_name); + + return LDB_SUCCESS; +} + + +/* + callback for replmd_replicated_apply_add() + This copes with the creation of conflict records in the case where + the DN exists, but with a different objectGUID + */ +static int replmd_op_possible_conflict_callback(struct ldb_request *req, struct ldb_reply *ares, int (*callback)(struct ldb_request *req, struct ldb_reply *ares)) +{ + struct ldb_dn *conflict_dn; + struct replmd_replicated_request *ar = + talloc_get_type_abort(req->context, struct replmd_replicated_request); + struct ldb_result *res; + int ret; + bool rename_incoming_record; + struct ldb_message *msg; + struct ldb_request *down_req = NULL; + + /* call the normal callback for success */ + if (ares->error == LDB_SUCCESS) { + return callback(req, ares); + } + + /* + * we have a conflict, and need to decide if we will keep the + * new record or the old record + */ + + msg = ar->objs->objects[ar->index_current].msg; + conflict_dn = msg->dn; + + /* For failures other than conflicts, fail the whole operation here */ + if (ares->error != LDB_ERR_ENTRY_ALREADY_EXISTS) { + ldb_asprintf_errstring(ldb_module_get_ctx(ar->module), "Failed to locally apply remote add of %s: %s", + ldb_dn_get_linearized(conflict_dn), + ldb_errstring(ldb_module_get_ctx(ar->module))); + + return ldb_module_done(ar->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + + ret = incoming_dn_should_be_renamed(req, ar, conflict_dn, &res, + &rename_incoming_record); + if (ret != LDB_SUCCESS) { + goto failed; + } + + if (rename_incoming_record) { + struct GUID guid; + struct ldb_dn *new_dn; + + guid = samdb_result_guid(msg, "objectGUID"); + if (GUID_all_zero(&guid)) { + DEBUG(0,(__location__ ": Failed to find objectGUID for conflicting incoming record %s\n", + ldb_dn_get_linearized(conflict_dn))); + goto failed; + } + new_dn = replmd_conflict_dn(req, + ldb_module_get_ctx(ar->module), + conflict_dn, &guid); + if (new_dn == NULL) { + DEBUG(0,(__location__ ": Failed to form conflict DN for %s\n", + ldb_dn_get_linearized(conflict_dn))); + goto failed; + } + + DEBUG(2,(__location__ ": Resolving conflict record via incoming rename '%s' -> '%s'\n", + ldb_dn_get_linearized(conflict_dn), ldb_dn_get_linearized(new_dn))); + + /* re-submit the request, but with the new DN */ + callback = replmd_op_name_modify_callback; + msg->dn = new_dn; + } else { + /* we are renaming the existing record */ + struct GUID guid; + struct ldb_dn *new_dn; + + guid = samdb_result_guid(res->msgs[0], "objectGUID"); + if (GUID_all_zero(&guid)) { + DEBUG(0,(__location__ ": Failed to find objectGUID for existing conflict record %s\n", + ldb_dn_get_linearized(conflict_dn))); + goto failed; + } + + new_dn = replmd_conflict_dn(req, + ldb_module_get_ctx(ar->module), + conflict_dn, &guid); + if (new_dn == NULL) { + DEBUG(0,(__location__ ": Failed to form conflict DN for %s\n", + ldb_dn_get_linearized(conflict_dn))); + goto failed; + } + + DEBUG(2,(__location__ ": Resolving conflict record via existing-record rename '%s' -> '%s'\n", + ldb_dn_get_linearized(conflict_dn), ldb_dn_get_linearized(new_dn))); + + ret = dsdb_module_rename(ar->module, conflict_dn, new_dn, + DSDB_FLAG_OWN_MODULE, req); + if (ret != LDB_SUCCESS) { + DEBUG(0,(__location__ ": Failed to rename conflict dn '%s' to '%s' - %s\n", + ldb_dn_get_linearized(conflict_dn), + ldb_dn_get_linearized(new_dn), + ldb_errstring(ldb_module_get_ctx(ar->module)))); + goto failed; + } + + /* + * now we need to ensure that the rename is seen as an + * originating update. We do that with a modify. + */ + ret = replmd_name_modify(ar, req, new_dn); + if (ret != LDB_SUCCESS) { + goto failed; + } + + DEBUG(2,(__location__ ": With conflicting record renamed, re-apply replicated creation of '%s'\n", + ldb_dn_get_linearized(req->op.add.message->dn))); + } + + ret = ldb_build_add_req(&down_req, + ldb_module_get_ctx(ar->module), + req, + msg, + ar->controls, + ar, + callback, + req); + if (ret != LDB_SUCCESS) { + goto failed; + } + LDB_REQ_SET_LOCATION(down_req); + + /* current partition control needed by "repmd_op_callback" */ + ret = ldb_request_add_control(down_req, + DSDB_CONTROL_CURRENT_PARTITION_OID, + false, NULL); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + + if (ar->objs->dsdb_repl_flags & DSDB_REPL_FLAG_PARTIAL_REPLICA) { + /* this tells the partition module to make it a + partial replica if creating an NC */ + ret = ldb_request_add_control(down_req, + DSDB_CONTROL_PARTIAL_REPLICA, + false, NULL); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + } + + /* + * Finally we re-run the add, otherwise the new record won't + * exist, as we are here because of that exact failure! + */ + return ldb_next_request(ar->module, down_req); +failed: + + /* on failure make the caller get the error. This means + * replication will stop with an error, but there is not much + * else we can do. + */ + if (ret == LDB_SUCCESS) { + ret = LDB_ERR_OPERATIONS_ERROR; + } + return ldb_module_done(ar->req, NULL, NULL, + ret); +} + +/* + callback for replmd_replicated_apply_add() + This copes with the creation of conflict records in the case where + the DN exists, but with a different objectGUID + */ +static int replmd_op_add_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct replmd_replicated_request *ar = + talloc_get_type_abort(req->context, struct replmd_replicated_request); + + if (ar->objs->objects[ar->index_current].last_known_parent) { + /* This is like a conflict DN, where we put the object in LostAndFound + see MS-DRSR 4.1.10.6.10 FindBestParentObject */ + return replmd_op_possible_conflict_callback(req, ares, replmd_op_name_modify_callback); + } + + return replmd_op_possible_conflict_callback(req, ares, replmd_op_callback); +} + +/* + this is called when a new object comes in over DRS + */ +static int replmd_replicated_apply_add(struct replmd_replicated_request *ar) +{ + struct ldb_context *ldb; + struct ldb_request *change_req; + enum ndr_err_code ndr_err; + struct ldb_message *msg; + struct replPropertyMetaDataBlob *md; + struct ldb_val md_value; + unsigned int i; + int ret; + bool remote_isDeleted = false; + bool is_schema_nc; + NTTIME now; + time_t t = time(NULL); + const struct ldb_val *rdn_val; + struct replmd_private *replmd_private = + talloc_get_type(ldb_module_get_private(ar->module), + struct replmd_private); + unix_to_nt_time(&now, t); + + ldb = ldb_module_get_ctx(ar->module); + msg = ar->objs->objects[ar->index_current].msg; + md = ar->objs->objects[ar->index_current].meta_data; + is_schema_nc = ldb_dn_compare_base(replmd_private->schema_dn, msg->dn) == 0; + + ret = ldb_sequence_number(ldb, LDB_SEQ_NEXT, &ar->seq_num); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + + ret = dsdb_msg_add_guid(msg, + &ar->objs->objects[ar->index_current].object_guid, + "objectGUID"); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + + ret = ldb_msg_add_string(msg, "whenChanged", ar->objs->objects[ar->index_current].when_changed); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + + ret = samdb_msg_add_uint64(ldb, msg, msg, "uSNCreated", ar->seq_num); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + + ret = samdb_msg_add_uint64(ldb, msg, msg, "uSNChanged", ar->seq_num); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + + /* remove any message elements that have zero values */ + for (i=0; i<msg->num_elements; i++) { + struct ldb_message_element *el = &msg->elements[i]; + + if (el->num_values == 0) { + if (ldb_attr_cmp(msg->elements[i].name, "objectClass") == 0) { + ldb_asprintf_errstring(ldb, __location__ + ": empty objectClass sent on %s, aborting replication\n", + ldb_dn_get_linearized(msg->dn)); + return replmd_replicated_request_error(ar, LDB_ERR_OBJECT_CLASS_VIOLATION); + } + + DEBUG(4,(__location__ ": Removing attribute %s with num_values==0\n", + el->name)); + ldb_msg_remove_element(msg, &msg->elements[i]); + i--; + continue; + } + } + + if (DEBUGLVL(8)) { + struct GUID_txt_buf guid_txt; + + char *s = ldb_ldif_message_redacted_string(ldb, ar, + LDB_CHANGETYPE_ADD, + msg); + DEBUG(8, ("DRS replication add message of %s:\n%s\n", + GUID_buf_string(&ar->objs->objects[ar->index_current].object_guid, &guid_txt), + s)); + talloc_free(s); + } else if (DEBUGLVL(4)) { + struct GUID_txt_buf guid_txt; + DEBUG(4, ("DRS replication add DN of %s is %s\n", + GUID_buf_string(&ar->objs->objects[ar->index_current].object_guid, &guid_txt), + ldb_dn_get_linearized(msg->dn))); + } + remote_isDeleted = ldb_msg_find_attr_as_bool(msg, + "isDeleted", false); + + /* + * the meta data array is already sorted by the caller, except + * for the RDN, which needs to be added. + */ + + + rdn_val = ldb_dn_get_rdn_val(msg->dn); + ret = replmd_update_rpmd_rdn_attr(ldb, msg, rdn_val, NULL, + md, ar, now, is_schema_nc, + false); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "%s: error during DRS repl ADD: %s", __func__, ldb_errstring(ldb)); + return replmd_replicated_request_error(ar, ret); + } + + ret = replmd_replPropertyMetaDataCtr1_sort_and_verify(ldb, &md->ctr.ctr1, msg->dn); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "%s: error during DRS repl ADD: %s", __func__, ldb_errstring(ldb)); + return replmd_replicated_request_error(ar, ret); + } + + for (i=0; i < md->ctr.ctr1.count; i++) { + md->ctr.ctr1.array[i].local_usn = ar->seq_num; + } + ndr_err = ndr_push_struct_blob(&md_value, msg, md, + (ndr_push_flags_fn_t)ndr_push_replPropertyMetaDataBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS nt_status = ndr_map_error2ntstatus(ndr_err); + return replmd_replicated_request_werror(ar, ntstatus_to_werror(nt_status)); + } + ret = ldb_msg_add_value(msg, "replPropertyMetaData", &md_value, NULL); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + + replmd_ldb_message_sort(msg, ar->schema); + + if (!remote_isDeleted) { + /* + * Ensure any local ACL inheritence is applied from + * the parent object. + * + * This is needed because descriptor is above + * repl_meta_data in the module stack, so this will + * not be trigered 'naturally' by the flow of + * operations. + */ + ret = dsdb_module_schedule_sd_propagation(ar->module, + ar->objs->partition_dn, + ar->objs->objects[ar->index_current].object_guid, + ar->objs->objects[ar->index_current].parent_guid ? + *ar->objs->objects[ar->index_current].parent_guid : + GUID_zero(), + true); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + } + + ar->isDeleted = remote_isDeleted; + + ret = ldb_build_add_req(&change_req, + ldb, + ar, + msg, + ar->controls, + ar, + replmd_op_add_callback, + ar->req); + LDB_REQ_SET_LOCATION(change_req); + if (ret != LDB_SUCCESS) return replmd_replicated_request_error(ar, ret); + + /* current partition control needed by "repmd_op_callback" */ + ret = ldb_request_add_control(change_req, + DSDB_CONTROL_CURRENT_PARTITION_OID, + false, NULL); + if (ret != LDB_SUCCESS) return replmd_replicated_request_error(ar, ret); + + if (ar->objs->dsdb_repl_flags & DSDB_REPL_FLAG_PARTIAL_REPLICA) { + /* this tells the partition module to make it a + partial replica if creating an NC */ + ret = ldb_request_add_control(change_req, + DSDB_CONTROL_PARTIAL_REPLICA, + false, NULL); + if (ret != LDB_SUCCESS) return replmd_replicated_request_error(ar, ret); + } + + return ldb_next_request(ar->module, change_req); +} + +static int replmd_replicated_apply_search_for_parent_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct replmd_replicated_request *ar = talloc_get_type(req->context, + struct replmd_replicated_request); + int ret; + + if (!ares) { + return ldb_module_done(ar->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + /* + * The error NO_SUCH_OBJECT is not expected, unless the search + * base is the partition DN, and that case doesn't happen here + * because then we wouldn't get a parent_guid_value in any + * case. + */ + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ar->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + { + struct ldb_message *parent_msg = ares->message; + struct ldb_message *msg = ar->objs->objects[ar->index_current].msg; + struct ldb_dn *parent_dn = NULL; + int comp_num; + + if (!ldb_msg_check_string_attribute(msg, "isDeleted", "TRUE") + && ldb_msg_check_string_attribute(parent_msg, "isDeleted", "TRUE")) { + /* Per MS-DRSR 4.1.10.6.10 + * FindBestParentObject we need to move this + * new object under a deleted object to + * lost-and-found */ + struct ldb_dn *nc_root; + + ret = dsdb_find_nc_root(ldb_module_get_ctx(ar->module), msg, msg->dn, &nc_root); + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + ldb_asprintf_errstring(ldb_module_get_ctx(ar->module), + "No suitable NC root found for %s. " + "We need to move this object because parent object %s " + "is deleted, but this object is not.", + ldb_dn_get_linearized(msg->dn), + ldb_dn_get_linearized(parent_msg->dn)); + return ldb_module_done(ar->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } else if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(ar->module), + "Unable to find NC root for %s: %s. " + "We need to move this object because parent object %s " + "is deleted, but this object is not.", + ldb_dn_get_linearized(msg->dn), + ldb_errstring(ldb_module_get_ctx(ar->module)), + ldb_dn_get_linearized(parent_msg->dn)); + return ldb_module_done(ar->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + + ret = dsdb_wellknown_dn(ldb_module_get_ctx(ar->module), msg, + nc_root, + DS_GUID_LOSTANDFOUND_CONTAINER, + &parent_dn); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(ar->module), + "Unable to find LostAndFound Container for %s " + "in partition %s: %s. " + "We need to move this object because parent object %s " + "is deleted, but this object is not.", + ldb_dn_get_linearized(msg->dn), ldb_dn_get_linearized(nc_root), + ldb_errstring(ldb_module_get_ctx(ar->module)), + ldb_dn_get_linearized(parent_msg->dn)); + return ldb_module_done(ar->req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR); + } + ar->objs->objects[ar->index_current].last_known_parent + = talloc_steal(ar->objs->objects[ar->index_current].msg, parent_msg->dn); + + } else { + parent_dn + = talloc_steal(ar->objs->objects[ar->index_current].msg, parent_msg->dn); + + } + ar->objs->objects[ar->index_current].local_parent_dn = parent_dn; + + comp_num = ldb_dn_get_comp_num(msg->dn); + if (comp_num > 1) { + if (!ldb_dn_remove_base_components(msg->dn, comp_num - 1)) { + talloc_free(ares); + return ldb_module_done(ar->req, NULL, NULL, ldb_module_operr(ar->module)); + } + } + if (!ldb_dn_add_base(msg->dn, parent_dn)) { + talloc_free(ares); + return ldb_module_done(ar->req, NULL, NULL, ldb_module_operr(ar->module)); + } + break; + } + case LDB_REPLY_REFERRAL: + /* we ignore referrals */ + break; + + case LDB_REPLY_DONE: + + if (ar->objs->objects[ar->index_current].local_parent_dn == NULL) { + struct GUID_txt_buf str_buf; + if (ar->search_msg != NULL) { + ldb_asprintf_errstring(ldb_module_get_ctx(ar->module), + "No parent with GUID %s found for object locally known as %s", + GUID_buf_string(ar->objs->objects[ar->index_current].parent_guid, &str_buf), + ldb_dn_get_linearized(ar->search_msg->dn)); + } else { + ldb_asprintf_errstring(ldb_module_get_ctx(ar->module), + "No parent with GUID %s found for object remotely known as %s", + GUID_buf_string(ar->objs->objects[ar->index_current].parent_guid, &str_buf), + ldb_dn_get_linearized(ar->objs->objects[ar->index_current].msg->dn)); + } + + /* + * This error code is really important, as it + * is the flag back to the callers to retry + * this with DRSUAPI_DRS_GET_ANC, and so get + * the parent objects before the child + * objects + */ + return ldb_module_done(ar->req, NULL, NULL, + replmd_replicated_request_werror(ar, WERR_DS_DRA_MISSING_PARENT)); + } + + if (ar->search_msg != NULL) { + ret = replmd_replicated_apply_merge(ar); + } else { + ret = replmd_replicated_apply_add(ar); + } + if (ret != LDB_SUCCESS) { + return ldb_module_done(ar->req, NULL, NULL, ret); + } + } + + talloc_free(ares); + return LDB_SUCCESS; +} + +/* + * Look for the parent object, so we put the new object in the right + * place This is akin to NameObject in MS-DRSR - this routine and the + * callbacks find the right parent name, and correct name for this + * object + */ + +static int replmd_replicated_apply_search_for_parent(struct replmd_replicated_request *ar) +{ + struct ldb_context *ldb; + int ret; + char *tmp_str; + char *filter; + struct ldb_request *search_req; + static const char *attrs[] = {"isDeleted", NULL}; + struct GUID_txt_buf guid_str_buf; + + ldb = ldb_module_get_ctx(ar->module); + + if (ar->objs->objects[ar->index_current].parent_guid == NULL) { + if (ar->search_msg != NULL) { + return replmd_replicated_apply_merge(ar); + } else { + return replmd_replicated_apply_add(ar); + } + } + + tmp_str = GUID_buf_string(ar->objs->objects[ar->index_current].parent_guid, + &guid_str_buf); + + filter = talloc_asprintf(ar, "(objectGUID=%s)", tmp_str); + if (!filter) return replmd_replicated_request_werror(ar, WERR_NOT_ENOUGH_MEMORY); + + ret = ldb_build_search_req(&search_req, + ldb, + ar, + ar->objs->partition_dn, + LDB_SCOPE_SUBTREE, + filter, + attrs, + NULL, + ar, + replmd_replicated_apply_search_for_parent_callback, + ar->req); + LDB_REQ_SET_LOCATION(search_req); + + ret = dsdb_request_add_controls(search_req, + DSDB_SEARCH_SHOW_RECYCLED| + DSDB_SEARCH_SHOW_DELETED| + DSDB_SEARCH_SHOW_EXTENDED_DN); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(ar->module, search_req); +} + +/* + handle renames that come in over DRS replication + */ +static int replmd_replicated_handle_rename(struct replmd_replicated_request *ar, + struct ldb_message *msg, + struct ldb_request *parent, + bool *renamed_to_conflict) +{ + int ret; + TALLOC_CTX *tmp_ctx = talloc_new(msg); + struct ldb_result *res; + struct ldb_dn *conflict_dn; + bool rename_incoming_record; + struct ldb_dn *new_dn; + struct GUID guid; + + DEBUG(4,("replmd_replicated_request rename %s => %s\n", + ldb_dn_get_linearized(ar->search_msg->dn), + ldb_dn_get_linearized(msg->dn))); + + + ret = dsdb_module_rename(ar->module, ar->search_msg->dn, msg->dn, + DSDB_FLAG_NEXT_MODULE, ar->req); + if (ret == LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + if (ret != LDB_ERR_ENTRY_ALREADY_EXISTS) { + talloc_free(tmp_ctx); + ldb_asprintf_errstring(ldb_module_get_ctx(ar->module), "Failed to locally apply remote rename from %s to %s: %s", + ldb_dn_get_linearized(ar->search_msg->dn), + ldb_dn_get_linearized(msg->dn), + ldb_errstring(ldb_module_get_ctx(ar->module))); + return ret; + } + + conflict_dn = msg->dn; + + + ret = incoming_dn_should_be_renamed(tmp_ctx, ar, conflict_dn, &res, + &rename_incoming_record); + if (ret != LDB_SUCCESS) { + goto failed; + } + + if (rename_incoming_record) { + + new_dn = replmd_conflict_dn(msg, + ldb_module_get_ctx(ar->module), + msg->dn, + &ar->objs->objects[ar->index_current].object_guid); + if (new_dn == NULL) { + ldb_asprintf_errstring(ldb_module_get_ctx(ar->module), + "Failed to form conflict DN for %s\n", + ldb_dn_get_linearized(msg->dn)); + + talloc_free(tmp_ctx); + return replmd_replicated_request_werror(ar, WERR_NOT_ENOUGH_MEMORY); + } + + ret = dsdb_module_rename(ar->module, ar->search_msg->dn, new_dn, + DSDB_FLAG_NEXT_MODULE, ar->req); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(ar->module), + "Failed to rename incoming conflicting dn '%s' (was '%s') to '%s' - %s\n", + ldb_dn_get_linearized(conflict_dn), + ldb_dn_get_linearized(ar->search_msg->dn), + ldb_dn_get_linearized(new_dn), + ldb_errstring(ldb_module_get_ctx(ar->module))); + talloc_free(tmp_ctx); + return replmd_replicated_request_werror(ar, WERR_DS_DRA_DB_ERROR); + } + + msg->dn = new_dn; + *renamed_to_conflict = true; + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + + /* we are renaming the existing record */ + + guid = samdb_result_guid(res->msgs[0], "objectGUID"); + if (GUID_all_zero(&guid)) { + DEBUG(0,(__location__ ": Failed to find objectGUID for existing conflict record %s\n", + ldb_dn_get_linearized(conflict_dn))); + goto failed; + } + + new_dn = replmd_conflict_dn(tmp_ctx, + ldb_module_get_ctx(ar->module), + conflict_dn, &guid); + if (new_dn == NULL) { + DEBUG(0,(__location__ ": Failed to form conflict DN for %s\n", + ldb_dn_get_linearized(conflict_dn))); + goto failed; + } + + DEBUG(2,(__location__ ": Resolving conflict record via existing-record rename '%s' -> '%s'\n", + ldb_dn_get_linearized(conflict_dn), ldb_dn_get_linearized(new_dn))); + + ret = dsdb_module_rename(ar->module, conflict_dn, new_dn, + DSDB_FLAG_OWN_MODULE, ar->req); + if (ret != LDB_SUCCESS) { + DEBUG(0,(__location__ ": Failed to rename conflict dn '%s' to '%s' - %s\n", + ldb_dn_get_linearized(conflict_dn), + ldb_dn_get_linearized(new_dn), + ldb_errstring(ldb_module_get_ctx(ar->module)))); + goto failed; + } + + /* + * now we need to ensure that the rename is seen as an + * originating update. We do that with a modify. + */ + ret = replmd_name_modify(ar, ar->req, new_dn); + if (ret != LDB_SUCCESS) { + goto failed; + } + + DEBUG(2,(__location__ ": With conflicting record renamed, re-apply replicated rename '%s' -> '%s'\n", + ldb_dn_get_linearized(ar->search_msg->dn), + ldb_dn_get_linearized(msg->dn))); + + /* + * With the other record out of the way, do the rename we had + * at the top again + */ + ret = dsdb_module_rename(ar->module, ar->search_msg->dn, msg->dn, + DSDB_FLAG_NEXT_MODULE, ar->req); + if (ret != LDB_SUCCESS) { + DEBUG(0,(__location__ ": After conflict resolution, failed to rename dn '%s' to '%s' - %s\n", + ldb_dn_get_linearized(ar->search_msg->dn), + ldb_dn_get_linearized(msg->dn), + ldb_errstring(ldb_module_get_ctx(ar->module)))); + goto failed; + } + + talloc_free(tmp_ctx); + return ret; +failed: + /* + * On failure make the caller get the error + * This means replication will stop with an error, + * but there is not much else we can do. In the + * LDB_ERR_ENTRY_ALREADY_EXISTS case this is exactly what is + * needed. + */ + if (ret == LDB_SUCCESS) { + ret = LDB_ERR_OPERATIONS_ERROR; + } + + talloc_free(tmp_ctx); + return ret; +} + + +static int replmd_replicated_apply_merge(struct replmd_replicated_request *ar) +{ + struct ldb_context *ldb; + struct ldb_request *change_req; + enum ndr_err_code ndr_err; + struct ldb_message *msg; + struct replPropertyMetaDataBlob *rmd; + struct replPropertyMetaDataBlob omd; + const struct ldb_val *omd_value; + struct replPropertyMetaDataBlob nmd; + struct ldb_val nmd_value; + struct GUID remote_parent_guid; + unsigned int i; + uint32_t j,ni=0; + unsigned int removed_attrs = 0; + int ret; + int (*callback)(struct ldb_request *req, struct ldb_reply *ares) = replmd_op_callback; + bool isDeleted = false; + bool local_isDeleted = false; + bool remote_isDeleted = false; + bool take_remote_isDeleted = false; + bool sd_updated = false; + bool renamed = false; + bool renamed_to_conflict = false; + bool is_schema_nc = false; + NTSTATUS nt_status; + const struct ldb_val *old_rdn, *new_rdn; + struct replmd_private *replmd_private = + talloc_get_type(ldb_module_get_private(ar->module), + struct replmd_private); + NTTIME now; + time_t t = time(NULL); + unix_to_nt_time(&now, t); + + ldb = ldb_module_get_ctx(ar->module); + msg = ar->objs->objects[ar->index_current].msg; + + is_schema_nc = ldb_dn_compare_base(replmd_private->schema_dn, msg->dn) == 0; + + rmd = ar->objs->objects[ar->index_current].meta_data; + ZERO_STRUCT(omd); + omd.version = 1; + + /* find existing meta data */ + omd_value = ldb_msg_find_ldb_val(ar->search_msg, "replPropertyMetaData"); + if (omd_value) { + ndr_err = ndr_pull_struct_blob(omd_value, ar, &omd, + (ndr_pull_flags_fn_t)ndr_pull_replPropertyMetaDataBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + nt_status = ndr_map_error2ntstatus(ndr_err); + return replmd_replicated_request_werror(ar, ntstatus_to_werror(nt_status)); + } + + if (omd.version != 1) { + return replmd_replicated_request_werror(ar, WERR_DS_DRA_INTERNAL_ERROR); + } + } + + if (DEBUGLVL(8)) { + struct GUID_txt_buf guid_txt; + + char *s = ldb_ldif_message_redacted_string(ldb, ar, + LDB_CHANGETYPE_MODIFY, msg); + DEBUG(8, ("Initial DRS replication modify message of %s is:\n%s\n" + "%s\n" + "%s\n", + GUID_buf_string(&ar->objs->objects[ar->index_current].object_guid, &guid_txt), + s, + ndr_print_struct_string(s, + (ndr_print_fn_t)ndr_print_replPropertyMetaDataBlob, + "existing replPropertyMetaData", + &omd), + ndr_print_struct_string(s, + (ndr_print_fn_t)ndr_print_replPropertyMetaDataBlob, + "incoming replPropertyMetaData", + rmd))); + talloc_free(s); + } else if (DEBUGLVL(4)) { + struct GUID_txt_buf guid_txt; + + DEBUG(4, ("Initial DRS replication modify DN of %s is: %s\n", + GUID_buf_string(&ar->objs->objects[ar->index_current].object_guid, + &guid_txt), + ldb_dn_get_linearized(msg->dn))); + } + + local_isDeleted = ldb_msg_find_attr_as_bool(ar->search_msg, + "isDeleted", false); + remote_isDeleted = ldb_msg_find_attr_as_bool(msg, + "isDeleted", false); + + /* + * Fill in the remote_parent_guid with the GUID or an all-zero + * GUID. + */ + if (ar->objs->objects[ar->index_current].parent_guid != NULL) { + remote_parent_guid = *ar->objs->objects[ar->index_current].parent_guid; + } else { + remote_parent_guid = GUID_zero(); + } + + /* + * To ensure we follow a complex rename chain around, we have + * to confirm that the DN is the same (mostly to confirm the + * RDN) and the parentGUID is the same. + * + * This ensures we keep things under the correct parent, which + * replmd_replicated_handle_rename() will do. + */ + + if (strcmp(ldb_dn_get_linearized(msg->dn), ldb_dn_get_linearized(ar->search_msg->dn)) == 0 + && GUID_equal(&remote_parent_guid, &ar->local_parent_guid)) { + ret = LDB_SUCCESS; + } else { + /* + * handle renames, even just by case that come in over + * DRS. Changes in the parent DN don't hit us here, + * because the search for a parent will clean up those + * components. + * + * We also have already filtered out the case where + * the peer has an older name to what we have (see + * replmd_replicated_apply_search_callback()) + */ + ret = replmd_replicated_handle_rename(ar, msg, ar->req, &renamed_to_conflict); + + /* + * This looks strange, but we must set this after any + * rename, otherwise the SD propegation will not + * happen (which might matter if we have a new parent) + * + * The additional case of calling + * replmd_op_name_modify_callback (below) is + * controlled by renamed_to_conflict. + */ + renamed = true; + } + + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_FATAL, + "replmd_replicated_request rename %s => %s failed - %s\n", + ldb_dn_get_linearized(ar->search_msg->dn), + ldb_dn_get_linearized(msg->dn), + ldb_errstring(ldb)); + return replmd_replicated_request_werror(ar, WERR_DS_DRA_DB_ERROR); + } + + if (renamed_to_conflict == true) { + /* + * Set the callback to one that will fix up the name + * metadata on the new conflict DN + */ + callback = replmd_op_name_modify_callback; + } + + ZERO_STRUCT(nmd); + nmd.version = 1; + nmd.ctr.ctr1.count = omd.ctr.ctr1.count + rmd->ctr.ctr1.count; + nmd.ctr.ctr1.array = talloc_array(ar, + struct replPropertyMetaData1, + nmd.ctr.ctr1.count); + if (!nmd.ctr.ctr1.array) return replmd_replicated_request_werror(ar, WERR_NOT_ENOUGH_MEMORY); + + /* first copy the old meta data */ + for (i=0; i < omd.ctr.ctr1.count; i++) { + nmd.ctr.ctr1.array[ni] = omd.ctr.ctr1.array[i]; + ni++; + } + + ar->seq_num = 0; + /* now merge in the new meta data */ + for (i=0; i < rmd->ctr.ctr1.count; i++) { + bool found = false; + + for (j=0; j < ni; j++) { + bool cmp; + + if (rmd->ctr.ctr1.array[i].attid != nmd.ctr.ctr1.array[j].attid) { + continue; + } + + cmp = replmd_replPropertyMetaData1_new_should_be_taken( + ar->objs->dsdb_repl_flags, + &nmd.ctr.ctr1.array[j], + &rmd->ctr.ctr1.array[i]); + if (cmp) { + /* replace the entry */ + nmd.ctr.ctr1.array[j] = rmd->ctr.ctr1.array[i]; + if (ar->seq_num == 0) { + ret = ldb_sequence_number(ldb, LDB_SEQ_NEXT, &ar->seq_num); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + } + nmd.ctr.ctr1.array[j].local_usn = ar->seq_num; + switch (nmd.ctr.ctr1.array[j].attid) { + case DRSUAPI_ATTID_ntSecurityDescriptor: + sd_updated = true; + break; + case DRSUAPI_ATTID_isDeleted: + take_remote_isDeleted = true; + break; + default: + break; + } + found = true; + break; + } + + if (rmd->ctr.ctr1.array[i].attid != DRSUAPI_ATTID_instanceType) { + DEBUG(3,("Discarding older DRS attribute update to %s on %s from %s\n", + msg->elements[i-removed_attrs].name, + ldb_dn_get_linearized(msg->dn), + GUID_string(ar, &rmd->ctr.ctr1.array[i].originating_invocation_id))); + } + + /* we don't want to apply this change so remove the attribute */ + ldb_msg_remove_element(msg, &msg->elements[i-removed_attrs]); + removed_attrs++; + + found = true; + break; + } + + if (found) continue; + + nmd.ctr.ctr1.array[ni] = rmd->ctr.ctr1.array[i]; + if (ar->seq_num == 0) { + ret = ldb_sequence_number(ldb, LDB_SEQ_NEXT, &ar->seq_num); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + } + nmd.ctr.ctr1.array[ni].local_usn = ar->seq_num; + switch (nmd.ctr.ctr1.array[ni].attid) { + case DRSUAPI_ATTID_ntSecurityDescriptor: + sd_updated = true; + break; + case DRSUAPI_ATTID_isDeleted: + take_remote_isDeleted = true; + break; + default: + break; + } + ni++; + } + + /* + * finally correct the size of the meta_data array + */ + nmd.ctr.ctr1.count = ni; + + new_rdn = ldb_dn_get_rdn_val(msg->dn); + old_rdn = ldb_dn_get_rdn_val(ar->search_msg->dn); + + if (renamed) { + ret = replmd_update_rpmd_rdn_attr(ldb, msg, new_rdn, old_rdn, + &nmd, ar, now, is_schema_nc, + false); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "%s: error during DRS repl merge: %s", __func__, ldb_errstring(ldb)); + return replmd_replicated_request_error(ar, ret); + } + } + /* + * sort the new meta data array + */ + ret = replmd_replPropertyMetaDataCtr1_sort_and_verify(ldb, &nmd.ctr.ctr1, msg->dn); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "%s: error during DRS repl merge: %s", __func__, ldb_errstring(ldb)); + return ret; + } + + /* + * Work out if this object is deleted, so we can prune any extra attributes. See MS-DRSR 4.1.10.6.9 + * UpdateObject. + * + * This also controls SD propagation below + */ + if (take_remote_isDeleted) { + isDeleted = remote_isDeleted; + } else { + isDeleted = local_isDeleted; + } + + ar->isDeleted = isDeleted; + + /* + * check if some replicated attributes left, otherwise skip the ldb_modify() call + */ + if (msg->num_elements == 0) { + ldb_debug(ldb, LDB_DEBUG_TRACE, "replmd_replicated_apply_merge[%u]: skip replace\n", + ar->index_current); + + return replmd_replicated_apply_isDeleted(ar); + } + + ldb_debug(ldb, LDB_DEBUG_TRACE, "replmd_replicated_apply_merge[%u]: replace %u attributes\n", + ar->index_current, msg->num_elements); + + if (renamed) { + /* + * This is an new name for this object, so we must + * inherit from the parent + * + * This is needed because descriptor is above + * repl_meta_data in the module stack, so this will + * not be trigered 'naturally' by the flow of + * operations. + */ + ret = dsdb_module_schedule_sd_propagation(ar->module, + ar->objs->partition_dn, + ar->objs->objects[ar->index_current].object_guid, + ar->objs->objects[ar->index_current].parent_guid ? + *ar->objs->objects[ar->index_current].parent_guid : + GUID_zero(), + true); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + } + + if (sd_updated && !isDeleted) { + /* + * This is an existing object, so there is no need to + * inherit from the parent, but we must inherit any + * incoming changes to our child objects. + * + * This is needed because descriptor is above + * repl_meta_data in the module stack, so this will + * not be trigered 'naturally' by the flow of + * operations. + */ + ret = dsdb_module_schedule_sd_propagation(ar->module, + ar->objs->partition_dn, + ar->objs->objects[ar->index_current].object_guid, + ar->objs->objects[ar->index_current].parent_guid ? + *ar->objs->objects[ar->index_current].parent_guid : + GUID_zero(), + false); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + } + + /* create the meta data value */ + ndr_err = ndr_push_struct_blob(&nmd_value, msg, &nmd, + (ndr_push_flags_fn_t)ndr_push_replPropertyMetaDataBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + nt_status = ndr_map_error2ntstatus(ndr_err); + return replmd_replicated_request_werror(ar, ntstatus_to_werror(nt_status)); + } + + /* + * when we know that we'll modify the record, add the whenChanged, uSNChanged + * and replPopertyMetaData attributes + */ + ret = ldb_msg_add_string(msg, "whenChanged", ar->objs->objects[ar->index_current].when_changed); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + ret = samdb_msg_add_uint64(ldb, msg, msg, "uSNChanged", ar->seq_num); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + ret = ldb_msg_add_value(msg, "replPropertyMetaData", &nmd_value, NULL); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + + replmd_ldb_message_sort(msg, ar->schema); + + /* we want to replace the old values */ + for (i=0; i < msg->num_elements; i++) { + msg->elements[i].flags = LDB_FLAG_MOD_REPLACE; + if (ldb_attr_cmp(msg->elements[i].name, "objectClass") == 0) { + if (msg->elements[i].num_values == 0) { + ldb_asprintf_errstring(ldb, __location__ + ": objectClass removed on %s, aborting replication\n", + ldb_dn_get_linearized(msg->dn)); + return replmd_replicated_request_error(ar, LDB_ERR_OBJECT_CLASS_VIOLATION); + } + } + } + + if (DEBUGLVL(8)) { + struct GUID_txt_buf guid_txt; + + char *s = ldb_ldif_message_redacted_string(ldb, ar, + LDB_CHANGETYPE_MODIFY, + msg); + DEBUG(8, ("Final DRS replication modify message of %s:\n%s\n", + GUID_buf_string(&ar->objs->objects[ar->index_current].object_guid, + &guid_txt), + s)); + talloc_free(s); + } else if (DEBUGLVL(4)) { + struct GUID_txt_buf guid_txt; + + DEBUG(4, ("Final DRS replication modify DN of %s is %s\n", + GUID_buf_string(&ar->objs->objects[ar->index_current].object_guid, + &guid_txt), + ldb_dn_get_linearized(msg->dn))); + } + + ret = ldb_build_mod_req(&change_req, + ldb, + ar, + msg, + ar->controls, + ar, + callback, + ar->req); + LDB_REQ_SET_LOCATION(change_req); + if (ret != LDB_SUCCESS) return replmd_replicated_request_error(ar, ret); + + /* current partition control needed by "repmd_op_callback" */ + ret = ldb_request_add_control(change_req, + DSDB_CONTROL_CURRENT_PARTITION_OID, + false, NULL); + if (ret != LDB_SUCCESS) return replmd_replicated_request_error(ar, ret); + + return ldb_next_request(ar->module, change_req); +} + +static int replmd_replicated_apply_search_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct replmd_replicated_request *ar = talloc_get_type(req->context, + struct replmd_replicated_request); + int ret; + + if (!ares) { + return ldb_module_done(ar->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS && + ares->error != LDB_ERR_NO_SUCH_OBJECT) { + return ldb_module_done(ar->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + ar->search_msg = talloc_steal(ar, ares->message); + break; + + case LDB_REPLY_REFERRAL: + /* we ignore referrals */ + break; + + case LDB_REPLY_DONE: + { + struct replPropertyMetaData1 *md_remote; + struct replPropertyMetaData1 *md_local; + + struct replPropertyMetaDataBlob omd; + const struct ldb_val *omd_value; + struct replPropertyMetaDataBlob *rmd; + struct ldb_message *msg; + int instanceType; + ar->objs->objects[ar->index_current].local_parent_dn = NULL; + ar->objs->objects[ar->index_current].last_known_parent = NULL; + + /* + * This is the ADD case, find the appropriate parent, + * as this object doesn't exist locally: + */ + if (ar->search_msg == NULL) { + ret = replmd_replicated_apply_search_for_parent(ar); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ar->req, NULL, NULL, ret); + } + talloc_free(ares); + return LDB_SUCCESS; + } + + /* + * Otherwise, in the MERGE case, work out if we are + * attempting a rename, and if so find the parent the + * newly renamed object wants to belong under (which + * may not be the parent in it's attached string DN + */ + rmd = ar->objs->objects[ar->index_current].meta_data; + ZERO_STRUCT(omd); + omd.version = 1; + + /* find existing meta data */ + omd_value = ldb_msg_find_ldb_val(ar->search_msg, "replPropertyMetaData"); + if (omd_value) { + enum ndr_err_code ndr_err; + ndr_err = ndr_pull_struct_blob(omd_value, ar, &omd, + (ndr_pull_flags_fn_t)ndr_pull_replPropertyMetaDataBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS nt_status = ndr_map_error2ntstatus(ndr_err); + return replmd_replicated_request_werror(ar, ntstatus_to_werror(nt_status)); + } + + if (omd.version != 1) { + return replmd_replicated_request_werror(ar, WERR_DS_DRA_INTERNAL_ERROR); + } + } + + ar->local_parent_guid = samdb_result_guid(ar->search_msg, "parentGUID"); + + instanceType = ldb_msg_find_attr_as_int(ar->search_msg, "instanceType", 0); + if (((instanceType & INSTANCE_TYPE_IS_NC_HEAD) == 0) + && GUID_all_zero(&ar->local_parent_guid)) { + DEBUG(0, ("Refusing to replicate new version of %s " + "as local object has an all-zero parentGUID attribute, " + "despite not being an NC root\n", + ldb_dn_get_linearized(ar->search_msg->dn))); + return replmd_replicated_request_werror(ar, WERR_DS_DRA_INTERNAL_ERROR); + } + + /* + * now we need to check for double renames. We could have a + * local rename pending which our replication partner hasn't + * received yet. We choose which one wins by looking at the + * attribute stamps on the two objects, the newer one wins. + * + * This also simply applies the correct algorithms for + * determining if a change was made to name at all, or + * if the object has just been renamed under the same + * parent. + */ + md_remote = replmd_replPropertyMetaData1_find_attid(rmd, DRSUAPI_ATTID_name); + md_local = replmd_replPropertyMetaData1_find_attid(&omd, DRSUAPI_ATTID_name); + if (!md_local) { + DEBUG(0,(__location__ ": Failed to find name attribute in local LDB replPropertyMetaData for %s\n", + ldb_dn_get_linearized(ar->search_msg->dn))); + return replmd_replicated_request_werror(ar, WERR_DS_DRA_DB_ERROR); + } + + /* + * if there is no name attribute given then we have to assume the + * object we've received has the older name + */ + if (replmd_replPropertyMetaData1_new_should_be_taken( + ar->objs->dsdb_repl_flags & DSDB_REPL_FLAG_PRIORITISE_INCOMING, + md_local, md_remote)) { + struct GUID_txt_buf p_guid_local; + struct GUID_txt_buf p_guid_remote; + msg = ar->objs->objects[ar->index_current].msg; + + /* Merge on the existing object, with rename */ + + DEBUG(4,(__location__ ": Looking for new parent for object %s currently under %s " + "as incoming object changing to %s under %s\n", + ldb_dn_get_linearized(ar->search_msg->dn), + GUID_buf_string(&ar->local_parent_guid, &p_guid_local), + ldb_dn_get_linearized(msg->dn), + GUID_buf_string(ar->objs->objects[ar->index_current].parent_guid, + &p_guid_remote))); + ret = replmd_replicated_apply_search_for_parent(ar); + } else { + struct GUID_txt_buf p_guid_local; + struct GUID_txt_buf p_guid_remote; + msg = ar->objs->objects[ar->index_current].msg; + + /* + * Merge on the existing object, force no + * rename (code below just to explain why in + * the DEBUG() logs) + */ + + if (strcmp(ldb_dn_get_linearized(ar->search_msg->dn), + ldb_dn_get_linearized(msg->dn)) == 0) { + if (ar->objs->objects[ar->index_current].parent_guid != NULL && + GUID_equal(&ar->local_parent_guid, + ar->objs->objects[ar->index_current].parent_guid) + == false) { + DEBUG(4,(__location__ ": Keeping object %s at under %s " + "despite incoming object changing parent to %s\n", + ldb_dn_get_linearized(ar->search_msg->dn), + GUID_buf_string(&ar->local_parent_guid, &p_guid_local), + GUID_buf_string(ar->objs->objects[ar->index_current].parent_guid, + &p_guid_remote))); + } + } else { + DEBUG(4,(__location__ ": Keeping object %s at under %s " + " and rejecting older rename to %s under %s\n", + ldb_dn_get_linearized(ar->search_msg->dn), + GUID_buf_string(&ar->local_parent_guid, &p_guid_local), + ldb_dn_get_linearized(msg->dn), + GUID_buf_string(ar->objs->objects[ar->index_current].parent_guid, + &p_guid_remote))); + } + /* + * This assignment ensures that the strcmp() + * and GUID_equal() calls in + * replmd_replicated_apply_merge() avoids the + * rename call + */ + ar->objs->objects[ar->index_current].parent_guid = + &ar->local_parent_guid; + + msg->dn = ar->search_msg->dn; + ret = replmd_replicated_apply_merge(ar); + } + if (ret != LDB_SUCCESS) { + return ldb_module_done(ar->req, NULL, NULL, ret); + } + } + } + + talloc_free(ares); + return LDB_SUCCESS; +} + +/** + * Returns true if we can group together processing this link attribute, + * i.e. it has the same source-object and attribute ID as other links + * already in the group + */ +static bool la_entry_matches_group(struct la_entry *la_entry, + struct la_group *la_group) +{ + struct la_entry *prev = la_group->la_entries; + + return (la_entry->la->attid == prev->la->attid && + GUID_equal(&la_entry->la->identifier->guid, + &prev->la->identifier->guid)); +} + +/** + * Creates a new la_entry to store replication info for a single + * linked attribute. + */ +static struct la_entry * +create_la_entry(struct replmd_private *replmd_private, + struct drsuapi_DsReplicaLinkedAttribute *la, + uint32_t dsdb_repl_flags) +{ + struct la_entry *la_entry; + + if (replmd_private->la_ctx == NULL) { + replmd_private->la_ctx = talloc_new(replmd_private); + } + la_entry = talloc(replmd_private->la_ctx, struct la_entry); + if (la_entry == NULL) { + return NULL; + } + la_entry->la = talloc(la_entry, + struct drsuapi_DsReplicaLinkedAttribute); + if (la_entry->la == NULL) { + talloc_free(la_entry); + return NULL; + } + *la_entry->la = *la; + la_entry->dsdb_repl_flags = dsdb_repl_flags; + + /* + * we need to steal the non-scalars so they stay + * around until the end of the transaction + */ + talloc_steal(la_entry->la, la_entry->la->identifier); + talloc_steal(la_entry->la, la_entry->la->value.blob); + + return la_entry; +} + +/** + * Stores the linked attributes received in the replication chunk - these get + * applied at the end of the transaction. We also check that each linked + * attribute is valid, i.e. source and target objects are known. + */ +static int replmd_store_linked_attributes(struct replmd_replicated_request *ar) +{ + int ret = LDB_SUCCESS; + uint32_t i; + struct ldb_module *module = ar->module; + struct replmd_private *replmd_private = + talloc_get_type(ldb_module_get_private(module), struct replmd_private); + struct la_group *la_group = NULL; + struct ldb_context *ldb; + TALLOC_CTX *tmp_ctx = NULL; + struct ldb_message *src_msg = NULL; + const struct dsdb_attribute *attr = NULL; + + ldb = ldb_module_get_ctx(module); + + DEBUG(4,("linked_attributes_count=%u\n", ar->objs->linked_attributes_count)); + + /* save away the linked attributes for the end of the transaction */ + for (i = 0; i < ar->objs->linked_attributes_count; i++) { + struct la_entry *la_entry; + bool new_srcobj; + + /* create an entry to store the received link attribute info */ + la_entry = create_la_entry(replmd_private, + &ar->objs->linked_attributes[i], + ar->objs->dsdb_repl_flags); + if (la_entry == NULL) { + ldb_oom(ldb); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * check if we're still dealing with the same source object + * as the last link + */ + new_srcobj = (la_group == NULL || + !la_entry_matches_group(la_entry, la_group)); + + if (new_srcobj) { + + /* get a new mem_ctx to lookup the source object */ + TALLOC_FREE(tmp_ctx); + tmp_ctx = talloc_new(ar); + if (tmp_ctx == NULL) { + ldb_oom(ldb); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* verify the link source exists */ + ret = replmd_get_la_entry_source(module, la_entry, + tmp_ctx, &attr, + &src_msg); + + /* + * When we fail to find the source object, the error + * code we pass back here is really important. It flags + * back to the callers to retry this request with + * DRSUAPI_DRS_GET_ANC. This case should never happen + * if we're replicating from a Samba DC, but it is + * needed to talk to a Windows DC + */ + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + WERROR err = WERR_DS_DRA_MISSING_PARENT; + ret = replmd_replicated_request_werror(ar, + err); + break; + } + } + + ret = replmd_verify_link_target(ar, tmp_ctx, la_entry, + src_msg->dn, attr); + if (ret != LDB_SUCCESS) { + break; + } + + /* group the links together by source-object for efficiency */ + if (new_srcobj) { + la_group = talloc_zero(replmd_private->la_ctx, + struct la_group); + if (la_group == NULL) { + ldb_oom(ldb); + return LDB_ERR_OPERATIONS_ERROR; + } + DLIST_ADD(replmd_private->la_list, la_group); + } + DLIST_ADD(la_group->la_entries, la_entry); + replmd_private->total_links++; + } + + TALLOC_FREE(tmp_ctx); + return ret; +} + +static int replmd_replicated_uptodate_vector(struct replmd_replicated_request *ar); + +static int replmd_replicated_apply_next(struct replmd_replicated_request *ar) +{ + struct ldb_context *ldb; + int ret; + char *tmp_str; + char *filter; + struct ldb_request *search_req; + static const char *attrs[] = { "repsFrom", "replUpToDateVector", + "parentGUID", "instanceType", + "replPropertyMetaData", "nTSecurityDescriptor", + "isDeleted", NULL }; + struct GUID_txt_buf guid_str_buf; + + if (ar->index_current >= ar->objs->num_objects) { + + /* + * Now that we've applied all the objects, check the new linked + * attributes and store them (we apply them in .prepare_commit) + */ + ret = replmd_store_linked_attributes(ar); + + if (ret != LDB_SUCCESS) { + return ret; + } + + /* done applying objects, move on to the next stage */ + return replmd_replicated_uptodate_vector(ar); + } + + ldb = ldb_module_get_ctx(ar->module); + ar->search_msg = NULL; + ar->isDeleted = false; + + tmp_str = GUID_buf_string(&ar->objs->objects[ar->index_current].object_guid, + &guid_str_buf); + + filter = talloc_asprintf(ar, "(objectGUID=%s)", tmp_str); + if (!filter) return replmd_replicated_request_werror(ar, WERR_NOT_ENOUGH_MEMORY); + + ret = ldb_build_search_req(&search_req, + ldb, + ar, + ar->objs->partition_dn, + LDB_SCOPE_SUBTREE, + filter, + attrs, + NULL, + ar, + replmd_replicated_apply_search_callback, + ar->req); + LDB_REQ_SET_LOCATION(search_req); + + /* + * We set DSDB_SEARCH_SHOW_EXTENDED_DN to get the GUID on the + * DN. This in turn helps our operational module find the + * record by GUID, not DN lookup which is more error prone if + * DN indexing changes. We prefer to keep chasing GUIDs + * around if possible, even within a transaction. + * + * The aim here is to keep replication moving and allow a + * reindex later. + */ + ret = dsdb_request_add_controls(search_req, DSDB_SEARCH_SHOW_RECYCLED + |DSDB_SEARCH_SHOW_EXTENDED_DN); + + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(ar->module, search_req); +} + +/* + * Returns true if we need to do extra processing to handle deleted object + * changes received via replication + */ +static bool replmd_should_apply_isDeleted(struct replmd_replicated_request *ar, + struct ldb_message *msg) +{ + struct ldb_dn *deleted_objects_dn; + int ret; + + if (!ar->isDeleted) { + + /* not a deleted object, so don't set isDeleted */ + return false; + } + + ret = dsdb_get_deleted_objects_dn(ldb_module_get_ctx(ar->module), + msg, msg->dn, + &deleted_objects_dn); + + /* + * if the Deleted Object container lookup failed, then just apply + * isDeleted (note that it doesn't exist for the Schema partition) + */ + if (ret != LDB_SUCCESS) { + return true; + } + + /* + * the Deleted Objects container has isDeleted set but is not entirely + * a deleted object, so DON'T re-apply isDeleted to it + */ + if (ldb_dn_compare(msg->dn, deleted_objects_dn) == 0) { + return false; + } + + return true; +} + +/* + * This is essentially a wrapper for replmd_replicated_apply_next() + * + * This is needed to ensure that both codepaths call this handler. + */ +static int replmd_replicated_apply_isDeleted(struct replmd_replicated_request *ar) +{ + struct ldb_message *msg = ar->objs->objects[ar->index_current].msg; + int ret; + bool apply_isDeleted; + struct ldb_request *del_req = NULL; + struct ldb_result *res = NULL; + TALLOC_CTX *tmp_ctx = NULL; + + apply_isDeleted = replmd_should_apply_isDeleted(ar, msg); + + if (!apply_isDeleted) { + + /* nothing to do */ + ar->index_current++; + return replmd_replicated_apply_next(ar); + } + + /* + * Do a delete here again, so that if there is + * anything local that conflicts with this + * object being deleted, it is removed. This + * includes links. See MS-DRSR 4.1.10.6.9 + * UpdateObject. + * + * If the object is already deleted, and there + * is no more work required, it doesn't do + * anything. + */ + + /* This has been updated to point to the DN we eventually did the modify on */ + + tmp_ctx = talloc_new(ar); + if (!tmp_ctx) { + ret = ldb_oom(ldb_module_get_ctx(ar->module)); + return ret; + } + + res = talloc_zero(tmp_ctx, struct ldb_result); + if (!res) { + ret = ldb_oom(ldb_module_get_ctx(ar->module)); + talloc_free(tmp_ctx); + return ret; + } + + /* Build a delete request, which hopefully will artually turn into nothing */ + ret = ldb_build_del_req(&del_req, ldb_module_get_ctx(ar->module), tmp_ctx, + msg->dn, + NULL, + res, + ldb_modify_default_callback, + ar->req); + LDB_REQ_SET_LOCATION(del_req); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + /* + * This is the guts of the call, call back + * into our delete code, but setting the + * re_delete flag so we delete anything that + * shouldn't be there on a deleted or recycled + * object + */ + ret = replmd_delete_internals(ar->module, del_req, true); + if (ret == LDB_SUCCESS) { + ret = ldb_wait(del_req->handle, LDB_WAIT_ALL); + } + + talloc_free(tmp_ctx); + if (ret != LDB_SUCCESS) { + return ret; + } + + ar->index_current++; + return replmd_replicated_apply_next(ar); +} + +static int replmd_replicated_uptodate_modify_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct replmd_replicated_request *ar = talloc_get_type(req->context, + struct replmd_replicated_request); + ldb = ldb_module_get_ctx(ar->module); + + if (!ares) { + return ldb_module_done(ar->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ar->req, ares->controls, + ares->response, ares->error); + } + + if (ares->type != LDB_REPLY_DONE) { + ldb_asprintf_errstring(ldb, "Invalid LDB reply type %d", ares->type); + return ldb_module_done(ar->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + talloc_free(ares); + + return ldb_module_done(ar->req, NULL, NULL, LDB_SUCCESS); +} + +static int replmd_replicated_uptodate_modify(struct replmd_replicated_request *ar) +{ + struct ldb_context *ldb; + struct ldb_request *change_req; + enum ndr_err_code ndr_err; + struct ldb_message *msg; + struct replUpToDateVectorBlob ouv; + const struct ldb_val *ouv_value; + const struct drsuapi_DsReplicaCursor2CtrEx *ruv; + struct replUpToDateVectorBlob nuv; + struct ldb_val nuv_value; + struct ldb_message_element *nuv_el = NULL; + struct ldb_message_element *orf_el = NULL; + struct repsFromToBlob nrf; + struct ldb_val *nrf_value = NULL; + struct ldb_message_element *nrf_el = NULL; + unsigned int i; + uint32_t j,ni=0; + bool found = false; + time_t t = time(NULL); + NTTIME now; + int ret; + uint32_t instanceType; + + ldb = ldb_module_get_ctx(ar->module); + ruv = ar->objs->uptodateness_vector; + ZERO_STRUCT(ouv); + ouv.version = 2; + ZERO_STRUCT(nuv); + nuv.version = 2; + + unix_to_nt_time(&now, t); + + if (ar->search_msg == NULL) { + /* this happens for a REPL_OBJ call where we are + creating the target object by replicating it. The + subdomain join code does this for the partition DN + */ + DEBUG(4,(__location__ ": Skipping UDV and repsFrom update as no target DN\n")); + return ldb_module_done(ar->req, NULL, NULL, LDB_SUCCESS); + } + + instanceType = ldb_msg_find_attr_as_uint(ar->search_msg, "instanceType", 0); + if (! (instanceType & INSTANCE_TYPE_IS_NC_HEAD)) { + DEBUG(4,(__location__ ": Skipping UDV and repsFrom update as not NC root: %s\n", + ldb_dn_get_linearized(ar->search_msg->dn))); + return ldb_module_done(ar->req, NULL, NULL, LDB_SUCCESS); + } + + /* + * first create the new replUpToDateVector + */ + ouv_value = ldb_msg_find_ldb_val(ar->search_msg, "replUpToDateVector"); + if (ouv_value) { + ndr_err = ndr_pull_struct_blob(ouv_value, ar, &ouv, + (ndr_pull_flags_fn_t)ndr_pull_replUpToDateVectorBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS nt_status = ndr_map_error2ntstatus(ndr_err); + return replmd_replicated_request_werror(ar, ntstatus_to_werror(nt_status)); + } + + if (ouv.version != 2) { + return replmd_replicated_request_werror(ar, WERR_DS_DRA_INTERNAL_ERROR); + } + } + + /* + * the new uptodateness vector will at least + * contain 1 entry, one for the source_dsa + * + * plus optional values from our old vector and the one from the source_dsa + */ + nuv.ctr.ctr2.count = ouv.ctr.ctr2.count; + if (ruv) nuv.ctr.ctr2.count += ruv->count; + nuv.ctr.ctr2.cursors = talloc_array(ar, + struct drsuapi_DsReplicaCursor2, + nuv.ctr.ctr2.count); + if (!nuv.ctr.ctr2.cursors) return replmd_replicated_request_werror(ar, WERR_NOT_ENOUGH_MEMORY); + + /* first copy the old vector */ + for (i=0; i < ouv.ctr.ctr2.count; i++) { + nuv.ctr.ctr2.cursors[ni] = ouv.ctr.ctr2.cursors[i]; + ni++; + } + + /* merge in the source_dsa vector is available */ + for (i=0; (ruv && i < ruv->count); i++) { + found = false; + + if (GUID_equal(&ruv->cursors[i].source_dsa_invocation_id, + &ar->our_invocation_id)) { + continue; + } + + for (j=0; j < ni; j++) { + if (!GUID_equal(&ruv->cursors[i].source_dsa_invocation_id, + &nuv.ctr.ctr2.cursors[j].source_dsa_invocation_id)) { + continue; + } + + found = true; + + if (ruv->cursors[i].highest_usn > nuv.ctr.ctr2.cursors[j].highest_usn) { + nuv.ctr.ctr2.cursors[j] = ruv->cursors[i]; + } + break; + } + + if (found) continue; + + /* if it's not there yet, add it */ + nuv.ctr.ctr2.cursors[ni] = ruv->cursors[i]; + ni++; + } + + /* + * finally correct the size of the cursors array + */ + nuv.ctr.ctr2.count = ni; + + /* + * sort the cursors + */ + TYPESAFE_QSORT(nuv.ctr.ctr2.cursors, nuv.ctr.ctr2.count, drsuapi_DsReplicaCursor2_compare); + + /* + * create the change ldb_message + */ + msg = ldb_msg_new(ar); + if (!msg) return replmd_replicated_request_werror(ar, WERR_NOT_ENOUGH_MEMORY); + msg->dn = ar->search_msg->dn; + + ndr_err = ndr_push_struct_blob(&nuv_value, msg, &nuv, + (ndr_push_flags_fn_t)ndr_push_replUpToDateVectorBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS nt_status = ndr_map_error2ntstatus(ndr_err); + return replmd_replicated_request_werror(ar, ntstatus_to_werror(nt_status)); + } + ret = ldb_msg_add_value(msg, "replUpToDateVector", &nuv_value, &nuv_el); + if (ret != LDB_SUCCESS) { + return replmd_replicated_request_error(ar, ret); + } + nuv_el->flags = LDB_FLAG_MOD_REPLACE; + + /* + * now create the new repsFrom value from the given repsFromTo1 structure + */ + ZERO_STRUCT(nrf); + nrf.version = 1; + nrf.ctr.ctr1 = *ar->objs->source_dsa; + nrf.ctr.ctr1.last_attempt = now; + nrf.ctr.ctr1.last_success = now; + nrf.ctr.ctr1.result_last_attempt = WERR_OK; + + /* + * first see if we already have a repsFrom value for the current source dsa + * if so we'll later replace this value + */ + orf_el = ldb_msg_find_element(ar->search_msg, "repsFrom"); + if (orf_el) { + for (i=0; i < orf_el->num_values; i++) { + struct repsFromToBlob *trf; + + trf = talloc(ar, struct repsFromToBlob); + if (!trf) return replmd_replicated_request_werror(ar, WERR_NOT_ENOUGH_MEMORY); + + ndr_err = ndr_pull_struct_blob(&orf_el->values[i], trf, trf, + (ndr_pull_flags_fn_t)ndr_pull_repsFromToBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS nt_status = ndr_map_error2ntstatus(ndr_err); + return replmd_replicated_request_werror(ar, ntstatus_to_werror(nt_status)); + } + + if (trf->version != 1) { + return replmd_replicated_request_werror(ar, WERR_DS_DRA_INTERNAL_ERROR); + } + + /* + * we compare the source dsa objectGUID not the invocation_id + * because we want only one repsFrom value per source dsa + * and when the invocation_id of the source dsa has changed we don't need + * the old repsFrom with the old invocation_id + */ + if (!GUID_equal(&trf->ctr.ctr1.source_dsa_obj_guid, + &ar->objs->source_dsa->source_dsa_obj_guid)) { + talloc_free(trf); + continue; + } + + talloc_free(trf); + nrf_value = &orf_el->values[i]; + break; + } + + /* + * copy over all old values to the new ldb_message + */ + ret = ldb_msg_add_empty(msg, "repsFrom", 0, &nrf_el); + if (ret != LDB_SUCCESS) return replmd_replicated_request_error(ar, ret); + *nrf_el = *orf_el; + } + + /* + * if we haven't found an old repsFrom value for the current source dsa + * we'll add a new value + */ + if (!nrf_value) { + struct ldb_val zero_value; + ZERO_STRUCT(zero_value); + ret = ldb_msg_add_value(msg, "repsFrom", &zero_value, &nrf_el); + if (ret != LDB_SUCCESS) return replmd_replicated_request_error(ar, ret); + + nrf_value = &nrf_el->values[nrf_el->num_values - 1]; + } + + /* we now fill the value which is already attached to ldb_message */ + ndr_err = ndr_push_struct_blob(nrf_value, msg, + &nrf, + (ndr_push_flags_fn_t)ndr_push_repsFromToBlob); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + NTSTATUS nt_status = ndr_map_error2ntstatus(ndr_err); + return replmd_replicated_request_werror(ar, ntstatus_to_werror(nt_status)); + } + + /* + * the ldb_message_element for the attribute, has all the old values and the new one + * so we'll replace the whole attribute with all values + */ + nrf_el->flags = LDB_FLAG_MOD_REPLACE; + + if (CHECK_DEBUGLVL(4)) { + char *s = ldb_ldif_message_redacted_string(ldb, ar, + LDB_CHANGETYPE_MODIFY, + msg); + DEBUG(4, ("DRS replication uptodate modify message:\n%s\n", s)); + talloc_free(s); + } + + /* prepare the ldb_modify() request */ + ret = ldb_build_mod_req(&change_req, + ldb, + ar, + msg, + ar->controls, + ar, + replmd_replicated_uptodate_modify_callback, + ar->req); + LDB_REQ_SET_LOCATION(change_req); + if (ret != LDB_SUCCESS) return replmd_replicated_request_error(ar, ret); + + return ldb_next_request(ar->module, change_req); +} + +static int replmd_replicated_uptodate_search_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct replmd_replicated_request *ar = talloc_get_type(req->context, + struct replmd_replicated_request); + int ret; + + if (!ares) { + return ldb_module_done(ar->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS && + ares->error != LDB_ERR_NO_SUCH_OBJECT) { + return ldb_module_done(ar->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + ar->search_msg = talloc_steal(ar, ares->message); + break; + + case LDB_REPLY_REFERRAL: + /* we ignore referrals */ + break; + + case LDB_REPLY_DONE: + ret = replmd_replicated_uptodate_modify(ar); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ar->req, NULL, NULL, ret); + } + } + + talloc_free(ares); + return LDB_SUCCESS; +} + + +static int replmd_replicated_uptodate_vector(struct replmd_replicated_request *ar) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ar->module); + struct replmd_private *replmd_private = + talloc_get_type_abort(ldb_module_get_private(ar->module), + struct replmd_private); + int ret; + static const char *attrs[] = { + "replUpToDateVector", + "repsFrom", + "instanceType", + NULL + }; + struct ldb_request *search_req; + + ar->search_msg = NULL; + + /* + * Let the caller know that we did an originating updates + */ + ar->objs->originating_updates = replmd_private->originating_updates; + + ret = ldb_build_search_req(&search_req, + ldb, + ar, + ar->objs->partition_dn, + LDB_SCOPE_BASE, + "(objectClass=*)", + attrs, + NULL, + ar, + replmd_replicated_uptodate_search_callback, + ar->req); + LDB_REQ_SET_LOCATION(search_req); + if (ret != LDB_SUCCESS) return replmd_replicated_request_error(ar, ret); + + return ldb_next_request(ar->module, search_req); +} + + + +static int replmd_extended_replicated_objects(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct dsdb_extended_replicated_objects *objs; + struct replmd_replicated_request *ar; + struct ldb_control **ctrls; + int ret; + + ldb = ldb_module_get_ctx(module); + + ldb_debug(ldb, LDB_DEBUG_TRACE, "replmd_extended_replicated_objects\n"); + + objs = talloc_get_type(req->op.extended.data, struct dsdb_extended_replicated_objects); + if (!objs) { + ldb_debug(ldb, LDB_DEBUG_FATAL, "replmd_extended_replicated_objects: invalid extended data\n"); + return LDB_ERR_PROTOCOL_ERROR; + } + + if (objs->version != DSDB_EXTENDED_REPLICATED_OBJECTS_VERSION) { + ldb_debug(ldb, LDB_DEBUG_FATAL, "replmd_extended_replicated_objects: extended data invalid version [%u != %u]\n", + objs->version, DSDB_EXTENDED_REPLICATED_OBJECTS_VERSION); + return LDB_ERR_PROTOCOL_ERROR; + } + + ar = replmd_ctx_init(module, req); + if (!ar) + return LDB_ERR_OPERATIONS_ERROR; + + /* Set the flags to have the replmd_op_callback run over the full set of objects */ + ar->apply_mode = true; + ar->objs = objs; + ar->schema = dsdb_get_schema(ldb, ar); + if (!ar->schema) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, "replmd_ctx_init: no loaded schema found\n"); + talloc_free(ar); + DEBUG(0,(__location__ ": %s\n", ldb_errstring(ldb))); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + ctrls = req->controls; + + if (req->controls) { + req->controls = talloc_memdup(ar, req->controls, + talloc_get_size(req->controls)); + if (!req->controls) return replmd_replicated_request_werror(ar, WERR_NOT_ENOUGH_MEMORY); + } + + ret = ldb_request_add_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID, false, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* If this change contained linked attributes in the body + * (rather than in the links section) we need to update + * backlinks in linked_attributes */ + ret = ldb_request_add_control(req, DSDB_CONTROL_APPLY_LINKS, false, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + ar->controls = req->controls; + req->controls = ctrls; + + return replmd_replicated_apply_next(ar); +} + +/** + * Checks how to handle an missing target - either we need to fail the + * replication and retry with GET_TGT, ignore the link and continue, or try to + * add a partial link to an unknown target. + */ +static int replmd_allow_missing_target(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_dn *target_dn, + struct ldb_dn *source_dn, + bool is_obj_commit, + struct GUID *guid, + uint32_t dsdb_repl_flags, + bool *ignore_link, + const char * missing_str) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + bool is_in_same_nc; + + /* + * we may not be able to resolve link targets properly when + * dealing with subsets of objects, e.g. the source is a + * critical object and the target isn't + * + * TODO: + * When we implement Trusted Domains we need to consider + * whether they get treated as an incomplete replica here or not + */ + if (dsdb_repl_flags & DSDB_REPL_FLAG_OBJECT_SUBSET) { + + /* + * Ignore the link. We don't increase the highwater-mark in + * the object subset cases, so subsequent replications should + * resolve any missing links + */ + DEBUG(2, ("%s target %s linked from %s\n", missing_str, + ldb_dn_get_linearized(target_dn), + ldb_dn_get_linearized(source_dn))); + *ignore_link = true; + return LDB_SUCCESS; + } + + is_in_same_nc = dsdb_objects_have_same_nc(ldb, + mem_ctx, + source_dn, + target_dn); + if (is_in_same_nc) { + /* + * We allow the join.py code to point out that all + * replication is completed, so failing now would just + * trigger errors, rather than trigger a GET_TGT + */ + int *finished_full_join_ptr = + talloc_get_type(ldb_get_opaque(ldb, + DSDB_FULL_JOIN_REPLICATION_COMPLETED_OPAQUE_NAME), + int); + bool finished_full_join = finished_full_join_ptr && *finished_full_join_ptr; + + /* + * if the target is already be up-to-date there's no point in + * retrying. This could be due to bad timing, or if a target + * on a one-way link was deleted. We ignore the link rather + * than failing the replication cycle completely + */ + if (finished_full_join + || dsdb_repl_flags & DSDB_REPL_FLAG_TARGETS_UPTODATE) { + *ignore_link = true; + DBG_WARNING("%s is %s " + "but up to date. Ignoring link from %s\n", + ldb_dn_get_linearized(target_dn), missing_str, + ldb_dn_get_linearized(source_dn)); + return LDB_SUCCESS; + } + + /* otherwise fail the replication and retry with GET_TGT */ + ldb_asprintf_errstring(ldb, "%s target %s GUID %s linked from %s\n", + missing_str, + ldb_dn_get_linearized(target_dn), + GUID_string(mem_ctx, guid), + ldb_dn_get_linearized(source_dn)); + return LDB_ERR_NO_SUCH_OBJECT; + } + + /* + * The target of the cross-partition link is missing. Continue + * and try to at least add the forward-link. This isn't great, + * but a partial link can be fixed by dbcheck, so it's better + * than dropping the link completely. + */ + *ignore_link = false; + + if (is_obj_commit) { + + /* + * Only log this when we're actually committing the objects. + * This avoids spurious logs, i.e. if we're just verifying the + * received link during a join. + */ + DBG_WARNING("%s cross-partition target %s linked from %s\n", + missing_str, ldb_dn_get_linearized(target_dn), + ldb_dn_get_linearized(source_dn)); + } + + return LDB_SUCCESS; +} + +/** + * Checks that the target object for a linked attribute exists. + * @param guid returns the target object's GUID (is returned)if it exists) + * @param ignore_link set to true if the linked attribute should be ignored + * (i.e. the target doesn't exist, but that it's OK to skip the link) + */ +static int replmd_check_target_exists(struct ldb_module *module, + struct dsdb_dn *dsdb_dn, + struct la_entry *la_entry, + struct ldb_dn *source_dn, + bool is_obj_commit, + struct GUID *guid, + bool *ignore_link) +{ + struct drsuapi_DsReplicaLinkedAttribute *la = la_entry->la; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_result *target_res; + TALLOC_CTX *tmp_ctx = talloc_new(la_entry); + const char *attrs[] = { "isDeleted", "isRecycled", NULL }; + NTSTATUS ntstatus; + int ret; + enum deletion_state target_deletion_state = OBJECT_REMOVED; + bool active = (la->flags & DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE) ? true : false; + + *ignore_link = false; + ntstatus = dsdb_get_extended_dn_guid(dsdb_dn->dn, guid, "GUID"); + + if (!NT_STATUS_IS_OK(ntstatus) && !active) { + + /* + * This strange behaviour (allowing a NULL/missing + * GUID) originally comes from: + * + * commit e3054ce0fe0f8f62d2f5b2a77893e7a1479128bd + * Author: Andrew Tridgell <tridge@samba.org> + * Date: Mon Dec 21 21:21:55 2009 +1100 + * + * s4-drs: cope better with NULL GUIDS from DRS + * + * It is valid to get a NULL GUID over DRS for a deleted forward link. We + * need to match by DN if possible when seeing if we should update an + * existing link. + * + * Pair-Programmed-With: Andrew Bartlett <abartlet@samba.org> + */ + ret = dsdb_module_search_dn(module, tmp_ctx, &target_res, + dsdb_dn->dn, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_SEARCH_ALL_PARTITIONS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + NULL); + } else if (!NT_STATUS_IS_OK(ntstatus)) { + ldb_asprintf_errstring(ldb, "Failed to find GUID in linked attribute 0x%x blob for %s from %s", + la->attid, + ldb_dn_get_linearized(dsdb_dn->dn), + ldb_dn_get_linearized(source_dn)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } else { + ret = dsdb_module_search(module, tmp_ctx, &target_res, + NULL, LDB_SCOPE_SUBTREE, + attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_SEARCH_ALL_PARTITIONS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + NULL, + "objectGUID=%s", + GUID_string(tmp_ctx, guid)); + } + + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to re-resolve GUID %s: %s\n", + GUID_string(tmp_ctx, guid), + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + if (target_res->count == 0) { + + /* + * target object is unknown. Check whether to ignore the link, + * fail the replication, or add a partial link + */ + ret = replmd_allow_missing_target(module, tmp_ctx, dsdb_dn->dn, + source_dn, is_obj_commit, guid, + la_entry->dsdb_repl_flags, + ignore_link, "Unknown"); + + } else if (target_res->count != 1) { + ldb_asprintf_errstring(ldb, "More than one object found matching objectGUID %s\n", + GUID_string(tmp_ctx, guid)); + ret = LDB_ERR_OPERATIONS_ERROR; + } else { + struct ldb_message *target_msg = target_res->msgs[0]; + + dsdb_dn->dn = talloc_steal(dsdb_dn, target_msg->dn); + + /* Get the object's state (i.e. Not Deleted, Tombstone, etc) */ + replmd_deletion_state(module, target_msg, + &target_deletion_state, NULL); + + /* + * Check for deleted objects as per MS-DRSR 4.1.10.6.14 + * ProcessLinkValue(). Link updates should not be sent for + * recycled and tombstone objects (deleting the links should + * happen when we delete the object). This probably means our + * copy of the target object isn't up to date. + */ + if (target_deletion_state >= OBJECT_RECYCLED) { + + /* + * target object is deleted. Check whether to ignore the + * link, fail the replication, or add a partial link + */ + ret = replmd_allow_missing_target(module, tmp_ctx, + dsdb_dn->dn, source_dn, + is_obj_commit, guid, + la_entry->dsdb_repl_flags, + ignore_link, "Deleted"); + } + } + + talloc_free(tmp_ctx); + return ret; +} + +/** + * Extracts the key details about the source object for a + * linked-attribute entry. + * This returns the following details: + * @param ret_attr the schema details for the linked attribute + * @param source_msg the search result for the source object + */ +static int replmd_get_la_entry_source(struct ldb_module *module, + struct la_entry *la_entry, + TALLOC_CTX *mem_ctx, + const struct dsdb_attribute **ret_attr, + struct ldb_message **source_msg) +{ + struct drsuapi_DsReplicaLinkedAttribute *la = la_entry->la; + struct ldb_context *ldb = ldb_module_get_ctx(module); + const struct dsdb_schema *schema = dsdb_get_schema(ldb, mem_ctx); + int ret; + const struct dsdb_attribute *attr; + struct ldb_result *res; + const char *attrs[4]; + +/* +linked_attributes[0]: + &objs->linked_attributes[i]: struct drsuapi_DsReplicaLinkedAttribute + identifier : * + identifier: struct drsuapi_DsReplicaObjectIdentifier + __ndr_size : 0x0000003a (58) + __ndr_size_sid : 0x00000000 (0) + guid : 8e95b6a9-13dd-4158-89db-3220a5be5cc7 + sid : S-0-0 + __ndr_size_dn : 0x00000000 (0) + dn : '' + attid : DRSUAPI_ATTID_member (0x1F) + value: struct drsuapi_DsAttributeValue + __ndr_size : 0x0000007e (126) + blob : * + blob : DATA_BLOB length=126 + flags : 0x00000001 (1) + 1: DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE + originating_add_time : Wed Sep 2 22:20:01 2009 EST + meta_data: struct drsuapi_DsReplicaMetaData + version : 0x00000015 (21) + originating_change_time : Wed Sep 2 23:39:07 2009 EST + originating_invocation_id: 794640f3-18cf-40ee-a211-a93992b67a64 + originating_usn : 0x000000000001e19c (123292) + +(for cases where the link is to a normal DN) + &target: struct drsuapi_DsReplicaObjectIdentifier3 + __ndr_size : 0x0000007e (126) + __ndr_size_sid : 0x0000001c (28) + guid : 7639e594-db75-4086-b0d4-67890ae46031 + sid : S-1-5-21-2848215498-2472035911-1947525656-19924 + __ndr_size_dn : 0x00000022 (34) + dn : 'CN=UOne,OU=TestOU,DC=vsofs8,DC=com' + */ + + /* find the attribute being modified */ + attr = dsdb_attribute_by_attributeID_id(schema, la->attid); + if (attr == NULL) { + struct GUID_txt_buf guid_str; + ldb_asprintf_errstring(ldb, "Unable to find attributeID 0x%x for link on <GUID=%s>", + la->attid, + GUID_buf_string(&la->identifier->guid, + &guid_str)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * All attributes listed here must be dealt with in some way + * by replmd_process_linked_attribute() otherwise in the case + * of isDeleted: FALSE the modify will fail with: + * + * Failed to apply linked attribute change 'attribute 'isDeleted': + * invalid modify flags on + * 'CN=g1_1527570609273,CN=Users,DC=samba,DC=example,DC=com': + * 0x0' + * + * This is becaue isDeleted is a Boolean, so FALSE is a + * legitimate value (set by Samba's deletetest.py) + */ + attrs[0] = attr->lDAPDisplayName; + attrs[1] = "isDeleted"; + attrs[2] = "isRecycled"; + attrs[3] = NULL; + + /* + * get the existing message from the db for the object with + * this GUID, returning attribute being modified. We will then + * use this msg as the basis for a modify call + */ + ret = dsdb_module_search(module, mem_ctx, &res, NULL, LDB_SCOPE_SUBTREE, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SEARCH_ALL_PARTITIONS | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT | + DSDB_SEARCH_REVEAL_INTERNALS, + NULL, + "objectGUID=%s", GUID_string(mem_ctx, &la->identifier->guid)); + if (ret != LDB_SUCCESS) { + return ret; + } + if (res->count != 1) { + ldb_asprintf_errstring(ldb, "DRS linked attribute for GUID %s - DN not found", + GUID_string(mem_ctx, &la->identifier->guid)); + return LDB_ERR_NO_SUCH_OBJECT; + } + + *source_msg = res->msgs[0]; + *ret_attr = attr; + + return LDB_SUCCESS; +} + +/** + * Verifies the target object is known for a linked attribute + */ +static int replmd_verify_link_target(struct replmd_replicated_request *ar, + TALLOC_CTX *mem_ctx, + struct la_entry *la_entry, + struct ldb_dn *src_dn, + const struct dsdb_attribute *attr) +{ + int ret = LDB_SUCCESS; + struct ldb_module *module = ar->module; + struct dsdb_dn *tgt_dsdb_dn = NULL; + struct GUID guid = GUID_zero(); + bool dummy; + WERROR status; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct drsuapi_DsReplicaLinkedAttribute *la = la_entry->la; + const struct dsdb_schema *schema = dsdb_get_schema(ldb, mem_ctx); + + /* the value blob for the attribute holds the target object DN */ + status = dsdb_dn_la_from_blob(ldb, attr, schema, mem_ctx, + la->value.blob, &tgt_dsdb_dn); + if (!W_ERROR_IS_OK(status)) { + ldb_asprintf_errstring(ldb, "Failed to parsed linked attribute blob for %s on %s - %s\n", + attr->lDAPDisplayName, + ldb_dn_get_linearized(src_dn), + win_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * We can skip the target object checks if we're only syncing critical + * objects, or we know the target is up-to-date. If either case, we + * still continue even if the target doesn't exist + */ + if ((la_entry->dsdb_repl_flags & (DSDB_REPL_FLAG_OBJECT_SUBSET | + DSDB_REPL_FLAG_TARGETS_UPTODATE)) == 0) { + + ret = replmd_check_target_exists(module, tgt_dsdb_dn, la_entry, + src_dn, false, &guid, &dummy); + } + + /* + * When we fail to find the target object, the error code we pass + * back here is really important. It flags back to the callers to + * retry this request with DRSUAPI_DRS_GET_TGT + */ + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + ret = replmd_replicated_request_werror(ar, WERR_DS_DRA_RECYCLED_TARGET); + } + + return ret; +} + +/** + * Finds the current active Parsed-DN value for a single-valued linked + * attribute, if one exists. + * @param ret_pdn assigned the active Parsed-DN, or NULL if none was found + * @returns LDB_SUCCESS (regardless of whether a match was found), unless + * an error occurred + */ +static int replmd_get_active_singleval_link(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct parsed_dn pdn_list[], + unsigned int count, + const struct dsdb_attribute *attr, + struct parsed_dn **ret_pdn) +{ + unsigned int i; + + *ret_pdn = NULL; + + if (!(attr->ldb_schema_attribute->flags & LDB_ATTR_FLAG_SINGLE_VALUE)) { + + /* nothing to do for multi-valued linked attributes */ + return LDB_SUCCESS; + } + + for (i = 0; i < count; i++) { + int ret = LDB_SUCCESS; + struct parsed_dn *pdn = &pdn_list[i]; + + /* skip any inactive links */ + if (dsdb_dn_is_deleted_val(pdn->v)) { + continue; + } + + /* we've found an active value for this attribute */ + *ret_pdn = pdn; + + if (pdn->dsdb_dn == NULL) { + struct ldb_context *ldb = ldb_module_get_ctx(module); + + ret = really_parse_trusted_dn(mem_ctx, ldb, pdn, + attr->syntax->ldap_oid); + } + + return ret; + } + + /* no active link found */ + return LDB_SUCCESS; +} + +/** + * @returns true if the replication linked attribute info is newer than we + * already have in our DB + * @param pdn the existing linked attribute info in our DB + * @param la the new linked attribute info received during replication + */ +static bool replmd_link_update_is_newer(struct parsed_dn *pdn, + struct drsuapi_DsReplicaLinkedAttribute *la) +{ + /* see if this update is newer than what we have already */ + struct GUID invocation_id = GUID_zero(); + uint32_t version = 0; + NTTIME change_time = 0; + + if (pdn == NULL) { + + /* no existing info so update is newer */ + return true; + } + + dsdb_get_extended_dn_guid(pdn->dsdb_dn->dn, &invocation_id, "RMD_INVOCID"); + dsdb_get_extended_dn_uint32(pdn->dsdb_dn->dn, &version, "RMD_VERSION"); + dsdb_get_extended_dn_nttime(pdn->dsdb_dn->dn, &change_time, "RMD_CHANGETIME"); + + return replmd_update_is_newer(&invocation_id, + &la->meta_data.originating_invocation_id, + version, + la->meta_data.version, + change_time, + la->meta_data.originating_change_time); +} + +/** + * Marks an existing linked attribute value as deleted in the DB + * @param pdn the parsed-DN of the target-value to delete + */ +static int replmd_delete_link_value(struct ldb_module *module, + struct replmd_private *replmd_private, + TALLOC_CTX *mem_ctx, + struct ldb_dn *src_obj_dn, + const struct dsdb_schema *schema, + const struct dsdb_attribute *attr, + uint64_t seq_num, + bool is_active, + struct GUID *target_guid, + struct dsdb_dn *target_dsdb_dn, + struct ldb_val *output_val) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + time_t t; + NTTIME now; + const struct GUID *invocation_id = NULL; + int ret; + + t = time(NULL); + unix_to_nt_time(&now, t); + + invocation_id = samdb_ntds_invocation_id(ldb); + if (invocation_id == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + /* if the existing link is active, remove its backlink */ + if (is_active) { + + /* + * NOTE WELL: After this we will never (at runtime) be + * able to find this forward link (for instant + * removal) if/when the link target is deleted. + * + * We have dbcheck rules to cover this and cope otherwise + * by filtering at runtime (i.e. in the extended_dn module). + */ + ret = replmd_add_backlink(module, replmd_private, schema, + src_obj_dn, target_guid, false, + attr, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* mark the existing value as deleted */ + ret = replmd_update_la_val(mem_ctx, output_val, target_dsdb_dn, + target_dsdb_dn, invocation_id, seq_num, + seq_num, now, true); + return ret; +} + +/** + * Checks for a conflict in single-valued link attributes, and tries to + * resolve the problem if possible. + * + * Single-valued links should only ever have one active value. If we already + * have an active link value, and during replication we receive an active link + * value for a different target DN, then we need to resolve this inconsistency + * and determine which value should be active. If the received info is better/ + * newer than the existing link attribute, then we need to set our existing + * link as deleted. If the received info is worse/older, then we should continue + * to add it, but set it as an inactive link. + * + * Note that this is a corner-case that is unlikely to happen (but if it does + * happen, we don't want it to break replication completely). + * + * @param pdn_being_modified the parsed DN corresponding to the received link + * target (note this is NULL if the link does not already exist in our DB) + * @param pdn_list all the source object's Parsed-DNs for this attribute, i.e. + * any existing active or inactive values for the attribute in our DB. + * @param dsdb_dn the target DN for the received link attribute + * @param add_as_inactive gets set to true if the received link is worse than + * the existing link - it should still be added, but as an inactive link. + */ +static int replmd_check_singleval_la_conflict(struct ldb_module *module, + struct replmd_private *replmd_private, + TALLOC_CTX *mem_ctx, + struct ldb_dn *src_obj_dn, + struct drsuapi_DsReplicaLinkedAttribute *la, + struct dsdb_dn *dsdb_dn, + struct parsed_dn *pdn_being_modified, + struct parsed_dn *pdn_list, + struct ldb_message_element *old_el, + const struct dsdb_schema *schema, + const struct dsdb_attribute *attr, + uint64_t seq_num, + bool *add_as_inactive) +{ + struct parsed_dn *active_pdn = NULL; + bool update_is_newer = false; + int ret; + + /* + * check if there's a conflict for single-valued links, i.e. an active + * linked attribute already exists, but it has a different target value + */ + ret = replmd_get_active_singleval_link(module, mem_ctx, pdn_list, + old_el->num_values, attr, + &active_pdn); + + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + * If no active value exists (or the received info is for the currently + * active value), then no conflict exists + */ + if (active_pdn == NULL || active_pdn == pdn_being_modified) { + return LDB_SUCCESS; + } + + DBG_WARNING("Link conflict for %s attribute on %s\n", + attr->lDAPDisplayName, ldb_dn_get_linearized(src_obj_dn)); + + /* Work out how to resolve the conflict based on which info is better */ + update_is_newer = replmd_link_update_is_newer(active_pdn, la); + + if (update_is_newer) { + DBG_WARNING("Using received value %s, over existing target %s\n", + ldb_dn_get_linearized(dsdb_dn->dn), + ldb_dn_get_linearized(active_pdn->dsdb_dn->dn)); + + /* + * Delete our existing active link. The received info will then + * be added (through normal link processing) as the active value + */ + ret = replmd_delete_link_value(module, replmd_private, old_el, + src_obj_dn, schema, attr, + seq_num, true, &active_pdn->guid, + active_pdn->dsdb_dn, + active_pdn->v); + + if (ret != LDB_SUCCESS) { + return ret; + } + } else { + DBG_WARNING("Using existing target %s, over received value %s\n", + ldb_dn_get_linearized(active_pdn->dsdb_dn->dn), + ldb_dn_get_linearized(dsdb_dn->dn)); + + /* + * we want to keep our existing active link and add the + * received link as inactive + */ + *add_as_inactive = true; + } + + return LDB_SUCCESS; +} + +/** + * Processes one linked attribute received via replication. + * @param src_dn the DN of the source object for the link + * @param attr schema info for the linked attribute + * @param la_entry the linked attribute info received via DRS + * @param element_ctx mem context for msg->element[] (when adding a new value + * we need to realloc old_el->values) + * @param old_el the corresponding msg->element[] for the linked attribute + * @param pdn_list a (binary-searchable) parsed DN array for the existing link + * values in the msg. E.g. for a group, this is the existing members. + * @param change what got modified: either nothing, an existing link value was + * modified, or a new link value was added. + * @returns LDB_SUCCESS if OK, an error otherwise + */ +static int replmd_process_linked_attribute(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct replmd_private *replmd_private, + struct ldb_dn *src_dn, + const struct dsdb_attribute *attr, + struct la_entry *la_entry, + struct ldb_request *parent, + TALLOC_CTX *element_ctx, + struct ldb_message_element *old_el, + struct parsed_dn *pdn_list, + replmd_link_changed *change) +{ + struct drsuapi_DsReplicaLinkedAttribute *la = la_entry->la; + struct ldb_context *ldb = ldb_module_get_ctx(module); + const struct dsdb_schema *schema = dsdb_get_schema(ldb, mem_ctx); + int ret; + struct dsdb_dn *dsdb_dn = NULL; + uint64_t seq_num = 0; + struct parsed_dn *pdn, *next; + struct GUID guid = GUID_zero(); + bool active = (la->flags & DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE)?true:false; + bool ignore_link; + struct dsdb_dn *old_dsdb_dn = NULL; + struct ldb_val *val_to_update = NULL; + bool add_as_inactive = false; + WERROR status; + + *change = LINK_CHANGE_NONE; + + /* the value blob for the attribute holds the target object DN */ + status = dsdb_dn_la_from_blob(ldb, attr, schema, mem_ctx, + la->value.blob, &dsdb_dn); + if (!W_ERROR_IS_OK(status)) { + ldb_asprintf_errstring(ldb, "Failed to parsed linked attribute blob for %s on %s - %s\n", + attr->lDAPDisplayName, + ldb_dn_get_linearized(src_dn), + win_errstr(status)); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = replmd_check_target_exists(module, dsdb_dn, la_entry, src_dn, + true, &guid, &ignore_link); + + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + * there are some cases where the target object doesn't exist, but it's + * OK to ignore the linked attribute + */ + if (ignore_link) { + return ret; + } + + /* see if this link already exists */ + ret = parsed_dn_find(ldb, pdn_list, old_el->num_values, + &guid, + dsdb_dn->dn, + dsdb_dn->extra_part, 0, + &pdn, &next, + attr->syntax->ldap_oid, + true); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (!replmd_link_update_is_newer(pdn, la)) { + DEBUG(3,("Discarding older DRS linked attribute update to %s on %s from %s\n", + old_el->name, ldb_dn_get_linearized(src_dn), + GUID_string(mem_ctx, &la->meta_data.originating_invocation_id))); + return LDB_SUCCESS; + } + + /* get a seq_num for this change */ + ret = ldb_sequence_number(ldb, LDB_SEQ_NEXT, &seq_num); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + * check for single-valued link conflicts, i.e. an active linked + * attribute already exists, but it has a different target value + */ + if (active) { + ret = replmd_check_singleval_la_conflict(module, replmd_private, + mem_ctx, src_dn, la, + dsdb_dn, pdn, pdn_list, + old_el, schema, attr, + seq_num, + &add_as_inactive); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + if (pdn != NULL) { + uint32_t rmd_flags = dsdb_dn_rmd_flags(pdn->dsdb_dn->dn); + + if (!(rmd_flags & DSDB_RMD_FLAG_DELETED)) { + /* remove the existing backlink */ + ret = replmd_add_backlink(module, replmd_private, + schema, + src_dn, + &pdn->guid, false, attr, + parent); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + val_to_update = pdn->v; + old_dsdb_dn = pdn->dsdb_dn; + *change = LINK_CHANGE_MODIFIED; + + } else { + unsigned offset; + + /* + * We know where the new one needs to be, from the *next + * pointer into pdn_list. + */ + if (next == NULL) { + offset = old_el->num_values; + } else { + if (next->dsdb_dn == NULL) { + ret = really_parse_trusted_dn(mem_ctx, ldb, next, + attr->syntax->ldap_oid); + if (ret != LDB_SUCCESS) { + return ret; + } + } + offset = next - pdn_list; + if (offset > old_el->num_values) { + return LDB_ERR_OPERATIONS_ERROR; + } + } + + old_el->values = talloc_realloc(element_ctx, old_el->values, + struct ldb_val, old_el->num_values+1); + if (!old_el->values) { + ldb_module_oom(module); + return LDB_ERR_OPERATIONS_ERROR; + } + + if (offset != old_el->num_values) { + memmove(&old_el->values[offset + 1], &old_el->values[offset], + (old_el->num_values - offset) * sizeof(old_el->values[0])); + } + + old_el->num_values++; + + val_to_update = &old_el->values[offset]; + old_dsdb_dn = NULL; + *change = LINK_CHANGE_ADDED; + } + + /* set the link attribute's value to the info that was received */ + ret = replmd_set_la_val(mem_ctx, val_to_update, dsdb_dn, old_dsdb_dn, + &la->meta_data.originating_invocation_id, + la->meta_data.originating_usn, seq_num, + la->meta_data.originating_change_time, + la->meta_data.version, + !active); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (add_as_inactive) { + + /* Set the new link as inactive/deleted to avoid conflicts */ + ret = replmd_delete_link_value(module, replmd_private, old_el, + src_dn, schema, attr, seq_num, + false, &guid, dsdb_dn, + val_to_update); + + if (ret != LDB_SUCCESS) { + return ret; + } + + } else if (active) { + + /* if the new link is active, then add the new backlink */ + ret = replmd_add_backlink(module, replmd_private, + schema, + src_dn, + &guid, true, attr, + parent); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + ret = dsdb_check_single_valued_link(attr, old_el); + if (ret != LDB_SUCCESS) { + return ret; + } + + old_el->flags |= LDB_FLAG_INTERNAL_DISABLE_SINGLE_VALUE_CHECK; + + return ret; +} + +static int replmd_extended(struct ldb_module *module, struct ldb_request *req) +{ + if (strcmp(req->op.extended.oid, DSDB_EXTENDED_REPLICATED_OBJECTS_OID) == 0) { + return replmd_extended_replicated_objects(module, req); + } + + return ldb_next_request(module, req); +} + + +/* + we hook into the transaction operations to allow us to + perform the linked attribute updates at the end of the whole + transaction. This allows a forward linked attribute to be created + before the object is created. During a vampire, w2k8 sends us linked + attributes before the objects they are part of. + */ +static int replmd_start_transaction(struct ldb_module *module) +{ + /* create our private structure for this transaction */ + struct replmd_private *replmd_private = talloc_get_type(ldb_module_get_private(module), + struct replmd_private); + replmd_txn_cleanup(replmd_private); + + /* free any leftover mod_usn records from cancelled + transactions */ + while (replmd_private->ncs) { + struct nc_entry *e = replmd_private->ncs; + DLIST_REMOVE(replmd_private->ncs, e); + talloc_free(e); + } + + replmd_private->originating_updates = false; + + return ldb_next_start_trans(module); +} + +/** + * Processes a group of linked attributes that apply to the same source-object + * and attribute-ID (and were received in the same replication chunk). + */ +static int replmd_process_la_group(struct ldb_module *module, + struct replmd_private *replmd_private, + struct la_group *la_group) +{ + struct la_entry *la = NULL; + struct la_entry *prev = NULL; + int ret; + TALLOC_CTX *tmp_ctx = NULL; + struct la_entry *first_la = DLIST_TAIL(la_group->la_entries); + struct ldb_message *msg = NULL; + enum deletion_state deletion_state = OBJECT_NOT_DELETED; + struct ldb_context *ldb = ldb_module_get_ctx(module); + const struct dsdb_attribute *attr = NULL; + struct ldb_message_element *old_el = NULL; + struct parsed_dn *pdn_list = NULL; + replmd_link_changed change_type; + uint32_t num_changes = 0; + time_t t; + uint64_t seq_num = 0; + + tmp_ctx = talloc_new(la_group); + if (tmp_ctx == NULL) { + return ldb_oom(ldb); + } + + /* + * get the attribute being modified and the search result for the + * source object + */ + ret = replmd_get_la_entry_source(module, first_la, tmp_ctx, &attr, + &msg); + + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + * Check for deleted objects per MS-DRSR 4.1.10.6.14 + * ProcessLinkValue, because link updates are not applied to + * recycled and tombstone objects. We don't have to delete + * any existing link, that should have happened when the + * object deletion was replicated or initiated. + * + * This needs isDeleted and isRecycled to be included as + * attributes in the search and so in msg if set. + */ + replmd_deletion_state(module, msg, &deletion_state, NULL); + + if (deletion_state >= OBJECT_RECYCLED) { + TALLOC_FREE(tmp_ctx); + return LDB_SUCCESS; + } + + /* + * Now that we know the deletion_state, remove the extra + * attributes added for that purpose. We need to do this + * otherwise in the case of isDeleted: FALSE the modify will + * fail with: + * + * Failed to apply linked attribute change 'attribute 'isDeleted': + * invalid modify flags on + * 'CN=g1_1527570609273,CN=Users,DC=samba,DC=example,DC=com': + * 0x0' + * + * This is becaue isDeleted is a Boolean, so FALSE is a + * legitimate value (set by Samba's deletetest.py) + */ + ldb_msg_remove_attr(msg, "isDeleted"); + ldb_msg_remove_attr(msg, "isRecycled"); + + /* get the msg->element[] for the link attribute being processed */ + old_el = ldb_msg_find_element(msg, attr->lDAPDisplayName); + if (old_el == NULL) { + ret = ldb_msg_add_empty(msg, attr->lDAPDisplayName, + LDB_FLAG_MOD_REPLACE, &old_el); + if (ret != LDB_SUCCESS) { + ldb_module_oom(module); + return LDB_ERR_OPERATIONS_ERROR; + } + } else { + old_el->flags = LDB_FLAG_MOD_REPLACE; + } + + /* + * go through and process the link target value(s) for this particular + * source object and attribute. For optimization, the same msg is used + * across multiple calls to replmd_process_linked_attribute(). + * Note that we should not add or remove any msg attributes inside the + * loop (we should only add/modify *values* for the attribute being + * processed). Otherwise msg->elements is realloc'd and old_el/pdn_list + * pointers will be invalidated + */ + for (la = DLIST_TAIL(la_group->la_entries); la; la=prev) { + prev = DLIST_PREV(la); + DLIST_REMOVE(la_group->la_entries, la); + + /* + * parse the existing links (this can be costly for a large + * group, so we try to minimize the times we do it) + */ + if (pdn_list == NULL) { + ret = get_parsed_dns_trusted_fallback(module, + replmd_private, + tmp_ctx, old_el, + &pdn_list, + attr->syntax->ldap_oid, + NULL); + + if (ret != LDB_SUCCESS) { + return ret; + } + } + ret = replmd_process_linked_attribute(module, tmp_ctx, + replmd_private, + msg->dn, attr, la, NULL, + msg->elements, old_el, + pdn_list, &change_type); + if (ret != LDB_SUCCESS) { + replmd_txn_cleanup(replmd_private); + return ret; + } + + /* + * Adding a link reallocs memory, and so invalidates all the + * pointers in pdn_list. Reparse the PDNs on the next loop + */ + if (change_type == LINK_CHANGE_ADDED) { + TALLOC_FREE(pdn_list); + } + + if (change_type != LINK_CHANGE_NONE) { + num_changes++; + } + + if ((++replmd_private->num_processed % 8192) == 0) { + DBG_NOTICE("Processed %u/%u linked attributes\n", + replmd_private->num_processed, + replmd_private->total_links); + } + } + + /* + * it's possible we're already up-to-date and so don't need to modify + * the object at all (e.g. doing a 'drs replicate --full-sync') + */ + if (num_changes == 0) { + TALLOC_FREE(tmp_ctx); + return LDB_SUCCESS; + } + + /* + * Note that adding the whenChanged/etc attributes below will realloc + * msg->elements, invalidating the existing element/parsed-DN pointers + */ + old_el = NULL; + TALLOC_FREE(pdn_list); + + /* update whenChanged/uSNChanged as the object has changed */ + t = time(NULL); + ret = ldb_sequence_number(ldb, LDB_SEQ_HIGHEST_SEQ, + &seq_num); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = add_time_element(msg, "whenChanged", t); + if (ret != LDB_SUCCESS) { + ldb_operr(ldb); + return ret; + } + + ret = add_uint64_element(ldb, msg, "uSNChanged", seq_num); + if (ret != LDB_SUCCESS) { + ldb_operr(ldb); + return ret; + } + + /* apply the link changes to the source object */ + ret = linked_attr_modify(module, msg, NULL); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_WARNING, + "Failed to apply linked attribute change " + "Error: '%s' DN: '%s' Attribute: '%s'\n", + ldb_errstring(ldb), + ldb_dn_get_linearized(msg->dn), + attr->lDAPDisplayName); + TALLOC_FREE(tmp_ctx); + return ret; + } + + TALLOC_FREE(tmp_ctx); + return LDB_SUCCESS; +} + +/* + on prepare commit we loop over our queued la_context structures and + apply each of them + */ +static int replmd_prepare_commit(struct ldb_module *module) +{ + struct replmd_private *replmd_private = + talloc_get_type(ldb_module_get_private(module), struct replmd_private); + struct la_group *la_group, *prev; + int ret; + + if (replmd_private->la_list != NULL) { + DBG_NOTICE("Processing linked attributes\n"); + } + + /* + * Walk the list of linked attributes from DRS replication. + * + * We walk backwards, to do the first entry first, as we + * added the entries with DLIST_ADD() which puts them at the + * start of the list + * + * Links are grouped together so we process links for the same + * source object in one go. + */ + for (la_group = DLIST_TAIL(replmd_private->la_list); + la_group != NULL; + la_group = prev) { + + prev = DLIST_PREV(la_group); + DLIST_REMOVE(replmd_private->la_list, la_group); + ret = replmd_process_la_group(module, replmd_private, + la_group); + if (ret != LDB_SUCCESS) { + replmd_txn_cleanup(replmd_private); + return ret; + } + } + + replmd_txn_cleanup(replmd_private); + + /* possibly change @REPLCHANGED */ + ret = replmd_notify_store(module, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_prepare_commit(module); +} + +static int replmd_del_transaction(struct ldb_module *module) +{ + struct replmd_private *replmd_private = + talloc_get_type(ldb_module_get_private(module), struct replmd_private); + replmd_txn_cleanup(replmd_private); + + return ldb_next_del_trans(module); +} + + +static const struct ldb_module_ops ldb_repl_meta_data_module_ops = { + .name = "repl_meta_data", + .init_context = replmd_init, + .add = replmd_add, + .modify = replmd_modify, + .rename = replmd_rename, + .del = replmd_delete, + .extended = replmd_extended, + .start_transaction = replmd_start_transaction, + .prepare_commit = replmd_prepare_commit, + .del_transaction = replmd_del_transaction, +}; + +int ldb_repl_meta_data_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_repl_meta_data_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/resolve_oids.c b/source4/dsdb/samdb/ldb_modules/resolve_oids.c new file mode 100644 index 0000000..b5c5f8e --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/resolve_oids.c @@ -0,0 +1,704 @@ +/* + ldb database library + + Copyright (C) Stefan Metzmacher <metze@samba.org> 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/>. +*/ + +#include "includes.h" +#include "ldb_module.h" +#include "dsdb/samdb/samdb.h" + +static int resolve_oids_need_value(struct ldb_context *ldb, + struct dsdb_schema *schema, + const struct dsdb_attribute *a, + const struct ldb_val *valp) +{ + const struct dsdb_attribute *va = NULL; + const struct dsdb_class *vo = NULL; + const void *p2; + char *str = NULL; + + if (a->syntax->oMSyntax != 6) { + return LDB_ERR_COMPARE_FALSE; + } + + if (valp) { + p2 = memchr(valp->data, '.', valp->length); + } else { + p2 = NULL; + } + + if (!p2) { + return LDB_ERR_COMPARE_FALSE; + } + + switch (a->attributeID_id) { + case DRSUAPI_ATTID_objectClass: + case DRSUAPI_ATTID_subClassOf: + case DRSUAPI_ATTID_auxiliaryClass: + case DRSUAPI_ATTID_systemPossSuperiors: + case DRSUAPI_ATTID_possSuperiors: + str = talloc_strndup(ldb, (char *)valp->data, valp->length); + if (!str) { + return ldb_oom(ldb); + } + vo = dsdb_class_by_governsID_oid(schema, str); + talloc_free(str); + if (!vo) { + return LDB_ERR_COMPARE_FALSE; + } + return LDB_ERR_COMPARE_TRUE; + case DRSUAPI_ATTID_systemMustContain: + case DRSUAPI_ATTID_systemMayContain: + case DRSUAPI_ATTID_mustContain: + case DRSUAPI_ATTID_mayContain: + str = talloc_strndup(ldb, (char *)valp->data, valp->length); + if (!str) { + return ldb_oom(ldb); + } + va = dsdb_attribute_by_attributeID_oid(schema, str); + talloc_free(str); + if (!va) { + return LDB_ERR_COMPARE_FALSE; + } + return LDB_ERR_COMPARE_TRUE; + case DRSUAPI_ATTID_governsID: + case DRSUAPI_ATTID_attributeID: + case DRSUAPI_ATTID_attributeSyntax: + return LDB_ERR_COMPARE_FALSE; + } + + return LDB_ERR_COMPARE_FALSE; +} + +static int resolve_oids_parse_tree_need(struct ldb_context *ldb, + struct dsdb_schema *schema, + const struct ldb_parse_tree *tree) +{ + unsigned int i; + const struct dsdb_attribute *a = NULL; + const char *attr; + const char *p1; + const void *p2; + const struct ldb_val *valp = NULL; + int ret; + + switch (tree->operation) { + case LDB_OP_AND: + case LDB_OP_OR: + for (i=0;i<tree->u.list.num_elements;i++) { + ret = resolve_oids_parse_tree_need(ldb, schema, + tree->u.list.elements[i]); + if (ret != LDB_ERR_COMPARE_FALSE) { + return ret; + } + } + return LDB_ERR_COMPARE_FALSE; + case LDB_OP_NOT: + return resolve_oids_parse_tree_need(ldb, schema, + tree->u.isnot.child); + case LDB_OP_EQUALITY: + case LDB_OP_GREATER: + case LDB_OP_LESS: + case LDB_OP_APPROX: + attr = tree->u.equality.attr; + valp = &tree->u.equality.value; + break; + case LDB_OP_SUBSTRING: + attr = tree->u.substring.attr; + break; + case LDB_OP_PRESENT: + attr = tree->u.present.attr; + break; + case LDB_OP_EXTENDED: + attr = tree->u.extended.attr; + valp = &tree->u.extended.value; + break; + default: + return LDB_ERR_COMPARE_FALSE; + } + + p1 = strchr(attr, '.'); + + if (valp) { + p2 = memchr(valp->data, '.', valp->length); + } else { + p2 = NULL; + } + + if (!p1 && !p2) { + return LDB_ERR_COMPARE_FALSE; + } + + if (p1) { + a = dsdb_attribute_by_attributeID_oid(schema, attr); + } else { + a = dsdb_attribute_by_lDAPDisplayName(schema, attr); + } + if (!a) { + return LDB_ERR_COMPARE_FALSE; + } + + if (!p2) { + return LDB_ERR_COMPARE_FALSE; + } + + return resolve_oids_need_value(ldb, schema, a, valp); +} + +static int resolve_oids_element_need(struct ldb_context *ldb, + struct dsdb_schema *schema, + const struct ldb_message_element *el) +{ + unsigned int i; + const struct dsdb_attribute *a = NULL; + const char *p1; + + p1 = strchr(el->name, '.'); + + if (p1) { + a = dsdb_attribute_by_attributeID_oid(schema, el->name); + } else { + a = dsdb_attribute_by_lDAPDisplayName(schema, el->name); + } + if (!a) { + return LDB_ERR_COMPARE_FALSE; + } + + for (i=0; i < el->num_values; i++) { + int ret; + ret = resolve_oids_need_value(ldb, schema, a, + &el->values[i]); + if (ret != LDB_ERR_COMPARE_FALSE) { + return ret; + } + } + + return LDB_ERR_COMPARE_FALSE; +} + +static int resolve_oids_message_need(struct ldb_context *ldb, + struct dsdb_schema *schema, + const struct ldb_message *msg) +{ + unsigned int i; + + for (i=0; i < msg->num_elements; i++) { + int ret; + ret = resolve_oids_element_need(ldb, schema, + &msg->elements[i]); + if (ret != LDB_ERR_COMPARE_FALSE) { + return ret; + } + } + + return LDB_ERR_COMPARE_FALSE; +} + +static int resolve_oids_replace_value(struct ldb_context *ldb, + struct dsdb_schema *schema, + const struct dsdb_attribute *a, + struct ldb_val *valp) +{ + const struct dsdb_attribute *va = NULL; + const struct dsdb_class *vo = NULL; + const void *p2; + char *str = NULL; + + if (a->syntax->oMSyntax != 6) { + return LDB_SUCCESS; + } + + if (valp) { + p2 = memchr(valp->data, '.', valp->length); + } else { + p2 = NULL; + } + + if (!p2) { + return LDB_SUCCESS; + } + + switch (a->attributeID_id) { + case DRSUAPI_ATTID_objectClass: + case DRSUAPI_ATTID_subClassOf: + case DRSUAPI_ATTID_auxiliaryClass: + case DRSUAPI_ATTID_systemPossSuperiors: + case DRSUAPI_ATTID_possSuperiors: + str = talloc_strndup(schema, (char *)valp->data, valp->length); + if (!str) { + return ldb_oom(ldb); + } + vo = dsdb_class_by_governsID_oid(schema, str); + talloc_free(str); + if (!vo) { + return LDB_SUCCESS; + } + *valp = data_blob_string_const(vo->lDAPDisplayName); + return LDB_SUCCESS; + case DRSUAPI_ATTID_systemMustContain: + case DRSUAPI_ATTID_systemMayContain: + case DRSUAPI_ATTID_mustContain: + case DRSUAPI_ATTID_mayContain: + str = talloc_strndup(schema, (char *)valp->data, valp->length); + if (!str) { + return ldb_oom(ldb); + } + va = dsdb_attribute_by_attributeID_oid(schema, str); + talloc_free(str); + if (!va) { + return LDB_SUCCESS; + } + *valp = data_blob_string_const(va->lDAPDisplayName); + return LDB_SUCCESS; + case DRSUAPI_ATTID_governsID: + case DRSUAPI_ATTID_attributeID: + case DRSUAPI_ATTID_attributeSyntax: + return LDB_SUCCESS; + } + + return LDB_SUCCESS; +} + +static int resolve_oids_parse_tree_replace(struct ldb_context *ldb, + struct dsdb_schema *schema, + struct ldb_parse_tree *tree) +{ + unsigned int i; + const struct dsdb_attribute *a = NULL; + const char **attrp; + const char *p1; + const void *p2; + struct ldb_val *valp = NULL; + int ret; + + switch (tree->operation) { + case LDB_OP_AND: + case LDB_OP_OR: + for (i=0;i<tree->u.list.num_elements;i++) { + ret = resolve_oids_parse_tree_replace(ldb, schema, + tree->u.list.elements[i]); + if (ret != LDB_SUCCESS) { + return ret; + } + } + return LDB_SUCCESS; + case LDB_OP_NOT: + return resolve_oids_parse_tree_replace(ldb, schema, + tree->u.isnot.child); + case LDB_OP_EQUALITY: + case LDB_OP_GREATER: + case LDB_OP_LESS: + case LDB_OP_APPROX: + attrp = &tree->u.equality.attr; + valp = &tree->u.equality.value; + break; + case LDB_OP_SUBSTRING: + attrp = &tree->u.substring.attr; + break; + case LDB_OP_PRESENT: + attrp = &tree->u.present.attr; + break; + case LDB_OP_EXTENDED: + attrp = &tree->u.extended.attr; + valp = &tree->u.extended.value; + break; + default: + return LDB_SUCCESS; + } + + p1 = strchr(*attrp, '.'); + + if (valp) { + p2 = memchr(valp->data, '.', valp->length); + } else { + p2 = NULL; + } + + if (!p1 && !p2) { + return LDB_SUCCESS; + } + + if (p1) { + a = dsdb_attribute_by_attributeID_oid(schema, *attrp); + } else { + a = dsdb_attribute_by_lDAPDisplayName(schema, *attrp); + } + if (!a) { + return LDB_SUCCESS; + } + + *attrp = a->lDAPDisplayName; + + if (!p2) { + return LDB_SUCCESS; + } + + if (a->syntax->oMSyntax != 6) { + return LDB_SUCCESS; + } + + return resolve_oids_replace_value(ldb, schema, a, valp); +} + +static int resolve_oids_element_replace(struct ldb_context *ldb, + struct dsdb_schema *schema, + struct ldb_message_element *el) +{ + unsigned int i; + const struct dsdb_attribute *a = NULL; + const char *p1; + + p1 = strchr(el->name, '.'); + + if (p1) { + a = dsdb_attribute_by_attributeID_oid(schema, el->name); + } else { + a = dsdb_attribute_by_lDAPDisplayName(schema, el->name); + } + if (!a) { + return LDB_SUCCESS; + } + + el->name = a->lDAPDisplayName; + + for (i=0; i < el->num_values; i++) { + int ret; + ret = resolve_oids_replace_value(ldb, schema, a, + &el->values[i]); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + return LDB_SUCCESS; +} + +static int resolve_oids_message_replace(struct ldb_context *ldb, + struct dsdb_schema *schema, + struct ldb_message *msg) +{ + unsigned int i; + + for (i=0; i < msg->num_elements; i++) { + int ret; + ret = resolve_oids_element_replace(ldb, schema, + &msg->elements[i]); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + return LDB_SUCCESS; +} + +struct resolve_oids_context { + struct ldb_module *module; + struct ldb_request *req; +}; + +static int resolve_oids_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct resolve_oids_context *ac; + + ac = talloc_get_type_abort(req->context, struct resolve_oids_context); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + return ldb_module_send_entry(ac->req, ares->message, ares->controls); + + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral(ac->req, ares->referral); + + case LDB_REPLY_DONE: + return ldb_module_done(ac->req, ares->controls, + ares->response, LDB_SUCCESS); + + } + return LDB_SUCCESS; +} + +static int resolve_oids_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct dsdb_schema *schema; + struct ldb_parse_tree *tree; + struct ldb_request *down_req; + struct resolve_oids_context *ac; + int ret; + bool needed = false; + const char * const *attrs1; + const char **attrs2; + unsigned int i; + + ldb = ldb_module_get_ctx(module); + schema = dsdb_get_schema(ldb, NULL); + + if (!schema) { + return ldb_next_request(module, req); + } + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.search.base)) { + return ldb_next_request(module, req); + } + + ret = resolve_oids_parse_tree_need(ldb, schema, + req->op.search.tree); + if (ret == LDB_ERR_COMPARE_TRUE) { + needed = true; + } else if (ret != LDB_ERR_COMPARE_FALSE) { + return ret; + } + + attrs1 = req->op.search.attrs; + + for (i=0; attrs1 && attrs1[i]; i++) { + const char *p; + const struct dsdb_attribute *a; + + p = strchr(attrs1[i], '.'); + if (p == NULL) { + continue; + } + + a = dsdb_attribute_by_attributeID_oid(schema, attrs1[i]); + if (a == NULL) { + continue; + } + + needed = true; + break; + } + + if (!needed) { + return ldb_next_request(module, req); + } + + ac = talloc(req, struct resolve_oids_context); + if (ac == NULL) { + return ldb_oom(ldb); + } + ac->module = module; + ac->req = req; + + tree = ldb_parse_tree_copy_shallow(ac, req->op.search.tree); + if (!tree) { + return ldb_oom(ldb); + } + + schema = talloc_reference(tree, schema); + if (!schema) { + return ldb_oom(ldb); + } + + ret = resolve_oids_parse_tree_replace(ldb, schema, + tree); + if (ret != LDB_SUCCESS) { + return ret; + } + + attrs2 = str_list_copy_const(ac, + discard_const_p(const char *, req->op.search.attrs)); + if (req->op.search.attrs && !attrs2) { + return ldb_oom(ldb); + } + + for (i=0; attrs2 && attrs2[i]; i++) { + const char *p; + const struct dsdb_attribute *a; + + p = strchr(attrs2[i], '.'); + if (p == NULL) { + continue; + } + + a = dsdb_attribute_by_attributeID_oid(schema, attrs2[i]); + if (a == NULL) { + continue; + } + + attrs2[i] = a->lDAPDisplayName; + } + + ret = ldb_build_search_req_ex(&down_req, ldb, ac, + req->op.search.base, + req->op.search.scope, + tree, + attrs2, + req->controls, + ac, resolve_oids_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* go on with the call chain */ + return ldb_next_request(module, down_req); +} + +static int resolve_oids_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct dsdb_schema *schema; + int ret; + struct ldb_message *msg; + struct ldb_request *down_req; + struct resolve_oids_context *ac; + + ldb = ldb_module_get_ctx(module); + schema = dsdb_get_schema(ldb, NULL); + + if (!schema) { + return ldb_next_request(module, req); + } + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.add.message->dn)) { + return ldb_next_request(module, req); + } + + ret = resolve_oids_message_need(ldb, schema, + req->op.add.message); + if (ret == LDB_ERR_COMPARE_FALSE) { + return ldb_next_request(module, req); + } else if (ret != LDB_ERR_COMPARE_TRUE) { + return ret; + } + + ac = talloc(req, struct resolve_oids_context); + if (ac == NULL) { + return ldb_oom(ldb); + } + ac->module = module; + ac->req = req; + + msg = ldb_msg_copy_shallow(ac, ac->req->op.add.message); + if (!msg) { + return ldb_oom(ldb); + } + + if (!talloc_reference(msg, schema)) { + return ldb_oom(ldb); + } + + ret = resolve_oids_message_replace(ldb, schema, msg); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_build_add_req(&down_req, ldb, ac, + msg, + req->controls, + ac, resolve_oids_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* go on with the call chain */ + return ldb_next_request(module, down_req); +} + +static int resolve_oids_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct dsdb_schema *schema; + int ret; + struct ldb_message *msg; + struct ldb_request *down_req; + struct resolve_oids_context *ac; + + ldb = ldb_module_get_ctx(module); + schema = dsdb_get_schema(ldb, NULL); + + if (!schema) { + return ldb_next_request(module, req); + } + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.mod.message->dn)) { + return ldb_next_request(module, req); + } + + ret = resolve_oids_message_need(ldb, schema, + req->op.mod.message); + if (ret == LDB_ERR_COMPARE_FALSE) { + return ldb_next_request(module, req); + } else if (ret != LDB_ERR_COMPARE_TRUE) { + return ret; + } + + ac = talloc(req, struct resolve_oids_context); + if (ac == NULL) { + return ldb_oom(ldb); + } + ac->module = module; + ac->req = req; + + /* we have to copy the message as the caller might have it as a const */ + msg = ldb_msg_copy_shallow(ac, req->op.mod.message); + if (msg == NULL) { + return ldb_oom(ldb); + } + + if (!talloc_reference(msg, schema)) { + return ldb_oom(ldb); + } + + ret = resolve_oids_message_replace(ldb, schema, msg); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_build_mod_req(&down_req, ldb, ac, + msg, + req->controls, + ac, resolve_oids_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* go on with the call chain */ + return ldb_next_request(module, down_req); +} + +static const struct ldb_module_ops ldb_resolve_oids_module_ops = { + .name = "resolve_oids", + .search = resolve_oids_search, + .add = resolve_oids_add, + .modify = resolve_oids_modify, +}; + + +int ldb_resolve_oids_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_resolve_oids_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/ridalloc.c b/source4/dsdb/samdb/ldb_modules/ridalloc.c new file mode 100644 index 0000000..072a434 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/ridalloc.c @@ -0,0 +1,829 @@ +/* + RID allocation helper functions + + Copyright (C) Andrew Bartlett 2010 + Copyright (C) Andrew Tridgell 2010 + + 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: RID allocation logic + * + * Description: manage RID Set and RID Manager objects + * + */ + +#include "includes.h" +#include "ldb_module.h" +#include "lib/util/server_id.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "lib/messaging/irpc.h" +#include "param/param.h" +#include "librpc/gen_ndr/ndr_misc.h" +#include "dsdb/samdb/ldb_modules/ridalloc.h" + +/* + Note: the RID allocation attributes in AD are very badly named. Here + is what we think they really do: + + in RID Set object: + - rIDPreviousAllocationPool: the pool which a DC is currently + pulling RIDs from. Managed by client DC + + - rIDAllocationPool: the pool that the DC will switch to next, + when rIDPreviousAllocationPool is exhausted. Managed by RID Manager. + + - rIDNextRID: the last RID allocated by this DC. Managed by client DC + + in RID Manager object: + - rIDAvailablePool: the pool where the RID Manager gets new rID + pools from when it gets a EXOP_RID_ALLOC getncchanges call (or + locally when the DC is the RID Manager) + */ + + +/* + make a IRPC call to the drepl task to ask it to get the RID + Manager to give us another RID pool. + + This function just sends the message to the drepl task then + returns immediately. It should be called well before we + completely run out of RIDs + */ +static int ridalloc_poke_rid_manager(struct ldb_module *module) +{ + struct imessaging_context *msg; + unsigned num_servers; + struct server_id *servers; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct loadparm_context *lp_ctx = + (struct loadparm_context *)ldb_get_opaque(ldb, "loadparm"); + TALLOC_CTX *tmp_ctx = talloc_new(module); + NTSTATUS status; + + msg = imessaging_client_init(tmp_ctx, lp_ctx, + ldb_get_event_context(ldb)); + if (!msg) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "Failed to send MSG_DREPL_ALLOCATE_RID, " + "unable init client messaging context"); + DEBUG(3,(__location__ ": Failed to create messaging context\n")); + talloc_free(tmp_ctx); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + status = irpc_servers_byname(msg, msg, "dreplsrv", + &num_servers, &servers); + if (!NT_STATUS_IS_OK(status)) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "Failed to send MSG_DREPL_ALLOCATE_RID, " + "unable to locate dreplsrv"); + /* this means the drepl service is not running */ + talloc_free(tmp_ctx); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + status = imessaging_send(msg, servers[0], MSG_DREPL_ALLOCATE_RID, NULL); + + /* Only error out if an error happened, not on STATUS_MORE_ENTRIES, ie a delayed message */ + if (NT_STATUS_IS_ERR(status)) { + struct server_id_buf idbuf; + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "Failed to send MSG_DREPL_ALLOCATE_RID to dreplsrv at %s: %s", + server_id_str_buf(*servers, &idbuf), + nt_errstr(status)); + talloc_free(tmp_ctx); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + + +static const char * const ridalloc_ridset_attrs[] = { + "rIDAllocationPool", + "rIDPreviousAllocationPool", + "rIDNextRID", + "rIDUsedPool", + NULL +}; + +struct ridalloc_ridset_values { + uint64_t alloc_pool; + uint64_t prev_pool; + uint32_t next_rid; + uint32_t used_pool; +}; + +static void ridalloc_get_ridset_values(struct ldb_message *msg, struct ridalloc_ridset_values *v) +{ + v->alloc_pool = ldb_msg_find_attr_as_uint64(msg, "rIDAllocationPool", UINT64_MAX); + v->prev_pool = ldb_msg_find_attr_as_uint64(msg, "rIDPreviousAllocationPool", UINT64_MAX); + v->next_rid = ldb_msg_find_attr_as_uint(msg, "rIDNextRID", UINT32_MAX); + v->used_pool = ldb_msg_find_attr_as_uint(msg, "rIDUsedPool", UINT32_MAX); +} + +static int ridalloc_set_ridset_values(struct ldb_module *module, + struct ldb_message *msg, + const struct ridalloc_ridset_values *o, + const struct ridalloc_ridset_values *n) +{ + const uint32_t *o32, *n32; + const uint64_t *o64, *n64; + int ret; + +#define SETUP_PTRS(field, optr, nptr, max) do { \ + optr = &o->field; \ + nptr = &n->field; \ + if (o->field == max) { \ + optr = NULL; \ + } \ + if (n->field == max) { \ + nptr = NULL; \ + } \ + if (o->field == n->field) { \ + optr = NULL; \ + nptr = NULL; \ + } \ +} while(0) + + SETUP_PTRS(alloc_pool, o64, n64, UINT64_MAX); + ret = dsdb_msg_constrainted_update_uint64(module, msg, + "rIDAllocationPool", + o64, n64); + if (ret != LDB_SUCCESS) { + return ret; + } + + SETUP_PTRS(prev_pool, o64, n64, UINT64_MAX); + ret = dsdb_msg_constrainted_update_uint64(module, msg, + "rIDPreviousAllocationPool", + o64, n64); + if (ret != LDB_SUCCESS) { + return ret; + } + + SETUP_PTRS(next_rid, o32, n32, UINT32_MAX); + ret = dsdb_msg_constrainted_update_uint32(module, msg, + "rIDNextRID", + o32, n32); + if (ret != LDB_SUCCESS) { + return ret; + } + + SETUP_PTRS(used_pool, o32, n32, UINT32_MAX); + ret = dsdb_msg_constrainted_update_uint32(module, msg, + "rIDUsedPool", + o32, n32); + if (ret != LDB_SUCCESS) { + return ret; + } +#undef SETUP_PTRS + + return LDB_SUCCESS; +} + +/* + allocate a new range of RIDs in the RID Manager object + */ +static int ridalloc_rid_manager_allocate(struct ldb_module *module, struct ldb_dn *rid_manager_dn, uint64_t *new_pool, + struct ldb_request *parent) +{ + int ret; + TALLOC_CTX *tmp_ctx = talloc_new(module); + const char *attrs[] = { "rIDAvailablePool", NULL }; + uint64_t rid_pool, new_rid_pool, dc_pool; + uint32_t rid_pool_lo, rid_pool_hi; + struct ldb_result *res; + struct ldb_context *ldb = ldb_module_get_ctx(module); + const unsigned alloc_size = 500; + + ret = dsdb_module_search_dn(module, tmp_ctx, &res, rid_manager_dn, + attrs, DSDB_FLAG_NEXT_MODULE, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to find rIDAvailablePool in %s - %s", + ldb_dn_get_linearized(rid_manager_dn), ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + rid_pool = ldb_msg_find_attr_as_uint64(res->msgs[0], "rIDAvailablePool", 0); + rid_pool_lo = rid_pool & 0xFFFFFFFF; + rid_pool_hi = rid_pool >> 32; + if (rid_pool_lo >= rid_pool_hi) { + ldb_asprintf_errstring(ldb, "Out of RIDs in RID Manager - rIDAvailablePool is %u-%u", + rid_pool_lo, rid_pool_hi); + talloc_free(tmp_ctx); + return ret; + } + + /* lower part of new pool is the low part of the rIDAvailablePool */ + dc_pool = rid_pool_lo; + + /* allocate 500 RIDs to this DC */ + rid_pool_lo = MIN(rid_pool_hi, rid_pool_lo + alloc_size); + + /* work out upper part of new pool */ + dc_pool |= (((uint64_t)rid_pool_lo-1)<<32); + + /* and new rIDAvailablePool value */ + new_rid_pool = rid_pool_lo | (((uint64_t)rid_pool_hi)<<32); + + ret = dsdb_module_constrainted_update_uint64(module, rid_manager_dn, "rIDAvailablePool", + &rid_pool, &new_rid_pool, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to update rIDAvailablePool - %s", + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + (*new_pool) = dc_pool; + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + +/* + create a RID Set object for the specified DC + */ +static int ridalloc_create_rid_set_ntds(struct ldb_module *module, TALLOC_CTX *mem_ctx, + struct ldb_dn *rid_manager_dn, + struct ldb_dn *ntds_dn, struct ldb_dn **dn, + struct ldb_request *parent) +{ + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + struct ldb_dn *server_dn, *machine_dn, *rid_set_dn; + int ret; + struct ldb_message *msg; + struct ldb_context *ldb = ldb_module_get_ctx(module); + static const struct ridalloc_ridset_values o = { + .alloc_pool = UINT64_MAX, + .prev_pool = UINT64_MAX, + .next_rid = UINT32_MAX, + .used_pool = UINT32_MAX, + }; + struct ridalloc_ridset_values n = { + .alloc_pool = 0, + .prev_pool = 0, + .next_rid = 0, + .used_pool = 0, + }; + const char *no_attrs[] = { NULL }; + struct ldb_result *res; + + /* + steps: + + find the machine object for the DC + construct the RID Set DN + load rIDAvailablePool to find next available set + modify RID Manager object to update rIDAvailablePool + add the RID Set object + link to the RID Set object in machine object + */ + + server_dn = ldb_dn_get_parent(tmp_ctx, ntds_dn); + if (!server_dn) { + talloc_free(tmp_ctx); + return ldb_module_oom(module); + } + + ret = dsdb_module_reference_dn(module, tmp_ctx, server_dn, "serverReference", &machine_dn, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to find serverReference in %s - %s", + ldb_dn_get_linearized(server_dn), ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + rid_set_dn = ldb_dn_copy(tmp_ctx, machine_dn); + if (rid_set_dn == NULL) { + talloc_free(tmp_ctx); + return ldb_module_oom(module); + } + + if (! ldb_dn_add_child_fmt(rid_set_dn, "CN=RID Set")) { + talloc_free(tmp_ctx); + return ldb_module_oom(module); + } + + /* grab a pool from the RID Manager object */ + ret = ridalloc_rid_manager_allocate(module, rid_manager_dn, &n.alloc_pool, parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + /* create the RID Set object */ + msg = ldb_msg_new(tmp_ctx); + msg->dn = rid_set_dn; + + ret = ldb_msg_add_string(msg, "objectClass", "rIDSet"); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = ridalloc_set_ridset_values(module, msg, &o, &n); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + /* we need this to go all the way to the top of the module + * stack, as we need all the extra attributes added (including + * complex ones like ntsecuritydescriptor). We must do this + * as system, otherwise a user might end up owning the RID + * set, and that would be bad... */ + ret = dsdb_module_add(module, msg, + DSDB_FLAG_TOP_MODULE | DSDB_FLAG_AS_SYSTEM + | DSDB_MODIFY_RELAX, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to add RID Set %s - %s", + ldb_dn_get_linearized(msg->dn), + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + /* add the rIDSetReferences link */ + msg = ldb_msg_new(tmp_ctx); + msg->dn = machine_dn; + + /* we need the extended DN of the RID Set object for + * rIDSetReferences */ + ret = dsdb_module_search_dn(module, msg, &res, rid_set_dn, no_attrs, + DSDB_FLAG_NEXT_MODULE | DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to find extended DN of RID Set %s - %s", + ldb_dn_get_linearized(msg->dn), + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + rid_set_dn = res->msgs[0]->dn; + + + ret = ldb_msg_add_string(msg, "rIDSetReferences", ldb_dn_get_extended_linearized(msg, rid_set_dn, 1)); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + msg->elements[0].flags = LDB_FLAG_MOD_ADD; + + ret = dsdb_module_modify(module, msg, + DSDB_FLAG_NEXT_MODULE|DSDB_FLAG_AS_SYSTEM, + parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to add rIDSetReferences to %s - %s", + ldb_dn_get_linearized(msg->dn), + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + (*dn) = talloc_steal(mem_ctx, rid_set_dn); + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + + +/* + create a RID Set object for this DC + */ +int ridalloc_create_own_rid_set(struct ldb_module *module, TALLOC_CTX *mem_ctx, + struct ldb_dn **dn, struct ldb_request *parent) +{ + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + struct ldb_dn *rid_manager_dn, *fsmo_role_dn; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct GUID fsmo_role_guid; + const struct GUID *our_ntds_guid; + NTSTATUS status; + + /* work out who is the RID Manager */ + ret = dsdb_module_rid_manager_dn(module, tmp_ctx, &rid_manager_dn, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to find RID Manager object - %s", + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + /* find the DN of the RID Manager */ + ret = dsdb_module_reference_dn(module, tmp_ctx, rid_manager_dn, "fSMORoleOwner", &fsmo_role_dn, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to find fSMORoleOwner in RID Manager object - %s", + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + status = dsdb_get_extended_dn_guid(fsmo_role_dn, &fsmo_role_guid, "GUID"); + if (!NT_STATUS_IS_OK(status)) { + talloc_free(tmp_ctx); + return ldb_operr(ldb_module_get_ctx(module)); + } + + /* clear the cache so we don't get an old ntds_guid */ + if (ldb_set_opaque(ldb, "cache.ntds_guid", NULL) != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ldb_operr(ldb_module_get_ctx(module)); + } + + our_ntds_guid = samdb_ntds_objectGUID(ldb_module_get_ctx(module)); + if (!our_ntds_guid) { + talloc_free(tmp_ctx); + return ldb_operr(ldb_module_get_ctx(module)); + } + + if (!GUID_equal(&fsmo_role_guid, our_ntds_guid)) { + ret = ridalloc_poke_rid_manager(module); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "Request for remote creation of " + "RID Set for this DC failed: %s", + ldb_errstring(ldb)); + } else { + ldb_asprintf_errstring(ldb, + "Remote RID Set creation needed"); + } + talloc_free(tmp_ctx); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + ret = ridalloc_create_rid_set_ntds(module, mem_ctx, rid_manager_dn, fsmo_role_dn, dn, parent); + talloc_free(tmp_ctx); + return ret; +} + +/* + get a new RID pool for ourselves + also returns the first rid for the new pool + */ + +int ridalloc_new_own_pool(struct ldb_module *module, uint64_t *new_pool, struct ldb_request *parent) +{ + TALLOC_CTX *tmp_ctx = talloc_new(module); + struct ldb_dn *rid_manager_dn, *fsmo_role_dn; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + bool is_us; + + /* work out who is the RID Manager */ + ret = dsdb_module_rid_manager_dn(module, tmp_ctx, &rid_manager_dn, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to find RID Manager object - %s", + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + /* find the DN of the RID Manager */ + ret = dsdb_module_reference_dn(module, tmp_ctx, rid_manager_dn, "fSMORoleOwner", &fsmo_role_dn, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to find fSMORoleOwner in RID Manager object - %s", + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + ret = samdb_dn_is_our_ntdsa(ldb, fsmo_role_dn, &is_us); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to confirm if our ntdsDsa is %s: %s", + ldb_dn_get_linearized(fsmo_role_dn), ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + if (!is_us) { + ret = ridalloc_poke_rid_manager(module); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Request for remote refresh of RID Set allocation failed: %s", + ldb_errstring(ldb)); + } else { + ldb_asprintf_errstring(ldb, "Remote RID Set refresh needed"); + } + talloc_free(tmp_ctx); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* grab a pool from the RID Manager object */ + ret = ridalloc_rid_manager_allocate(module, rid_manager_dn, new_pool, parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + talloc_free(tmp_ctx); + return ret; +} + + +/* allocate a RID using our RID Set + If we run out of RIDs then allocate a new pool + either locally or by contacting the RID Manager +*/ +int ridalloc_allocate_rid(struct ldb_module *module, uint32_t *rid, struct ldb_request *parent) +{ + struct ldb_context *ldb; + int ret; + struct ldb_dn *rid_set_dn; + struct ldb_result *res; + struct ldb_message *msg; + struct ridalloc_ridset_values oridset; + struct ridalloc_ridset_values nridset; + uint32_t prev_pool_lo, prev_pool_hi; + TALLOC_CTX *tmp_ctx = talloc_new(module); + + (*rid) = 0; + ldb = ldb_module_get_ctx(module); + + ret = samdb_rid_set_dn(ldb, tmp_ctx, &rid_set_dn); + if (ret == LDB_ERR_NO_SUCH_ATTRIBUTE) { + ret = ridalloc_create_own_rid_set(module, tmp_ctx, &rid_set_dn, parent); + } + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, __location__ ": No RID Set DN - %s", + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_module_search_dn(module, tmp_ctx, &res, rid_set_dn, + ridalloc_ridset_attrs, DSDB_FLAG_NEXT_MODULE, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, __location__ ": No RID Set %s", + ldb_dn_get_linearized(rid_set_dn)); + talloc_free(tmp_ctx); + return ret; + } + + ridalloc_get_ridset_values(res->msgs[0], &oridset); + if (oridset.alloc_pool == UINT64_MAX) { + ldb_asprintf_errstring(ldb, __location__ ": Bad RID Set %s", + ldb_dn_get_linearized(rid_set_dn)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + nridset = oridset; + + /* + * If we never used a pool, setup out first pool + */ + if (nridset.prev_pool == UINT64_MAX || + nridset.next_rid == UINT32_MAX) { + nridset.prev_pool = nridset.alloc_pool; + nridset.next_rid = nridset.prev_pool & 0xFFFFFFFF; + } else { + nridset.next_rid += 1; + } + + /* + * Now check if our current pool is still usable + */ + prev_pool_lo = nridset.prev_pool & 0xFFFFFFFF; + prev_pool_hi = nridset.prev_pool >> 32; + if (nridset.next_rid > prev_pool_hi) { + /* + * We need a new pool, check if we already have a new one + * Otherwise we need to get a new pool. + */ + if (nridset.alloc_pool == nridset.prev_pool) { + /* + * if we are the RID Manager, + * we can get a new pool localy. + * Otherwise we fail the operation and + * ask async for a new pool. + */ + ret = ridalloc_new_own_pool(module, &nridset.alloc_pool, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "NO RID values available: %s", + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + } + + /* + * increment the rIDUsedPool attribute + * + * Note: w2k8r2 doesn't update this attribute, + * at least if it's itself the rid master. + */ + nridset.used_pool += 1; + + /* now use the new pool */ + nridset.prev_pool = nridset.alloc_pool; + prev_pool_lo = nridset.prev_pool & 0xFFFFFFFF; + prev_pool_hi = nridset.prev_pool >> 32; + nridset.next_rid = prev_pool_lo; + } + + if (nridset.next_rid < prev_pool_lo || nridset.next_rid > prev_pool_hi) { + ldb_asprintf_errstring(ldb, __location__ ": Bad rid chosen %u from range %u-%u", + (unsigned)nridset.next_rid, + (unsigned)prev_pool_lo, + (unsigned)prev_pool_hi); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* + * if we are half-exhausted then try to get a new pool. + */ + if (nridset.next_rid > (prev_pool_hi + prev_pool_lo)/2 && + nridset.alloc_pool == nridset.prev_pool) { + /* + * if we are the RID Manager, + * we can get a new pool localy. + * Otherwise we fail the operation and + * ask async for a new pool. + */ + ret = ridalloc_new_own_pool(module, &nridset.alloc_pool, parent); + if (ret == LDB_ERR_UNWILLING_TO_PERFORM) { + ldb_reset_err_string(ldb); + ret = LDB_SUCCESS; + } + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + + /* + * update the values + */ + msg = ldb_msg_new(tmp_ctx); + if (msg == NULL) { + return ldb_module_oom(module); + } + msg->dn = rid_set_dn; + + ret = ridalloc_set_ridset_values(module, msg, + &oridset, &nridset); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_module_modify(module, msg, DSDB_FLAG_NEXT_MODULE|DSDB_FLAG_AS_SYSTEM, parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + talloc_free(tmp_ctx); + *rid = nridset.next_rid; + return LDB_SUCCESS; +} + + +/* + called by DSDB_EXTENDED_ALLOCATE_RID_POOL extended operation in samldb + + This is for the DRS server to allocate a RID Pool for another server. + + Called by another server over DRS (which calls this extended + operation), it runs on the RID Manager only. + */ +int ridalloc_allocate_rid_pool_fsmo(struct ldb_module *module, struct dsdb_fsmo_extended_op *exop, + struct ldb_request *parent) +{ + struct ldb_dn *ntds_dn, *server_dn, *machine_dn, *rid_set_dn; + struct ldb_dn *rid_manager_dn; + TALLOC_CTX *tmp_ctx = talloc_new(module); + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_result *res; + struct ldb_message *msg; + struct ridalloc_ridset_values oridset, nridset; + + ret = dsdb_module_dn_by_guid(module, tmp_ctx, &exop->destination_dsa_guid, &ntds_dn, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, __location__ ": Unable to find NTDS object for guid %s - %s\n", + GUID_string(tmp_ctx, &exop->destination_dsa_guid), ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + server_dn = ldb_dn_get_parent(tmp_ctx, ntds_dn); + if (!server_dn) { + talloc_free(tmp_ctx); + return ldb_module_oom(module); + } + + ret = dsdb_module_reference_dn(module, tmp_ctx, server_dn, "serverReference", &machine_dn, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, __location__ ": Failed to find serverReference in %s - %s", + ldb_dn_get_linearized(server_dn), ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_module_rid_manager_dn(module, tmp_ctx, &rid_manager_dn, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, __location__ ": Failed to find RID Manager object - %s", + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_module_reference_dn(module, tmp_ctx, machine_dn, "rIDSetReferences", &rid_set_dn, parent); + if (ret == LDB_ERR_NO_SUCH_ATTRIBUTE) { + ret = ridalloc_create_rid_set_ntds(module, tmp_ctx, rid_manager_dn, ntds_dn, &rid_set_dn, parent); + talloc_free(tmp_ctx); + return ret; + } + + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to find rIDSetReferences in %s - %s", + ldb_dn_get_linearized(machine_dn), ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_module_search_dn(module, tmp_ctx, &res, rid_set_dn, + ridalloc_ridset_attrs, DSDB_FLAG_NEXT_MODULE, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, __location__ ": No RID Set %s", + ldb_dn_get_linearized(rid_set_dn)); + talloc_free(tmp_ctx); + return ret; + } + + ridalloc_get_ridset_values(res->msgs[0], &oridset); + if (oridset.alloc_pool == UINT64_MAX) { + ldb_asprintf_errstring(ldb, __location__ ": Bad RID Set %s", + ldb_dn_get_linearized(rid_set_dn)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + nridset = oridset; + + if (exop->fsmo_info != 0) { + + if (nridset.alloc_pool != exop->fsmo_info) { + /* it has already been updated */ + DEBUG(2,(__location__ ": rIDAllocationPool fsmo_info mismatch - already changed (0x%llx 0x%llx)\n", + (unsigned long long)exop->fsmo_info, + (unsigned long long)nridset.alloc_pool)); + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + } + + /* grab a pool from the RID Manager object */ + ret = ridalloc_rid_manager_allocate(module, rid_manager_dn, &nridset.alloc_pool, parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + /* + * update the values + */ + msg = ldb_msg_new(tmp_ctx); + if (msg == NULL) { + return ldb_module_oom(module); + } + msg->dn = rid_set_dn; + + ret = ridalloc_set_ridset_values(module, msg, + &oridset, &nridset); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_module_modify(module, msg, DSDB_FLAG_NEXT_MODULE|DSDB_FLAG_AS_SYSTEM, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to modify RID Set object %s - %s", + ldb_dn_get_linearized(rid_set_dn), ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} diff --git a/source4/dsdb/samdb/ldb_modules/rootdse.c b/source4/dsdb/samdb/ldb_modules/rootdse.c new file mode 100644 index 0000000..865a432 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/rootdse.c @@ -0,0 +1,1796 @@ +/* + Unix SMB/CIFS implementation. + + rootDSE ldb module + + Copyright (C) Andrew Tridgell 2005 + Copyright (C) Simo Sorce 2005-2008 + Copyright (C) Matthieu Patou <mat@matws.net> 2011 + + 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/>. +*/ + +#include "includes.h" +#include <ldb.h> +#include <ldb_module.h> +#include "system/time.h" +#include "dsdb/samdb/samdb.h" +#include "version.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "libcli/security/security.h" +#include "librpc/ndr/libndr.h" +#include "auth/auth.h" +#include "param/param.h" +#include "lib/messaging/irpc.h" +#include "librpc/gen_ndr/ndr_irpc_c.h" +#include "lib/tsocket/tsocket.h" +#include "cldap_server/cldap_server.h" +#include "lib/events/events.h" + +#undef strcasecmp + +struct rootdse_private_data { + unsigned int num_controls; + char **controls; + unsigned int num_partitions; + struct ldb_dn **partitions; + bool block_anonymous; + struct tevent_context *saved_ev; + struct tevent_context *private_ev; +}; + +struct rootdse_context { + struct ldb_module *module; + struct ldb_request *req; + struct ldb_val netlogon; +}; + +/* + return 1 if a specific attribute has been requested +*/ +static int do_attribute(const char * const *attrs, const char *name) +{ + return attrs == NULL || + ldb_attr_in_list(attrs, name) || + ldb_attr_in_list(attrs, "*"); +} + +static int do_attribute_explicit(const char * const *attrs, const char *name) +{ + return attrs != NULL && ldb_attr_in_list(attrs, name); +} + + +/* + expand a DN attribute to include extended DN information if requested + */ +static int expand_dn_in_message(struct ldb_module *module, struct ldb_message *msg, + const char *attrname, struct ldb_control *edn_control, + struct ldb_request *req) +{ + struct ldb_dn *dn, *dn2; + struct ldb_val *v; + int ret; + struct ldb_request *req2; + char *dn_string; + const char *no_attrs[] = { NULL }; + struct ldb_result *res; + struct ldb_extended_dn_control *edn; + TALLOC_CTX *tmp_ctx = talloc_new(req); + struct ldb_context *ldb; + int edn_type = 0; + unsigned int i; + struct ldb_message_element *el; + + ldb = ldb_module_get_ctx(module); + + edn = talloc_get_type(edn_control->data, struct ldb_extended_dn_control); + if (edn) { + edn_type = edn->type; + } + + el = ldb_msg_find_element(msg, attrname); + if (!el || el->num_values == 0) { + return LDB_SUCCESS; + } + + for (i = 0; i < el->num_values; i++) { + v = &el->values[i]; + if (v == NULL) { + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + + dn_string = talloc_strndup(tmp_ctx, (const char *)v->data, v->length); + if (dn_string == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + res = talloc_zero(tmp_ctx, struct ldb_result); + if (res == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + dn = ldb_dn_new(tmp_ctx, ldb, dn_string); + if (dn == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + ret = ldb_build_search_req(&req2, ldb, tmp_ctx, + dn, + LDB_SCOPE_BASE, + NULL, + no_attrs, + NULL, + res, ldb_search_default_callback, + req); + LDB_REQ_SET_LOCATION(req2); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_request_add_controls(req2, DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_EXTENDED_DN); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ldb_error(ldb, ret, "Failed to add control"); + } + + ret = ldb_next_request(module, req2); + if (ret == LDB_SUCCESS) { + ret = ldb_wait(req2->handle, LDB_WAIT_ALL); + } + + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + if (!res || res->count != 1) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + dn2 = res->msgs[0]->dn; + + v->data = (uint8_t *)ldb_dn_get_extended_linearized(msg->elements, dn2, edn_type); + if (v->data == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + v->length = strlen((char *)v->data); + } + + talloc_free(tmp_ctx); + + return LDB_SUCCESS; +} + +/* + see if we are master for a FSMO role + */ +static int dsdb_module_we_are_master(struct ldb_module *module, struct ldb_dn *dn, bool *master, + struct ldb_request *parent) +{ + const char *attrs[] = { "fSMORoleOwner", NULL }; + TALLOC_CTX *tmp_ctx = talloc_new(parent); + struct ldb_result *res; + int ret; + struct ldb_dn *owner_dn; + + ret = dsdb_module_search_dn(module, tmp_ctx, &res, + dn, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_EXTENDED_DN, + parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + owner_dn = ldb_msg_find_attr_as_dn(ldb_module_get_ctx(module), + tmp_ctx, res->msgs[0], "fSMORoleOwner"); + if (!owner_dn) { + *master = false; + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + + ret = samdb_dn_is_our_ntdsa(ldb_module_get_ctx(module), dn, master); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), "Failed to confirm if our ntdsDsa is %s: %s", + ldb_dn_get_linearized(owner_dn), ldb_errstring(ldb_module_get_ctx(module))); + talloc_free(tmp_ctx); + return ret; + } + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + +/* + add dynamically generated attributes to rootDSE result +*/ +static int rootdse_add_dynamic(struct rootdse_context *ac, struct ldb_message *msg) +{ + struct ldb_context *ldb; + struct rootdse_private_data *priv = talloc_get_type(ldb_module_get_private(ac->module), struct rootdse_private_data); + const char * const *attrs = ac->req->op.search.attrs; + const char **server_sasl = NULL; + const struct dsdb_schema *schema; + int *val; + struct ldb_control *edn_control; + const char *dn_attrs[] = { + "configurationNamingContext", + "defaultNamingContext", + "rootDomainNamingContext", + "schemaNamingContext", + "serverName", + "validFSMOs", + "namingContexts", + NULL + }; + const char *guid_attrs[] = { + "dsServiceName", + NULL + }; + unsigned int i; + + ldb = ldb_module_get_ctx(ac->module); + schema = dsdb_get_schema(ldb, NULL); + + msg->dn = ldb_dn_new(msg, ldb, NULL); + + /* don't return the distinguishedName, cn and name attributes */ + ldb_msg_remove_attr(msg, "distinguishedName"); + ldb_msg_remove_attr(msg, "cn"); + ldb_msg_remove_attr(msg, "name"); + + if (do_attribute(attrs, "serverName")) { + if (ldb_msg_add_linearized_dn(msg, "serverName", + samdb_server_dn(ldb, msg)) != LDB_SUCCESS) { + goto failed; + } + } + + if (do_attribute(attrs, "dnsHostName")) { + struct ldb_result *res; + int ret; + const char *dns_attrs[] = { "dNSHostName", NULL }; + ret = dsdb_module_search_dn(ac->module, msg, &res, samdb_server_dn(ldb, msg), + dns_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM, + ac->req); + if (ret == LDB_SUCCESS) { + const char *hostname = ldb_msg_find_attr_as_string(res->msgs[0], "dNSHostName", NULL); + if (hostname != NULL) { + if (ldb_msg_add_string(msg, "dnsHostName", hostname)) { + goto failed; + } + } + } + } + + if (do_attribute(attrs, "ldapServiceName")) { + struct loadparm_context *lp_ctx + = talloc_get_type(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + char *ldap_service_name, *hostname; + + hostname = strlower_talloc(msg, lpcfg_netbios_name(lp_ctx)); + if (hostname == NULL) { + goto failed; + } + + ldap_service_name = talloc_asprintf(msg, "%s:%s$@%s", + samdb_forest_name(ldb, msg), + hostname, lpcfg_realm(lp_ctx)); + if (ldap_service_name == NULL) { + goto failed; + } + + if (ldb_msg_add_string(msg, "ldapServiceName", + ldap_service_name) != LDB_SUCCESS) { + goto failed; + } + } + + if (do_attribute(attrs, "currentTime")) { + char *timestr = ldb_timestring(msg, time(NULL)); + + if (timestr == NULL) { + goto failed; + } + + if (ldb_msg_add_steal_string( + msg, "currentTime", timestr) != LDB_SUCCESS) { + goto failed; + } + } + + if (priv && do_attribute(attrs, "supportedControl")) { + for (i = 0; i < priv->num_controls; i++) { + char *control = talloc_strdup(msg, priv->controls[i]); + if (!control) { + goto failed; + } + if (ldb_msg_add_steal_string(msg, "supportedControl", + control) != LDB_SUCCESS) { + goto failed; + } + } + } + + if (priv && do_attribute(attrs, "namingContexts")) { + for (i = 0; i < priv->num_partitions; i++) { + struct ldb_dn *dn = priv->partitions[i]; + if (ldb_msg_add_steal_string(msg, "namingContexts", + ldb_dn_alloc_linearized(msg, dn)) != LDB_SUCCESS) { + goto failed; + } + } + } + + server_sasl = talloc_get_type(ldb_get_opaque(ldb, "supportedSASLMechanisms"), + const char *); + if (server_sasl && do_attribute(attrs, "supportedSASLMechanisms")) { + for (i = 0; server_sasl && server_sasl[i]; i++) { + char *sasl_name = talloc_strdup(msg, server_sasl[i]); + if (!sasl_name) { + goto failed; + } + if (ldb_msg_add_steal_string(msg, "supportedSASLMechanisms", + sasl_name) != LDB_SUCCESS) { + goto failed; + } + } + } + + if (do_attribute(attrs, "highestCommittedUSN")) { + uint64_t seq_num; + int ret = ldb_sequence_number(ldb, LDB_SEQ_HIGHEST_SEQ, &seq_num); + if (ret == LDB_SUCCESS) { + if (samdb_msg_add_uint64(ldb, msg, msg, + "highestCommittedUSN", + seq_num) != LDB_SUCCESS) { + goto failed; + } + } + } + + if (schema && do_attribute_explicit(attrs, "dsSchemaAttrCount")) { + struct dsdb_attribute *cur; + unsigned int n = 0; + + for (cur = schema->attributes; cur; cur = cur->next) { + n++; + } + + if (samdb_msg_add_uint(ldb, msg, msg, "dsSchemaAttrCount", + n) != LDB_SUCCESS) { + goto failed; + } + } + + if (schema && do_attribute_explicit(attrs, "dsSchemaClassCount")) { + struct dsdb_class *cur; + unsigned int n = 0; + + for (cur = schema->classes; cur; cur = cur->next) { + n++; + } + + if (samdb_msg_add_uint(ldb, msg, msg, "dsSchemaClassCount", + n) != LDB_SUCCESS) { + goto failed; + } + } + + if (schema && do_attribute_explicit(attrs, "dsSchemaPrefixCount")) { + if (samdb_msg_add_uint(ldb, msg, msg, "dsSchemaPrefixCount", + schema->prefixmap->length) != LDB_SUCCESS) { + goto failed; + } + } + + if (do_attribute_explicit(attrs, "validFSMOs")) { + struct ldb_dn *dns[3]; + + dns[0] = ldb_get_schema_basedn(ldb); + dns[1] = samdb_partitions_dn(ldb, msg); + dns[2] = ldb_get_default_basedn(ldb); + + for (i=0; i<3; i++) { + bool master; + int ret = dsdb_module_we_are_master(ac->module, dns[i], &master, ac->req); + if (ret != LDB_SUCCESS) { + goto failed; + } + if (master && ldb_msg_add_fmt(msg, "validFSMOs", "%s", + ldb_dn_get_linearized(dns[i])) != LDB_SUCCESS) { + goto failed; + } + } + } + + if (do_attribute_explicit(attrs, "vendorVersion")) { + if (ldb_msg_add_fmt(msg, "vendorVersion", + "%s", SAMBA_VERSION_STRING) != LDB_SUCCESS) { + goto failed; + } + } + + if (do_attribute(attrs, "domainFunctionality")) { + if (samdb_msg_add_int(ldb, msg, msg, "domainFunctionality", + dsdb_functional_level(ldb)) != LDB_SUCCESS) { + goto failed; + } + } + + if (do_attribute(attrs, "forestFunctionality")) { + if (samdb_msg_add_int(ldb, msg, msg, "forestFunctionality", + dsdb_forest_functional_level(ldb)) != LDB_SUCCESS) { + goto failed; + } + } + + if (do_attribute(attrs, "domainControllerFunctionality") + && (val = talloc_get_type(ldb_get_opaque(ldb, "domainControllerFunctionality"), int))) { + if (samdb_msg_add_int(ldb, msg, msg, + "domainControllerFunctionality", + *val) != LDB_SUCCESS) { + goto failed; + } + } + + if (do_attribute(attrs, "isGlobalCatalogReady")) { + /* MS-ADTS 3.1.1.3.2.10 + Note, we should only return true here is we have + completed at least one synchronisation. As both + provision and vampire do a full sync, this means we + can return true is the gc bit is set in the NTDSDSA + options */ + if (ldb_msg_add_fmt(msg, "isGlobalCatalogReady", + "%s", samdb_is_gc(ldb)?"TRUE":"FALSE") != LDB_SUCCESS) { + goto failed; + } + } + + if (do_attribute_explicit(attrs, "tokenGroups")) { + /* Obtain the user's session_info */ + struct auth_session_info *session_info + = (struct auth_session_info *)ldb_get_opaque( + ldb, + DSDB_SESSION_INFO); + if (session_info && session_info->security_token) { + /* The list of groups this user is in */ + for (i = 0; i < session_info->security_token->num_sids; i++) { + if (samdb_msg_add_dom_sid(ldb, msg, msg, + "tokenGroups", + &session_info->security_token->sids[i]) != LDB_SUCCESS) { + goto failed; + } + } + } + } + + if (ac->netlogon.length > 0) { + if (ldb_msg_add_steal_value(msg, "netlogon", &ac->netlogon) != LDB_SUCCESS) { + goto failed; + } + } + + /* TODO: lots more dynamic attributes should be added here */ + + edn_control = ldb_request_get_control(ac->req, LDB_CONTROL_EXTENDED_DN_OID); + + /* convert any GUID attributes to be in the right form */ + for (i=0; guid_attrs[i]; i++) { + struct ldb_result *res; + struct ldb_message_element *el; + struct ldb_dn *attr_dn; + const char *no_attrs[] = { NULL }; + int ret; + + if (!do_attribute(attrs, guid_attrs[i])) continue; + + attr_dn = ldb_msg_find_attr_as_dn(ldb, ac->req, msg, guid_attrs[i]); + if (attr_dn == NULL) { + continue; + } + + ret = dsdb_module_search_dn(ac->module, ac->req, &res, + attr_dn, no_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SHOW_EXTENDED_DN, + ac->req); + if (ret != LDB_SUCCESS) { + DBG_WARNING("Failed to convert GUID into full DN in rootDSE for %s: %s: %s\n", + guid_attrs[i], + ldb_dn_get_extended_linearized(ac, attr_dn, 1), + ldb_errstring(ldb)); + /* + * Provide a meaninful error string but not + * confidential DB contents possibly in the + * original string + */ + ldb_asprintf_errstring(ldb, + "Failed to find full DN for %s: %s", + guid_attrs[i], + ldb_dn_get_extended_linearized(ac, attr_dn, 1)); + /* Overstamp the error code, it would confuse the caller */ + return LDB_ERR_OPERATIONS_ERROR; + } + + el = ldb_msg_find_element(msg, guid_attrs[i]); + if (el == NULL) { + return ldb_operr(ldb); + } + + talloc_steal(el->values, res->msgs[0]->dn); + if (edn_control) { + struct ldb_extended_dn_control *edn; + int edn_type = 0; + edn = talloc_get_type(edn_control->data, struct ldb_extended_dn_control); + if (edn != NULL) { + edn_type = edn->type; + } + el->values[0].data = (uint8_t *)ldb_dn_get_extended_linearized(el->values, + res->msgs[0]->dn, + edn_type); + } else { + el->values[0].data = (uint8_t *)talloc_strdup(el->values, + ldb_dn_get_linearized(res->msgs[0]->dn)); + } + if (el->values[0].data == NULL) { + return ldb_oom(ldb); + } + el->values[0].length = strlen((const char *)el->values[0].data); + } + + /* if the client sent us the EXTENDED_DN control then we need + to expand the DNs to have GUID and SID. W2K8 join relies on + this */ + if (edn_control) { + int ret; + for (i=0; dn_attrs[i]; i++) { + if (!do_attribute(attrs, dn_attrs[i])) continue; + ret = expand_dn_in_message(ac->module, msg, dn_attrs[i], + edn_control, ac->req); + if (ret != LDB_SUCCESS) { + DEBUG(0,(__location__ ": Failed to expand DN in rootDSE for %s\n", + dn_attrs[i])); + goto failed; + } + } + } + + return LDB_SUCCESS; + +failed: + return ldb_operr(ldb); +} + +/* + handle search requests +*/ + +static struct rootdse_context *rootdse_init_context(struct ldb_module *module, + struct ldb_request *req) +{ + struct ldb_context *ldb; + struct rootdse_context *ac; + + ldb = ldb_module_get_ctx(module); + + ac = talloc_zero(req, struct rootdse_context); + if (ac == NULL) { + ldb_set_errstring(ldb, "Out of Memory"); + return NULL; + } + + ac->module = module; + ac->req = req; + + return ac; +} + +static int rootdse_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct rootdse_context *ac; + int ret; + + ac = talloc_get_type(req->context, struct rootdse_context); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + /* for each record returned post-process to add any dynamic + attributes that have been asked for */ + ret = rootdse_add_dynamic(ac, ares->message); + if (ret != LDB_SUCCESS) { + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + return ldb_module_send_entry(ac->req, ares->message, ares->controls); + + case LDB_REPLY_REFERRAL: + /* should we allow the backend to return referrals in this case + * ?? */ + break; + + case LDB_REPLY_DONE: + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + talloc_free(ares); + return LDB_SUCCESS; +} + +/* + filter from controls from clients in several ways + + 1) mark our registered controls as non-critical in the request + + This is needed as clients may mark controls as critical even if + they are not needed at all in a request. For example, the centrify + client sets the SD_FLAGS control as critical on ldap modify + requests which are setting the dNSHostName attribute on the + machine account. That request doesn't need SD_FLAGS at all, but + centrify adds it on all ldap requests. + + 2) if this request is untrusted then remove any non-registered + controls that are non-critical + + This is used on ldap:// connections to prevent remote users from + setting an internal control that may be dangerous + + 3) if this request is untrusted then fail any request that includes + a critical non-registered control + */ +static int rootdse_filter_controls(struct ldb_module *module, struct ldb_request *req) +{ + unsigned int i, j; + struct rootdse_private_data *priv = talloc_get_type(ldb_module_get_private(module), struct rootdse_private_data); + bool is_untrusted; + + if (!req->controls) { + return LDB_SUCCESS; + } + + is_untrusted = ldb_req_is_untrusted(req); + + for (i=0; req->controls[i]; i++) { + bool is_registered = false; + bool is_critical = (req->controls[i]->critical != 0); + + if (req->controls[i]->oid == NULL) { + continue; + } + + if (is_untrusted || is_critical) { + for (j=0; j<priv->num_controls; j++) { + if (strcasecmp(priv->controls[j], req->controls[i]->oid) == 0) { + is_registered = true; + break; + } + } + } + + if (is_untrusted && !is_registered) { + if (!is_critical) { + /* remove it by marking the oid NULL */ + req->controls[i]->oid = NULL; + req->controls[i]->data = NULL; + req->controls[i]->critical = 0; + continue; + } + /* its a critical unregistered control - give + an error */ + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "Attempt to use critical non-registered control '%s'", + req->controls[i]->oid); + return LDB_ERR_UNSUPPORTED_CRITICAL_EXTENSION; + } + + if (!is_critical) { + continue; + } + + /* + * If the control is DIRSYNC, SORT or VLV then we keep the + * critical flag as the modules will need to act upon it. + * + * These modules have to unset the critical flag after the + * request has been seen by the correct module. + */ + if (is_registered && + strcmp(req->controls[i]->oid, + LDB_CONTROL_DIRSYNC_OID) != 0 && + strcmp(req->controls[i]->oid, + LDB_CONTROL_VLV_REQ_OID) != 0 && + strcmp(req->controls[i]->oid, + LDB_CONTROL_SERVER_SORT_OID) != 0) { + req->controls[i]->critical = 0; + } + } + + return LDB_SUCCESS; +} + +/* Ensure that anonymous users are not allowed to make anything other than rootDSE search operations */ + +static int rootdse_filter_operations(struct ldb_module *module, struct ldb_request *req) +{ + struct auth_session_info *session_info; + struct rootdse_private_data *priv = talloc_get_type(ldb_module_get_private(module), struct rootdse_private_data); + bool is_untrusted = ldb_req_is_untrusted(req); + bool is_anonymous = true; + if (is_untrusted == false) { + return LDB_SUCCESS; + } + + session_info = (struct auth_session_info *)ldb_get_opaque( + ldb_module_get_ctx(module), + DSDB_SESSION_INFO); + if (session_info) { + is_anonymous = security_token_is_anonymous(session_info->security_token); + } + + if (is_anonymous == false || (priv && priv->block_anonymous == false)) { + return LDB_SUCCESS; + } + + if (req->operation == LDB_SEARCH) { + if (req->op.search.scope == LDB_SCOPE_BASE && ldb_dn_is_null(req->op.search.base)) { + return LDB_SUCCESS; + } + } + ldb_set_errstring(ldb_module_get_ctx(module), "Operation unavailable without authentication"); + return LDB_ERR_OPERATIONS_ERROR; +} + +static int rootdse_handle_netlogon(struct rootdse_context *ac) +{ + struct ldb_context *ldb; + struct ldb_parse_tree *tree; + struct loadparm_context *lp_ctx; + struct tsocket_address *src_addr; + TALLOC_CTX *tmp_ctx = talloc_new(ac->req); + const char *domain, *host, *user, *domain_guid; + char *src_addr_s = NULL; + struct dom_sid *domain_sid; + int acct_control = -1; + int version = -1; + NTSTATUS status; + struct netlogon_samlogon_response netlogon; + int ret = LDB_ERR_OPERATIONS_ERROR; + + ldb = ldb_module_get_ctx(ac->module); + tree = ac->req->op.search.tree; + lp_ctx = talloc_get_type(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + src_addr = talloc_get_type(ldb_get_opaque(ldb, "remoteAddress"), + struct tsocket_address); + if (src_addr) { + src_addr_s = tsocket_address_inet_addr_string(src_addr, + tmp_ctx); + } + + status = parse_netlogon_request(tree, lp_ctx, tmp_ctx, + &domain, &host, &user, &domain_guid, + &domain_sid, &acct_control, &version); + if (!NT_STATUS_IS_OK(status)) { + goto failed; + } + + status = fill_netlogon_samlogon_response(ldb, tmp_ctx, + domain, NULL, domain_sid, + domain_guid, + user, acct_control, + src_addr_s, + version, lp_ctx, + &netlogon, false); + if (!NT_STATUS_IS_OK(status)) { + goto failed; + } + + status = push_netlogon_samlogon_response(&ac->netlogon, ac, &netlogon); + if (!NT_STATUS_IS_OK(status)) { + goto failed; + } + + ret = LDB_SUCCESS; +failed: + talloc_free(tmp_ctx); + return ret; +} + +static int rootdse_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct rootdse_context *ac; + struct ldb_request *down_req; + int ret; + + ret = rootdse_filter_operations(module, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = rootdse_filter_controls(module, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ldb = ldb_module_get_ctx(module); + + /* see if its for the rootDSE - only a base search on the "" DN qualifies */ + if (!(req->op.search.scope == LDB_SCOPE_BASE && ldb_dn_is_null(req->op.search.base))) { + /* Otherwise, pass down to the rest of the stack */ + return ldb_next_request(module, req); + } + + ac = rootdse_init_context(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + if (do_attribute_explicit(req->op.search.attrs, "netlogon")) { + ret = rootdse_handle_netlogon(ac); + /* We have to return an empty result, so don't forward `ret' */ + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, LDB_SUCCESS); + } + } + + /* in our db we store the rootDSE with a DN of @ROOTDSE */ + ret = ldb_build_search_req(&down_req, ldb, ac, + ldb_dn_new(ac, ldb, "@ROOTDSE"), + LDB_SCOPE_BASE, + NULL, + req->op.search.attrs, + NULL,/* for now skip the controls from the client */ + ac, rootdse_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, down_req); +} + +static struct rootdse_private_data *rootdse_get_private_data(struct ldb_module *module) +{ + void *priv = ldb_module_get_private(module); + struct rootdse_private_data *data = NULL; + struct ldb_context *ldb + = ldb_module_get_ctx(module); + + if (priv != NULL) { + data = talloc_get_type_abort(priv, + struct rootdse_private_data); + } + + if (data != NULL) { + return data; + } + + data = talloc_zero(module, struct rootdse_private_data); + if (data == NULL) { + return NULL; + } + + data->num_controls = 0; + data->controls = NULL; + data->num_partitions = 0; + data->partitions = NULL; + data->block_anonymous = true; + + ldb_module_set_private(module, data); + + ldb_set_default_dns(ldb); + + return data; +} + + +static int rootdse_register_control(struct ldb_module *module, struct ldb_request *req) +{ + struct rootdse_private_data *priv = + rootdse_get_private_data(module); + char **list; + + if (priv == NULL) { + return ldb_module_oom(module); + } + + list = talloc_realloc(priv, priv->controls, char *, priv->num_controls + 1); + if (!list) { + return ldb_oom(ldb_module_get_ctx(module)); + } + + list[priv->num_controls] = talloc_strdup(list, req->op.reg_control.oid); + if (!list[priv->num_controls]) { + return ldb_oom(ldb_module_get_ctx(module)); + } + + priv->num_controls += 1; + priv->controls = list; + + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS); +} + +static int rootdse_register_partition(struct ldb_module *module, struct ldb_request *req) +{ + struct rootdse_private_data *priv = + rootdse_get_private_data(module); + struct ldb_dn **list; + + if (priv == NULL) { + return ldb_module_oom(module); + } + + list = talloc_realloc(priv, priv->partitions, struct ldb_dn *, priv->num_partitions + 1); + if (!list) { + return ldb_oom(ldb_module_get_ctx(module)); + } + + list[priv->num_partitions] = ldb_dn_copy(list, req->op.reg_partition.dn); + if (!list[priv->num_partitions]) { + return ldb_operr(ldb_module_get_ctx(module)); + } + + priv->num_partitions += 1; + priv->partitions = list; + + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS); +} + + +static int rootdse_request(struct ldb_module *module, struct ldb_request *req) +{ + switch (req->operation) { + + case LDB_REQ_REGISTER_CONTROL: + return rootdse_register_control(module, req); + case LDB_REQ_REGISTER_PARTITION: + return rootdse_register_partition(module, req); + + default: + break; + } + return ldb_next_request(module, req); +} + +static int rootdse_init(struct ldb_module *module) +{ + int ret; + struct ldb_result *res; + const char *attrs[] = { "msDS-Behavior-Version", NULL }; + const char *ds_attrs[] = { "dsServiceName", NULL }; + TALLOC_CTX *mem_ctx; + + struct ldb_context *ldb + = ldb_module_get_ctx(module); + + struct rootdse_private_data *data + = rootdse_get_private_data(module); + + if (data == NULL) { + return ldb_module_oom(module); + } + + ret = ldb_next_init(module); + + if (ret != LDB_SUCCESS) { + return ret; + } + + mem_ctx = talloc_new(data); + if (!mem_ctx) { + return ldb_oom(ldb); + } + + /* Now that the partitions are set up, do a search for: + - domainControllerFunctionality + - domainFunctionality + - forestFunctionality + + Then stuff these values into an opaque + */ + ret = dsdb_module_search(module, mem_ctx, &res, + ldb_get_default_basedn(ldb), + LDB_SCOPE_BASE, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM, + NULL, NULL); + if (ret == LDB_SUCCESS && res->count == 1) { + int domain_behaviour_version + = ldb_msg_find_attr_as_int(res->msgs[0], + "msDS-Behavior-Version", -1); + if (domain_behaviour_version != -1) { + int *val = talloc(ldb, int); + if (!val) { + talloc_free(mem_ctx); + return ldb_oom(ldb); + } + *val = domain_behaviour_version; + ret = ldb_set_opaque(ldb, "domainFunctionality", val); + if (ret != LDB_SUCCESS) { + talloc_free(mem_ctx); + return ret; + } + } + } + + ret = dsdb_module_search(module, mem_ctx, &res, + samdb_partitions_dn(ldb, mem_ctx), + LDB_SCOPE_BASE, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM, + NULL, NULL); + if (ret == LDB_SUCCESS && res->count == 1) { + int forest_behaviour_version + = ldb_msg_find_attr_as_int(res->msgs[0], + "msDS-Behavior-Version", -1); + if (forest_behaviour_version != -1) { + int *val = talloc(ldb, int); + if (!val) { + talloc_free(mem_ctx); + return ldb_oom(ldb); + } + *val = forest_behaviour_version; + ret = ldb_set_opaque(ldb, "forestFunctionality", val); + if (ret != LDB_SUCCESS) { + talloc_free(mem_ctx); + return ret; + } + } + } + + /* For now, our own server's location in the DB is recorded in + * the @ROOTDSE record */ + ret = dsdb_module_search(module, mem_ctx, &res, + ldb_dn_new(mem_ctx, ldb, "@ROOTDSE"), + LDB_SCOPE_BASE, ds_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM, + NULL, NULL); + if (ret == LDB_SUCCESS && res->count == 1) { + struct ldb_dn *ds_dn + = ldb_msg_find_attr_as_dn(ldb, mem_ctx, res->msgs[0], + "dsServiceName"); + if (ds_dn) { + ret = dsdb_module_search(module, mem_ctx, &res, ds_dn, + LDB_SCOPE_BASE, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM, + NULL, NULL); + if (ret == LDB_SUCCESS && res->count == 1) { + int domain_controller_behaviour_version + = ldb_msg_find_attr_as_int(res->msgs[0], + "msDS-Behavior-Version", -1); + if (domain_controller_behaviour_version != -1) { + int *val = talloc(ldb, int); + if (!val) { + talloc_free(mem_ctx); + return ldb_oom(ldb); + } + *val = domain_controller_behaviour_version; + ret = ldb_set_opaque(ldb, + "domainControllerFunctionality", val); + if (ret != LDB_SUCCESS) { + talloc_free(mem_ctx); + return ret; + } + } + } + } + } + + data->block_anonymous = dsdb_block_anonymous_ops(module, NULL); + + talloc_free(mem_ctx); + + return LDB_SUCCESS; +} + +/* + * This function gets the string SCOPE_DN:OPTIONAL_FEATURE_GUID and parse it + * to a DN and a GUID object + */ +static int get_optional_feature_dn_guid(struct ldb_request *req, struct ldb_context *ldb, + TALLOC_CTX *mem_ctx, + struct ldb_dn **op_feature_scope_dn, + struct GUID *op_feature_guid) +{ + const struct ldb_message *msg = req->op.mod.message; + const char *ldb_val_str; + char *dn, *guid; + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + NTSTATUS status; + + ldb_val_str = ldb_msg_find_attr_as_string(msg, "enableOptionalFeature", NULL); + if (!ldb_val_str) { + ldb_set_errstring(ldb, + "rootdse: unable to find 'enableOptionalFeature'!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + guid = strchr(ldb_val_str, ':'); + if (!guid) { + ldb_set_errstring(ldb, + "rootdse: unable to find GUID in 'enableOptionalFeature'!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + status = GUID_from_string(guid+1, op_feature_guid); + if (!NT_STATUS_IS_OK(status)) { + ldb_set_errstring(ldb, + "rootdse: bad GUID in 'enableOptionalFeature'!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + dn = talloc_strndup(tmp_ctx, ldb_val_str, guid-ldb_val_str); + if (!dn) { + ldb_set_errstring(ldb, + "rootdse: bad DN in 'enableOptionalFeature'!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + *op_feature_scope_dn = ldb_dn_new(mem_ctx, ldb, dn); + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + +/* + * This function gets the OPTIONAL_FEATURE_GUID and looks for the optional feature + * ldb_message object. + */ +static int dsdb_find_optional_feature(struct ldb_module *module, struct ldb_context *ldb, + TALLOC_CTX *mem_ctx, struct GUID op_feature_guid, struct ldb_message **msg, + struct ldb_request *parent) +{ + struct ldb_result *res; + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + int ret; + + ret = dsdb_module_search(module, tmp_ctx, &res, NULL, LDB_SCOPE_SUBTREE, + NULL, + DSDB_FLAG_NEXT_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_SEARCH_SEARCH_ALL_PARTITIONS, + parent, + "(&(objectClass=msDS-OptionalFeature)" + "(msDS-OptionalFeatureGUID=%s))",GUID_string(tmp_ctx, &op_feature_guid)); + + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + if (res->count == 0) { + talloc_free(tmp_ctx); + return LDB_ERR_NO_SUCH_OBJECT; + } + if (res->count != 1) { + ldb_asprintf_errstring(ldb, + "More than one object found matching optional feature GUID %s\n", + GUID_string(tmp_ctx, &op_feature_guid)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + *msg = talloc_steal(mem_ctx, res->msgs[0]); + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + +static int rootdse_enable_recycle_bin(struct ldb_module *module,struct ldb_context *ldb, + TALLOC_CTX *mem_ctx, struct ldb_dn *op_feature_scope_dn, + struct ldb_message *op_feature_msg, struct ldb_request *parent) +{ + int ret; + const int domain_func_level = dsdb_functional_level(ldb); + struct ldb_dn *ntds_settings_dn; + TALLOC_CTX *tmp_ctx; + unsigned int el_count = 0; + struct ldb_message *msg; + + ret = ldb_msg_find_attr_as_int(op_feature_msg, "msDS-RequiredForestBehaviorVersion", 0); + if (domain_func_level < ret){ + ldb_asprintf_errstring(ldb, + "rootdse_enable_recycle_bin: Domain functional level must be at least %d\n", + ret); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + tmp_ctx = talloc_new(mem_ctx); + ntds_settings_dn = samdb_ntds_settings_dn(ldb, tmp_ctx); + if (!ntds_settings_dn) { + talloc_free(tmp_ctx); + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, "Failed to find NTDS settings DN"); + } + + ntds_settings_dn = ldb_dn_copy(tmp_ctx, ntds_settings_dn); + if (!ntds_settings_dn) { + talloc_free(tmp_ctx); + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, "Failed to copy NTDS settings DN"); + } + + msg = ldb_msg_new(tmp_ctx); + if (msg == NULL) { + talloc_free(tmp_ctx); + return ldb_module_oom(module); + } + msg->dn = ntds_settings_dn; + + ldb_msg_add_linearized_dn(msg, "msDS-EnabledFeature", op_feature_msg->dn); + msg->elements[el_count++].flags = LDB_FLAG_MOD_ADD; + + ret = dsdb_module_modify(module, msg, DSDB_FLAG_NEXT_MODULE, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "rootdse_enable_recycle_bin: Failed to modify object %s - %s", + ldb_dn_get_linearized(ntds_settings_dn), + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + msg->dn = op_feature_scope_dn; + ret = dsdb_module_modify(module, msg, DSDB_FLAG_NEXT_MODULE, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "rootdse_enable_recycle_bin: Failed to modify object %s - %s", + ldb_dn_get_linearized(op_feature_scope_dn), + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + return LDB_SUCCESS; +} + +static int rootdse_enableoptionalfeature(struct ldb_module *module, struct ldb_request *req) +{ + /* + steps: + - check for system (only system can enable features) + - extract GUID from the request + - find the feature object + - check functional level, must be at least msDS-RequiredForestBehaviorVersion + - check if it is already enabled (if enabled return LDAP_ATTRIBUTE_OR_VALUE_EXISTS) - probably not needed, just return error from the add/modify + - add/modify objects (see ntdsconnection code for an example) + */ + + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct GUID op_feature_guid; + struct ldb_dn *op_feature_scope_dn; + struct ldb_message *op_feature_msg; + struct auth_session_info *session_info = + (struct auth_session_info *)ldb_get_opaque( + ldb, + DSDB_SESSION_INFO); + TALLOC_CTX *tmp_ctx = talloc_new(ldb); + int ret; + const char *guid_string; + + if (security_session_user_level(session_info, NULL) != SECURITY_SYSTEM) { + ldb_set_errstring(ldb, "rootdse: Insufficient rights for enableoptionalfeature"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + ret = get_optional_feature_dn_guid(req, ldb, tmp_ctx, &op_feature_scope_dn, &op_feature_guid); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + guid_string = GUID_string(tmp_ctx, &op_feature_guid); + if (!guid_string) { + ldb_set_errstring(ldb, "rootdse: bad optional feature GUID"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + ret = dsdb_find_optional_feature(module, ldb, tmp_ctx, op_feature_guid, &op_feature_msg, req); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "rootdse: unable to find optional feature for %s - %s", + guid_string, ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + if (strcasecmp(DS_GUID_FEATURE_RECYCLE_BIN, guid_string) == 0) { + ret = rootdse_enable_recycle_bin(module, ldb, + tmp_ctx, op_feature_scope_dn, + op_feature_msg, req); + } else { + ldb_asprintf_errstring(ldb, + "rootdse: unknown optional feature %s", + guid_string); + talloc_free(tmp_ctx); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "rootdse: failed to set optional feature for %s - %s", + guid_string, ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + talloc_free(tmp_ctx); + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS);; +} + +static int rootdse_schemaupdatenow(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_result *ext_res; + int ret; + struct ldb_dn *schema_dn; + + schema_dn = ldb_get_schema_basedn(ldb); + if (!schema_dn) { + ldb_reset_err_string(ldb); + ldb_debug(ldb, LDB_DEBUG_WARNING, + "rootdse_modify: no schema dn present: (skip ldb_extended call)\n"); + return ldb_next_request(module, req); + } + + /* + * schemaUpdateNow has been requested. Allow this to refresh the schema + * even if we're currently in the middle of a transaction + */ + ret = ldb_set_opaque(ldb, "dsdb_schema_refresh_expected", (void *)1); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + ret = ldb_extended(ldb, DSDB_EXTENDED_SCHEMA_UPDATE_NOW_OID, schema_dn, &ext_res); + if (ret != LDB_SUCCESS) { + ldb_set_opaque(ldb, "dsdb_schema_refresh_expected", (void *)0); + return ldb_operr(ldb); + } + + talloc_free(ext_res); + + ret = ldb_set_opaque(ldb, "dsdb_schema_refresh_expected", (void *)0); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + return ldb_module_done(req, NULL, NULL, ret); +} + +static int rootdse_schemaupgradeinprogress(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + int ret = LDB_SUCCESS; + struct ldb_dn *schema_dn; + + schema_dn = ldb_get_schema_basedn(ldb); + if (!schema_dn) { + ldb_reset_err_string(ldb); + ldb_debug(ldb, LDB_DEBUG_WARNING, + "rootdse_modify: no schema dn present: (skip ldb_extended call)\n"); + return ldb_next_request(module, req); + } + + /* FIXME we have to do something in order to relax constraints for DRS + * setting schemaUpgradeInProgress cause the fschemaUpgradeInProgress + * in all LDAP connection (2K3/2K3R2) or in the current connection (2K8 and +) + * to be set to true. + */ + + /* from 5.113 LDAPConnections in DRSR.pdf + * fschemaUpgradeInProgress: A Boolean that specifies certain constraint + * validations are skipped when adding, updating, or removing directory + * objects on the opened connection. The skipped constraint validations + * are documented in the applicable constraint sections in [MS-ADTS]. + */ + return ldb_module_done(req, NULL, NULL, ret); +} + +static int rootdse_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + int ret; + + ret = rootdse_filter_operations(module, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = rootdse_filter_controls(module, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + If dn is not "" we should let it pass through + */ + if (!ldb_dn_is_null(req->op.add.message->dn)) { + return ldb_next_request(module, req); + } + + ldb_set_errstring(ldb, "rootdse_add: you cannot add a new rootdse entry!"); + return LDB_ERR_NAMING_VIOLATION; +} + +static int rootdse_start_trans(struct ldb_module *module) +{ + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct rootdse_private_data *data = talloc_get_type_abort(ldb_module_get_private(module), + struct rootdse_private_data); + ret = ldb_next_start_trans(module); + if (ret == LDB_SUCCESS) { + if (data->private_ev != NULL) { + return ldb_operr(ldb); + } + data->private_ev = s4_event_context_init(data); + if (data->private_ev == NULL) { + return ldb_operr(ldb); + } + data->saved_ev = ldb_get_event_context(ldb); + ldb_set_event_context(ldb, data->private_ev); + } + return ret; +} + +static int rootdse_end_trans(struct ldb_module *module) +{ + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct rootdse_private_data *data = talloc_get_type_abort(ldb_module_get_private(module), + struct rootdse_private_data); + ret = ldb_next_end_trans(module); + if (data->saved_ev == NULL) { + return ldb_operr(ldb); + } + + if (data->private_ev != ldb_get_event_context(ldb)) { + return ldb_operr(ldb); + } + ldb_set_event_context(ldb, data->saved_ev); + data->saved_ev = NULL; + TALLOC_FREE(data->private_ev); + return ret; +} + +static int rootdse_del_trans(struct ldb_module *module) +{ + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct rootdse_private_data *data = talloc_get_type_abort(ldb_module_get_private(module), + struct rootdse_private_data); + ret = ldb_next_del_trans(module); + if (data->saved_ev == NULL) { + return ldb_operr(ldb); + } + + if (data->private_ev != ldb_get_event_context(ldb)) { + return ldb_operr(ldb); + } + ldb_set_event_context(ldb, data->saved_ev); + data->saved_ev = NULL; + TALLOC_FREE(data->private_ev); + return ret; +} + +struct fsmo_transfer_state { + struct ldb_context *ldb; + struct ldb_request *req; + struct ldb_module *module; +}; + +/* + called when a FSMO transfer operation has completed + */ +static void rootdse_fsmo_transfer_callback(struct tevent_req *treq) +{ + struct fsmo_transfer_state *fsmo = tevent_req_callback_data(treq, struct fsmo_transfer_state); + NTSTATUS status; + WERROR werr; + int ret; + struct ldb_request *req = fsmo->req; + struct ldb_context *ldb = fsmo->ldb; + struct ldb_module *module = fsmo->module; + + status = dcerpc_drepl_takeFSMORole_recv(treq, fsmo, &werr); + talloc_free(fsmo); + if (!NT_STATUS_IS_OK(status)) { + ldb_asprintf_errstring(ldb, "Failed FSMO transfer: %s", nt_errstr(status)); + /* + * Now that it is failed, start the transaction up + * again so the wrappers can close it without additional error + */ + rootdse_start_trans(module); + ldb_module_done(req, NULL, NULL, LDB_ERR_UNAVAILABLE); + return; + } + if (!W_ERROR_IS_OK(werr)) { + ldb_asprintf_errstring(ldb, "Failed FSMO transfer: %s", win_errstr(werr)); + /* + * Now that it is failed, start the transaction up + * again so the wrappers can close it without additional error + */ + rootdse_start_trans(module); + ldb_module_done(req, NULL, NULL, LDB_ERR_UNAVAILABLE); + return; + } + + /* + * Now that it is done, start the transaction up again so the + * wrappers can close it without error + */ + ret = rootdse_start_trans(module); + ldb_module_done(req, NULL, NULL, ret); +} + +static int rootdse_become_master(struct ldb_module *module, + struct ldb_request *req, + enum drepl_role_master role) +{ + struct imessaging_context *msg; + struct ldb_context *ldb = ldb_module_get_ctx(module); + TALLOC_CTX *tmp_ctx = talloc_new(req); + struct loadparm_context *lp_ctx = ldb_get_opaque(ldb, "loadparm"); + bool am_rodc; + struct dcerpc_binding_handle *irpc_handle; + int ret; + struct auth_session_info *session_info; + enum security_user_level level; + struct fsmo_transfer_state *fsmo; + struct tevent_req *treq; + + session_info = (struct auth_session_info *)ldb_get_opaque( + ldb_module_get_ctx(module), + DSDB_SESSION_INFO); + level = security_session_user_level(session_info, NULL); + if (level < SECURITY_ADMINISTRATOR) { + return ldb_error(ldb, LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS, "Denied rootDSE modify for non-administrator"); + } + + ret = samdb_rodc(ldb, &am_rodc); + if (ret != LDB_SUCCESS) { + return ldb_error(ldb, ret, "Could not determine if server is RODC."); + } + + if (am_rodc) { + return ldb_error(ldb, LDB_ERR_UNWILLING_TO_PERFORM, + "RODC cannot become a role master."); + } + + /* + * We always delete the transaction, not commit it, because + * this gives the least surprise to this surprising action (as + * we will never record anything done to this point + */ + rootdse_del_trans(module); + + /* + * We must use the global event loop to run this IRPC in + * single process mode + */ + ldb_handle_use_global_event_context(req->handle); + + msg = imessaging_client_init(tmp_ctx, lp_ctx, + ldb_get_event_context(ldb)); + if (!msg) { + ldb_asprintf_errstring(ldb, "Failed to generate client messaging context in %s", lpcfg_imessaging_path(tmp_ctx, lp_ctx)); + return LDB_ERR_OPERATIONS_ERROR; + } + irpc_handle = irpc_binding_handle_by_name(tmp_ctx, msg, + "dreplsrv", + &ndr_table_irpc); + if (irpc_handle == NULL) { + return ldb_oom(ldb); + } + fsmo = talloc_zero(req, struct fsmo_transfer_state); + if (fsmo == NULL) { + return ldb_oom(ldb); + } + fsmo->ldb = ldb; + fsmo->req = req; + fsmo->module = module; + + /* + * we send the call asynchronously, as the ldap client is + * expecting to get an error back if the role transfer fails + * + * We need more than the default 10 seconds IRPC allows, so + * set a longer timeout (default ldb timeout is 300 seconds). + * We send an async reply when we are done. + * + * We are the first module, so don't bother working out how + * long we have spent so far. + */ + dcerpc_binding_handle_set_timeout(irpc_handle, req->timeout); + + treq = dcerpc_drepl_takeFSMORole_send(req, ldb_get_event_context(ldb), irpc_handle, role); + if (treq == NULL) { + return ldb_oom(ldb); + } + + tevent_req_set_callback(treq, rootdse_fsmo_transfer_callback, fsmo); + return LDB_SUCCESS; +} + +static int rootdse_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + int ret; + + ret = rootdse_filter_operations(module, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = rootdse_filter_controls(module, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + If dn is not "" we should let it pass through + */ + if (!ldb_dn_is_null(req->op.mod.message->dn)) { + return ldb_next_request(module, req); + } + + /* + dn is empty so check for schemaUpdateNow attribute + "The type of modification and values specified in the LDAP modify operation do not matter." MSDN + */ + if (ldb_msg_find_element(req->op.mod.message, "schemaUpdateNow")) { + return rootdse_schemaupdatenow(module, req); + } + if (ldb_msg_find_element(req->op.mod.message, "becomeDomainMaster")) { + return rootdse_become_master(module, req, DREPL_NAMING_MASTER); + } + if (ldb_msg_find_element(req->op.mod.message, "becomeInfrastructureMaster")) { + return rootdse_become_master(module, req, DREPL_INFRASTRUCTURE_MASTER); + } + if (ldb_msg_find_element(req->op.mod.message, "becomeRidMaster")) { + return rootdse_become_master(module, req, DREPL_RID_MASTER); + } + if (ldb_msg_find_element(req->op.mod.message, "becomeSchemaMaster")) { + return rootdse_become_master(module, req, DREPL_SCHEMA_MASTER); + } + if (ldb_msg_find_element(req->op.mod.message, "becomePdc")) { + return rootdse_become_master(module, req, DREPL_PDC_MASTER); + } + if (ldb_msg_find_element(req->op.mod.message, "enableOptionalFeature")) { + return rootdse_enableoptionalfeature(module, req); + } + if (ldb_msg_find_element(req->op.mod.message, "schemaUpgradeInProgress")) { + return rootdse_schemaupgradeinprogress(module, req); + } + + ldb_set_errstring(ldb, "rootdse_modify: unknown attribute to change!"); + return LDB_ERR_UNWILLING_TO_PERFORM; +} + +static int rootdse_rename(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + int ret; + + ret = rootdse_filter_operations(module, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = rootdse_filter_controls(module, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + If dn is not "" we should let it pass through + */ + if (!ldb_dn_is_null(req->op.rename.olddn)) { + return ldb_next_request(module, req); + } + + ldb_set_errstring(ldb, "rootdse_remove: you cannot rename the rootdse entry!"); + return LDB_ERR_NO_SUCH_OBJECT; +} + +static int rootdse_delete(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + int ret; + + ret = rootdse_filter_operations(module, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = rootdse_filter_controls(module, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + If dn is not "" we should let it pass through + */ + if (!ldb_dn_is_null(req->op.del.dn)) { + return ldb_next_request(module, req); + } + + ldb_set_errstring(ldb, "rootdse_remove: you cannot delete the rootdse entry!"); + return LDB_ERR_NO_SUCH_OBJECT; +} + +static int rootdse_extended(struct ldb_module *module, struct ldb_request *req) +{ + int ret; + + ret = rootdse_filter_operations(module, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = rootdse_filter_controls(module, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, req); +} + +static const struct ldb_module_ops ldb_rootdse_module_ops = { + .name = "rootdse", + .init_context = rootdse_init, + .search = rootdse_search, + .request = rootdse_request, + .add = rootdse_add, + .modify = rootdse_modify, + .rename = rootdse_rename, + .extended = rootdse_extended, + .del = rootdse_delete, + .start_transaction = rootdse_start_trans, + .end_transaction = rootdse_end_trans, + .del_transaction = rootdse_del_trans +}; + +int ldb_rootdse_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_rootdse_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/samba3sam.c b/source4/dsdb/samdb/ldb_modules/samba3sam.c new file mode 100644 index 0000000..ebf25ac --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/samba3sam.c @@ -0,0 +1,977 @@ +/* + ldb database library - Samba3 SAM compatibility backend + + Copyright (C) Jelmer Vernooij 2005 + Copyright (C) Martin Kuehl <mkhl@samba.org> 2006 +*/ + +#include "includes.h" +#include "ldb_module.h" +#include "ldb/ldb_map/ldb_map.h" +#include "system/passwd.h" + +#include "librpc/gen_ndr/ndr_security.h" +#include "librpc/gen_ndr/ndr_samr.h" +#include "librpc/ndr/libndr.h" +#include "libcli/security/security.h" +#include "lib/samba3/samba3.h" + +/* + * sambaSID -> member (dn!) + * sambaSIDList -> member (dn!) + * sambaDomainName -> name + * sambaTrustPassword + * sambaUnixIdPool + * sambaIdmapEntry + * sambaSidEntry + * sambaAcctFlags -> systemFlags ? + * sambaPasswordHistory -> ntPwdHistory*/ + +/* Not necessary: + * sambaConfig + * sambaShare + * sambaConfigOption + * sambaNextGroupRid + * sambaNextUserRid + * sambaAlgorithmicRidBase + */ + +/* Not in Samba4: + * sambaKickoffTime + * sambaPwdCanChange + * sambaPwdMustChange + * sambaHomePath + * sambaHomeDrive + * sambaLogonScript + * sambaProfilePath + * sambaUserWorkstations + * sambaMungedDial + * sambaLogonHours */ + +/* In Samba4 but not in Samba3: +*/ + +/* From a sambaPrimaryGroupSID, generate a primaryGroupID (integer) attribute */ +static struct ldb_message_element *generate_primaryGroupID(struct ldb_module *module, TALLOC_CTX *ctx, const char *local_attr, const struct ldb_message *remote) +{ + struct ldb_message_element *el; + const char *sid = ldb_msg_find_attr_as_string(remote, "sambaPrimaryGroupSID", NULL); + const char *p; + + if (!sid) + return NULL; + + p = strrchr(sid, '-'); + if (!p) + return NULL; + + el = talloc_zero(ctx, struct ldb_message_element); + el->name = talloc_strdup(ctx, "primaryGroupID"); + el->num_values = 1; + el->values = talloc_array(ctx, struct ldb_val, 1); + el->values[0].data = (uint8_t *)talloc_strdup(el->values, p+1); + el->values[0].length = strlen((char *)el->values[0].data); + + return el; +} + +static void generate_sambaPrimaryGroupSID(struct ldb_module *module, const char *local_attr, const struct ldb_message *local, struct ldb_message *remote_mp, struct ldb_message *remote_fb) +{ + const struct ldb_val *sidval; + char *sidstring; + struct dom_sid *sid; + enum ndr_err_code ndr_err; + + /* We need the domain, so we get it from the objectSid that we hope is here... */ + sidval = ldb_msg_find_ldb_val(local, "objectSid"); + + if (!sidval) + return; /* Sorry, no SID today.. */ + + sid = talloc(remote_mp, struct dom_sid); + if (sid == NULL) { + return; + } + + ndr_err = ndr_pull_struct_blob(sidval, sid, sid, (ndr_pull_flags_fn_t)ndr_pull_dom_sid); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + talloc_free(sid); + return; + } + + if (!ldb_msg_find_ldb_val(local, "primaryGroupID")) + return; /* Sorry, no SID today.. */ + + sid->num_auths--; + + sidstring = dom_sid_string(remote_mp, sid); + talloc_free(sid); + ldb_msg_add_fmt(remote_mp, "sambaPrimaryGroupSID", "%s-%u", sidstring, + ldb_msg_find_attr_as_uint(local, "primaryGroupID", 0)); + talloc_free(sidstring); +} + +/* Just copy the old value. */ +static struct ldb_val convert_uid_samaccount(struct ldb_module *module, TALLOC_CTX *ctx, const struct ldb_val *val) +{ + struct ldb_val out = data_blob(NULL, 0); + out = ldb_val_dup(ctx, val); + + return out; +} + +static struct ldb_val lookup_homedir(struct ldb_module *module, TALLOC_CTX *ctx, const struct ldb_val *val) +{ + struct ldb_context *ldb; + struct passwd *pwd; + struct ldb_val retval; + + ldb = ldb_module_get_ctx(module); + + pwd = getpwnam((char *)val->data); + + if (!pwd) { + ldb_debug(ldb, LDB_DEBUG_WARNING, "Unable to lookup '%s' in passwd", (char *)val->data); + return *talloc_zero(ctx, struct ldb_val); + } + + retval.data = (uint8_t *)talloc_strdup(ctx, pwd->pw_dir); + retval.length = strlen((char *)retval.data); + + return retval; +} + +static struct ldb_val lookup_gid(struct ldb_module *module, TALLOC_CTX *ctx, const struct ldb_val *val) +{ + struct passwd *pwd; + struct ldb_val retval; + + pwd = getpwnam((char *)val->data); + + if (!pwd) { + return *talloc_zero(ctx, struct ldb_val); + } + + /* "pw_gid" is per POSIX definition "unsigned". + * But write it out as "signed" for LDAP compliance. */ + retval.data = (uint8_t *)talloc_asprintf(ctx, "%d", (int) pwd->pw_gid); + retval.length = strlen((char *)retval.data); + + return retval; +} + +static struct ldb_val lookup_uid(struct ldb_module *module, TALLOC_CTX *ctx, const struct ldb_val *val) +{ + struct passwd *pwd; + struct ldb_val retval; + + pwd = getpwnam((char *)val->data); + + if (!pwd) { + return *talloc_zero(ctx, struct ldb_val); + } + + /* "pw_uid" is per POSIX definition "unsigned". + * But write it out as "signed" for LDAP compliance. */ + retval.data = (uint8_t *)talloc_asprintf(ctx, "%d", (int) pwd->pw_uid); + retval.length = strlen((char *)retval.data); + + return retval; +} + +/* Encode a sambaSID to an objectSid. */ +static struct ldb_val encode_sid(struct ldb_module *module, TALLOC_CTX *ctx, const struct ldb_val *val) +{ + struct ldb_val out = data_blob(NULL, 0); + struct dom_sid *sid; + enum ndr_err_code ndr_err; + + sid = dom_sid_parse_talloc(ctx, (char *)val->data); + if (sid == NULL) { + return out; + } + + ndr_err = ndr_push_struct_blob(&out, ctx, + sid, (ndr_push_flags_fn_t)ndr_push_dom_sid); + talloc_free(sid); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + return out; + } + + return out; +} + +/* Decode an objectSid to a sambaSID. */ +static struct ldb_val decode_sid(struct ldb_module *module, TALLOC_CTX *ctx, const struct ldb_val *val) +{ + struct ldb_val out = data_blob(NULL, 0); + struct dom_sid *sid; + enum ndr_err_code ndr_err; + + sid = talloc(ctx, struct dom_sid); + if (sid == NULL) { + return out; + } + + ndr_err = ndr_pull_struct_blob(val, sid, sid, + (ndr_pull_flags_fn_t)ndr_pull_dom_sid); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + goto done; + } + + out.data = (uint8_t *)dom_sid_string(ctx, sid); + if (out.data == NULL) { + goto done; + } + out.length = strlen((const char *)out.data); + +done: + talloc_free(sid); + return out; +} + +/* Convert 16 bytes to 32 hex digits. */ +static struct ldb_val bin2hex(struct ldb_module *module, TALLOC_CTX *ctx, const struct ldb_val *val) +{ + struct ldb_val out; + struct samr_Password pwd; + if (val->length != sizeof(pwd.hash)) { + return data_blob(NULL, 0); + } + memcpy(pwd.hash, val->data, sizeof(pwd.hash)); + out = data_blob_string_const(smbpasswd_sethexpwd(ctx, &pwd, 0)); + if (!out.data) { + return data_blob(NULL, 0); + } + return out; +} + +/* Convert 32 hex digits to 16 bytes. */ +static struct ldb_val hex2bin(struct ldb_module *module, TALLOC_CTX *ctx, const struct ldb_val *val) +{ + struct ldb_val out; + struct samr_Password *pwd; + pwd = smbpasswd_gethexpwd(ctx, (const char *)val->data); + if (!pwd) { + return data_blob(NULL, 0); + } + out = data_blob_talloc(ctx, pwd->hash, sizeof(pwd->hash)); + return out; +} + +const struct ldb_map_objectclass samba3_objectclasses[] = { + { + .local_name = "user", + .remote_name = "posixAccount", + .base_classes = { "top", NULL }, + .musts = { "cn", "uid", "uidNumber", "gidNumber", "homeDirectory", NULL }, + .mays = { "userPassword", "loginShell", "gecos", "description", NULL }, + }, + { + .local_name = "group", + .remote_name = "posixGroup", + .base_classes = { "top", NULL }, + .musts = { "cn", "gidNumber", NULL }, + .mays = { "userPassword", "memberUid", "description", NULL }, + }, + { + .local_name = "group", + .remote_name = "sambaGroupMapping", + .base_classes = { "top", "posixGroup", NULL }, + .musts = { "gidNumber", "sambaSID", "sambaGroupType", NULL }, + .mays = { "displayName", "description", "sambaSIDList", NULL }, + }, + { + .local_name = "user", + .remote_name = "sambaSAMAccount", + .base_classes = { "top", "posixAccount", NULL }, + .musts = { "uid", "sambaSID", NULL }, + .mays = { "cn", "sambaLMPassword", "sambaNTPassword", + "sambaPwdLastSet", "sambaLogonTime", "sambaLogoffTime", + "sambaKickoffTime", "sambaPwdCanChange", "sambaPwdMustChange", + "sambaAcctFlags", "displayName", "sambaHomePath", "sambaHomeDrive", + "sambaLogonScript", "sambaProfilePath", "description", "sambaUserWorkstations", + "sambaPrimaryGroupSID", "sambaDomainName", "sambaMungedDial", + "sambaBadPasswordCount", "sambaBadPasswordTime", + "sambaPasswordHistory", "sambaLogonHours", NULL } + + }, + { + .local_name = "domain", + .remote_name = "sambaDomain", + .base_classes = { "top", NULL }, + .musts = { "sambaDomainName", "sambaSID", NULL }, + .mays = { "sambaNextRid", "sambaNextGroupRid", "sambaNextUserRid", "sambaAlgorithmicRidBase", NULL }, + }, + { .local_name = NULL } +}; + +const struct ldb_map_attribute samba3_attributes[] = +{ + /* sambaNextRid -> nextRid */ + { + .local_name = "nextRid", + .type = LDB_MAP_RENAME, + .u = { + .rename = { + .remote_name = "sambaNextRid", + }, + }, + }, + + /* sambaBadPasswordTime -> badPasswordtime*/ + { + .local_name = "badPasswordTime", + .type = LDB_MAP_RENAME, + .u = { + .rename = { + .remote_name = "sambaBadPasswordTime", + }, + }, + }, + + /* sambaLMPassword -> lmPwdHash*/ + { + .local_name = "dBCSPwd", + .type = LDB_MAP_CONVERT, + .u = { + .convert = { + .remote_name = "sambaLMPassword", + .convert_local = bin2hex, + .convert_remote = hex2bin, + }, + }, + }, + + /* sambaGroupType -> groupType */ + { + .local_name = "groupType", + .type = LDB_MAP_RENAME, + .u = { + .rename = { + .remote_name = "sambaGroupType", + }, + }, + }, + + /* sambaNTPassword -> ntPwdHash*/ + { + .local_name = "ntpwdhash", + .type = LDB_MAP_CONVERT, + .u = { + .convert = { + .remote_name = "sambaNTPassword", + .convert_local = bin2hex, + .convert_remote = hex2bin, + }, + }, + }, + + /* sambaPrimaryGroupSID -> primaryGroupID */ + { + .local_name = "primaryGroupID", + .type = LDB_MAP_GENERATE, + .u = { + .generate = { + .remote_names = { "sambaPrimaryGroupSID", NULL }, + .generate_local = generate_primaryGroupID, + .generate_remote = generate_sambaPrimaryGroupSID, + }, + }, + }, + + /* sambaBadPasswordCount -> badPwdCount */ + { + .local_name = "badPwdCount", + .type = LDB_MAP_RENAME, + .u = { + .rename = { + .remote_name = "sambaBadPasswordCount", + }, + }, + }, + + /* sambaLogonTime -> lastLogon*/ + { + .local_name = "lastLogon", + .type = LDB_MAP_RENAME, + .u = { + .rename = { + .remote_name = "sambaLogonTime", + }, + }, + }, + + /* sambaLogoffTime -> lastLogoff*/ + { + .local_name = "lastLogoff", + .type = LDB_MAP_RENAME, + .u = { + .rename = { + .remote_name = "sambaLogoffTime", + }, + }, + }, + + /* uid -> unixName */ + { + .local_name = "unixName", + .type = LDB_MAP_RENAME, + .u = { + .rename = { + .remote_name = "uid", + }, + }, + }, + + /* displayName -> name */ + { + .local_name = "name", + .type = LDB_MAP_RENAME, + .u = { + .rename = { + .remote_name = "displayName", + }, + }, + }, + + /* cn */ + { + .local_name = "cn", + .type = LDB_MAP_KEEP, + }, + + /* sAMAccountName -> cn */ + { + .local_name = "sAMAccountName", + .type = LDB_MAP_CONVERT, + .u = { + .convert = { + .remote_name = "uid", + .convert_remote = convert_uid_samaccount, + }, + }, + }, + + /* objectCategory */ + { + .local_name = "objectCategory", + .type = LDB_MAP_IGNORE, + }, + + /* objectGUID */ + { + .local_name = "objectGUID", + .type = LDB_MAP_IGNORE, + }, + + /* objectVersion */ + { + .local_name = "objectVersion", + .type = LDB_MAP_IGNORE, + }, + + /* codePage */ + { + .local_name = "codePage", + .type = LDB_MAP_IGNORE, + }, + + /* dNSHostName */ + { + .local_name = "dNSHostName", + .type = LDB_MAP_IGNORE, + }, + + + /* dnsDomain */ + { + .local_name = "dnsDomain", + .type = LDB_MAP_IGNORE, + }, + + /* dnsRoot */ + { + .local_name = "dnsRoot", + .type = LDB_MAP_IGNORE, + }, + + /* countryCode */ + { + .local_name = "countryCode", + .type = LDB_MAP_IGNORE, + }, + + /* nTMixedDomain */ + { + .local_name = "nTMixedDomain", + .type = LDB_MAP_IGNORE, + }, + + /* operatingSystem */ + { + .local_name = "operatingSystem", + .type = LDB_MAP_IGNORE, + }, + + /* operatingSystemVersion */ + { + .local_name = "operatingSystemVersion", + .type = LDB_MAP_IGNORE, + }, + + + /* servicePrincipalName */ + { + .local_name = "servicePrincipalName", + .type = LDB_MAP_IGNORE, + }, + + /* msDS-Behavior-Version */ + { + .local_name = "msDS-Behavior-Version", + .type = LDB_MAP_IGNORE, + }, + + /* msDS-KeyVersionNumber */ + { + .local_name = "msDS-KeyVersionNumber", + .type = LDB_MAP_IGNORE, + }, + + /* msDs-masteredBy */ + { + .local_name = "msDs-masteredBy", + .type = LDB_MAP_IGNORE, + }, + + /* ou */ + { + .local_name = "ou", + .type = LDB_MAP_KEEP, + }, + + /* dc */ + { + .local_name = "dc", + .type = LDB_MAP_KEEP, + }, + + /* description */ + { + .local_name = "description", + .type = LDB_MAP_KEEP, + }, + + /* sambaSID -> objectSid*/ + { + .local_name = "objectSid", + .type = LDB_MAP_CONVERT, + .u = { + .convert = { + .remote_name = "sambaSID", + .convert_local = decode_sid, + .convert_remote = encode_sid, + }, + }, + }, + + /* sambaPwdLastSet -> pwdLastSet */ + { + .local_name = "pwdLastSet", + .type = LDB_MAP_RENAME, + .u = { + .rename = { + .remote_name = "sambaPwdLastSet", + }, + }, + }, + + /* accountExpires */ + { + .local_name = "accountExpires", + .type = LDB_MAP_IGNORE, + }, + + /* adminCount */ + { + .local_name = "adminCount", + .type = LDB_MAP_IGNORE, + }, + + /* canonicalName */ + { + .local_name = "canonicalName", + .type = LDB_MAP_IGNORE, + }, + + /* createTimestamp */ + { + .local_name = "createTimestamp", + .type = LDB_MAP_IGNORE, + }, + + /* creationTime */ + { + .local_name = "creationTime", + .type = LDB_MAP_IGNORE, + }, + + /* dMDLocation */ + { + .local_name = "dMDLocation", + .type = LDB_MAP_IGNORE, + }, + + /* fSMORoleOwner */ + { + .local_name = "fSMORoleOwner", + .type = LDB_MAP_IGNORE, + }, + + /* forceLogoff */ + { + .local_name = "forceLogoff", + .type = LDB_MAP_IGNORE, + }, + + /* instanceType */ + { + .local_name = "instanceType", + .type = LDB_MAP_IGNORE, + }, + + /* invocationId */ + { + .local_name = "invocationId", + .type = LDB_MAP_IGNORE, + }, + + /* isCriticalSystemObject */ + { + .local_name = "isCriticalSystemObject", + .type = LDB_MAP_IGNORE, + }, + + /* localPolicyFlags */ + { + .local_name = "localPolicyFlags", + .type = LDB_MAP_IGNORE, + }, + + /* lockOutObservationWindow */ + { + .local_name = "lockOutObservationWindow", + .type = LDB_MAP_IGNORE, + }, + + /* lockoutDuration */ + { + .local_name = "lockoutDuration", + .type = LDB_MAP_IGNORE, + }, + + /* lockoutThreshold */ + { + .local_name = "lockoutThreshold", + .type = LDB_MAP_IGNORE, + }, + + /* logonCount */ + { + .local_name = "logonCount", + .type = LDB_MAP_IGNORE, + }, + + /* masteredBy */ + { + .local_name = "masteredBy", + .type = LDB_MAP_IGNORE, + }, + + /* maxPwdAge */ + { + .local_name = "maxPwdAge", + .type = LDB_MAP_IGNORE, + }, + + /* member */ + { + .local_name = "member", + .type = LDB_MAP_IGNORE, + }, + + /* memberOf */ + { + .local_name = "memberOf", + .type = LDB_MAP_IGNORE, + }, + + /* minPwdAge */ + { + .local_name = "minPwdAge", + .type = LDB_MAP_IGNORE, + }, + + /* minPwdLength */ + { + .local_name = "minPwdLength", + .type = LDB_MAP_IGNORE, + }, + + /* modifiedCount */ + { + .local_name = "modifiedCount", + .type = LDB_MAP_IGNORE, + }, + + /* modifiedCountAtLastProm */ + { + .local_name = "modifiedCountAtLastProm", + .type = LDB_MAP_IGNORE, + }, + + /* modifyTimestamp */ + { + .local_name = "modifyTimestamp", + .type = LDB_MAP_IGNORE, + }, + + /* nCName */ + { + .local_name = "nCName", + .type = LDB_MAP_IGNORE, + }, + + /* nETBIOSName */ + { + .local_name = "nETBIOSName", + .type = LDB_MAP_IGNORE, + }, + + /* oEMInformation */ + { + .local_name = "oEMInformation", + .type = LDB_MAP_IGNORE, + }, + + /* privilege */ + { + .local_name = "privilege", + .type = LDB_MAP_IGNORE, + }, + + /* pwdHistoryLength */ + { + .local_name = "pwdHistoryLength", + .type = LDB_MAP_IGNORE, + }, + + /* pwdProperties */ + { + .local_name = "pwdProperties", + .type = LDB_MAP_IGNORE, + }, + + /* rIDAvailablePool */ + { + .local_name = "rIDAvailablePool", + .type = LDB_MAP_IGNORE, + }, + + /* revision */ + { + .local_name = "revision", + .type = LDB_MAP_IGNORE, + }, + + /* ridManagerReference */ + { + .local_name = "ridManagerReference", + .type = LDB_MAP_IGNORE, + }, + + /* sAMAccountType */ + { + .local_name = "sAMAccountType", + .type = LDB_MAP_IGNORE, + }, + + /* sPNMappings */ + { + .local_name = "sPNMappings", + .type = LDB_MAP_IGNORE, + }, + + /* serverReference */ + { + .local_name = "serverReference", + .type = LDB_MAP_IGNORE, + }, + + /* serverState */ + { + .local_name = "serverState", + .type = LDB_MAP_IGNORE, + }, + + /* showInAdvancedViewOnly */ + { + .local_name = "showInAdvancedViewOnly", + .type = LDB_MAP_IGNORE, + }, + + /* subRefs */ + { + .local_name = "subRefs", + .type = LDB_MAP_IGNORE, + }, + + /* systemFlags */ + { + .local_name = "systemFlags", + .type = LDB_MAP_IGNORE, + }, + + /* uASCompat */ + { + .local_name = "uASCompat", + .type = LDB_MAP_IGNORE, + }, + + /* uSNChanged */ + { + .local_name = "uSNChanged", + .type = LDB_MAP_IGNORE, + }, + + /* uSNCreated */ + { + .local_name = "uSNCreated", + .type = LDB_MAP_IGNORE, + }, + + /* userPassword */ + { + .local_name = "userPassword", + .type = LDB_MAP_IGNORE, + }, + + /* userAccountControl */ + { + .local_name = "userAccountControl", + .type = LDB_MAP_IGNORE, + }, + + /* whenChanged */ + { + .local_name = "whenChanged", + .type = LDB_MAP_IGNORE, + }, + + /* whenCreated */ + { + .local_name = "whenCreated", + .type = LDB_MAP_IGNORE, + }, + + /* uidNumber */ + { + .local_name = "unixName", + .type = LDB_MAP_CONVERT, + .u = { + .convert = { + .remote_name = "uidNumber", + .convert_local = lookup_uid, + }, + }, + }, + + /* gidNumber. Perhaps make into generate so we can distinguish between + * groups and accounts? */ + { + .local_name = "unixName", + .type = LDB_MAP_CONVERT, + .u = { + .convert = { + .remote_name = "gidNumber", + .convert_local = lookup_gid, + }, + }, + }, + + /* homeDirectory */ + { + .local_name = "unixName", + .type = LDB_MAP_CONVERT, + .u = { + .convert = { + .remote_name = "homeDirectory", + .convert_local = lookup_homedir, + }, + }, + }, + { + .local_name = NULL, + } +}; + +/* the context init function */ +static int samba3sam_init(struct ldb_module *module) +{ + int ret; + + ret = ldb_map_init(module, samba3_attributes, samba3_objectclasses, NULL, NULL, "samba3sam"); + if (ret != LDB_SUCCESS) + return ret; + + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_samba3sam_module_ops = { + LDB_MAP_OPS + .name = "samba3sam", + .init_context = samba3sam_init, +}; + + +/* A dummy module to help the samba3sam tests */ +static int show_deleted_ignore_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_control *show_del, *show_rec; + + /* check if there's a show deleted control */ + show_del = ldb_request_get_control(req, LDB_CONTROL_SHOW_DELETED_OID); + /* check if there's a show recycled control */ + show_rec = ldb_request_get_control(req, LDB_CONTROL_SHOW_RECYCLED_OID); + + /* mark the controls as done */ + if (show_del != NULL) { + show_del->critical = 0; + } + if (show_rec != NULL) { + show_rec->critical = 0; + } + + /* perform the search */ + return ldb_next_request(module, req); +} + +static const struct ldb_module_ops ldb_show_deleted_module_ops = { + .name = "show_deleted_ignore", + .search = show_deleted_ignore_search +}; + +int ldb_samba3sam_module_init(const char *version) +{ + int ret; + + LDB_MODULE_CHECK_VERSION(version); + ret = ldb_register_module(&ldb_show_deleted_module_ops); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_register_module(&ldb_samba3sam_module_ops); +} + diff --git a/source4/dsdb/samdb/ldb_modules/samba3sid.c b/source4/dsdb/samdb/ldb_modules/samba3sid.c new file mode 100644 index 0000000..f38ab40 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/samba3sid.c @@ -0,0 +1,207 @@ +/* + samba3sid module + + Copyright (C) Andrew Bartlett 2010 + Copyright (C) Andrew Tridgell 2010 + + 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/>. +*/ + +/* + add objectSid to users and groups using samba3 nextRid method + */ + +#include "includes.h" +#include "libcli/ldap/ldap_ndr.h" +#include "ldb_module.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "libcli/security/security.h" +#include "librpc/gen_ndr/ndr_security.h" +#include "ldb_wrap.h" +#include "param/param.h" + +/* + RID algorithm from pdb_ldap.c in source3/passdb/ + (loosely based on Volkers code) + */ +static int samba3sid_next_sid(struct ldb_module *module, + TALLOC_CTX *mem_ctx, char **sid, + struct ldb_request *parent) +{ + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + struct ldb_result *res; + const char *attrs[] = { "sambaNextRid", "sambaNextUserRid", + "sambaNextGroupRid", "sambaSID", NULL }; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_message *msg; + uint32_t sambaNextRid, sambaNextGroupRid, sambaNextUserRid, rid; + const char *sambaSID; + + ret = dsdb_module_search(module, tmp_ctx, &res, NULL, LDB_SCOPE_SUBTREE, + attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SEARCH_ALL_PARTITIONS, + parent, + "(&(objectClass=sambaDomain)(sambaDomainName=%s))", + lpcfg_sam_name(ldb_get_opaque(ldb, "loadparm"))); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + __location__ + ": Failed to find domain object - %s", + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + if (res->count != 1) { + ldb_asprintf_errstring(ldb, + __location__ + ": Expected exactly 1 domain object - got %u", + res->count); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + msg = res->msgs[0]; + + sambaNextRid = ldb_msg_find_attr_as_uint(msg, "sambaNextRid", + (uint32_t) -1); + sambaNextUserRid = ldb_msg_find_attr_as_uint(msg, "sambaNextUserRid", + (uint32_t) -1); + sambaNextGroupRid = ldb_msg_find_attr_as_uint(msg, "sambaNextGroupRid", + (uint32_t) -1); + sambaSID = ldb_msg_find_attr_as_string(msg, "sambaSID", NULL); + + if (sambaSID == NULL) { + ldb_asprintf_errstring(ldb, + __location__ + ": No sambaSID in %s", + ldb_dn_get_linearized(msg->dn)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* choose the highest of the 3 - see pdb_ldap.c for an + * explaination */ + rid = sambaNextRid; + if ((sambaNextUserRid != (uint32_t) -1) && (sambaNextUserRid > rid)) { + rid = sambaNextUserRid; + } + if ((sambaNextGroupRid != (uint32_t) -1) && (sambaNextGroupRid > rid)) { + rid = sambaNextGroupRid; + } + if (rid == (uint32_t) -1) { + ldb_asprintf_errstring(ldb, + __location__ + ": No sambaNextRid in %s", + ldb_dn_get_linearized(msg->dn)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* sambaNextRid is actually the previous RID .... */ + rid += 1; + + (*sid) = talloc_asprintf(tmp_ctx, "%s-%u", sambaSID, rid); + if (!*sid) { + talloc_free(tmp_ctx); + return ldb_module_oom(module); + } + + ret = dsdb_module_constrainted_update_uint32(module, msg->dn, + "sambaNextRid", + &sambaNextRid, &rid, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + __location__ + ": Failed to update sambaNextRid - %s", + ldb_errstring(ldb)); + talloc_free(tmp_ctx); + return ret; + } + + talloc_steal(mem_ctx, *sid); + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + + + +/* add */ +static int samba3sid_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + int ret; + const struct ldb_message *msg = req->op.add.message; + struct ldb_message *new_msg; + char *sid; + struct ldb_request *new_req; + + ldb = ldb_module_get_ctx(module); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.add.message->dn)) { + return ldb_next_request(module, req); + } + + if (!samdb_find_attribute(ldb, msg, "objectclass", "posixAccount") && + !samdb_find_attribute(ldb, msg, "objectclass", "posixGroup")) { + /* its not a user or a group */ + return ldb_next_request(module, req); + } + + if (ldb_msg_find_element(msg, "sambaSID")) { + /* a SID was supplied */ + return ldb_next_request(module, req); + } + + new_msg = ldb_msg_copy_shallow(req, req->op.add.message); + if (!new_msg) { + return ldb_module_oom(module); + } + + ret = samba3sid_next_sid(module, new_msg, &sid, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_msg_add_steal_string(new_msg, "sambaSID", sid); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_build_add_req(&new_req, ldb, req, + new_msg, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(new_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, new_req); +} + +static const struct ldb_module_ops ldb_samba3sid_module_ops = { + .name = "samba3sid", + .add = samba3sid_add, +}; + + +int ldb_samba3sid_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_samba3sid_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/samba_dsdb.c b/source4/dsdb/samdb/ldb_modules/samba_dsdb.c new file mode 100644 index 0000000..d9de16e --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/samba_dsdb.c @@ -0,0 +1,608 @@ +/* + Samba4 module loading module + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 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: Samba4 module loading module + * + * Description: Implement a single 'module' in the ldb database, + * which loads the remaining modules based on 'choice of configuration' attributes + * + * This is to avoid forcing a reprovision of the ldb databases when we change the internal structure of the code + * + * Author: Andrew Bartlett + */ + +#include "includes.h" +#include <ldb.h> +#include <ldb_errors.h> +#include <ldb_module.h> +#include "dsdb/samdb/ldb_modules/util.h" +#include "dsdb/samdb/samdb.h" +#include "librpc/ndr/libndr.h" +#include "auth/credentials/credentials.h" +#include "param/secrets.h" +#include "lib/ldb-samba/ldb_wrap.h" + +static int read_at_rootdse_record(struct ldb_context *ldb, struct ldb_module *module, TALLOC_CTX *mem_ctx, + struct ldb_message **msg, struct ldb_request *parent) +{ + int ret; + static const char *rootdse_attrs[] = { "defaultNamingContext", "configurationNamingContext", "schemaNamingContext", NULL }; + struct ldb_result *rootdse_res; + struct ldb_dn *rootdse_dn; + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + if (!tmp_ctx) { + return ldb_oom(ldb); + } + + rootdse_dn = ldb_dn_new(tmp_ctx, ldb, "@ROOTDSE"); + if (!rootdse_dn) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + + ret = dsdb_module_search_dn(module, tmp_ctx, &rootdse_res, rootdse_dn, + rootdse_attrs, DSDB_FLAG_NEXT_MODULE, parent); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + talloc_steal(mem_ctx, rootdse_res->msgs); + *msg = rootdse_res->msgs[0]; + + talloc_free(tmp_ctx); + + return ret; +} + +static int prepare_modules_line(struct ldb_context *ldb, + TALLOC_CTX *mem_ctx, + const struct ldb_message *rootdse_msg, + struct ldb_message *msg, const char *backend_attr, + const char *backend_mod, const char **backend_mod_list) +{ + int ret; + const char **backend_full_list; + const char *backend_dn; + char *mod_list_string; + char *full_string; + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + if (!tmp_ctx) { + return ldb_oom(ldb); + } + + if (backend_attr) { + backend_dn = ldb_msg_find_attr_as_string(rootdse_msg, backend_attr, NULL); + if (!backend_dn) { + ldb_asprintf_errstring(ldb, + "samba_dsdb_init: " + "unable to read %s from %s:%s", + backend_attr, ldb_dn_get_linearized(rootdse_msg->dn), + ldb_errstring(ldb)); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } else { + backend_dn = "*"; + } + + if (backend_mod) { + char **b = str_list_make_single(tmp_ctx, backend_mod); + backend_full_list = discard_const_p(const char *, b); + } else { + char **b = str_list_make_empty(tmp_ctx); + backend_full_list = discard_const_p(const char *, b); + } + if (!backend_full_list) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + + backend_full_list = str_list_append_const(backend_full_list, backend_mod_list); + if (!backend_full_list) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + + mod_list_string = str_list_join(tmp_ctx, backend_full_list, ','); + + /* str_list_append allocates on NULL */ + talloc_free(backend_full_list); + + if (!mod_list_string) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + + full_string = talloc_asprintf(tmp_ctx, "%s:%s", backend_dn, mod_list_string); + ret = ldb_msg_add_steal_string(msg, "modules", full_string); + talloc_free(tmp_ctx); + return ret; +} + +static bool check_required_features(struct ldb_message_element *el) +{ + if (el != NULL) { + int k; + DATA_BLOB esf = data_blob_string_const( + SAMBA_ENCRYPTED_SECRETS_FEATURE); + DATA_BLOB lmdbl1 = data_blob_string_const( + SAMBA_LMDB_LEVEL_ONE_FEATURE); + for (k = 0; k < el->num_values; k++) { + if ((data_blob_cmp(&esf, &el->values[k]) != 0) && + (data_blob_cmp(&lmdbl1, &el->values[k]) != 0)) { + return false; + } + } + } + return true; +} + +static int samba_dsdb_init(struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + int ret, lock_ret, len, i, j; + TALLOC_CTX *tmp_ctx = talloc_new(module); + struct ldb_result *res; + struct ldb_message *rootdse_msg = NULL, *partition_msg; + struct ldb_dn *samba_dsdb_dn, *partition_dn, *indexlist_dn; + struct ldb_module *backend_module, *module_chain; + const char **final_module_list, **reverse_module_list; + /* + Add modules to the list to activate them by default + beware often order is important + + Some Known ordering constraints: + - rootdse must be first, as it makes redirects from "" -> cn=rootdse + - extended_dn_in must be before objectclass.c, as it resolves the DN + - objectclass must be before password_hash and samldb since these LDB + modules require the expanded "objectClass" list + - objectclass must be before descriptor and acl, as both assume that + objectClass values are sorted + - objectclass_attrs must be behind operational in order to see all + attributes (the operational module protects and therefore + suppresses per default some important ones) + - partition must be last + - each partition has its own module list then + + The list is presented here as a set of declarations to show the + stack visually - the code below then handles the creation of the list + based on the parameters loaded from the database. + */ + static const char *modules_list1[] = {"resolve_oids", + "rootdse", + "dsdb_notification", + "schema_load", + "lazy_commit", + "dirsync", + "dsdb_paged_results", + "vlv", + "ranged_results", + "anr", + "server_sort", + "asq", + "extended_dn_store", + NULL }; + /* extended_dn_in or extended_dn_in_openldap goes here */ + static const char *modules_list1a[] = {"audit_log", + "objectclass", + "tombstone_reanimate", + "descriptor", + "acl", + "aclread", + "samldb", + "password_hash", + "instancetype", + "objectclass_attrs", + NULL }; + + const char **link_modules; + static const char *tdb_modules_list[] = { + "rdn_name", + "subtree_delete", + "repl_meta_data", + "group_audit_log", + "encrypted_secrets", + "operational", + "unique_object_sids", + "subtree_rename", + "linked_attributes", + NULL}; + + const char *extended_dn_module; + const char *extended_dn_module_ldb = "extended_dn_out_ldb"; + const char *extended_dn_in_module = "extended_dn_in"; + + static const char *modules_list2[] = {"dns_notify", + "show_deleted", + "new_partition", + "partition", + NULL }; + + const char **backend_modules; + static const char *samba_dsdb_attrs[] = { SAMBA_COMPATIBLE_FEATURES_ATTR, + SAMBA_REQUIRED_FEATURES_ATTR, NULL }; + static const char *indexlist_attrs[] = { SAMBA_FEATURES_SUPPORTED_FLAG, NULL }; + + const char *current_supportedFeatures[] = {SAMBA_SORTED_LINKS_FEATURE}; + + if (!tmp_ctx) { + return ldb_oom(ldb); + } + + ret = ldb_register_samba_handlers(ldb); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + samba_dsdb_dn = ldb_dn_new(tmp_ctx, ldb, "@SAMBA_DSDB"); + if (!samba_dsdb_dn) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + + indexlist_dn = ldb_dn_new(tmp_ctx, ldb, "@INDEXLIST"); + if (!samba_dsdb_dn) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + + partition_dn = ldb_dn_new(tmp_ctx, ldb, DSDB_PARTITION_DN); + if (!partition_dn) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + +#define CHECK_LDB_RET(check_ret) \ + do { \ + if (check_ret != LDB_SUCCESS) { \ + talloc_free(tmp_ctx); \ + return check_ret; \ + } \ + } while (0) + + ret = dsdb_module_search_dn(module, tmp_ctx, &res, samba_dsdb_dn, + samba_dsdb_attrs, DSDB_FLAG_NEXT_MODULE, NULL); + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + /* do nothing, a very old db being upgraded */ + } else if (ret == LDB_SUCCESS) { + struct ldb_message_element *requiredFeatures; + struct ldb_message_element *old_compatibleFeatures; + + requiredFeatures = ldb_msg_find_element(res->msgs[0], SAMBA_REQUIRED_FEATURES_ATTR); + if (!check_required_features(requiredFeatures)) { + ldb_set_errstring( + ldb, + "This Samba database was created with " + "a newer Samba version and is marked " + "with extra requiredFeatures in " + "@SAMBA_DSDB. This database can not " + "safely be read by this Samba version"); + return LDB_ERR_OPERATIONS_ERROR; + } + + old_compatibleFeatures = ldb_msg_find_element(res->msgs[0], + SAMBA_COMPATIBLE_FEATURES_ATTR); + + if (old_compatibleFeatures) { + struct ldb_message *features_msg; + struct ldb_message_element *features_el; + int samba_options_supported = 0; + ret = dsdb_module_search_dn(module, tmp_ctx, &res, + indexlist_dn, + indexlist_attrs, + DSDB_FLAG_NEXT_MODULE, NULL); + if (ret == LDB_SUCCESS) { + samba_options_supported + = ldb_msg_find_attr_as_int(res->msgs[0], + SAMBA_FEATURES_SUPPORTED_FLAG, + 0); + + } else if (ret == LDB_ERR_NO_SUCH_OBJECT) { + /* + * If we don't have @INDEXLIST yet, then we + * are so early in set-up that we know this is + * a blank DB, so no need to wripe out old + * features + */ + samba_options_supported = 1; + } + + features_msg = ldb_msg_new(res); + if (features_msg == NULL) { + return ldb_module_operr(module); + } + features_msg->dn = samba_dsdb_dn; + + ldb_msg_add_empty(features_msg, SAMBA_COMPATIBLE_FEATURES_ATTR, + LDB_FLAG_MOD_DELETE, &features_el); + + if (samba_options_supported == 1) { + for (i = 0; + old_compatibleFeatures && i < old_compatibleFeatures->num_values; + i++) { + for (j = 0; + j < ARRAY_SIZE(current_supportedFeatures); j++) { + if (strcmp((char *)old_compatibleFeatures->values[i].data, + current_supportedFeatures[j]) == 0) { + break; + } + } + if (j == ARRAY_SIZE(current_supportedFeatures)) { + /* + * Add to list of features to remove + * (rather than all features) + */ + ret = ldb_msg_add_value(features_msg, SAMBA_COMPATIBLE_FEATURES_ATTR, + &old_compatibleFeatures->values[i], + NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } + + if (features_el->num_values > 0) { + /* Delete by list */ + ret = ldb_next_start_trans(module); + if (ret != LDB_SUCCESS) { + return ret; + } + ret = dsdb_module_modify(module, features_msg, DSDB_FLAG_NEXT_MODULE, NULL); + if (ret != LDB_SUCCESS) { + ldb_next_del_trans(module); + return ret; + } + ret = ldb_next_end_trans(module); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } else { + /* Delete all */ + ret = ldb_next_start_trans(module); + if (ret != LDB_SUCCESS) { + return ret; + } + ret = dsdb_module_modify(module, features_msg, DSDB_FLAG_NEXT_MODULE, NULL); + if (ret != LDB_SUCCESS) { + ldb_next_del_trans(module); + return ret; + } + ret = ldb_next_end_trans(module); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } + + } else { + talloc_free(tmp_ctx); + return ret; + } + + backend_modules = NULL; + extended_dn_module = extended_dn_module_ldb; + link_modules = tdb_modules_list; + +#define CHECK_MODULE_LIST \ + do { \ + if (!final_module_list) { \ + talloc_free(tmp_ctx); \ + return ldb_oom(ldb); \ + } \ + } while (0) + + final_module_list = str_list_copy_const(tmp_ctx, modules_list1); + CHECK_MODULE_LIST; + + final_module_list = str_list_add_const(final_module_list, extended_dn_in_module); + CHECK_MODULE_LIST; + + final_module_list = str_list_append_const(final_module_list, modules_list1a); + CHECK_MODULE_LIST; + + final_module_list = str_list_append_const(final_module_list, link_modules); + CHECK_MODULE_LIST; + + final_module_list = str_list_add_const(final_module_list, extended_dn_module); + CHECK_MODULE_LIST; + + final_module_list = str_list_append_const(final_module_list, modules_list2); + CHECK_MODULE_LIST; + + + ret = read_at_rootdse_record(ldb, module, tmp_ctx, &rootdse_msg, NULL); + CHECK_LDB_RET(ret); + + partition_msg = ldb_msg_new(tmp_ctx); + partition_msg->dn = ldb_dn_new(partition_msg, ldb, "@" DSDB_OPAQUE_PARTITION_MODULE_MSG_OPAQUE_NAME); + + ret = prepare_modules_line(ldb, tmp_ctx, + rootdse_msg, + partition_msg, "schemaNamingContext", + "schema_data", backend_modules); + CHECK_LDB_RET(ret); + + ret = prepare_modules_line(ldb, tmp_ctx, + rootdse_msg, + partition_msg, NULL, + NULL, backend_modules); + CHECK_LDB_RET(ret); + + ret = ldb_set_opaque(ldb, DSDB_OPAQUE_PARTITION_MODULE_MSG_OPAQUE_NAME, partition_msg); + CHECK_LDB_RET(ret); + + talloc_steal(ldb, partition_msg); + + /* Now prepare the module chain. Oddly, we must give it to + * ldb_module_load_list in REVERSE */ + for (len = 0; final_module_list[len]; len++) { /* noop */}; + + reverse_module_list = talloc_array(tmp_ctx, const char *, len+1); + if (!reverse_module_list) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + for (i=0; i < len; i++) { + reverse_module_list[i] = final_module_list[(len - 1) - i]; + } + reverse_module_list[i] = NULL; + + /* The backend (at least until the partitions module + * reconfigures things) is the next module in the currently + * loaded chain */ + backend_module = ldb_module_next(module); + ret = ldb_module_load_list(ldb, reverse_module_list, backend_module, &module_chain); + CHECK_LDB_RET(ret); + + talloc_free(tmp_ctx); + /* Set this as the 'next' module, so that we effectively append it to + * module chain */ + ldb_module_set_next(module, module_chain); + + ret = ldb_next_read_lock(module); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_next_init(module); + + lock_ret = ldb_next_read_unlock(module); + + if (lock_ret != LDB_SUCCESS) { + return lock_ret; + } + + return ret; +} + +static const struct ldb_module_ops ldb_samba_dsdb_module_ops = { + .name = "samba_dsdb", + .init_context = samba_dsdb_init, +}; + +static struct ldb_message *dsdb_flags_ignore_fixup(TALLOC_CTX *mem_ctx, + const struct ldb_message *_msg) +{ + struct ldb_message *msg = NULL; + unsigned int i; + + /* we have to copy the message as the caller might have it as a const */ + msg = ldb_msg_copy_shallow(mem_ctx, _msg); + if (msg == NULL) { + return NULL; + } + + for (i=0; i < msg->num_elements;) { + struct ldb_message_element *e = &msg->elements[i]; + + if (!(e->flags & DSDB_FLAG_INTERNAL_FORCE_META_DATA)) { + i++; + continue; + } + + e->flags &= ~DSDB_FLAG_INTERNAL_FORCE_META_DATA; + + if (e->num_values != 0) { + i++; + continue; + } + + ldb_msg_remove_element(msg, e); + } + + return msg; +} + +static int dsdb_flags_ignore_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_request *down_req = NULL; + struct ldb_message *msg = NULL; + int ret; + + msg = dsdb_flags_ignore_fixup(req, req->op.add.message); + if (msg == NULL) { + return ldb_module_oom(module); + } + + ret = ldb_build_add_req(&down_req, ldb, req, + msg, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* go on with the call chain */ + return ldb_next_request(module, down_req); +} + +static int dsdb_flags_ignore_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_request *down_req = NULL; + struct ldb_message *msg = NULL; + int ret; + + msg = dsdb_flags_ignore_fixup(req, req->op.mod.message); + if (msg == NULL) { + return ldb_module_oom(module); + } + + ret = ldb_build_mod_req(&down_req, ldb, req, + msg, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* go on with the call chain */ + return ldb_next_request(module, down_req); +} + +static const struct ldb_module_ops ldb_dsdb_flags_ignore_module_ops = { + .name = "dsdb_flags_ignore", + .add = dsdb_flags_ignore_add, + .modify = dsdb_flags_ignore_modify, +}; + +int ldb_samba_dsdb_module_init(const char *version) +{ + int ret; + LDB_MODULE_CHECK_VERSION(version); + ret = ldb_register_module(&ldb_samba_dsdb_module_ops); + if (ret != LDB_SUCCESS) { + return ret; + } + ret = ldb_register_module(&ldb_dsdb_flags_ignore_module_ops); + if (ret != LDB_SUCCESS) { + return ret; + } + return LDB_SUCCESS; +} diff --git a/source4/dsdb/samdb/ldb_modules/samba_secrets.c b/source4/dsdb/samdb/ldb_modules/samba_secrets.c new file mode 100644 index 0000000..d184ee7 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/samba_secrets.c @@ -0,0 +1,103 @@ +/* + Samba4 module loading module (for secrets) + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 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: Samba4 module loading module (for secrets.ldb) + * + * Description: Implement a single 'module' in the secrets.ldb database + * + * This is to avoid forcing a reprovision of the ldb databases when we change the internal structure of the code + * + * Author: Andrew Bartlett + */ + +#include "includes.h" +#include <ldb.h> +#include <ldb_errors.h> +#include <ldb_module.h> +#include "dsdb/samdb/ldb_modules/util.h" +#include "dsdb/samdb/samdb.h" + + +static int samba_secrets_init(struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + int ret, len, i; + TALLOC_CTX *tmp_ctx = talloc_new(module); + struct ldb_module *backend_module, *module_chain; + const char **reverse_module_list; + /* + Add modules to the list to activate them by default + beware often order is important + + The list is presented here as a set of declarations to show the + stack visually + */ + static const char *modules_list[] = {"update_keytab", + "secrets_tdb_sync", + "objectguid", + "rdn_name", + NULL }; + + if (!tmp_ctx) { + return ldb_oom(ldb); + } + + /* Now prepare the module chain. Oddly, we must give it to ldb_load_modules_list in REVERSE */ + for (len = 0; modules_list[len]; len++) { /* noop */}; + + reverse_module_list = talloc_array(tmp_ctx, const char *, len+1); + if (!reverse_module_list) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + for (i=0; i < len; i++) { + reverse_module_list[i] = modules_list[(len - 1) - i]; + } + reverse_module_list[i] = NULL; + + /* The backend (at least until the partitions module + * reconfigures things) is the next module in the currently + * loaded chain */ + backend_module = ldb_module_next(module); + ret = ldb_module_load_list(ldb, reverse_module_list, backend_module, &module_chain); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + talloc_free(tmp_ctx); + /* Set this as the 'next' module, so that we effectivly append it to module chain */ + ldb_module_set_next(module, module_chain); + + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_samba_secrets_module_ops = { + .name = "samba_secrets", + .init_context = samba_secrets_init, +}; + +int ldb_samba_secrets_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_samba_secrets_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/samldb.c b/source4/dsdb/samdb/ldb_modules/samldb.c new file mode 100644 index 0000000..d501973 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/samldb.c @@ -0,0 +1,5724 @@ +/* + SAM ldb module + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2005-2014 + Copyright (C) Simo Sorce 2004-2008 + Copyright (C) Matthias Dieter Wallnöfer 2009-2011 + Copyright (C) Matthieu Patou 2012 + Copyright (C) Catalyst.Net Ltd 2017 + + 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 samldb module + * + * Description: various internal DSDB triggers - most for SAM specific objects + * + * Author: Simo Sorce + */ + +#include "includes.h" +#include "libcli/ldap/ldap_ndr.h" +#include "ldb_module.h" +#include "auth/auth.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "dsdb/samdb/ldb_modules/ridalloc.h" +#include "libcli/security/security.h" +#include "librpc/gen_ndr/ndr_security.h" +#include "ldb_wrap.h" +#include "param/param.h" +#include "libds/common/flag_mapping.h" +#include "system/network.h" +#include "librpc/gen_ndr/irpc.h" +#include "lib/util/smb_strtox.h" + +#undef strcasecmp + +struct samldb_ctx; +enum samldb_add_type { + SAMLDB_TYPE_USER, + SAMLDB_TYPE_GROUP, + SAMLDB_TYPE_CLASS, + SAMLDB_TYPE_ATTRIBUTE +}; + +typedef int (*samldb_step_fn_t)(struct samldb_ctx *); + +struct samldb_step { + struct samldb_step *next; + samldb_step_fn_t fn; +}; + +struct samldb_ctx { + struct ldb_module *module; + struct ldb_request *req; + + /* used for add operations */ + enum samldb_add_type type; + + /* + * should we apply the need_trailing_dollar restriction to + * samAccountName + */ + + bool need_trailing_dollar; + + /* the resulting message */ + struct ldb_message *msg; + + /* used in "samldb_find_for_defaultObjectCategory" */ + struct ldb_dn *dn, *res_dn; + + /* all the async steps necessary to complete the operation */ + struct samldb_step *steps; + struct samldb_step *curstep; + + /* If someone set an ares to forward controls and response back to the caller */ + struct ldb_reply *ares; +}; + +static struct samldb_ctx *samldb_ctx_init(struct ldb_module *module, + struct ldb_request *req) +{ + struct ldb_context *ldb; + struct samldb_ctx *ac; + + ldb = ldb_module_get_ctx(module); + + ac = talloc_zero(req, struct samldb_ctx); + if (ac == NULL) { + ldb_oom(ldb); + return NULL; + } + + ac->module = module; + ac->req = req; + + return ac; +} + +static int samldb_add_step(struct samldb_ctx *ac, samldb_step_fn_t fn) +{ + struct samldb_step *step, *stepper; + + step = talloc_zero(ac, struct samldb_step); + if (step == NULL) { + return ldb_oom(ldb_module_get_ctx(ac->module)); + } + + step->fn = fn; + + if (ac->steps == NULL) { + ac->steps = step; + ac->curstep = step; + } else { + if (ac->curstep == NULL) + return ldb_operr(ldb_module_get_ctx(ac->module)); + for (stepper = ac->curstep; stepper->next != NULL; + stepper = stepper->next); + stepper->next = step; + } + + return LDB_SUCCESS; +} + +static int samldb_first_step(struct samldb_ctx *ac) +{ + if (ac->steps == NULL) { + return ldb_operr(ldb_module_get_ctx(ac->module)); + } + + ac->curstep = ac->steps; + return ac->curstep->fn(ac); +} + +static int samldb_next_step(struct samldb_ctx *ac) +{ + if (ac->curstep->next) { + ac->curstep = ac->curstep->next; + return ac->curstep->fn(ac); + } + + /* We exit the samldb module here. If someone set an "ares" to forward + * controls and response back to the caller, use them. */ + if (ac->ares) { + return ldb_module_done(ac->req, ac->ares->controls, + ac->ares->response, LDB_SUCCESS); + } else { + return ldb_module_done(ac->req, NULL, NULL, LDB_SUCCESS); + } +} + +static int samldb_get_single_valued_attr(struct ldb_context *ldb, + struct samldb_ctx *ac, + const char *attr, + const char **value) +{ + /* + * The steps we end up going through to get and check a single valued + * attribute. + */ + struct ldb_message_element *el = NULL; + int ret; + + *value = NULL; + + ret = dsdb_get_expected_new_values(ac, + ac->msg, + attr, + &el, + ac->req->operation); + + if (ret != LDB_SUCCESS) { + return ret; + } + if (el == NULL) { + /* we are not affected */ + return LDB_SUCCESS; + } + + if (el->num_values > 1) { + ldb_asprintf_errstring( + ldb, + "samldb: %s has %u values, should be single-valued!", + attr, el->num_values); + return LDB_ERR_CONSTRAINT_VIOLATION; + } else if (el->num_values == 0) { + ldb_asprintf_errstring( + ldb, + "samldb: new value for %s " + "not provided for mandatory, single-valued attribute!", + attr); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + + + if (el->values[0].length == 0) { + ldb_asprintf_errstring( + ldb, + "samldb: %s is of zero length, should have a value!", + attr); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + + *value = (char *)el->values[0].data; + + return LDB_SUCCESS; +} + +static int samldb_unique_attr_check(struct samldb_ctx *ac, const char *attr, + const char *attr_conflict, + struct ldb_dn *base_dn) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + const char * const no_attrs[] = { NULL }; + struct ldb_result *res = NULL; + const char *str = NULL; + const char *enc_str = NULL; + int ret; + + ret = samldb_get_single_valued_attr(ldb, ac, attr, &str); + if (ret != LDB_SUCCESS) { + return ret; + } + if (str == NULL) { + /* the attribute wasn't found */ + return LDB_SUCCESS; + } + + enc_str = ldb_binary_encode_string(ac, str); + if (enc_str == NULL) { + return ldb_module_oom(ac->module); + } + + /* + * No other object should have the attribute with this value. + */ + if (attr_conflict != NULL) { + ret = dsdb_module_search(ac->module, ac, &res, + base_dn, + LDB_SCOPE_SUBTREE, no_attrs, + DSDB_FLAG_NEXT_MODULE, ac->req, + "(|(%s=%s)(%s=%s))", + attr, enc_str, + attr_conflict, enc_str); + } else { + ret = dsdb_module_search(ac->module, ac, &res, + base_dn, + LDB_SCOPE_SUBTREE, no_attrs, + DSDB_FLAG_NEXT_MODULE, ac->req, + "(%s=%s)", attr, enc_str); + } + if (ret != LDB_SUCCESS) { + return ret; + } + if (res->count > 1) { + return ldb_operr(ldb); + } else if (res->count == 1) { + if (ldb_dn_compare(res->msgs[0]->dn, ac->msg->dn) != 0) { + ldb_asprintf_errstring(ldb, + "samldb: %s '%s' already in use!", + attr, enc_str); + return LDB_ERR_ENTRY_ALREADY_EXISTS; + } + } + talloc_free(res); + + return LDB_SUCCESS; +} + + + +static inline int samldb_sam_account_upn_clash_sub_search( + struct samldb_ctx *ac, + TALLOC_CTX *mem_ctx, + struct ldb_dn *base_dn, + const char *attr, + const char *value, + const char *err_msg + ) +{ + /* + * A very specific helper function for samldb_sam_account_upn_clash(), + * where we end up doing this same thing several times in a row. + */ + const char * const no_attrs[] = { NULL }; + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + struct ldb_result *res = NULL; + int ret; + char *enc_value = ldb_binary_encode_string(ac, value); + if (enc_value == NULL) { + return ldb_module_oom(ac->module); + } + ret = dsdb_module_search(ac->module, mem_ctx, &res, + base_dn, + LDB_SCOPE_SUBTREE, no_attrs, + DSDB_FLAG_NEXT_MODULE, ac->req, + "(%s=%s)", + attr, enc_value); + talloc_free(enc_value); + + if (ret != LDB_SUCCESS) { + return ret; + } else if (res->count > 1) { + return ldb_operr(ldb); + } else if (res->count == 1) { + if (ldb_dn_compare(res->msgs[0]->dn, ac->msg->dn) != 0){ + ldb_asprintf_errstring(ldb, + "samldb: %s '%s' " + "is already in use %s", + attr, value, err_msg); + /* different errors for different attrs */ + if (strcasecmp("userPrincipalName", attr) == 0) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + return LDB_ERR_ENTRY_ALREADY_EXISTS; + } + } + return LDB_SUCCESS; +} + +static int samaccountname_bad_chars_check(struct samldb_ctx *ac, + const char *name) +{ + /* + * The rules here are based on + * + * https://social.technet.microsoft.com/wiki/contents/articles/11216.active-directory-requirements-for-creating-objects.aspx + * + * Windows considers UTF-8 sequences that map to "similar" characters + * (e.g. 'a', 'ā') to be the same sAMAccountName, and we don't. Names + * that are not valid UTF-8 *are* allowed. + * + * Additionally, Samba collapses multiple spaces, and Windows doesn't. + */ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + size_t i; + + for (i = 0; name[i] != '\0'; i++) { + uint8_t c = name[i]; + char *p = NULL; + if (c < 32 || c == 127) { + ldb_asprintf_errstring( + ldb, + "samldb: sAMAccountName contains invalid " + "0x%.2x character\n", c); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + p = strchr("\"[]:;|=+*?<>/\\,", c); + if (p != NULL) { + ldb_asprintf_errstring( + ldb, + "samldb: sAMAccountName contains invalid " + "'%c' character\n", c); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } + + if (i == 0) { + ldb_asprintf_errstring( + ldb, + "samldb: sAMAccountName is empty\n"); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + if (name[i - 1] == '.') { + ldb_asprintf_errstring( + ldb, + "samldb: sAMAccountName ends with '.'"); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + return LDB_SUCCESS; +} + +static int samldb_sam_account_upn_clash(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + int ret; + struct ldb_dn *base_dn = ldb_get_default_basedn(ldb); + TALLOC_CTX *tmp_ctx = NULL; + const char *real_sam = NULL; + const char *real_upn = NULL; + char *implied_sam = NULL; + char *implied_upn = NULL; + const char *realm = NULL; + + ret = samldb_get_single_valued_attr(ldb, ac, + "sAMAccountName", + &real_sam); + if (ret != LDB_SUCCESS) { + return ret; + } + ret = samldb_get_single_valued_attr(ldb, ac, + "userPrincipalName", + &real_upn); + if (ret != LDB_SUCCESS) { + return ret; + } + if (real_upn == NULL && real_sam == NULL) { + /* Not changing these things, so we're done */ + return LDB_SUCCESS; + } + + tmp_ctx = talloc_new(ac); + realm = samdb_dn_to_dns_domain(tmp_ctx, base_dn); + if (realm == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb); + } + + if (real_upn != NULL) { + /* + * note we take the last @ in the upn because the first (i.e. + * sAMAccountName equivalent) part can contain @. + * + * It is also OK (per Windows) for a UPN to have zero @s. + */ + char *at = NULL; + char *upn_realm = NULL; + implied_sam = talloc_strdup(tmp_ctx, real_upn); + if (implied_sam == NULL) { + talloc_free(tmp_ctx); + return ldb_module_oom(ac->module); + } + + at = strrchr(implied_sam, '@'); + if (at == NULL) { + /* + * there is no @ in this UPN, so we treat the whole + * thing as a sAMAccountName for the purposes of a + * clash. + */ + DBG_INFO("samldb: userPrincipalName '%s' contains " + "no '@' character\n", implied_sam); + } else { + /* + * Now, this upn only implies a sAMAccountName if the + * realm is our realm. So we need to compare the tail + * of the upn to the realm. + */ + *at = '\0'; + upn_realm = at + 1; + if (strcasecmp(upn_realm, realm) != 0) { + /* implied_sam is not the implied + * sAMAccountName after all, because it is + * from a different realm. */ + TALLOC_FREE(implied_sam); + } + } + } + + if (real_sam != NULL) { + implied_upn = talloc_asprintf(tmp_ctx, "%s@%s", + real_sam, realm); + if (implied_upn == NULL) { + talloc_free(tmp_ctx); + return ldb_module_oom(ac->module); + } + } + + /* + * Now we have all of the actual and implied names, in which to search + * for conflicts. + */ + if (real_sam != NULL) { + ret = samldb_sam_account_upn_clash_sub_search( + ac, tmp_ctx, base_dn, "sAMAccountName", + real_sam, ""); + + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + ret = samaccountname_bad_chars_check(ac, real_sam); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + if (implied_upn != NULL) { + ret = samldb_sam_account_upn_clash_sub_search( + ac, tmp_ctx, base_dn, "userPrincipalName", implied_upn, + "(implied by sAMAccountName)"); + + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + if (real_upn != NULL) { + ret = samldb_sam_account_upn_clash_sub_search( + ac, tmp_ctx, base_dn, "userPrincipalName", + real_upn, ""); + + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + if (implied_sam != NULL) { + ret = samldb_sam_account_upn_clash_sub_search( + ac, tmp_ctx, base_dn, "sAMAccountName", implied_sam, + "(implied by userPrincipalName)"); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + } + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + + +/* This is run during an add or modify */ +static int samldb_sam_accountname_valid_check(struct samldb_ctx *ac) +{ + int ret = 0; + bool is_admin; + struct security_token *user_token = NULL; + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + struct ldb_message_element *el = NULL; + + ret = dsdb_get_expected_new_values(ac, + ac->msg, + "samAccountName", + &el, + ac->req->operation); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (el == NULL || el->num_values == 0) { + ldb_asprintf_errstring(ldb, + "%08X: samldb: 'samAccountName' can't be deleted/empty!", + W_ERROR_V(WERR_DS_ILLEGAL_MOD_OPERATION)); + if (ac->req->operation == LDB_ADD) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } else { + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + + ret = samldb_unique_attr_check(ac, "samAccountName", NULL, + ldb_get_default_basedn( + ldb_module_get_ctx(ac->module))); + + /* + * Error code munging to try and match what must be some quite + * strange code-paths in Windows + */ + if (ret == LDB_ERR_CONSTRAINT_VIOLATION + && ac->req->operation == LDB_MODIFY) { + ret = LDB_ERR_ATTRIBUTE_OR_VALUE_EXISTS; + } else if (ret == LDB_ERR_OBJECT_CLASS_VIOLATION) { + ret = LDB_ERR_CONSTRAINT_VIOLATION; + } + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = samldb_sam_account_upn_clash(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (!ac->need_trailing_dollar) { + return LDB_SUCCESS; + } + + /* This does not permit a single $ */ + if (el->values[0].length < 2) { + ldb_asprintf_errstring(ldb, + "%08X: samldb: 'samAccountName' " + "can't just be one character!", + W_ERROR_V(WERR_DS_ILLEGAL_MOD_OPERATION)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + user_token = acl_user_token(ac->module); + if (user_token == NULL) { + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + + is_admin + = security_token_has_builtin_administrators(user_token); + + if (is_admin) { + /* + * Administrators are allowed to select strange names. + * This is poor practice but not prevented. + */ + return false; + } + + if (el->values[0].data[el->values[0].length - 1] != '$') { + ldb_asprintf_errstring(ldb, + "%08X: samldb: 'samAccountName' " + "must have a trailing $!", + W_ERROR_V(WERR_DS_ILLEGAL_MOD_OPERATION)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + if (el->values[0].data[el->values[0].length - 2] == '$') { + ldb_asprintf_errstring(ldb, + "%08X: samldb: 'samAccountName' " + "must not have a double trailing $!", + W_ERROR_V(WERR_DS_ILLEGAL_MOD_OPERATION)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + return ret; +} + +static int samldb_schema_attributeid_valid_check(struct samldb_ctx *ac) +{ + int ret = samldb_unique_attr_check(ac, "attributeID", "governsID", + ldb_get_schema_basedn( + ldb_module_get_ctx(ac->module))); + if (ret == LDB_ERR_ENTRY_ALREADY_EXISTS) { + ret = LDB_ERR_UNWILLING_TO_PERFORM; + } + return ret; +} + +static int samldb_schema_governsid_valid_check(struct samldb_ctx *ac) +{ + int ret = samldb_unique_attr_check(ac, "governsID", "attributeID", + ldb_get_schema_basedn( + ldb_module_get_ctx(ac->module))); + if (ret == LDB_ERR_ENTRY_ALREADY_EXISTS) { + ret = LDB_ERR_UNWILLING_TO_PERFORM; + } + return ret; +} + +static int samldb_schema_ldapdisplayname_valid_check(struct samldb_ctx *ac) +{ + int ret = samldb_unique_attr_check(ac, "lDAPDisplayName", NULL, + ldb_get_schema_basedn( + ldb_module_get_ctx(ac->module))); + if (ret == LDB_ERR_ENTRY_ALREADY_EXISTS) { + ret = LDB_ERR_UNWILLING_TO_PERFORM; + } + return ret; +} + +static int samldb_check_linkid_used(struct samldb_ctx *ac, + struct dsdb_schema *schema, + struct ldb_dn *schema_dn, + struct ldb_context *ldb, + int32_t linkID, + bool *found) +{ + int ret; + struct ldb_result *ldb_res; + + if (dsdb_attribute_by_linkID(schema, linkID)) { + *found = true; + return LDB_SUCCESS; + } + + ret = dsdb_module_search(ac->module, ac, + &ldb_res, + schema_dn, LDB_SCOPE_ONELEVEL, NULL, + DSDB_FLAG_NEXT_MODULE, + ac->req, + "(linkID=%d)", linkID); + if (ret != LDB_SUCCESS) { + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + __location__": Searching for linkID=%d failed - %s\n", + linkID, + ldb_errstring(ldb)); + return ldb_operr(ldb); + } + + *found = (ldb_res->count != 0); + talloc_free(ldb_res); + + return LDB_SUCCESS; +} + +/* Find the next open forward linkID in the schema. */ +static int samldb_generate_next_linkid(struct samldb_ctx *ac, + struct dsdb_schema *schema, + int32_t *next_linkID) +{ + int ret; + struct ldb_context *ldb; + struct ldb_dn *schema_dn; + bool linkID_used = true; + + /* + * Windows starts at about 0xB0000000 in order to stop potential + * collisions with future additions to the schema. We pass this + * around as a signed int sometimes, but this should be sufficient. + */ + *next_linkID = 0x40000000; + + ldb = ldb_module_get_ctx(ac->module); + schema_dn = ldb_get_schema_basedn(ldb); + + while (linkID_used) { + *next_linkID += 2; + ret = samldb_check_linkid_used(ac, schema, + schema_dn, ldb, + *next_linkID, &linkID_used); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + return LDB_SUCCESS; +} + +static int samldb_schema_add_handle_linkid(struct samldb_ctx *ac) +{ + int ret; + bool ok, found = false; + struct ldb_message_element *el; + const char *enc_str; + const struct dsdb_attribute *attr; + struct ldb_context *ldb; + struct ldb_dn *schema_dn; + struct dsdb_schema *schema; + int32_t new_linkID = 0; + + ldb = ldb_module_get_ctx(ac->module); + schema = dsdb_get_schema(ldb, ac); + schema_dn = ldb_get_schema_basedn(ldb); + + ret = dsdb_get_expected_new_values(ac, + ac->msg, + "linkID", + &el, + ac->req->operation); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (el == NULL || el->num_values == 0) { + return LDB_SUCCESS; + } + + enc_str = ldb_binary_encode(ac, el->values[0]); + if (enc_str == NULL) { + return ldb_module_oom(ac->module); + } + + ok = (strcmp(enc_str, "0") == 0); + if (ok) { + return LDB_SUCCESS; + } + + /* + * This OID indicates that the caller wants the linkID + * to be automatically generated. We therefore assign + * it the next open linkID. + */ + ok = (strcmp(enc_str, "1.2.840.113556.1.2.50") == 0); + if (ok) { + ret = samldb_generate_next_linkid(ac, schema, &new_linkID); + if (ret != LDB_SUCCESS) { + return ret; + } + + ldb_msg_remove_element(ac->msg, el); + ret = samdb_msg_add_int(ldb, ac->msg, ac->msg, "linkID", + new_linkID); + return ret; + } + + /* + * Using either the attributeID or lDAPDisplayName of + * another attribute in the linkID field indicates that + * we should make this the backlink of that attribute. + */ + attr = dsdb_attribute_by_attributeID_oid(schema, enc_str); + if (attr == NULL) { + attr = dsdb_attribute_by_lDAPDisplayName(schema, enc_str); + } + + if (attr != NULL) { + /* + * The attribute we're adding this as a backlink of must + * be a forward link. + */ + if (attr->linkID % 2 != 0) { + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + new_linkID = attr->linkID + 1; + + /* Make sure that this backlink doesn't already exist. */ + ret = samldb_check_linkid_used(ac, schema, + schema_dn, ldb, + new_linkID, &found); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (found) { + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + ldb_msg_remove_element(ac->msg, el); + ret = samdb_msg_add_int(ldb, ac->msg, ac->msg, "linkID", + new_linkID); + return ret; + } + + schema_dn = ldb_get_schema_basedn(ldb_module_get_ctx(ac->module)); + ret = samldb_unique_attr_check(ac, "linkID", NULL, schema_dn); + if (ret == LDB_ERR_ENTRY_ALREADY_EXISTS) { + return LDB_ERR_UNWILLING_TO_PERFORM; + } else { + return ret; + } +} + +static int samldb_check_mapiid_used(struct samldb_ctx *ac, + struct dsdb_schema *schema, + struct ldb_dn *schema_dn, + struct ldb_context *ldb, + int32_t mapiid, + bool *found) +{ + int ret; + struct ldb_result *ldb_res; + + ret = dsdb_module_search(ac->module, ac, + &ldb_res, + schema_dn, LDB_SCOPE_ONELEVEL, NULL, + DSDB_FLAG_NEXT_MODULE, + ac->req, + "(mAPIID=%d)", mapiid); + if (ret != LDB_SUCCESS) { + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + __location__": Searching for mAPIID=%d failed - %s\n", + mapiid, + ldb_errstring(ldb)); + return ldb_operr(ldb); + } + + *found = (ldb_res->count != 0); + talloc_free(ldb_res); + + return LDB_SUCCESS; +} + +static int samldb_generate_next_mapiid(struct samldb_ctx *ac, + struct dsdb_schema *schema, + int32_t *next_mapiid) +{ + int ret; + struct ldb_context *ldb; + struct ldb_dn *schema_dn; + bool mapiid_used = true; + + /* Windows' generation seems to start about here */ + *next_mapiid = 60000; + + ldb = ldb_module_get_ctx(ac->module); + schema_dn = ldb_get_schema_basedn(ldb); + + while (mapiid_used) { + *next_mapiid += 1; + ret = samldb_check_mapiid_used(ac, schema, + schema_dn, ldb, + *next_mapiid, &mapiid_used); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + return LDB_SUCCESS; +} + +static int samldb_schema_add_handle_mapiid(struct samldb_ctx *ac) +{ + int ret; + bool ok; + struct ldb_message_element *el; + const char *enc_str; + struct ldb_context *ldb; + struct ldb_dn *schema_dn; + struct dsdb_schema *schema; + int32_t new_mapiid = 0; + + /* + * The mAPIID of a new attribute should be automatically generated + * if a specific OID is put as the mAPIID, as according to + * [MS-ADTS] 3.1.1.2.3.2. + */ + + ldb = ldb_module_get_ctx(ac->module); + schema = dsdb_get_schema(ldb, ac); + schema_dn = ldb_get_schema_basedn(ldb); + + ret = dsdb_get_expected_new_values(ac, + ac->msg, + "mAPIID", + &el, + ac->req->operation); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (el == NULL || el->num_values == 0) { + return LDB_SUCCESS; + } + + enc_str = ldb_binary_encode(ac, el->values[0]); + if (enc_str == NULL) { + return ldb_module_oom(ac->module); + } + + ok = (strcmp(enc_str, "1.2.840.113556.1.2.49") == 0); + if (ok) { + ret = samldb_generate_next_mapiid(ac, schema, + &new_mapiid); + if (ret != LDB_SUCCESS) { + return ret; + } + + ldb_msg_remove_element(ac->msg, el); + ret = samdb_msg_add_int(ldb, ac->msg, ac->msg, + "mAPIID", new_mapiid); + return ret; + } + + schema_dn = ldb_get_schema_basedn(ldb_module_get_ctx(ac->module)); + ret = samldb_unique_attr_check(ac, "mAPIID", NULL, schema_dn); + if (ret == LDB_ERR_ENTRY_ALREADY_EXISTS) { + return LDB_ERR_UNWILLING_TO_PERFORM; + } else { + return ret; + } +} + +/* sAMAccountName handling */ +static int samldb_generate_sAMAccountName(struct samldb_ctx *ac, + struct ldb_message *msg) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + char *name; + + /* + * This is currently a Samba-only behaviour, to add a trailing + * $ even for the generated accounts. + */ + + if (ac->need_trailing_dollar) { + /* Format: $000000-00000000000$ */ + name = talloc_asprintf(msg, "$%.6X-%.6X%.5X$", + (unsigned int)generate_random(), + (unsigned int)generate_random(), + (unsigned int)generate_random()); + } else { + /* Format: $000000-000000000000 */ + + name = talloc_asprintf(msg, "$%.6X-%.6X%.6X", + (unsigned int)generate_random(), + (unsigned int)generate_random(), + (unsigned int)generate_random()); + } + if (name == NULL) { + return ldb_oom(ldb); + } + return ldb_msg_add_steal_string(msg, "sAMAccountName", name); +} + +static int samldb_check_sAMAccountName(struct samldb_ctx *ac) +{ + int ret; + + if (ldb_msg_find_element(ac->msg, "sAMAccountName") == NULL) { + ret = samldb_generate_sAMAccountName(ac, ac->msg); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + ret = samldb_sam_accountname_valid_check(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + + return samldb_next_step(ac); +} + + +static bool samldb_msg_add_sid(struct ldb_message *msg, + const char *name, + const struct dom_sid *sid) +{ + struct ldb_val v; + enum ndr_err_code ndr_err; + + ndr_err = ndr_push_struct_blob(&v, msg, sid, + (ndr_push_flags_fn_t)ndr_push_dom_sid); + if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + return false; + } + return (ldb_msg_add_value(msg, name, &v, NULL) == 0); +} + + +/* allocate a SID using our RID Set */ +static int samldb_allocate_sid(struct samldb_ctx *ac) +{ + uint32_t rid; + struct dom_sid *sid; + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + int ret; + + ret = ridalloc_allocate_rid(ac->module, &rid, ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + + sid = dom_sid_add_rid(ac, samdb_domain_sid(ldb), rid); + if (sid == NULL) { + return ldb_module_oom(ac->module); + } + + if ( ! samldb_msg_add_sid(ac->msg, "objectSid", sid)) { + return ldb_operr(ldb); + } + + return samldb_next_step(ac); +} + +/* + see if a krbtgt_number is available + */ +static bool samldb_krbtgtnumber_available(struct samldb_ctx *ac, + uint32_t krbtgt_number) +{ + TALLOC_CTX *tmp_ctx = talloc_new(ac); + struct ldb_result *res; + const char * const no_attrs[] = { NULL }; + int ret; + + ret = dsdb_module_search(ac->module, tmp_ctx, &res, + ldb_get_default_basedn(ldb_module_get_ctx(ac->module)), + LDB_SCOPE_SUBTREE, no_attrs, + DSDB_FLAG_NEXT_MODULE, + ac->req, + "(msDS-SecondaryKrbTgtNumber=%u)", + krbtgt_number); + if (ret == LDB_SUCCESS && res->count == 0) { + talloc_free(tmp_ctx); + return true; + } + talloc_free(tmp_ctx); + return false; +} + +/* special handling for add in RODC join */ +static int samldb_rodc_add(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + uint32_t krbtgt_number, i_start, i; + int ret; + struct ldb_val newpass_utf16; + + /* find a unused msDS-SecondaryKrbTgtNumber */ + i_start = generate_random() & 0xFFFF; + if (i_start == 0) { + i_start = 1; + } + + for (i=i_start; i<=0xFFFF; i++) { + if (samldb_krbtgtnumber_available(ac, i)) { + krbtgt_number = i; + goto found; + } + } + for (i=1; i<i_start; i++) { + if (samldb_krbtgtnumber_available(ac, i)) { + krbtgt_number = i; + goto found; + } + } + + ldb_asprintf_errstring(ldb, + "%08X: Unable to find available msDS-SecondaryKrbTgtNumber", + W_ERROR_V(WERR_NO_SYSTEM_RESOURCES)); + return LDB_ERR_OTHER; + +found: + + ldb_msg_remove_attr(ac->msg, "msDS-SecondaryKrbTgtNumber"); + ret = samdb_msg_append_uint(ldb, ac->msg, ac->msg, + "msDS-SecondaryKrbTgtNumber", krbtgt_number, + LDB_FLAG_INTERNAL_DISABLE_VALIDATION); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + ret = ldb_msg_add_fmt(ac->msg, "sAMAccountName", "krbtgt_%u", + krbtgt_number); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + newpass_utf16 = data_blob_talloc_zero(ac->module, 256); + if (newpass_utf16.data == NULL) { + return ldb_oom(ldb); + } + /* + * Note that the password_hash module will ignore + * this value and use it's own generate_secret_buffer() + * that's why we can just use generate_random_buffer() + * here. + */ + generate_random_buffer(newpass_utf16.data, newpass_utf16.length); + ret = ldb_msg_add_steal_value(ac->msg, "clearTextPassword", &newpass_utf16); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + return samldb_next_step(ac); +} + +static int samldb_find_for_defaultObjectCategory(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + struct ldb_result *res; + const char * const no_attrs[] = { NULL }; + int ret; + + ac->res_dn = NULL; + + ret = dsdb_module_search(ac->module, ac, &res, + ac->dn, LDB_SCOPE_BASE, no_attrs, + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT + | DSDB_FLAG_NEXT_MODULE, + ac->req, + "(objectClass=classSchema)"); + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + /* Don't be pricky when the DN doesn't exist if we have the */ + /* RELAX control specified */ + if (ldb_request_get_control(ac->req, + LDB_CONTROL_RELAX_OID) == NULL) { + ldb_set_errstring(ldb, + "samldb_find_defaultObjectCategory: " + "Invalid DN for 'defaultObjectCategory'!"); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } + if ((ret != LDB_ERR_NO_SUCH_OBJECT) && (ret != LDB_SUCCESS)) { + return ret; + } + + if (ret == LDB_SUCCESS) { + /* ensure the defaultObjectCategory has a full GUID */ + struct ldb_message *m; + m = ldb_msg_new(ac->msg); + if (m == NULL) { + return ldb_oom(ldb); + } + m->dn = ac->msg->dn; + if (ldb_msg_add_string(m, "defaultObjectCategory", + ldb_dn_get_extended_linearized(m, res->msgs[0]->dn, 1)) != + LDB_SUCCESS) { + return ldb_oom(ldb); + } + m->elements[0].flags = LDB_FLAG_MOD_REPLACE; + + ret = dsdb_module_modify(ac->module, m, + DSDB_FLAG_NEXT_MODULE, + ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + + ac->res_dn = ac->dn; + + return samldb_next_step(ac); +} + +/** + * msDS-IntId attributeSchema attribute handling + * during LDB_ADD request processing + */ +static int samldb_add_handle_msDS_IntId(struct samldb_ctx *ac) +{ + int ret; + bool id_exists; + uint32_t msds_intid; + int32_t system_flags; + struct ldb_context *ldb; + struct ldb_result *ldb_res; + struct ldb_dn *schema_dn; + struct samldb_msds_intid_persistant *msds_intid_struct; + struct dsdb_schema *schema; + + ldb = ldb_module_get_ctx(ac->module); + schema_dn = ldb_get_schema_basedn(ldb); + + /* replicated update should always go through */ + if (ldb_request_get_control(ac->req, + DSDB_CONTROL_REPLICATED_UPDATE_OID)) { + return LDB_SUCCESS; + } + + /* msDS-IntId is handled by system and should never be + * passed by clients */ + if (ldb_msg_find_element(ac->msg, "msDS-IntId")) { + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* do not generate msDS-IntId if Relax control is passed */ + if (ldb_request_get_control(ac->req, LDB_CONTROL_RELAX_OID)) { + return LDB_SUCCESS; + } + + /* check Functional Level */ + if (dsdb_functional_level(ldb) < DS_DOMAIN_FUNCTION_2003) { + return LDB_SUCCESS; + } + + /* check systemFlags for SCHEMA_BASE_OBJECT flag */ + system_flags = ldb_msg_find_attr_as_int(ac->msg, "systemFlags", 0); + if (system_flags & SYSTEM_FLAG_SCHEMA_BASE_OBJECT) { + return LDB_SUCCESS; + } + schema = dsdb_get_schema(ldb, NULL); + if (!schema) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "samldb_schema_info_update: no dsdb_schema loaded"); + DEBUG(0,(__location__ ": %s\n", ldb_errstring(ldb))); + return ldb_operr(ldb); + } + + msds_intid_struct = (struct samldb_msds_intid_persistant*) ldb_get_opaque(ldb, SAMLDB_MSDS_INTID_OPAQUE); + if (!msds_intid_struct) { + msds_intid_struct = talloc(ldb, struct samldb_msds_intid_persistant); + /* Generate new value for msDs-IntId + * Value should be in 0x80000000..0xBFFFFFFF range */ + msds_intid = generate_random() % 0X3FFFFFFF; + msds_intid += 0x80000000; + msds_intid_struct->msds_intid = msds_intid; + DEBUG(2, ("No samldb_msds_intid_persistant struct, allocating a new one\n")); + } else { + msds_intid = msds_intid_struct->msds_intid; + } + + /* probe id values until unique one is found */ + do { + msds_intid++; + if (msds_intid > 0xBFFFFFFF) { + msds_intid = 0x80000001; + } + /* + * We search in the schema if we have already this + * intid (using dsdb_attribute_by_attributeID_id + * because in the range 0x80000000 0xBFFFFFFFF, + * attributeID is a DSDB_ATTID_TYPE_INTID). + * + * If so generate another random value. + * + * We have to check the DB in case someone else has + * modified the database while we are doing our + * changes too (this case should be very bery rare) in + * order to be sure. + */ + if (dsdb_attribute_by_attributeID_id(schema, msds_intid)) { + id_exists = true; + msds_intid = generate_random() % 0X3FFFFFFF; + msds_intid += 0x80000000; + continue; + } + + + ret = dsdb_module_search(ac->module, ac, + &ldb_res, + schema_dn, LDB_SCOPE_ONELEVEL, NULL, + DSDB_FLAG_NEXT_MODULE, + ac->req, + "(msDS-IntId=%d)", msds_intid); + if (ret != LDB_SUCCESS) { + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + __location__": Searching for msDS-IntId=%d failed - %s\n", + msds_intid, + ldb_errstring(ldb)); + return ldb_operr(ldb); + } + id_exists = (ldb_res->count > 0); + talloc_free(ldb_res); + + } while(id_exists); + msds_intid_struct->msds_intid = msds_intid; + ldb_set_opaque(ldb, SAMLDB_MSDS_INTID_OPAQUE, msds_intid_struct); + + return samdb_msg_add_int(ldb, ac->msg, ac->msg, "msDS-IntId", + msds_intid); +} + + +/* + * samldb_add_entry (async) + */ + +static int samldb_add_entry_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct samldb_ctx *ac; + int ret; + + ac = talloc_get_type(req->context, struct samldb_ctx); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + if (ares->type == LDB_REPLY_REFERRAL) { + return ldb_module_send_referral(ac->req, ares->referral); + } + + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + if (ares->type != LDB_REPLY_DONE) { + ldb_asprintf_errstring(ldb, "Invalid LDB reply type %d", ares->type); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + /* The caller may wish to get controls back from the add */ + ac->ares = talloc_steal(ac, ares); + + ret = samldb_next_step(ac); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + return ret; +} + +static int samldb_add_entry(struct samldb_ctx *ac) +{ + struct ldb_context *ldb; + struct ldb_request *req; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + ret = ldb_build_add_req(&req, ldb, ac, + ac->msg, + ac->req->controls, + ac, samldb_add_entry_callback, + ac->req); + LDB_REQ_SET_LOCATION(req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(ac->module, req); +} + +/* + * return true if msg carries an attributeSchema that is intended to be RODC + * filtered but is also a system-critical attribute. + */ +static bool check_rodc_critical_attribute(struct ldb_message *msg) +{ + uint32_t schemaFlagsEx, searchFlags, rodc_filtered_flags; + + schemaFlagsEx = ldb_msg_find_attr_as_uint(msg, "schemaFlagsEx", 0); + searchFlags = ldb_msg_find_attr_as_uint(msg, "searchFlags", 0); + rodc_filtered_flags = (SEARCH_FLAG_RODC_ATTRIBUTE + | SEARCH_FLAG_CONFIDENTIAL); + + if ((schemaFlagsEx & SCHEMA_FLAG_ATTR_IS_CRITICAL) && + ((searchFlags & rodc_filtered_flags) == rodc_filtered_flags)) { + return true; + } else { + return false; + } +} + + +static int samldb_fill_object(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + int ret; + + /* Add information for the different account types */ + switch(ac->type) { + case SAMLDB_TYPE_USER: { + struct ldb_control *rodc_control = ldb_request_get_control(ac->req, + LDB_CONTROL_RODC_DCPROMO_OID); + if (rodc_control != NULL) { + /* see [MS-ADTS] 3.1.1.3.4.1.23 LDAP_SERVER_RODC_DCPROMO_OID */ + rodc_control->critical = false; + ret = samldb_add_step(ac, samldb_rodc_add); + if (ret != LDB_SUCCESS) return ret; + } + + /* check if we have a valid sAMAccountName */ + ret = samldb_add_step(ac, samldb_check_sAMAccountName); + if (ret != LDB_SUCCESS) return ret; + + ret = samldb_add_step(ac, samldb_add_entry); + if (ret != LDB_SUCCESS) return ret; + break; + } + + case SAMLDB_TYPE_GROUP: { + /* check if we have a valid sAMAccountName */ + ret = samldb_add_step(ac, samldb_check_sAMAccountName); + if (ret != LDB_SUCCESS) return ret; + + ret = samldb_add_step(ac, samldb_add_entry); + if (ret != LDB_SUCCESS) return ret; + break; + } + + case SAMLDB_TYPE_CLASS: { + const char *lDAPDisplayName = NULL; + const struct ldb_val *rdn_value, *def_obj_cat_val; + unsigned int v = ldb_msg_find_attr_as_uint(ac->msg, "objectClassCategory", -2); + + /* As discussed with Microsoft through dochelp in April 2012 this is the behavior of windows*/ + if (!ldb_msg_find_element(ac->msg, "subClassOf")) { + ret = ldb_msg_add_string(ac->msg, "subClassOf", "top"); + if (ret != LDB_SUCCESS) return ret; + } + + ret = samdb_find_or_add_attribute(ldb, ac->msg, + "rdnAttId", "cn"); + if (ret != LDB_SUCCESS) return ret; + + /* do not allow one to mark an attributeSchema as RODC filtered if it + * is system-critical */ + if (check_rodc_critical_attribute(ac->msg)) { + ldb_asprintf_errstring(ldb, "Refusing schema add of %s - cannot combine critical class with RODC filtering", + ldb_dn_get_linearized(ac->msg->dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + rdn_value = ldb_dn_get_rdn_val(ac->msg->dn); + if (rdn_value == NULL) { + return ldb_operr(ldb); + } + if (!ldb_msg_find_element(ac->msg, "lDAPDisplayName")) { + /* the RDN has prefix "CN" */ + ret = ldb_msg_add_string(ac->msg, "lDAPDisplayName", + samdb_cn_to_lDAPDisplayName(ac->msg, + (const char *) rdn_value->data)); + if (ret != LDB_SUCCESS) { + ldb_oom(ldb); + return ret; + } + } + + lDAPDisplayName = ldb_msg_find_attr_as_string(ac->msg, + "lDAPDisplayName", + NULL); + ret = ldb_valid_attr_name(lDAPDisplayName); + if (ret != 1 || + lDAPDisplayName[0] == '*' || + lDAPDisplayName[0] == '@') + { + return dsdb_module_werror(ac->module, + LDB_ERR_UNWILLING_TO_PERFORM, + WERR_DS_INVALID_LDAP_DISPLAY_NAME, + "lDAPDisplayName is invalid"); + } + + if (!ldb_msg_find_element(ac->msg, "schemaIDGUID")) { + struct GUID guid; + /* a new GUID */ + guid = GUID_random(); + ret = dsdb_msg_add_guid(ac->msg, &guid, "schemaIDGUID"); + if (ret != LDB_SUCCESS) { + ldb_oom(ldb); + return ret; + } + } + + def_obj_cat_val = ldb_msg_find_ldb_val(ac->msg, + "defaultObjectCategory"); + if (def_obj_cat_val != NULL) { + /* "defaultObjectCategory" has been set by the caller. + * Do some checks for consistency. + * NOTE: The real constraint check (that + * 'defaultObjectCategory' is the DN of the new + * objectclass or any parent of it) is still incomplete. + * For now we say that 'defaultObjectCategory' is valid + * if it exists and it is of objectclass "classSchema". + */ + ac->dn = ldb_dn_from_ldb_val(ac, ldb, def_obj_cat_val); + if (ac->dn == NULL) { + ldb_set_errstring(ldb, + "Invalid DN for 'defaultObjectCategory'!"); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } else { + /* "defaultObjectCategory" has not been set by the + * caller. Use the entry DN for it. */ + ac->dn = ac->msg->dn; + + ret = ldb_msg_add_string(ac->msg, "defaultObjectCategory", + ldb_dn_alloc_linearized(ac->msg, ac->dn)); + if (ret != LDB_SUCCESS) { + ldb_oom(ldb); + return ret; + } + } + + ret = samldb_add_step(ac, samldb_add_entry); + if (ret != LDB_SUCCESS) return ret; + + /* Now perform the checks for the 'defaultObjectCategory'. The + * lookup DN was already saved in "ac->dn" */ + ret = samldb_add_step(ac, samldb_find_for_defaultObjectCategory); + if (ret != LDB_SUCCESS) return ret; + + /* -2 is not a valid objectClassCategory so it means the attribute wasn't present */ + if (v == -2) { + /* Windows 2003 does this*/ + ret = samdb_msg_add_uint(ldb, ac->msg, ac->msg, "objectClassCategory", 0); + if (ret != LDB_SUCCESS) { + return ret; + } + } + break; + } + + case SAMLDB_TYPE_ATTRIBUTE: { + const char *lDAPDisplayName = NULL; + const struct ldb_val *rdn_value; + struct ldb_message_element *el; + rdn_value = ldb_dn_get_rdn_val(ac->msg->dn); + if (rdn_value == NULL) { + return ldb_operr(ldb); + } + if (!ldb_msg_find_element(ac->msg, "lDAPDisplayName")) { + /* the RDN has prefix "CN" */ + ret = ldb_msg_add_string(ac->msg, "lDAPDisplayName", + samdb_cn_to_lDAPDisplayName(ac->msg, + (const char *) rdn_value->data)); + if (ret != LDB_SUCCESS) { + ldb_oom(ldb); + return ret; + } + } + + lDAPDisplayName = ldb_msg_find_attr_as_string(ac->msg, + "lDAPDisplayName", + NULL); + ret = ldb_valid_attr_name(lDAPDisplayName); + if (ret != 1 || + lDAPDisplayName[0] == '*' || + lDAPDisplayName[0] == '@') + { + return dsdb_module_werror(ac->module, + LDB_ERR_UNWILLING_TO_PERFORM, + WERR_DS_INVALID_LDAP_DISPLAY_NAME, + "lDAPDisplayName is invalid"); + } + + /* do not allow one to mark an attributeSchema as RODC filtered if it + * is system-critical */ + if (check_rodc_critical_attribute(ac->msg)) { + ldb_asprintf_errstring(ldb, + "samldb: refusing schema add of %s - cannot combine critical attribute with RODC filtering", + ldb_dn_get_linearized(ac->msg->dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + ret = samdb_find_or_add_attribute(ldb, ac->msg, + "isSingleValued", "FALSE"); + if (ret != LDB_SUCCESS) return ret; + + if (!ldb_msg_find_element(ac->msg, "schemaIDGUID")) { + struct GUID guid; + /* a new GUID */ + guid = GUID_random(); + ret = dsdb_msg_add_guid(ac->msg, &guid, "schemaIDGUID"); + if (ret != LDB_SUCCESS) { + ldb_oom(ldb); + return ret; + } + } + + el = ldb_msg_find_element(ac->msg, "attributeSyntax"); + if (el) { + /* + * No need to scream if there isn't as we have code later on + * that will take care of it. + */ + const struct dsdb_syntax *syntax = find_syntax_map_by_ad_oid((const char *)el->values[0].data); + if (!syntax) { + DEBUG(9, ("Can't find dsdb_syntax object for attributeSyntax %s\n", + (const char *)el->values[0].data)); + } else { + unsigned int v = ldb_msg_find_attr_as_uint(ac->msg, "oMSyntax", 0); + const struct ldb_val *val = ldb_msg_find_ldb_val(ac->msg, "oMObjectClass"); + + if (v == 0) { + ret = samdb_msg_add_uint(ldb, ac->msg, ac->msg, "oMSyntax", syntax->oMSyntax); + if (ret != LDB_SUCCESS) { + return ret; + } + } + if (!val) { + struct ldb_val val2 = ldb_val_dup(ldb, &syntax->oMObjectClass); + if (val2.length > 0) { + ret = ldb_msg_add_value(ac->msg, "oMObjectClass", &val2, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } + } + } + + /* handle msDS-IntID attribute */ + ret = samldb_add_handle_msDS_IntId(ac); + if (ret != LDB_SUCCESS) return ret; + + ret = samldb_add_step(ac, samldb_add_entry); + if (ret != LDB_SUCCESS) return ret; + break; + } + + default: + ldb_asprintf_errstring(ldb, "Invalid entry type!"); + return LDB_ERR_OPERATIONS_ERROR; + break; + } + + return samldb_first_step(ac); +} + +static int samldb_fill_foreignSecurityPrincipal_object(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = NULL; + const struct ldb_val *rdn_value = NULL; + struct ldb_message_element *sid_el = NULL; + struct dom_sid *sid = NULL; + struct ldb_control *as_system = NULL; + struct ldb_control *provision = NULL; + bool allowed = false; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + as_system = ldb_request_get_control(ac->req, LDB_CONTROL_AS_SYSTEM_OID); + if (as_system != NULL) { + allowed = true; + } + + provision = ldb_request_get_control(ac->req, LDB_CONTROL_PROVISION_OID); + if (provision != NULL) { + allowed = true; + } + + sid_el = ldb_msg_find_element(ac->msg, "objectSid"); + + if (!allowed && sid_el == NULL) { + return dsdb_module_werror(ac->module, + LDB_ERR_OBJECT_CLASS_VIOLATION, + WERR_DS_MISSING_REQUIRED_ATT, + "objectSid missing on foreignSecurityPrincipal"); + } + + if (!allowed) { + return dsdb_module_werror(ac->module, + LDB_ERR_UNWILLING_TO_PERFORM, + WERR_DS_ILLEGAL_MOD_OPERATION, + "foreignSecurityPrincipal object not allowed"); + } + + if (sid_el != NULL) { + sid = samdb_result_dom_sid(ac->msg, ac->msg, "objectSid"); + if (sid == NULL) { + ldb_set_errstring(ldb, + "samldb: invalid objectSid!"); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } + + if (sid == NULL) { + rdn_value = ldb_dn_get_rdn_val(ac->msg->dn); + if (rdn_value == NULL) { + return ldb_operr(ldb); + } + sid = dom_sid_parse_talloc(ac->msg, + (const char *)rdn_value->data); + if (sid == NULL) { + ldb_set_errstring(ldb, + "samldb: No valid SID found in ForeignSecurityPrincipal CN!"); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + if (! samldb_msg_add_sid(ac->msg, "objectSid", sid)) { + return ldb_operr(ldb); + } + } + + /* finally proceed with adding the entry */ + ret = samldb_add_step(ac, samldb_add_entry); + if (ret != LDB_SUCCESS) return ret; + + return samldb_first_step(ac); +} + +static int samldb_schema_info_update(struct samldb_ctx *ac) +{ + int ret; + struct ldb_context *ldb; + struct dsdb_schema *schema; + + /* replicated update should always go through */ + if (ldb_request_get_control(ac->req, + DSDB_CONTROL_REPLICATED_UPDATE_OID)) { + return LDB_SUCCESS; + } + + /* do not update schemaInfo during provisioning */ + if (ldb_request_get_control(ac->req, LDB_CONTROL_PROVISION_OID)) { + return LDB_SUCCESS; + } + + ldb = ldb_module_get_ctx(ac->module); + schema = dsdb_get_schema(ldb, NULL); + if (!schema) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "samldb_schema_info_update: no dsdb_schema loaded"); + DEBUG(0,(__location__ ": %s\n", ldb_errstring(ldb))); + return ldb_operr(ldb); + } + + ret = dsdb_module_schema_info_update(ac->module, schema, + DSDB_FLAG_NEXT_MODULE| + DSDB_FLAG_AS_SYSTEM, + ac->req); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "samldb_schema_info_update: dsdb_module_schema_info_update failed with %s", + ldb_errstring(ldb)); + return ret; + } + + return LDB_SUCCESS; +} + +static int samldb_prim_group_tester(struct samldb_ctx *ac, uint32_t rid); +static int samldb_check_user_account_control_rules(struct samldb_ctx *ac, + struct dom_sid *sid, + uint32_t req_uac, + uint32_t user_account_control, + uint32_t user_account_control_old, + bool is_computer_objectclass); + +/* + * "Objectclass" trigger (MS-SAMR 3.1.1.8.1) + * + * Has to be invoked on "add" operations on "user", "computer" and + * "group" objects. + * ac->msg contains the "add" + * ac->type contains the object type (main objectclass) + */ +static int samldb_objectclass_trigger(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + void *skip_allocate_sids = ldb_get_opaque(ldb, + "skip_allocate_sids"); + struct ldb_message_element *el; + struct dom_sid *sid; + int ret; + + /* make sure that "sAMAccountType" is not specified */ + el = ldb_msg_find_element(ac->msg, "sAMAccountType"); + if (el != NULL) { + ldb_set_errstring(ldb, + "samldb: sAMAccountType must not be specified!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* Step 1: objectSid assignment */ + + /* Don't allow the objectSid to be changed. But beside the RELAX + * control we have also to guarantee that it can always be set with + * SYSTEM permissions. This is needed for the "samba3sam" backend. */ + sid = samdb_result_dom_sid(ac, ac->msg, "objectSid"); + if ((sid != NULL) && (!dsdb_module_am_system(ac->module)) && + (ldb_request_get_control(ac->req, LDB_CONTROL_RELAX_OID) == NULL)) { + ldb_set_errstring(ldb, + "samldb: objectSid must not be specified!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* but generate a new SID when we do have an add operations */ + if ((sid == NULL) && (ac->req->operation == LDB_ADD) && !skip_allocate_sids) { + ret = samldb_add_step(ac, samldb_allocate_sid); + if (ret != LDB_SUCCESS) return ret; + } + + switch(ac->type) { + case SAMLDB_TYPE_USER: { + uint32_t raw_uac; + uint32_t user_account_control; + bool is_computer_objectclass; + bool uac_generated = false, uac_add_flags = false; + uint32_t default_user_account_control = UF_NORMAL_ACCOUNT; + /* Step 1.2: Default values */ + ret = dsdb_user_obj_set_defaults(ldb, ac->msg, ac->req); + if (ret != LDB_SUCCESS) return ret; + + is_computer_objectclass + = (samdb_find_attribute(ldb, + ac->msg, + "objectclass", + "computer") + != NULL); + + if (is_computer_objectclass) { + default_user_account_control + = UF_WORKSTATION_TRUST_ACCOUNT; + } + + + /* On add operations we might need to generate a + * "userAccountControl" (if it isn't specified). */ + el = ldb_msg_find_element(ac->msg, "userAccountControl"); + if (el == NULL) { + ret = samdb_msg_set_uint(ldb, ac->msg, ac->msg, + "userAccountControl", + default_user_account_control); + if (ret != LDB_SUCCESS) { + return ret; + } + uac_generated = true; + uac_add_flags = true; + } + + el = ldb_msg_find_element(ac->msg, "userAccountControl"); + SMB_ASSERT(el != NULL); + + /* Step 1.3: "userAccountControl" -> "sAMAccountType" mapping */ + user_account_control = ldb_msg_find_attr_as_uint(ac->msg, + "userAccountControl", + 0); + raw_uac = user_account_control; + /* + * "userAccountControl" = 0 or missing one of + * the types means "UF_NORMAL_ACCOUNT" + * or "UF_WORKSTATION_TRUST_ACCOUNT" (if a computer). + * See MS-SAMR 3.1.1.8.10 point 8 + */ + if ((user_account_control & UF_ACCOUNT_TYPE_MASK) == 0) { + user_account_control + = default_user_account_control + | user_account_control; + uac_generated = true; + } + + /* + * As per MS-SAMR 3.1.1.8.10 these flags have not to be set + */ + if ((user_account_control & UF_LOCKOUT) != 0) { + user_account_control &= ~UF_LOCKOUT; + uac_generated = true; + } + if ((user_account_control & UF_PASSWORD_EXPIRED) != 0) { + user_account_control &= ~UF_PASSWORD_EXPIRED; + uac_generated = true; + } + + ret = samldb_check_user_account_control_rules(ac, NULL, + raw_uac, + user_account_control, + 0, + is_computer_objectclass); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* + * Require, for non-admin modifications, a trailing $ + * for either objectclass=computer or a trust account + * type in userAccountControl + */ + if ((user_account_control + & UF_TRUST_ACCOUNT_MASK) != 0) { + ac->need_trailing_dollar = true; + } + + if (is_computer_objectclass) { + ac->need_trailing_dollar = true; + } + + /* add "sAMAccountType" attribute */ + ret = dsdb_user_obj_set_account_type(ldb, ac->msg, user_account_control, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* "isCriticalSystemObject" might be set */ + if (user_account_control & + (UF_SERVER_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT)) { + ret = ldb_msg_add_string_flags(ac->msg, "isCriticalSystemObject", + "TRUE", LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ret; + } + } else if (user_account_control & UF_WORKSTATION_TRUST_ACCOUNT) { + ret = ldb_msg_add_string_flags(ac->msg, "isCriticalSystemObject", + "FALSE", LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* Step 1.4: "userAccountControl" -> "primaryGroupID" mapping */ + if (!ldb_msg_find_element(ac->msg, "primaryGroupID")) { + uint32_t rid; + + ret = dsdb_user_obj_set_primary_group_id(ldb, ac->msg, user_account_control, &rid); + if (ret != LDB_SUCCESS) { + return ret; + } + /* + * Older AD deployments don't know about the + * RODC group + */ + if (rid == DOMAIN_RID_READONLY_DCS) { + ret = samldb_prim_group_tester(ac, rid); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } + + /* Step 1.5: Add additional flags when needed */ + /* Obviously this is done when the "userAccountControl" + * has been generated here (tested against Windows + * Server) */ + if (uac_generated) { + if (uac_add_flags) { + user_account_control |= UF_ACCOUNTDISABLE; + user_account_control |= UF_PASSWD_NOTREQD; + } + + ret = samdb_msg_set_uint(ldb, ac->msg, ac->msg, + "userAccountControl", + user_account_control); + if (ret != LDB_SUCCESS) { + return ret; + } + } + break; + } + + case SAMLDB_TYPE_GROUP: { + const char *tempstr; + + /* Step 2.2: Default values */ + tempstr = talloc_asprintf(ac->msg, "%d", + GTYPE_SECURITY_GLOBAL_GROUP); + if (tempstr == NULL) return ldb_operr(ldb); + ret = samdb_find_or_add_attribute(ldb, ac->msg, + "groupType", tempstr); + if (ret != LDB_SUCCESS) return ret; + + /* Step 2.3: "groupType" -> "sAMAccountType" */ + el = ldb_msg_find_element(ac->msg, "groupType"); + if (el != NULL) { + uint32_t group_type, account_type; + + group_type = ldb_msg_find_attr_as_uint(ac->msg, + "groupType", 0); + + /* The creation of builtin groups requires the + * RELAX control */ + if (group_type == GTYPE_SECURITY_BUILTIN_LOCAL_GROUP) { + if (ldb_request_get_control(ac->req, + LDB_CONTROL_RELAX_OID) == NULL) { + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + + account_type = ds_gtype2atype(group_type); + if (account_type == 0) { + ldb_set_errstring(ldb, "samldb: Unrecognized account type!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + ret = samdb_msg_add_uint_flags(ldb, ac->msg, ac->msg, + "sAMAccountType", + account_type, + LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ret; + } + } + break; + } + + default: + ldb_asprintf_errstring(ldb, + "Invalid entry type!"); + return LDB_ERR_OPERATIONS_ERROR; + break; + } + + return LDB_SUCCESS; +} + +/* + * "Primary group ID" trigger (MS-SAMR 3.1.1.8.2) + * + * Has to be invoked on "add" and "modify" operations on "user" and "computer" + * objects. + * ac->msg contains the "add"/"modify" message + */ + +static int samldb_prim_group_tester(struct samldb_ctx *ac, uint32_t rid) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + struct dom_sid *sid; + struct ldb_result *res; + int ret; + const char * const noattrs[] = { NULL }; + + sid = dom_sid_add_rid(ac, samdb_domain_sid(ldb), rid); + if (sid == NULL) { + return ldb_operr(ldb); + } + + ret = dsdb_module_search(ac->module, ac, &res, + ldb_get_default_basedn(ldb), + LDB_SCOPE_SUBTREE, + noattrs, DSDB_FLAG_NEXT_MODULE, + ac->req, + "(objectSid=%s)", + ldap_encode_ndr_dom_sid(ac, sid)); + if (ret != LDB_SUCCESS) { + return ret; + } + if (res->count != 1) { + talloc_free(res); + ldb_asprintf_errstring(ldb, + "Failed to find primary group with RID %u!", + rid); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + talloc_free(res); + + return LDB_SUCCESS; +} + +static int samldb_prim_group_set(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + uint32_t rid; + + rid = ldb_msg_find_attr_as_uint(ac->msg, "primaryGroupID", (uint32_t) -1); + if (rid == (uint32_t) -1) { + /* we aren't affected of any primary group set */ + return LDB_SUCCESS; + + } else if (!ldb_request_get_control(ac->req, LDB_CONTROL_RELAX_OID)) { + ldb_set_errstring(ldb, + "The primary group isn't settable on add operations!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + return samldb_prim_group_tester(ac, rid); +} + +static int samldb_prim_group_change(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + const char * const attrs[] = { + "primaryGroupID", + "memberOf", + "userAccountControl", + NULL }; + struct ldb_result *res, *group_res; + struct ldb_message_element *el; + struct ldb_message *msg; + uint32_t search_flags = + DSDB_FLAG_NEXT_MODULE | DSDB_SEARCH_SHOW_EXTENDED_DN; + uint32_t prev_rid, new_rid, uac; + struct dom_sid *prev_sid, *new_sid; + struct ldb_dn *prev_prim_group_dn, *new_prim_group_dn; + const char *new_prim_group_dn_ext_str = NULL; + struct ldb_dn *user_dn = NULL; + const char *user_dn_ext_str = NULL; + int ret; + const char * const noattrs[] = { NULL }; + + ret = dsdb_get_expected_new_values(ac, + ac->msg, + "primaryGroupID", + &el, + ac->req->operation); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (el == NULL) { + /* we are not affected */ + return LDB_SUCCESS; + } + + /* Fetch information from the existing object */ + + ret = dsdb_module_search_dn(ac->module, ac, &res, ac->msg->dn, attrs, + search_flags, ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + user_dn = res->msgs[0]->dn; + user_dn_ext_str = ldb_dn_get_extended_linearized(ac, user_dn, 1); + if (user_dn_ext_str == NULL) { + return ldb_operr(ldb); + } + + uac = ldb_msg_find_attr_as_uint(res->msgs[0], "userAccountControl", 0); + + /* Finds out the DN of the old primary group */ + + prev_rid = ldb_msg_find_attr_as_uint(res->msgs[0], "primaryGroupID", + (uint32_t) -1); + if (prev_rid == (uint32_t) -1) { + /* User objects do always have a mandatory "primaryGroupID" + * attribute. If this doesn't exist then the object is of the + * wrong type. This is the exact Windows error code */ + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + + prev_sid = dom_sid_add_rid(ac, samdb_domain_sid(ldb), prev_rid); + if (prev_sid == NULL) { + return ldb_operr(ldb); + } + + /* Finds out the DN of the new primary group + * Notice: in order to parse the primary group ID correctly we create + * a temporary message here. */ + + msg = ldb_msg_new(ac->msg); + if (msg == NULL) { + return ldb_module_oom(ac->module); + } + ret = ldb_msg_add(msg, el, 0); + if (ret != LDB_SUCCESS) { + return ret; + } + new_rid = ldb_msg_find_attr_as_uint(msg, "primaryGroupID", (uint32_t) -1); + talloc_free(msg); + if (new_rid == (uint32_t) -1) { + /* we aren't affected of any primary group change */ + return LDB_SUCCESS; + } + + if (prev_rid == new_rid) { + return LDB_SUCCESS; + } + + if ((uac & UF_SERVER_TRUST_ACCOUNT) && new_rid != DOMAIN_RID_DCS) { + ldb_asprintf_errstring(ldb, + "%08X: samldb: UF_SERVER_TRUST_ACCOUNT requires " + "primaryGroupID=%u!", + W_ERROR_V(WERR_DS_CANT_MOD_PRIMARYGROUPID), + DOMAIN_RID_DCS); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + if ((uac & UF_PARTIAL_SECRETS_ACCOUNT) && new_rid != DOMAIN_RID_READONLY_DCS) { + ldb_asprintf_errstring(ldb, + "%08X: samldb: UF_PARTIAL_SECRETS_ACCOUNT requires " + "primaryGroupID=%u!", + W_ERROR_V(WERR_DS_CANT_MOD_PRIMARYGROUPID), + DOMAIN_RID_READONLY_DCS); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + ret = dsdb_module_search(ac->module, ac, &group_res, + ldb_get_default_basedn(ldb), + LDB_SCOPE_SUBTREE, + noattrs, search_flags, + ac->req, + "(objectSid=%s)", + ldap_encode_ndr_dom_sid(ac, prev_sid)); + if (ret != LDB_SUCCESS) { + return ret; + } + if (group_res->count != 1) { + return ldb_operr(ldb); + } + prev_prim_group_dn = group_res->msgs[0]->dn; + + new_sid = dom_sid_add_rid(ac, samdb_domain_sid(ldb), new_rid); + if (new_sid == NULL) { + return ldb_operr(ldb); + } + + ret = dsdb_module_search(ac->module, ac, &group_res, + ldb_get_default_basedn(ldb), + LDB_SCOPE_SUBTREE, + noattrs, search_flags, + ac->req, + "(objectSid=%s)", + ldap_encode_ndr_dom_sid(ac, new_sid)); + if (ret != LDB_SUCCESS) { + return ret; + } + if (group_res->count != 1) { + /* Here we know if the specified new primary group candidate is + * valid or not. */ + return LDB_ERR_UNWILLING_TO_PERFORM; + } + new_prim_group_dn = group_res->msgs[0]->dn; + new_prim_group_dn_ext_str = ldb_dn_get_extended_linearized(ac, + new_prim_group_dn, 1); + if (new_prim_group_dn_ext_str == NULL) { + return ldb_operr(ldb); + } + + /* We need to be already a normal member of the new primary + * group in order to be successful. */ + el = samdb_find_attribute(ldb, res->msgs[0], "memberOf", + new_prim_group_dn_ext_str); + if (el == NULL) { + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* Remove the "member" attribute on the new primary group */ + msg = ldb_msg_new(ac->msg); + if (msg == NULL) { + return ldb_module_oom(ac->module); + } + msg->dn = new_prim_group_dn; + + ret = samdb_msg_add_delval(ldb, msg, msg, "member", user_dn_ext_str); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = dsdb_module_modify(ac->module, msg, DSDB_FLAG_NEXT_MODULE, ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + talloc_free(msg); + + /* Add a "member" attribute for the previous primary group */ + msg = ldb_msg_new(ac->msg); + if (msg == NULL) { + return ldb_module_oom(ac->module); + } + msg->dn = prev_prim_group_dn; + + ret = samdb_msg_add_addval(ldb, msg, msg, "member", user_dn_ext_str); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = dsdb_module_modify(ac->module, msg, DSDB_FLAG_NEXT_MODULE, ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + talloc_free(msg); + + return LDB_SUCCESS; +} + +static int samldb_prim_group_trigger(struct samldb_ctx *ac) +{ + int ret; + + if (ac->req->operation == LDB_ADD) { + ret = samldb_prim_group_set(ac); + } else { + ret = samldb_prim_group_change(ac); + } + + return ret; +} + +static int samldb_check_user_account_control_invariants(struct samldb_ctx *ac, + uint32_t user_account_control) +{ + size_t i; + int ret = 0; + bool need_check = false; + const struct uac_to_guid { + uint32_t uac; + bool never; + uint32_t needs; + uint32_t not_with; + const char *error_string; + } map[] = { + { + .uac = UF_TEMP_DUPLICATE_ACCOUNT, + .never = true, + .error_string = "Updating the UF_TEMP_DUPLICATE_ACCOUNT flag is never allowed" + }, + { + .uac = UF_PARTIAL_SECRETS_ACCOUNT, + .needs = UF_WORKSTATION_TRUST_ACCOUNT, + .error_string = "Setting UF_PARTIAL_SECRETS_ACCOUNT only permitted with UF_WORKSTATION_TRUST_ACCOUNT" + }, + { + .uac = UF_TRUSTED_FOR_DELEGATION, + .not_with = UF_PARTIAL_SECRETS_ACCOUNT, + .error_string = "Setting UF_TRUSTED_FOR_DELEGATION not allowed with UF_PARTIAL_SECRETS_ACCOUNT" + }, + { + .uac = UF_NORMAL_ACCOUNT, + .not_with = UF_ACCOUNT_TYPE_MASK & ~UF_NORMAL_ACCOUNT, + .error_string = "Setting more than one account type not permitted" + }, + { + .uac = UF_WORKSTATION_TRUST_ACCOUNT, + .not_with = UF_ACCOUNT_TYPE_MASK & ~UF_WORKSTATION_TRUST_ACCOUNT, + .error_string = "Setting more than one account type not permitted" + }, + { + .uac = UF_INTERDOMAIN_TRUST_ACCOUNT, + .not_with = UF_ACCOUNT_TYPE_MASK & ~UF_INTERDOMAIN_TRUST_ACCOUNT, + .error_string = "Setting more than one account type not permitted" + }, + { + .uac = UF_SERVER_TRUST_ACCOUNT, + .not_with = UF_ACCOUNT_TYPE_MASK & ~UF_SERVER_TRUST_ACCOUNT, + .error_string = "Setting more than one account type not permitted" + }, + { + .uac = UF_TRUSTED_FOR_DELEGATION, + .not_with = UF_PARTIAL_SECRETS_ACCOUNT, + .error_string = "Setting UF_TRUSTED_FOR_DELEGATION not allowed with UF_PARTIAL_SECRETS_ACCOUNT" + } + }; + + for (i = 0; i < ARRAY_SIZE(map); i++) { + if (user_account_control & map[i].uac) { + need_check = true; + break; + } + } + if (need_check == false) { + return LDB_SUCCESS; + } + + for (i = 0; i < ARRAY_SIZE(map); i++) { + uint32_t this_uac = user_account_control & map[i].uac; + if (this_uac != 0) { + if (map[i].never) { + ret = LDB_ERR_OTHER; + break; + } else if (map[i].needs != 0) { + if ((map[i].needs & user_account_control) == 0) { + ret = LDB_ERR_OTHER; + break; + } + } else if (map[i].not_with != 0) { + if ((map[i].not_with & user_account_control) != 0) { + ret = LDB_ERR_OTHER; + break; + } + } + } + } + if (ret != LDB_SUCCESS) { + switch (ac->req->operation) { + case LDB_ADD: + ldb_asprintf_errstring(ldb_module_get_ctx(ac->module), + "Failed to add %s: %s", + ldb_dn_get_linearized(ac->msg->dn), + map[i].error_string); + break; + case LDB_MODIFY: + ldb_asprintf_errstring(ldb_module_get_ctx(ac->module), + "Failed to modify %s: %s", + ldb_dn_get_linearized(ac->msg->dn), + map[i].error_string); + break; + default: + return ldb_module_operr(ac->module); + } + } + return ret; +} + +/* + * It would be best if these rules apply, always, but for now they + * apply only to non-admins + */ +static int samldb_check_user_account_control_objectclass_invariants( + struct samldb_ctx *ac, + uint32_t user_account_control, + uint32_t user_account_control_old, + bool is_computer_objectclass) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + + uint32_t old_ufa = user_account_control_old & UF_ACCOUNT_TYPE_MASK; + uint32_t new_ufa = user_account_control & UF_ACCOUNT_TYPE_MASK; + + uint32_t old_rodc = user_account_control_old & UF_PARTIAL_SECRETS_ACCOUNT; + uint32_t new_rodc = user_account_control & UF_PARTIAL_SECRETS_ACCOUNT; + + bool is_admin; + struct security_token *user_token + = acl_user_token(ac->module); + if (user_token == NULL) { + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + + is_admin + = security_token_has_builtin_administrators(user_token); + + + /* + * We want to allow changes to (eg) disable an account + * that was created wrong, only checking the + * objectclass if the account type changes. + */ + if (old_ufa == new_ufa && old_rodc == new_rodc) { + return LDB_SUCCESS; + } + + switch (new_ufa) { + case UF_NORMAL_ACCOUNT: + if (is_computer_objectclass && !is_admin) { + ldb_asprintf_errstring(ldb, + "%08X: samldb: UF_NORMAL_ACCOUNT " + "requires objectclass 'user' not 'computer'!", + W_ERROR_V(WERR_DS_MACHINE_ACCOUNT_CREATED_PRENT4)); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + break; + + case UF_INTERDOMAIN_TRUST_ACCOUNT: + if (is_computer_objectclass) { + ldb_asprintf_errstring(ldb, + "%08X: samldb: UF_INTERDOMAIN_TRUST_ACCOUNT " + "requires objectclass 'user' not 'computer'!", + W_ERROR_V(WERR_DS_MACHINE_ACCOUNT_CREATED_PRENT4)); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + break; + + case UF_WORKSTATION_TRUST_ACCOUNT: + if (!is_computer_objectclass) { + /* + * Modify of a user account account into a + * workstation without objectclass computer + * as an admin is still permitted, but not + * to make an RODC + */ + if (is_admin + && ac->req->operation == LDB_MODIFY + && new_rodc == 0) { + break; + } + ldb_asprintf_errstring(ldb, + "%08X: samldb: UF_WORKSTATION_TRUST_ACCOUNT " + "requires objectclass 'computer'!", + W_ERROR_V(WERR_DS_MACHINE_ACCOUNT_CREATED_PRENT4)); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + break; + + case UF_SERVER_TRUST_ACCOUNT: + if (!is_computer_objectclass) { + ldb_asprintf_errstring(ldb, + "%08X: samldb: UF_SERVER_TRUST_ACCOUNT " + "requires objectclass 'computer'!", + W_ERROR_V(WERR_DS_MACHINE_ACCOUNT_CREATED_PRENT4)); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + break; + + default: + ldb_asprintf_errstring(ldb, + "%08X: samldb: invalid userAccountControl[0x%08X]", + W_ERROR_V(WERR_INVALID_PARAMETER), + user_account_control); + return LDB_ERR_OTHER; + } + return LDB_SUCCESS; +} + +static int samldb_get_domain_secdesc_and_oc(struct samldb_ctx *ac, + struct security_descriptor **domain_sd, + const struct dsdb_class **objectclass) +{ + const char * const sd_attrs[] = {"ntSecurityDescriptor", "objectClass", NULL}; + struct ldb_result *res; + struct ldb_dn *domain_dn = ldb_get_default_basedn(ldb_module_get_ctx(ac->module)); + const struct dsdb_schema *schema = NULL; + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + int ret = dsdb_module_search_dn(ac->module, ac, &res, + domain_dn, + sd_attrs, + DSDB_FLAG_NEXT_MODULE | DSDB_SEARCH_SHOW_DELETED, + ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + if (res->count != 1) { + return ldb_module_operr(ac->module); + } + + schema = dsdb_get_schema(ldb, ac->req); + if (!schema) { + return ldb_module_operr(ac->module);; + } + *objectclass = dsdb_get_structural_oc_from_msg(schema, res->msgs[0]); + return dsdb_get_sd_from_ldb_message(ldb_module_get_ctx(ac->module), + ac, res->msgs[0], domain_sd); + +} + +/** + * Validate that the restriction in point 5 of MS-SAMR 3.1.1.8.10 userAccountControl is honoured + * + */ +static int samldb_check_user_account_control_acl(struct samldb_ctx *ac, + struct dom_sid *sid, + uint32_t user_account_control, + uint32_t user_account_control_old) +{ + size_t i; + int ret = 0; + bool need_acl_check = false; + struct security_token *user_token; + struct security_descriptor *domain_sd; + const struct dsdb_class *objectclass = NULL; + const struct uac_to_guid { + uint32_t uac; + uint32_t priv_to_change_from; + const char *oid; + const char *guid; + enum sec_privilege privilege; + bool delete_is_privileged; + bool admin_required; + const char *error_string; + } map[] = { + { + .uac = UF_PASSWD_NOTREQD, + .guid = GUID_DRS_UPDATE_PASSWORD_NOT_REQUIRED_BIT, + .error_string = "Adding the UF_PASSWD_NOTREQD bit in userAccountControl requires the Update-Password-Not-Required-Bit right that was not given on the Domain object" + }, + { + .uac = UF_DONT_EXPIRE_PASSWD, + .guid = GUID_DRS_UNEXPIRE_PASSWORD, + .error_string = "Adding the UF_DONT_EXPIRE_PASSWD bit in userAccountControl requires the Unexpire-Password right that was not given on the Domain object" + }, + { + .uac = UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED, + .guid = GUID_DRS_ENABLE_PER_USER_REVERSIBLY_ENCRYPTED_PASSWORD, + .error_string = "Adding the UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED bit in userAccountControl requires the Enable-Per-User-Reversibly-Encrypted-Password right that was not given on the Domain object" + }, + { + .uac = UF_SERVER_TRUST_ACCOUNT, + .guid = GUID_DRS_DS_INSTALL_REPLICA, + .error_string = "Adding the UF_SERVER_TRUST_ACCOUNT bit in userAccountControl requires the DS-Install-Replica right that was not given on the Domain object" + }, + { + .uac = UF_PARTIAL_SECRETS_ACCOUNT, + .guid = GUID_DRS_DS_INSTALL_REPLICA, + .error_string = "Adding the UF_PARTIAL_SECRETS_ACCOUNT bit in userAccountControl requires the DS-Install-Replica right that was not given on the Domain object" + }, + { + .uac = UF_WORKSTATION_TRUST_ACCOUNT, + .priv_to_change_from = UF_NORMAL_ACCOUNT, + .error_string = "Swapping UF_NORMAL_ACCOUNT to UF_WORKSTATION_TRUST_ACCOUNT requires the user to be a member of the domain admins group" + }, + { + .uac = UF_NORMAL_ACCOUNT, + .priv_to_change_from = UF_WORKSTATION_TRUST_ACCOUNT, + .error_string = "Swapping UF_WORKSTATION_TRUST_ACCOUNT to UF_NORMAL_ACCOUNT requires the user to be a member of the domain admins group" + }, + { + .uac = UF_INTERDOMAIN_TRUST_ACCOUNT, + .oid = DSDB_CONTROL_PERMIT_INTERDOMAIN_TRUST_UAC_OID, + .error_string = "Updating the UF_INTERDOMAIN_TRUST_ACCOUNT bit in userAccountControl is not permitted over LDAP. This bit is restricted to the LSA CreateTrustedDomain interface", + .delete_is_privileged = true + }, + { + .uac = UF_TRUSTED_FOR_DELEGATION, + .privilege = SEC_PRIV_ENABLE_DELEGATION, + .delete_is_privileged = true, + .error_string = "Updating the UF_TRUSTED_FOR_DELEGATION bit in userAccountControl is not permitted without the SeEnableDelegationPrivilege" + }, + { + .uac = UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, + .privilege = SEC_PRIV_ENABLE_DELEGATION, + .delete_is_privileged = true, + .error_string = "Updating the UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION bit in userAccountControl is not permitted without the SeEnableDelegationPrivilege" + } + + }; + + if (dsdb_module_am_system(ac->module)) { + return LDB_SUCCESS; + } + + for (i = 0; i < ARRAY_SIZE(map); i++) { + if (user_account_control & map[i].uac) { + need_acl_check = true; + break; + } + } + if (need_acl_check == false) { + return LDB_SUCCESS; + } + + user_token = acl_user_token(ac->module); + if (user_token == NULL) { + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + + ret = samldb_get_domain_secdesc_and_oc(ac, &domain_sd, &objectclass); + if (ret != LDB_SUCCESS) { + return ret; + } + + for (i = 0; i < ARRAY_SIZE(map); i++) { + uint32_t this_uac_new = user_account_control & map[i].uac; + uint32_t this_uac_old = user_account_control_old & map[i].uac; + if (this_uac_new != this_uac_old) { + if (this_uac_old != 0) { + if (map[i].delete_is_privileged == false) { + continue; + } + } + if (map[i].oid) { + struct ldb_control *control = ldb_request_get_control(ac->req, map[i].oid); + if (control == NULL) { + ret = LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + } else if (map[i].privilege != SEC_PRIV_INVALID) { + bool have_priv = security_token_has_privilege(user_token, + map[i].privilege); + if (have_priv == false) { + ret = LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + } else if (map[i].priv_to_change_from & user_account_control_old) { + bool is_admin = security_token_has_builtin_administrators(user_token); + if (is_admin == false) { + ret = LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + } else if (map[i].guid) { + ret = acl_check_extended_right(ac, + ac->module, + ac->req, + objectclass, + domain_sd, + user_token, + map[i].guid, + SEC_ADS_CONTROL_ACCESS, + sid); + } else { + ret = LDB_SUCCESS; + } + if (ret != LDB_SUCCESS) { + break; + } + } + } + if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) { + switch (ac->req->operation) { + case LDB_ADD: + ldb_asprintf_errstring(ldb_module_get_ctx(ac->module), + "Failed to add %s: %s", + ldb_dn_get_linearized(ac->msg->dn), + map[i].error_string); + break; + case LDB_MODIFY: + ldb_asprintf_errstring(ldb_module_get_ctx(ac->module), + "Failed to modify %s: %s", + ldb_dn_get_linearized(ac->msg->dn), + map[i].error_string); + break; + default: + return ldb_module_operr(ac->module); + } + if (map[i].guid) { + struct ldb_dn *domain_dn + = ldb_get_default_basedn(ldb_module_get_ctx(ac->module)); + dsdb_acl_debug(domain_sd, acl_user_token(ac->module), + domain_dn, + true, + 10); + } + } + return ret; +} + +static int samldb_check_user_account_control_rules(struct samldb_ctx *ac, + struct dom_sid *sid, + uint32_t req_uac, + uint32_t user_account_control, + uint32_t user_account_control_old, + bool is_computer_objectclass) +{ + int ret; + struct dsdb_control_password_user_account_control *uac = NULL; + + ret = samldb_check_user_account_control_invariants(ac, user_account_control); + if (ret != LDB_SUCCESS) { + return ret; + } + ret = samldb_check_user_account_control_objectclass_invariants(ac, + user_account_control, + user_account_control_old, + is_computer_objectclass); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = samldb_check_user_account_control_acl(ac, sid, user_account_control, user_account_control_old); + if (ret != LDB_SUCCESS) { + return ret; + } + + uac = talloc_zero(ac->req, + struct dsdb_control_password_user_account_control); + if (uac == NULL) { + return ldb_module_oom(ac->module); + } + + uac->req_flags = req_uac; + uac->old_flags = user_account_control_old; + uac->new_flags = user_account_control; + + ret = ldb_request_add_control(ac->req, + DSDB_CONTROL_PASSWORD_USER_ACCOUNT_CONTROL_OID, + false, uac); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ret; +} + + +/** + * This function is called on LDB modify operations. It performs some additions/ + * replaces on the current LDB message when "userAccountControl" changes. + */ +static int samldb_user_account_control_change(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + uint32_t old_uac; + uint32_t new_uac; + uint32_t raw_uac; + uint32_t old_ufa; + uint32_t new_ufa; + uint32_t old_uac_computed; + uint32_t clear_uac; + uint32_t old_atype; + uint32_t new_atype; + uint32_t old_pgrid; + uint32_t new_pgrid; + NTTIME old_lockoutTime; + struct ldb_message_element *el; + struct ldb_val *val; + struct ldb_val computer_val; + struct ldb_message *tmp_msg; + struct dom_sid *sid; + int ret; + struct ldb_result *res; + const char * const attrs[] = { + "objectClass", + "isCriticalSystemObject", + "userAccountControl", + "msDS-User-Account-Control-Computed", + "lockoutTime", + "objectSid", + NULL + }; + bool is_computer_objectclass = false; + bool old_is_critical = false; + bool new_is_critical = false; + + ret = dsdb_get_expected_new_values(ac, + ac->msg, + "userAccountControl", + &el, + ac->req->operation); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (el == NULL || el->num_values == 0) { + ldb_asprintf_errstring(ldb, + "%08X: samldb: 'userAccountControl' can't be deleted!", + W_ERROR_V(WERR_DS_ILLEGAL_MOD_OPERATION)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* Create a temporary message for fetching the "userAccountControl" */ + tmp_msg = ldb_msg_new(ac->msg); + if (tmp_msg == NULL) { + return ldb_module_oom(ac->module); + } + ret = ldb_msg_add(tmp_msg, el, 0); + if (ret != LDB_SUCCESS) { + return ret; + } + raw_uac = ldb_msg_find_attr_as_uint(tmp_msg, + "userAccountControl", + 0); + talloc_free(tmp_msg); + /* + * UF_LOCKOUT, UF_PASSWD_CANT_CHANGE and UF_PASSWORD_EXPIRED + * are only generated and not stored. We ignore them almost + * completely, along with unknown bits and UF_SCRIPT. + * + * The only exception is ACB_AUTOLOCK, which features in + * clear_acb when the bit is cleared in this modify operation. + * + * MS-SAMR 2.2.1.13 UF_FLAG Codes states that some bits are + * ignored by clients and servers + */ + new_uac = raw_uac & UF_SETTABLE_BITS; + + /* Fetch the old "userAccountControl" and "objectClass" */ + ret = dsdb_module_search_dn(ac->module, ac, &res, ac->msg->dn, attrs, + DSDB_FLAG_NEXT_MODULE, ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + old_uac = ldb_msg_find_attr_as_uint(res->msgs[0], "userAccountControl", 0); + if (old_uac == 0) { + return ldb_operr(ldb); + } + old_uac_computed = ldb_msg_find_attr_as_uint(res->msgs[0], + "msDS-User-Account-Control-Computed", 0); + old_lockoutTime = ldb_msg_find_attr_as_int64(res->msgs[0], + "lockoutTime", 0); + old_is_critical = ldb_msg_find_attr_as_bool(res->msgs[0], + "isCriticalSystemObject", 0); + /* + * When we do not have objectclass "computer" we cannot + * switch to a workstation or (RO)DC + */ + el = ldb_msg_find_element(res->msgs[0], "objectClass"); + if (el == NULL) { + return ldb_operr(ldb); + } + computer_val = data_blob_string_const("computer"); + val = ldb_msg_find_val(el, &computer_val); + if (val != NULL) { + is_computer_objectclass = true; + } + + old_ufa = old_uac & UF_ACCOUNT_TYPE_MASK; + old_atype = ds_uf2atype(old_ufa); + old_pgrid = ds_uf2prim_group_rid(old_uac); + + new_ufa = new_uac & UF_ACCOUNT_TYPE_MASK; + if (new_ufa == 0) { + /* + * "userAccountControl" = 0 or missing one of the + * types means "UF_NORMAL_ACCOUNT". See MS-SAMR + * 3.1.1.8.10 point 8 + */ + new_ufa = UF_NORMAL_ACCOUNT; + new_uac |= new_ufa; + } + sid = samdb_result_dom_sid(res, res->msgs[0], "objectSid"); + if (sid == NULL) { + return ldb_module_operr(ac->module); + } + + ret = samldb_check_user_account_control_rules(ac, sid, + raw_uac, + new_uac, + old_uac, + is_computer_objectclass); + if (ret != LDB_SUCCESS) { + return ret; + } + + new_atype = ds_uf2atype(new_ufa); + new_pgrid = ds_uf2prim_group_rid(new_uac); + + clear_uac = (old_uac | old_uac_computed) & ~raw_uac; + + switch (new_ufa) { + case UF_NORMAL_ACCOUNT: + new_is_critical = old_is_critical; + break; + + case UF_INTERDOMAIN_TRUST_ACCOUNT: + new_is_critical = true; + break; + + case UF_WORKSTATION_TRUST_ACCOUNT: + new_is_critical = false; + if (new_uac & UF_PARTIAL_SECRETS_ACCOUNT) { + new_is_critical = true; + } + break; + + case UF_SERVER_TRUST_ACCOUNT: + new_is_critical = true; + break; + + default: + ldb_asprintf_errstring(ldb, + "%08X: samldb: invalid userAccountControl[0x%08X]", + W_ERROR_V(WERR_INVALID_PARAMETER), raw_uac); + return LDB_ERR_OTHER; + } + + if (old_atype != new_atype) { + ret = samdb_msg_append_uint(ldb, ac->msg, ac->msg, + "sAMAccountType", new_atype, + LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* As per MS-SAMR 3.1.1.8.10 these flags have not to be set */ + if ((clear_uac & UF_LOCKOUT) && (old_lockoutTime != 0)) { + /* "lockoutTime" reset as per MS-SAMR 3.1.1.8.10 */ + ldb_msg_remove_attr(ac->msg, "lockoutTime"); + ret = samdb_msg_append_uint64(ldb, ac->msg, ac->msg, "lockoutTime", + (NTTIME)0, LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* + * "isCriticalSystemObject" might be set/changed + * + * Even a change from UF_NORMAL_ACCOUNT (implicitly FALSE) to + * UF_WORKSTATION_TRUST_ACCOUNT (actually FALSE) triggers + * creating the attribute. + */ + if (old_is_critical != new_is_critical || old_atype != new_atype) { + ret = ldb_msg_append_string(ac->msg, "isCriticalSystemObject", + new_is_critical ? "TRUE": "FALSE", + LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + if (!ldb_msg_find_element(ac->msg, "primaryGroupID") && + (old_pgrid != new_pgrid)) { + /* Older AD deployments don't know about the RODC group */ + if (new_pgrid == DOMAIN_RID_READONLY_DCS) { + ret = samldb_prim_group_tester(ac, new_pgrid); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + ret = samdb_msg_append_uint(ldb, ac->msg, ac->msg, + "primaryGroupID", new_pgrid, + LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* Propagate eventual "userAccountControl" attribute changes */ + if (old_uac != new_uac) { + char *tempstr = talloc_asprintf(ac->msg, "%d", + new_uac); + if (tempstr == NULL) { + return ldb_module_oom(ac->module); + } + + ret = ldb_msg_add_empty(ac->msg, + "userAccountControl", + LDB_FLAG_MOD_REPLACE, + &el); + el->values = talloc(ac->msg, struct ldb_val); + el->num_values = 1; + el->values[0].data = (uint8_t *) tempstr; + el->values[0].length = strlen(tempstr); + } else { + ldb_msg_remove_attr(ac->msg, "userAccountControl"); + } + + return LDB_SUCCESS; +} + +static int samldb_check_pwd_last_set_acl(struct samldb_ctx *ac, + struct dom_sid *sid) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + int ret = 0; + struct security_token *user_token = NULL; + struct security_descriptor *domain_sd = NULL; + struct ldb_dn *domain_dn = ldb_get_default_basedn(ldb_module_get_ctx(ac->module)); + const char *operation = ""; + const struct dsdb_class *objectclass = NULL; + + if (dsdb_module_am_system(ac->module)) { + return LDB_SUCCESS; + } + + switch (ac->req->operation) { + case LDB_ADD: + operation = "add"; + break; + case LDB_MODIFY: + operation = "modify"; + break; + default: + return ldb_module_operr(ac->module); + } + + user_token = acl_user_token(ac->module); + if (user_token == NULL) { + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + + ret = samldb_get_domain_secdesc_and_oc(ac, &domain_sd, &objectclass); + if (ret != LDB_SUCCESS) { + return ret; + } + ret = acl_check_extended_right(ac, + ac->module, + ac->req, + objectclass, + domain_sd, + user_token, + GUID_DRS_UNEXPIRE_PASSWORD, + SEC_ADS_CONTROL_ACCESS, + sid); + if (ret != LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) { + return ret; + } + + ldb_debug_set(ldb, LDB_DEBUG_WARNING, + "Failed to %s %s: " + "Setting pwdLastSet to -1 requires the " + "Unexpire-Password right that was not given " + "on the Domain object", + operation, + ldb_dn_get_linearized(ac->msg->dn)); + dsdb_acl_debug(domain_sd, user_token, + domain_dn, true, 10); + + return ret; +} + +/** + * This function is called on LDB modify operations. It performs some additions/ + * replaces on the current LDB message when "pwdLastSet" changes. + */ +static int samldb_pwd_last_set_change(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + NTTIME last_set = 0; + struct ldb_message_element *el = NULL; + struct ldb_message *tmp_msg = NULL; + struct dom_sid *self_sid = NULL; + int ret; + struct ldb_result *res = NULL; + const char * const attrs[] = { + "objectSid", + NULL + }; + + ret = dsdb_get_expected_new_values(ac, + ac->msg, + "pwdLastSet", + &el, + ac->req->operation); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (el == NULL || el->num_values == 0) { + ldb_asprintf_errstring(ldb, + "%08X: samldb: 'pwdLastSet' can't be deleted!", + W_ERROR_V(WERR_DS_ILLEGAL_MOD_OPERATION)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* Create a temporary message for fetching the "userAccountControl" */ + tmp_msg = ldb_msg_new(ac->msg); + if (tmp_msg == NULL) { + return ldb_module_oom(ac->module); + } + ret = ldb_msg_add(tmp_msg, el, 0); + if (ret != LDB_SUCCESS) { + return ret; + } + last_set = samdb_result_nttime(tmp_msg, "pwdLastSet", 0); + talloc_free(tmp_msg); + + /* + * Setting -1 (0xFFFFFFFFFFFFFFFF) requires the Unexpire-Password right + */ + if (last_set != UINT64_MAX) { + return LDB_SUCCESS; + } + + /* Fetch the "objectSid" */ + ret = dsdb_module_search_dn(ac->module, ac, &res, ac->msg->dn, attrs, + DSDB_FLAG_NEXT_MODULE, ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + self_sid = samdb_result_dom_sid(res, res->msgs[0], "objectSid"); + if (self_sid == NULL) { + return ldb_module_operr(ac->module); + } + + ret = samldb_check_pwd_last_set_acl(ac, self_sid); + if (ret != LDB_SUCCESS) { + return ret; + } + + return LDB_SUCCESS; +} + +static int samldb_lockout_time(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + NTTIME lockoutTime; + struct ldb_message_element *el; + struct ldb_message *tmp_msg; + int ret; + + ret = dsdb_get_expected_new_values(ac, + ac->msg, + "lockoutTime", + &el, + ac->req->operation); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (el == NULL || el->num_values == 0) { + ldb_asprintf_errstring(ldb, + "%08X: samldb: 'lockoutTime' can't be deleted!", + W_ERROR_V(WERR_DS_ILLEGAL_MOD_OPERATION)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* Create a temporary message for fetching the "lockoutTime" */ + tmp_msg = ldb_msg_new(ac->msg); + if (tmp_msg == NULL) { + return ldb_module_oom(ac->module); + } + ret = ldb_msg_add(tmp_msg, el, 0); + if (ret != LDB_SUCCESS) { + return ret; + } + lockoutTime = ldb_msg_find_attr_as_int64(tmp_msg, + "lockoutTime", + 0); + talloc_free(tmp_msg); + + if (lockoutTime != 0) { + return LDB_SUCCESS; + } + + /* lockoutTime == 0 resets badPwdCount */ + ldb_msg_remove_attr(ac->msg, "badPwdCount"); + ret = samdb_msg_append_int(ldb, ac->msg, ac->msg, + "badPwdCount", 0, + LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ret; + } + + return LDB_SUCCESS; +} + +static int samldb_group_type_change(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + uint32_t group_type, old_group_type, account_type; + struct ldb_message_element *el; + struct ldb_message *tmp_msg; + int ret; + struct ldb_result *res; + const char * const attrs[] = { "groupType", NULL }; + + ret = dsdb_get_expected_new_values(ac, + ac->msg, + "groupType", + &el, + ac->req->operation); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (el == NULL) { + /* we are not affected */ + return LDB_SUCCESS; + } + + /* Create a temporary message for fetching the "groupType" */ + tmp_msg = ldb_msg_new(ac->msg); + if (tmp_msg == NULL) { + return ldb_module_oom(ac->module); + } + ret = ldb_msg_add(tmp_msg, el, 0); + if (ret != LDB_SUCCESS) { + return ret; + } + group_type = ldb_msg_find_attr_as_uint(tmp_msg, "groupType", 0); + talloc_free(tmp_msg); + + ret = dsdb_module_search_dn(ac->module, ac, &res, ac->msg->dn, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_DELETED, ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + old_group_type = ldb_msg_find_attr_as_uint(res->msgs[0], "groupType", 0); + if (old_group_type == 0) { + return ldb_operr(ldb); + } + + /* Group type switching isn't so easy as it seems: We can only + * change in this directions: global <-> universal <-> local + * On each step also the group type itself + * (security/distribution) is variable. */ + + if (ldb_request_get_control(ac->req, LDB_CONTROL_PROVISION_OID) == NULL) { + switch (group_type) { + case GTYPE_SECURITY_GLOBAL_GROUP: + case GTYPE_DISTRIBUTION_GLOBAL_GROUP: + /* change to "universal" allowed */ + if ((old_group_type == GTYPE_SECURITY_DOMAIN_LOCAL_GROUP) || + (old_group_type == GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)) { + ldb_set_errstring(ldb, + "samldb: Change from security/distribution local group forbidden!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + break; + + case GTYPE_SECURITY_UNIVERSAL_GROUP: + case GTYPE_DISTRIBUTION_UNIVERSAL_GROUP: + /* each change allowed */ + break; + case GTYPE_SECURITY_DOMAIN_LOCAL_GROUP: + case GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP: + /* change to "universal" allowed */ + if ((old_group_type == GTYPE_SECURITY_GLOBAL_GROUP) || + (old_group_type == GTYPE_DISTRIBUTION_GLOBAL_GROUP)) { + ldb_set_errstring(ldb, + "samldb: Change from security/distribution global group forbidden!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + break; + + case GTYPE_SECURITY_BUILTIN_LOCAL_GROUP: + default: + /* we don't allow this "groupType" values */ + return LDB_ERR_UNWILLING_TO_PERFORM; + break; + } + } + + account_type = ds_gtype2atype(group_type); + if (account_type == 0) { + ldb_set_errstring(ldb, "samldb: Unrecognized account type!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + ret = samdb_msg_append_uint(ldb, ac->msg, ac->msg, "sAMAccountType", + account_type, LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ret; + } + + return LDB_SUCCESS; +} + +static int samldb_member_check(struct samldb_ctx *ac) +{ + const char * const attrs[] = { "objectSid", NULL }; + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + struct ldb_message_element *el; + struct ldb_dn *member_dn; + struct dom_sid *sid; + struct ldb_result *res; + struct dom_sid *group_sid; + unsigned int i, j; + int ret; + + /* Fetch information from the existing object */ + + ret = dsdb_module_search(ac->module, ac, &res, ac->msg->dn, LDB_SCOPE_BASE, attrs, + DSDB_FLAG_NEXT_MODULE | DSDB_SEARCH_SHOW_DELETED, ac->req, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + if (res->count != 1) { + return ldb_operr(ldb); + } + + group_sid = samdb_result_dom_sid(res, res->msgs[0], "objectSid"); + if (group_sid == NULL) { + return ldb_operr(ldb); + } + + /* We've to walk over all modification entries and consider the "member" + * ones. */ + for (i = 0; i < ac->msg->num_elements; i++) { + if (ldb_attr_cmp(ac->msg->elements[i].name, "member") != 0) { + continue; + } + + el = &ac->msg->elements[i]; + for (j = 0; j < el->num_values; j++) { + struct ldb_result *group_res; + const char *group_attrs[] = { "primaryGroupID" , NULL }; + uint32_t prim_group_rid; + + if (LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_DELETE) { + /* Deletes will be handled in + * repl_meta_data, and deletes not + * matching a member will return + * LDB_ERR_UNWILLING_TO_PERFORM + * there */ + continue; + } + + member_dn = ldb_dn_from_ldb_val(ac, ldb, + &el->values[j]); + if (!ldb_dn_validate(member_dn)) { + return ldb_operr(ldb); + } + + /* Denies to add "member"s to groups which are primary + * ones for them - in this case return + * ERR_ENTRY_ALREADY_EXISTS. */ + + ret = dsdb_module_search_dn(ac->module, ac, &group_res, + member_dn, group_attrs, + DSDB_FLAG_NEXT_MODULE, ac->req); + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + /* member DN doesn't exist yet */ + continue; + } + if (ret != LDB_SUCCESS) { + return ret; + } + prim_group_rid = ldb_msg_find_attr_as_uint(group_res->msgs[0], "primaryGroupID", (uint32_t)-1); + if (prim_group_rid == (uint32_t) -1) { + /* the member hasn't to be a user account -> + * therefore no check needed in this case. */ + continue; + } + + sid = dom_sid_add_rid(ac, samdb_domain_sid(ldb), + prim_group_rid); + if (sid == NULL) { + return ldb_operr(ldb); + } + + if (dom_sid_equal(group_sid, sid)) { + ldb_asprintf_errstring(ldb, + "samldb: member %s already set via primaryGroupID %u", + ldb_dn_get_linearized(member_dn), prim_group_rid); + return LDB_ERR_ENTRY_ALREADY_EXISTS; + } + } + } + + talloc_free(res); + + return LDB_SUCCESS; +} + +/* SAM objects have special rules regarding the "description" attribute on + * modify operations. */ +static int samldb_description_check(struct samldb_ctx *ac, bool *modified) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + const char * const attrs[] = { "objectClass", "description", NULL }; + struct ldb_result *res; + unsigned int i; + int ret; + + /* Fetch information from the existing object */ + ret = dsdb_module_search(ac->module, ac, &res, ac->msg->dn, LDB_SCOPE_BASE, attrs, + DSDB_FLAG_NEXT_MODULE | DSDB_SEARCH_SHOW_DELETED, ac->req, + "(|(objectclass=user)(objectclass=group)(objectclass=samDomain)(objectclass=samServer))"); + if (ret != LDB_SUCCESS) { + /* don't treat it specially ... let normal error codes + happen from other places */ + ldb_reset_err_string(ldb); + return LDB_SUCCESS; + } + if (res->count == 0) { + /* we didn't match the filter */ + talloc_free(res); + return LDB_SUCCESS; + } + + /* We've to walk over all modification entries and consider the + * "description" ones. */ + for (i = 0; i < ac->msg->num_elements; i++) { + if (ldb_attr_cmp(ac->msg->elements[i].name, "description") == 0) { + ac->msg->elements[i].flags |= LDB_FLAG_INTERNAL_FORCE_SINGLE_VALUE_CHECK; + *modified = true; + } + } + + talloc_free(res); + + return LDB_SUCCESS; +} + +#define SPN_ALIAS_NONE 0 +#define SPN_ALIAS_LINK 1 +#define SPN_ALIAS_TARGET 2 + +static int find_spn_aliases(struct ldb_context *ldb, + TALLOC_CTX *mem_ctx, + const char *service_class, + char ***aliases, + size_t *n_aliases, + int *direction) +{ + /* + * If you change the way this works, you should also look at changing + * LDB_lookup_spn_alias() in source4/dsdb/samdb/cracknames.c, which + * does some of the same work. + * + * In particular, note that sPNMappings are resolved on a first come, + * first served basis. For example, if we have + * + * host=ldap,cifs + * foo=ldap + * cifs=host,alerter + * + * then 'ldap', 'cifs', and 'host' will resolve to 'host', and + * 'alerter' will resolve to 'cifs'. + * + * If this resolution method is made more complicated, then the + * cracknames function should also be changed. + */ + size_t i, j; + int ret; + bool ok; + struct ldb_result *res = NULL; + struct ldb_message_element *spnmappings = NULL; + TALLOC_CTX *tmp_ctx = NULL; + struct ldb_dn *service_dn = NULL; + + const char *attrs[] = { + "sPNMappings", + NULL + }; + + *direction = SPN_ALIAS_NONE; + + tmp_ctx = talloc_new(mem_ctx); + if (tmp_ctx == NULL) { + return ldb_oom(ldb); + } + + service_dn = ldb_dn_new( + tmp_ctx, ldb, + "CN=Directory Service,CN=Windows NT,CN=Services"); + if (service_dn == NULL) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + + ok = ldb_dn_add_base(service_dn, ldb_get_config_basedn(ldb)); + if (! ok) { + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = ldb_search(ldb, tmp_ctx, &res, service_dn, LDB_SCOPE_BASE, + attrs, "(objectClass=nTDSService)"); + + if (ret != LDB_SUCCESS || res->count != 1) { + DBG_WARNING("sPNMappings not found.\n"); + talloc_free(tmp_ctx); + return ret; + } + + spnmappings = ldb_msg_find_element(res->msgs[0], "sPNMappings"); + if (spnmappings == NULL || spnmappings->num_values == 0) { + DBG_WARNING("no sPNMappings attribute\n"); + talloc_free(tmp_ctx); + return LDB_ERR_NO_SUCH_OBJECT; + } + *n_aliases = 0; + + for (i = 0; i < spnmappings->num_values; i++) { + char *p = NULL; + char *mapping = talloc_strndup( + tmp_ctx, + (char *)spnmappings->values[i].data, + spnmappings->values[i].length); + if (mapping == NULL) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + + p = strchr(mapping, '='); + if (p == NULL) { + talloc_free(tmp_ctx); + return LDB_ERR_ALIAS_PROBLEM; + } + p[0] = '\0'; + p++; + + if (strcasecmp(mapping, service_class) == 0) { + /* + * We need to return the reverse aliases for this one. + * + * typically, this means the service_class is "host" + * and the mapping is "host=alerter,appmgmt,cisvc,..", + * so we get "alerter", "appmgmt", etc in the list of + * aliases. + */ + + /* There is one more field than there are commas */ + size_t n = 1; + + for (j = 0; p[j] != '\0'; j++) { + if (p[j] == ',') { + n++; + p[j] = '\0'; + } + } + *aliases = talloc_array(mem_ctx, char*, n); + if (*aliases == NULL) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + *n_aliases = n; + talloc_steal(mem_ctx, mapping); + for (j = 0; j < n; j++) { + (*aliases)[j] = p; + p += strlen(p) + 1; + } + talloc_free(tmp_ctx); + *direction = SPN_ALIAS_LINK; + return LDB_SUCCESS; + } + /* + * We need to look along the list to see if service_class is + * there; if so, we return a list of one item (probably "host"). + */ + do { + char *str = p; + p = strchr(p, ','); + if (p != NULL) { + p[0] = '\0'; + p++; + } + if (strcasecmp(str, service_class) == 0) { + *aliases = talloc_array(mem_ctx, char*, 1); + if (*aliases == NULL) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + *n_aliases = 1; + (*aliases)[0] = mapping; + talloc_steal(mem_ctx, mapping); + talloc_free(tmp_ctx); + *direction = SPN_ALIAS_TARGET; + return LDB_SUCCESS; + } + } while (p != NULL); + } + DBG_INFO("no sPNMappings alias for '%s'\n", service_class); + talloc_free(tmp_ctx); + *aliases = NULL; + *n_aliases = 0; + return LDB_SUCCESS; +} + + +static int get_spn_dn(struct ldb_context *ldb, + TALLOC_CTX *tmp_ctx, + const char *candidate, + struct ldb_dn **dn) +{ + int ret; + const char *empty_attrs[] = { NULL }; + struct ldb_message *msg = NULL; + struct ldb_dn *base_dn = ldb_get_default_basedn(ldb); + + const char *enc_candidate = NULL; + + *dn = NULL; + + enc_candidate = ldb_binary_encode_string(tmp_ctx, candidate); + if (enc_candidate == NULL) { + return ldb_operr(ldb); + } + + ret = dsdb_search_one(ldb, + tmp_ctx, + &msg, + base_dn, + LDB_SCOPE_SUBTREE, + empty_attrs, + 0, + "(servicePrincipalName=%s)", + enc_candidate); + if (ret != LDB_SUCCESS) { + return ret; + } + *dn = msg->dn; + return LDB_SUCCESS; +} + + +static int check_spn_write_rights(struct ldb_context *ldb, + TALLOC_CTX *mem_ctx, + const char *spn, + struct ldb_dn *dn) +{ + int ret; + struct ldb_message *msg = NULL; + struct ldb_message_element *del_el = NULL; + struct ldb_message_element *add_el = NULL; + struct ldb_val val = { + .data = discard_const_p(uint8_t, spn), + .length = strlen(spn) + }; + + msg = ldb_msg_new(mem_ctx); + if (msg == NULL) { + return ldb_oom(ldb); + } + msg->dn = dn; + + ret = ldb_msg_add_empty(msg, + "servicePrincipalName", + LDB_FLAG_MOD_DELETE, + &del_el); + if (ret != LDB_SUCCESS) { + talloc_free(msg); + return ret; + } + + del_el->values = talloc_array(msg->elements, struct ldb_val, 1); + if (del_el->values == NULL) { + talloc_free(msg); + return ret; + } + + del_el->values[0] = val; + del_el->num_values = 1; + + ret = ldb_msg_add_empty(msg, + "servicePrincipalName", + LDB_FLAG_MOD_ADD, + &add_el); + if (ret != LDB_SUCCESS) { + talloc_free(msg); + return ret; + } + + add_el->values = talloc_array(msg->elements, struct ldb_val, 1); + if (add_el->values == NULL) { + talloc_free(msg); + return ret; + } + + add_el->values[0] = val; + add_el->num_values = 1; + + ret = ldb_modify(ldb, msg); + if (ret == LDB_ERR_NO_SUCH_ATTRIBUTE) { + DBG_ERR("hmm I think we're OK, but not sure\n"); + } else if (ret != LDB_SUCCESS) { + DBG_ERR("SPN write rights check failed with %d\n", ret); + talloc_free(msg); + return ret; + } + talloc_free(msg); + return LDB_SUCCESS; +} + + +static int check_spn_alias_collision(struct ldb_context *ldb, + TALLOC_CTX *mem_ctx, + const char *spn, + struct ldb_dn *target_dn) +{ + int ret; + char *service_class = NULL; + char *spn_tail = NULL; + char *p = NULL; + char **aliases = NULL; + size_t n_aliases = 0; + size_t i, len; + TALLOC_CTX *tmp_ctx = NULL; + const char *target_dnstr = ldb_dn_get_linearized(target_dn); + int link_direction; + + tmp_ctx = talloc_new(mem_ctx); + if (tmp_ctx == NULL) { + return ldb_oom(ldb); + } + + /* + * "dns/example.com/xxx" gives + * service_class = "dns" + * spn_tail = "example.com/xxx" + */ + p = strchr(spn, '/'); + if (p == NULL) { + /* bad SPN */ + talloc_free(tmp_ctx); + return ldb_error(ldb, + LDB_ERR_OPERATIONS_ERROR, + "malformed servicePrincipalName"); + } + len = p - spn; + + service_class = talloc_strndup(tmp_ctx, spn, len); + if (service_class == NULL) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + spn_tail = p + 1; + + ret = find_spn_aliases(ldb, + tmp_ctx, + service_class, + &aliases, + &n_aliases, + &link_direction); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + /* + * we have the list of aliases, and now we need to combined them with + * spn_tail and see if we can find the SPN. + */ + for (i = 0; i < n_aliases; i++) { + struct ldb_dn *colliding_dn = NULL; + const char *colliding_dnstr = NULL; + + char *candidate = talloc_asprintf(tmp_ctx, + "%s/%s", + aliases[i], + spn_tail); + if (candidate == NULL) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + + ret = get_spn_dn(ldb, tmp_ctx, candidate, &colliding_dn); + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + DBG_DEBUG("SPN alias '%s' not found (good)\n", + candidate); + talloc_free(candidate); + continue; + } + if (ret != LDB_SUCCESS) { + DBG_ERR("SPN '%s' search error %d\n", candidate, ret); + talloc_free(tmp_ctx); + return ret; + } + + target_dnstr = ldb_dn_get_linearized(target_dn); + /* + * We have found an existing SPN that matches the alias. That + * is OK only if it is on the object we are trying to add to, + * or if the SPN on the other side is a more generic alias for + * this one and we also have rights to modify it. + * + * That is, we can put "host/X" and "cifs/X" on the same + * object, but not on different objects, unless we put the + * host/X on first, and could also change that object when we + * add cifs/X. It is forbidden to add the objects in the other + * order. + * + * The rationale for this is that adding "cifs/X" effectively + * changes "host/X" by diverting traffic. If "host/X" can be + * added after "cifs/X", a sneaky person could get "cifs/X" in + * first, making "host/X" have less effect than intended. + * + * Note: we also can't have "host/X" and "Host/X" on the same + * object, but that is not relevant here. + */ + + ret = ldb_dn_compare(colliding_dn, target_dn); + if (ret != 0) { + colliding_dnstr = ldb_dn_get_linearized(colliding_dn); + DBG_ERR("trying to add SPN '%s' on '%s' when '%s' is " + "on '%s'\n", + spn, + target_dnstr, + candidate, + colliding_dnstr); + + if (link_direction == SPN_ALIAS_LINK) { + /* we don't allow host/X if there is a + * cifs/X */ + talloc_free(tmp_ctx); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + ret = check_spn_write_rights(ldb, + tmp_ctx, + candidate, + colliding_dn); + if (ret != LDB_SUCCESS) { + DBG_ERR("SPN '%s' is on '%s' so '%s' can't be " + "added to '%s'\n", + candidate, + colliding_dnstr, + spn, + target_dnstr); + talloc_free(tmp_ctx); + ldb_asprintf_errstring(ldb, + "samldb: spn[%s] would cause a conflict", + spn); + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + } else { + DBG_INFO("SPNs '%s' and '%s' alias both on '%s'\n", + candidate, spn, target_dnstr); + } + talloc_free(candidate); + } + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + +static int check_spn_direct_collision(struct ldb_context *ldb, + TALLOC_CTX *mem_ctx, + const char *spn, + struct ldb_dn *target_dn) +{ + int ret; + TALLOC_CTX *tmp_ctx = NULL; + struct ldb_dn *colliding_dn = NULL; + const char *target_dnstr = NULL; + const char *colliding_dnstr = NULL; + + tmp_ctx = talloc_new(mem_ctx); + if (tmp_ctx == NULL) { + return ldb_oom(ldb); + } + + ret = get_spn_dn(ldb, tmp_ctx, spn, &colliding_dn); + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + DBG_DEBUG("SPN '%s' not found (good)\n", spn); + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } + if (ret != LDB_SUCCESS) { + DBG_ERR("SPN '%s' search error %d\n", spn, ret); + talloc_free(tmp_ctx); + if (ret == LDB_ERR_COMPARE_TRUE) { + /* + * COMPARE_TRUE has special meaning here and we don't + * want to return it by mistake. + */ + ret = LDB_ERR_OPERATIONS_ERROR; + } + return ret; + } + /* + * We have found this exact SPN. This is mostly harmless (depend on + * ADD vs REPLACE) when the spn is being put on the object that + * already has, so we let it through to succeed or fail as some other + * module sees fit. + */ + target_dnstr = ldb_dn_get_linearized(target_dn); + ret = ldb_dn_compare(colliding_dn, target_dn); + if (ret != 0) { + colliding_dnstr = ldb_dn_get_linearized(colliding_dn); + DBG_ERR("SPN '%s' is on '%s' so it can't be " + "added to '%s'\n", + spn, + colliding_dnstr, + target_dnstr); + ldb_asprintf_errstring(ldb, + "samldb: spn[%s] would cause a conflict", + spn); + talloc_free(tmp_ctx); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + DBG_INFO("SPN '%s' is already on '%s'\n", + spn, target_dnstr); + talloc_free(tmp_ctx); + return LDB_ERR_COMPARE_TRUE; +} + + +static int count_spn_components(struct ldb_val val) +{ + /* + * a 3 part servicePrincipalName has two slashes, like + * ldap/example.com/DomainDNSZones.example.com. + * + * In krb5_parse_name_flags() we don't count "\/" as a slash (i.e. + * escaped by a backslash), but this is not the behaviour of Windows + * on setting a servicePrincipalName -- slashes are counted regardless + * of backslashes. + * + * Accordingly, here we ignore backslashes. This will reject + * multi-slash SPNs that krb5_parse_name_flags() would accept, and + * allow ones in the form "a\/b" that it won't parse. + */ + size_t i; + int slashes = 0; + for (i = 0; i < val.length; i++) { + char c = val.data[i]; + if (c == '/') { + slashes++; + if (slashes == 3) { + /* at this point we don't care */ + return 4; + } + } + } + return slashes + 1; +} + + +/* Check that "servicePrincipalName" changes do not introduce a collision + * globally. */ +static int samldb_spn_uniqueness_check(struct samldb_ctx *ac, + struct ldb_message_element *spn_el) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + int ret; + const char *spn = NULL; + size_t i; + TALLOC_CTX *tmp_ctx = talloc_new(ac->msg); + if (tmp_ctx == NULL) { + return ldb_oom(ldb); + } + + for (i = 0; i < spn_el->num_values; i++) { + int n_components; + spn = (char *)spn_el->values[i].data; + + n_components = count_spn_components(spn_el->values[i]); + if (n_components > 3 || n_components < 2) { + ldb_asprintf_errstring(ldb, + "samldb: spn[%s] invalid with %u components", + spn, n_components); + talloc_free(tmp_ctx); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + ret = check_spn_direct_collision(ldb, + tmp_ctx, + spn, + ac->msg->dn); + if (ret == LDB_ERR_COMPARE_TRUE) { + DBG_INFO("SPN %s re-added to the same object\n", spn); + continue; + } + if (ret != LDB_SUCCESS) { + DBG_ERR("SPN %s failed direct uniqueness check\n", spn); + talloc_free(tmp_ctx); + return ret; + } + + ret = check_spn_alias_collision(ldb, + tmp_ctx, + spn, + ac->msg->dn); + + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + /* we have no sPNMappings, hence no aliases */ + break; + } + if (ret != LDB_SUCCESS) { + DBG_ERR("SPN %s failed alias uniqueness check\n", spn); + talloc_free(tmp_ctx); + return ret; + } + DBG_INFO("SPN %s seems to be unique\n", spn); + } + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + + + +/* This trigger adapts the "servicePrincipalName" attributes if the + * "dNSHostName" and/or "sAMAccountName" attribute change(s) */ +static int samldb_service_principal_names_change(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + struct ldb_message_element *el = NULL, *el2 = NULL; + struct ldb_message *msg; + const char * const attrs[] = { "servicePrincipalName", NULL }; + struct ldb_result *res; + const char *dns_hostname = NULL, *old_dns_hostname = NULL, + *sam_accountname = NULL, *old_sam_accountname = NULL; + unsigned int i, j; + int ret; + + ret = dsdb_get_expected_new_values(ac, + ac->msg, + "dNSHostName", + &el, + ac->req->operation); + if (ret != LDB_SUCCESS) { + return ret; + } + ret = dsdb_get_expected_new_values(ac, + ac->msg, + "sAMAccountName", + &el2, + ac->req->operation); + if (ret != LDB_SUCCESS) { + return ret; + } + if ((el == NULL) && (el2 == NULL)) { + /* we are not affected */ + return LDB_SUCCESS; + } + + /* Create a temporary message for fetching the "dNSHostName" */ + if (el != NULL) { + const char *dns_attrs[] = { "dNSHostName", NULL }; + msg = ldb_msg_new(ac->msg); + if (msg == NULL) { + return ldb_module_oom(ac->module); + } + ret = ldb_msg_add(msg, el, 0); + if (ret != LDB_SUCCESS) { + return ret; + } + dns_hostname = talloc_strdup(ac, + ldb_msg_find_attr_as_string(msg, "dNSHostName", NULL)); + if (dns_hostname == NULL) { + return ldb_module_oom(ac->module); + } + + talloc_free(msg); + + ret = dsdb_module_search_dn(ac->module, ac, &res, ac->msg->dn, + dns_attrs, DSDB_FLAG_NEXT_MODULE, ac->req); + if (ret == LDB_SUCCESS) { + old_dns_hostname = ldb_msg_find_attr_as_string(res->msgs[0], "dNSHostName", NULL); + } + } + + /* Create a temporary message for fetching the "sAMAccountName" */ + if (el2 != NULL) { + char *tempstr, *tempstr2 = NULL; + const char *acct_attrs[] = { "sAMAccountName", NULL }; + + msg = ldb_msg_new(ac->msg); + if (msg == NULL) { + return ldb_module_oom(ac->module); + } + ret = ldb_msg_add(msg, el2, 0); + if (ret != LDB_SUCCESS) { + return ret; + } + tempstr = talloc_strdup(ac, + ldb_msg_find_attr_as_string(msg, "sAMAccountName", NULL)); + talloc_free(msg); + + ret = dsdb_module_search_dn(ac->module, ac, &res, ac->msg->dn, acct_attrs, + DSDB_FLAG_NEXT_MODULE, ac->req); + if (ret == LDB_SUCCESS) { + tempstr2 = talloc_strdup(ac, + ldb_msg_find_attr_as_string(res->msgs[0], + "sAMAccountName", NULL)); + } + + + /* The "sAMAccountName" needs some additional trimming: we need + * to remove the trailing "$"s if they exist. */ + if ((tempstr != NULL) && (tempstr[0] != '\0') && + (tempstr[strlen(tempstr) - 1] == '$')) { + tempstr[strlen(tempstr) - 1] = '\0'; + } + if ((tempstr2 != NULL) && (tempstr2[0] != '\0') && + (tempstr2[strlen(tempstr2) - 1] == '$')) { + tempstr2[strlen(tempstr2) - 1] = '\0'; + } + sam_accountname = tempstr; + old_sam_accountname = tempstr2; + } + + if (old_dns_hostname == NULL) { + /* we cannot change when the old name is unknown */ + dns_hostname = NULL; + } + if ((old_dns_hostname != NULL) && (dns_hostname != NULL) && + (strcasecmp_m(old_dns_hostname, dns_hostname) == 0)) { + /* The "dNSHostName" didn't change */ + dns_hostname = NULL; + } + + if (old_sam_accountname == NULL) { + /* we cannot change when the old name is unknown */ + sam_accountname = NULL; + } + if ((old_sam_accountname != NULL) && (sam_accountname != NULL) && + (strcasecmp_m(old_sam_accountname, sam_accountname) == 0)) { + /* The "sAMAccountName" didn't change */ + sam_accountname = NULL; + } + + if ((dns_hostname == NULL) && (sam_accountname == NULL)) { + /* Well, there are information missing (old name(s)) or the + * names didn't change. We've nothing to do and can exit here */ + return LDB_SUCCESS; + } + + /* + * Potential "servicePrincipalName" changes in the same request have + * to be handled before the update (Windows behaviour). + * + * We extract the SPN changes into a new message and run it through + * the stack from this module, so that it subjects them to the SPN + * checks we have here. + */ + el = ldb_msg_find_element(ac->msg, "servicePrincipalName"); + if (el != NULL) { + msg = ldb_msg_new(ac->msg); + if (msg == NULL) { + return ldb_module_oom(ac->module); + } + msg->dn = ac->msg->dn; + + do { + ret = ldb_msg_add(msg, el, el->flags); + if (ret != LDB_SUCCESS) { + return ret; + } + + ldb_msg_remove_element(ac->msg, el); + + el = ldb_msg_find_element(ac->msg, + "servicePrincipalName"); + } while (el != NULL); + + ret = dsdb_module_modify(ac->module, msg, + DSDB_FLAG_OWN_MODULE, ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + talloc_free(msg); + } + + /* Fetch the "servicePrincipalName"s if any */ + ret = dsdb_module_search(ac->module, ac, &res, ac->msg->dn, LDB_SCOPE_BASE, attrs, + DSDB_FLAG_NEXT_MODULE, ac->req, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + if ((res->count != 1) || (res->msgs[0]->num_elements > 1)) { + return ldb_operr(ldb); + } + + if (res->msgs[0]->num_elements == 1) { + /* + * Yes, we do have "servicePrincipalName"s. First we update them + * locally, that means we do always substitute the current + * "dNSHostName" with the new one and/or "sAMAccountName" + * without "$" with the new one and then we append the + * modified "servicePrincipalName"s as a message element + * replace to the modification request (Windows behaviour). We + * need also to make sure that the values remain case- + * insensitively unique. + */ + + ret = ldb_msg_add_empty(ac->msg, "servicePrincipalName", + LDB_FLAG_MOD_REPLACE, &el); + if (ret != LDB_SUCCESS) { + return ret; + } + + for (i = 0; i < res->msgs[0]->elements[0].num_values; i++) { + char *old_str, *new_str; + char *pos = NULL; + const char *tok; + struct ldb_val *vals; + bool found = false; + + old_str = (char *) + res->msgs[0]->elements[0].values[i].data; + + new_str = talloc_strdup(ac->msg, + strtok_r(old_str, "/", &pos)); + if (new_str == NULL) { + return ldb_module_oom(ac->module); + } + + while ((tok = strtok_r(NULL, "/", &pos)) != NULL) { + if ((dns_hostname != NULL) && + (strcasecmp_m(tok, old_dns_hostname) == 0)) { + tok = dns_hostname; + } + if ((sam_accountname != NULL) && + (strcasecmp_m(tok, old_sam_accountname) == 0)) { + tok = sam_accountname; + } + + new_str = talloc_asprintf(ac->msg, "%s/%s", + new_str, tok); + if (new_str == NULL) { + return ldb_module_oom(ac->module); + } + } + + /* Uniqueness check */ + for (j = 0; (!found) && (j < el->num_values); j++) { + if (strcasecmp_m((char *)el->values[j].data, + new_str) == 0) { + found = true; + } + } + if (found) { + continue; + } + + /* + * append the new "servicePrincipalName" - + * code derived from ldb_msg_add_value(). + * + * Open coded to make it clear that we must + * append to the MOD_REPLACE el created above. + */ + vals = talloc_realloc(ac->msg, el->values, + struct ldb_val, + el->num_values + 1); + if (vals == NULL) { + return ldb_module_oom(ac->module); + } + el->values = vals; + el->values[el->num_values] = data_blob_string_const(new_str); + ++(el->num_values); + } + } + + talloc_free(res); + + return LDB_SUCCESS; +} + +/* This checks the "fSMORoleOwner" attributes */ +static int samldb_fsmo_role_owner_check(struct samldb_ctx *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + const char * const no_attrs[] = { NULL }; + struct ldb_message_element *el; + struct ldb_message *tmp_msg; + struct ldb_dn *res_dn; + struct ldb_result *res; + int ret; + ret = dsdb_get_expected_new_values(ac, + ac->msg, + "fSMORoleOwner", + &el, + ac->req->operation); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (el == NULL) { + /* we are not affected */ + return LDB_SUCCESS; + } + if (el->num_values != 1) { + goto choose_error_code; + } + + /* Create a temporary message for fetching the "fSMORoleOwner" */ + tmp_msg = ldb_msg_new(ac->msg); + if (tmp_msg == NULL) { + return ldb_module_oom(ac->module); + } + ret = ldb_msg_add(tmp_msg, el, 0); + if (ret != LDB_SUCCESS) { + return ret; + } + res_dn = ldb_msg_find_attr_as_dn(ldb, ac, tmp_msg, "fSMORoleOwner"); + talloc_free(tmp_msg); + + if (res_dn == NULL) { + ldb_set_errstring(ldb, + "samldb: 'fSMORoleOwner' attributes have to reference 'nTDSDSA' entries!"); + goto choose_error_code; + } + + /* Fetched DN has to reference a "nTDSDSA" entry */ + ret = dsdb_module_search(ac->module, ac, &res, res_dn, LDB_SCOPE_BASE, + no_attrs, + DSDB_FLAG_NEXT_MODULE | DSDB_SEARCH_SHOW_DELETED, + ac->req, "(objectClass=nTDSDSA)"); + if (ret != LDB_SUCCESS) { + return ret; + } + if (res->count != 1) { + ldb_set_errstring(ldb, + "samldb: 'fSMORoleOwner' attributes have to reference 'nTDSDSA' entries!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + talloc_free(res); + + return LDB_SUCCESS; + +choose_error_code: + /* this is just how it is */ + if (ac->req->operation == LDB_ADD) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } else { + return LDB_ERR_UNWILLING_TO_PERFORM; + } +} + +/* + * Return zero if the number of zero bits in the address (looking from low to + * high) is equal to or greater than the length minus the mask. Otherwise it + * returns -1. + */ +static int check_cidr_zero_bits(uint8_t *address, unsigned int len, + unsigned int mask) +{ + /* <address> is an integer in big-endian form, <len> bits long. All + bits between <mask> and <len> must be zero. */ + int i; + unsigned int byte_len; + unsigned int byte_mask; + unsigned int bit_mask; + if (len == 32) { + DBG_INFO("Looking at address %02x%02x%02x%02x, mask %u\n", + address[0], address[1], address[2], address[3], + mask); + } else if (len == 128){ + DBG_INFO("Looking at address " + "%02x%02x-%02x%02x-%02x%02x-%02x%02x-" + "%02x%02x-%02x%02x-%02x%02x-%02x%02x, mask %u\n", + address[0], address[1], address[2], address[3], + address[4], address[5], address[6], address[7], + address[8], address[9], address[10], address[11], + address[12], address[13], address[14], address[15], + mask); + } + + if (mask > len){ + DBG_INFO("mask %u is too big (> %u)\n", mask, len); + return -1; + } + if (mask == len){ + /* single address subnet. + * In IPv4 all 255s is invalid by the bitmask != address rule + * in MS-ADTS. IPv6 does not suffer. + */ + if (len == 32){ + if (address[0] == 255 && + address[1] == 255 && + address[2] == 255 && + address[3] == 255){ + return -1; + } + } + return 0; + } + + byte_len = len / 8; + byte_mask = mask / 8; + + for (i = byte_len - 1; i > byte_mask; i--){ + DBG_DEBUG("checking byte %d %02x\n", i, address[i]); + if (address[i] != 0){ + return -1; + } + } + bit_mask = (1 << (8 - (mask & 7))) - 1; + DBG_DEBUG("checking bitmask %02x & %02x overlap %02x\n", bit_mask, address[byte_mask], + bit_mask & address[byte_mask]); + if (address[byte_mask] & bit_mask){ + return -1; + } + + /* According to MS-ADTS, the mask can't exactly equal the bitmask for + * IPv4 (but this is fine for v6). That is 255.255.80.0/17 is bad, + * because the bitmask implied by "/17" is 255.255.80.0. + * + * The bit_mask used in the previous check is the complement of what + * we want here. + */ + if (len == 32 && address[byte_mask] == (uint8_t)~bit_mask){ + bool ok = false; + for (i = 0; i < byte_mask; i++){ + if (address[i] != 255){ + ok = true; + break; + } + } + if (ok == false){ + return -1; + } + } + return 0; +} + + + +static int check_address_roundtrip(const char *address, int family, + const uint8_t *address_bytes, + char *buffer, int buffer_len) +{ + /* + * Check that the address is in the canonical RFC5952 format for IPv6, + * and lacks extra leading zeros for each dotted decimal for IPv4. + * Handily this is what inet_ntop() gives you. + */ + const char *address_redux = inet_ntop(family, address_bytes, + buffer, buffer_len); + if (address_redux == NULL){ + DBG_INFO("Address round trip %s failed unexpectedly" + " with errno %d\n", address, errno); + return -1; + } + if (strcasecmp(address, address_redux) != 0){ + DBG_INFO("Address %s round trips to %s; fail!\n", + address, address_redux); + /* If the address family is IPv6, and the address is in a + certain range + + */ + if (strchr(address_redux, '.') != NULL){ + DEBUG(0, ("The IPv6 address '%s' has the misfortune of " + "lying in a range that was once used for " + "IPv4 embedding (that is, it might also be " + "represented as '%s').\n", address, + address_redux)); + } + return -1; + } + return 0; +} + + + +/* + * MS-ADTS v20150630 6.1.1.2.2.2.1 Subnet Object, refers to RFC1166 and + * RFC2373. It specifies something seemingly indistinguishable from an RFC4632 + * CIDR address range without saying so explicitly. Here we follow the CIDR + * spec. + * + * Return 0 on success, -1 on error. + */ +static int verify_cidr(const char *cidr) +{ + char *address = NULL, *slash = NULL; + bool has_colon, has_dot; + int res, ret; + unsigned long mask; + uint8_t *address_bytes = NULL; + char *address_redux = NULL; + unsigned int address_len; + TALLOC_CTX *frame = NULL; + int error = 0; + + DBG_DEBUG("CIDR is %s\n", cidr); + frame = talloc_stackframe(); + address = talloc_strdup(frame, cidr); + if (address == NULL){ + goto error; + } + + /* there must be a '/' */ + slash = strchr(address, '/'); + if (slash == NULL){ + goto error; + } + /* terminate the address for strchr, inet_pton */ + *slash = '\0'; + + mask = smb_strtoul(slash + 1, NULL, 10, &error, SMB_STR_FULL_STR_CONV); + if (mask == 0){ + DBG_INFO("Windows does not like the zero mask, " + "so nor do we: %s\n", cidr); + goto error; + } + + if (error != 0){ + DBG_INFO("CIDR mask is not a proper integer: %s\n", cidr); + goto error; + } + + address_bytes = talloc_size(frame, sizeof(struct in6_addr)); + if (address_bytes == NULL){ + goto error; + } + + address_redux = talloc_size(frame, INET6_ADDRSTRLEN); + if (address_redux == NULL){ + goto error; + } + + DBG_INFO("found address %s, mask %lu\n", address, mask); + has_colon = (strchr(address, ':') == NULL) ? false : true; + has_dot = (strchr(address, '.') == NULL) ? false : true; + if (has_dot && has_colon){ + /* This seems to be an IPv4 address embedded in IPv6, which is + icky. We don't support it. */ + DBG_INFO("Refusing to consider cidr '%s' with dots and colons\n", + cidr); + goto error; + } else if (has_colon){ /* looks like IPv6 */ + res = inet_pton(AF_INET6, address, address_bytes); + if (res != 1) { + DBG_INFO("Address in %s fails to parse as IPv6\n", cidr); + goto error; + } + address_len = 128; + if (check_address_roundtrip(address, AF_INET6, address_bytes, + address_redux, INET6_ADDRSTRLEN)){ + goto error; + } + } else if (has_dot) { + /* looks like IPv4 */ + if (strcmp(address, "0.0.0.0") == 0){ + DBG_INFO("Windows does not like the zero IPv4 address, " + "so nor do we.\n"); + goto error; + } + res = inet_pton(AF_INET, address, address_bytes); + if (res != 1) { + DBG_INFO("Address in %s fails to parse as IPv4\n", cidr); + goto error; + } + address_len = 32; + + if (check_address_roundtrip(address, AF_INET, address_bytes, + address_redux, INET_ADDRSTRLEN)){ + goto error; + } + } else { + /* This doesn't look like an IP address at all. */ + goto error; + } + + ret = check_cidr_zero_bits(address_bytes, address_len, mask); + talloc_free(frame); + return ret; + error: + talloc_free(frame); + return -1; +} + + +static int samldb_verify_subnet(struct samldb_ctx *ac, struct ldb_dn *dn) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + const char *cidr = NULL; + const struct ldb_val *rdn_value = NULL; + + rdn_value = ldb_dn_get_rdn_val(dn); + if (rdn_value == NULL) { + ldb_set_errstring(ldb, "samldb: ldb_dn_get_rdn_val " + "failed"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + cidr = ldb_dn_escape_value(ac, *rdn_value); + DBG_INFO("looking at cidr '%s'\n", cidr); + if (cidr == NULL) { + ldb_set_errstring(ldb, + "samldb: adding an empty subnet cidr seems wrong"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + if (verify_cidr(cidr)){ + ldb_set_errstring(ldb, + "samldb: subnet value is invalid"); + return LDB_ERR_INVALID_DN_SYNTAX; + } + + return LDB_SUCCESS; +} + +static char *refer_if_rodc(struct ldb_context *ldb, struct ldb_request *req, + struct ldb_dn *dn) +{ + bool rodc = false; + struct loadparm_context *lp_ctx; + char *referral; + int ret; + WERROR err; + + if (ldb_request_get_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID) || + ldb_request_get_control(req, DSDB_CONTROL_DBCHECK_MODIFY_RO_REPLICA)) { + return NULL; + } + + ret = samdb_rodc(ldb, &rodc); + if (ret != LDB_SUCCESS) { + DEBUG(4, (__location__ ": unable to tell if we are an RODC\n")); + return NULL; + } + + if (rodc) { + const char *domain = NULL; + struct ldb_dn *fsmo_role_dn; + struct ldb_dn *role_owner_dn; + ldb_set_errstring(ldb, "RODC modify is forbidden!"); + lp_ctx = talloc_get_type(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + + err = dsdb_get_fsmo_role_info(req, ldb, DREPL_PDC_MASTER, + &fsmo_role_dn, &role_owner_dn); + if (W_ERROR_IS_OK(err)) { + struct ldb_dn *server_dn = ldb_dn_copy(req, role_owner_dn); + if (server_dn != NULL) { + ldb_dn_remove_child_components(server_dn, 1); + + domain = samdb_dn_to_dnshostname(ldb, req, + server_dn); + } + } + if (domain == NULL) { + domain = lpcfg_dnsdomain(lp_ctx); + } + referral = talloc_asprintf(req, + "ldap://%s/%s", + domain, + ldb_dn_get_linearized(dn)); + return referral; + } + + return NULL; +} + +/* + * Restrict all access to sensitive attributes. + * + * We don't want to even inspect the values, so we can use the same + * routine for ADD and MODIFY. + * + */ + +static int samldb_check_sensitive_attributes(struct samldb_ctx *ac) +{ + struct ldb_message_element *el = NULL; + struct security_token *user_token = NULL; + int ret; + + if (dsdb_module_am_system(ac->module)) { + return LDB_SUCCESS; + } + + user_token = acl_user_token(ac->module); + if (user_token == NULL) { + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + + el = ldb_msg_find_element(ac->msg, "sidHistory"); + if (el) { + /* + * sidHistory is restricted to the (not implemented + * yet in Samba) DsAddSidHistory call (direct LDB access is + * as SYSTEM so will bypass this). + * + * If you want to modify this, say to merge domains, + * directly modify the sam.ldb as root. + */ + ldb_asprintf_errstring(ldb_module_get_ctx(ac->module), + "sidHistory " + "(entry %s) cannot be created " + "or changed over LDAP!", + ldb_dn_get_linearized(ac->msg->dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + el = ldb_msg_find_element(ac->msg, "msDS-SecondaryKrbTgtNumber"); + if (el) { + struct security_descriptor *domain_sd; + const struct dsdb_class *objectclass = NULL; + /* + * msDS-SecondaryKrbTgtNumber allows the creator to + * become an RODC, this is trusted as an RODC + * account + */ + ret = samldb_get_domain_secdesc_and_oc(ac, &domain_sd, &objectclass); + if (ret != LDB_SUCCESS) { + return ret; + } + ret = acl_check_extended_right(ac, + ac->module, + ac->req, + objectclass, + domain_sd, + user_token, + GUID_DRS_DS_INSTALL_REPLICA, + SEC_ADS_CONTROL_ACCESS, + NULL); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(ac->module), + "msDS-SecondaryKrbTgtNumber " + "(entry %s) cannot be created " + "or changed without " + "DS-Install-Replica extended right!", + ldb_dn_get_linearized(ac->msg->dn)); + return ret; + } + } + + el = ldb_msg_find_element(ac->msg, "msDS-AllowedToDelegateTo"); + if (el) { + /* + * msDS-AllowedToDelegateTo is incredibly powerful, + * given that it allows a server to become ANY USER on + * the target server only listed by SPN so needs to be + * protected just as the userAccountControl + * UF_TRUSTED_FOR_DELEGATION is. + */ + + bool have_priv = security_token_has_privilege(user_token, + SEC_PRIV_ENABLE_DELEGATION); + if (have_priv == false) { + ldb_asprintf_errstring(ldb_module_get_ctx(ac->module), + "msDS-AllowedToDelegateTo " + "(entry %s) cannot be created " + "or changed without SePrivEnableDelegation!", + ldb_dn_get_linearized(ac->msg->dn)); + return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS; + } + } + return LDB_SUCCESS; +} +/* add */ +static int samldb_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct samldb_ctx *ac; + struct ldb_message_element *el; + int ret; + char *referral = NULL; + + ldb = ldb_module_get_ctx(module); + ldb_debug(ldb, LDB_DEBUG_TRACE, "samldb_add\n"); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.add.message->dn)) { + return ldb_next_request(module, req); + } + + referral = refer_if_rodc(ldb, req, req->op.add.message->dn); + if (referral != NULL) { + ret = ldb_module_send_referral(req, referral); + return ret; + } + + el = ldb_msg_find_element(req->op.add.message, "userParameters"); + if (el != NULL && ldb_req_is_untrusted(req)) { + const char *reason = "samldb_add: " + "setting userParameters is not supported over LDAP, " + "see https://bugzilla.samba.org/show_bug.cgi?id=8077"; + ldb_debug(ldb, LDB_DEBUG_WARNING, "%s", reason); + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, reason); + } + + ac = samldb_ctx_init(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + /* build the new msg */ + ac->msg = ldb_msg_copy_shallow(ac, req->op.add.message); + if (ac->msg == NULL) { + talloc_free(ac); + ldb_debug(ldb, LDB_DEBUG_FATAL, + "samldb_add: ldb_msg_copy_shallow failed!\n"); + return ldb_operr(ldb); + } + + ret = samldb_check_sensitive_attributes(ac); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + + el = ldb_msg_find_element(ac->msg, "fSMORoleOwner"); + if (el != NULL) { + ret = samldb_fsmo_role_owner_check(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + el = ldb_msg_find_element(ac->msg, "servicePrincipalName"); + if ((el != NULL)) { + /* + * We need to check whether the SPN collides with an existing + * one (anywhere) including via aliases. + */ + ret = samldb_spn_uniqueness_check(ac, el); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + if (samdb_find_attribute(ldb, ac->msg, + "objectclass", "user") != NULL) { + ac->type = SAMLDB_TYPE_USER; + + ret = samldb_prim_group_trigger(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = samldb_objectclass_trigger(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + + return samldb_fill_object(ac); + } + + if (samdb_find_attribute(ldb, ac->msg, + "objectclass", "group") != NULL) { + ac->type = SAMLDB_TYPE_GROUP; + + ret = samldb_objectclass_trigger(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + + return samldb_fill_object(ac); + } + + /* perhaps a foreignSecurityPrincipal? */ + if (samdb_find_attribute(ldb, ac->msg, + "objectclass", + "foreignSecurityPrincipal") != NULL) { + return samldb_fill_foreignSecurityPrincipal_object(ac); + } + + if (samdb_find_attribute(ldb, ac->msg, + "objectclass", "classSchema") != NULL) { + ac->type = SAMLDB_TYPE_CLASS; + + /* If in provision, these checks are too slow to do */ + if (!ldb_request_get_control(req, DSDB_CONTROL_SKIP_DUPLICATES_CHECK_OID)) { + ret = samldb_schema_governsid_valid_check(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + ret = samldb_schema_ldapdisplayname_valid_check(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = samldb_schema_info_update(ac); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + + return samldb_fill_object(ac); + } + + if (samdb_find_attribute(ldb, ac->msg, + "objectclass", "attributeSchema") != NULL) { + ac->type = SAMLDB_TYPE_ATTRIBUTE; + + /* If in provision, these checks are too slow to do */ + if (!ldb_request_get_control(req, DSDB_CONTROL_SKIP_DUPLICATES_CHECK_OID)) { + ret = samldb_schema_attributeid_valid_check(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = samldb_schema_add_handle_linkid(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = samldb_schema_add_handle_mapiid(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + ret = samldb_schema_ldapdisplayname_valid_check(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = samldb_schema_info_update(ac); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + + return samldb_fill_object(ac); + } + + if (samdb_find_attribute(ldb, ac->msg, + "objectclass", "subnet") != NULL) { + ret = samldb_verify_subnet(ac, ac->msg->dn); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + /* We are just checking the value is valid, and there are no + values to fill in. */ + } + + talloc_free(ac); + + /* nothing matched, go on */ + return ldb_next_request(module, req); +} + +/* modify */ +static int samldb_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct samldb_ctx *ac; + struct ldb_message_element *el, *el2; + struct ldb_control *is_undelete; + bool modified = false; + int ret; + + if (ldb_dn_is_special(req->op.mod.message->dn)) { + /* do not manipulate our control entries */ + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + /* + * we are going to need some special handling if in Undelete call. + * Since tombstone_reanimate module will restore certain attributes, + * we need to relax checks for: sAMAccountType, primaryGroupID + */ + is_undelete = ldb_request_get_control(req, DSDB_CONTROL_RESTORE_TOMBSTONE_OID); + + /* make sure that "objectSid" is not specified */ + el = ldb_msg_find_element(req->op.mod.message, "objectSid"); + if (el != NULL) { + if (ldb_request_get_control(req, LDB_CONTROL_PROVISION_OID) == NULL) { + ldb_set_errstring(ldb, + "samldb: objectSid must not be specified!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + if (is_undelete == NULL) { + /* make sure that "sAMAccountType" is not specified */ + el = ldb_msg_find_element(req->op.mod.message, "sAMAccountType"); + if (el != NULL) { + ldb_set_errstring(ldb, + "samldb: sAMAccountType must not be specified!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + /* make sure that "isCriticalSystemObject" is not specified */ + el = ldb_msg_find_element(req->op.mod.message, "isCriticalSystemObject"); + if (el != NULL) { + if (ldb_request_get_control(req, LDB_CONTROL_RELAX_OID) == NULL) { + ldb_set_errstring(ldb, + "samldb: isCriticalSystemObject must not be specified!"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + + /* msDS-IntId is not allowed to be modified + * except when modification comes from replication */ + if (ldb_msg_find_element(req->op.mod.message, "msDS-IntId")) { + if (!ldb_request_get_control(req, + DSDB_CONTROL_REPLICATED_UPDATE_OID)) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } + + el = ldb_msg_find_element(req->op.mod.message, "userParameters"); + if (el != NULL && ldb_req_is_untrusted(req)) { + const char *reason = "samldb: " + "setting userParameters is not supported over LDAP, " + "see https://bugzilla.samba.org/show_bug.cgi?id=8077"; + ldb_debug(ldb, LDB_DEBUG_WARNING, "%s", reason); + return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION, reason); + } + + ac = samldb_ctx_init(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + /* build the new msg */ + ac->msg = ldb_msg_copy_shallow(ac, req->op.mod.message); + if (ac->msg == NULL) { + talloc_free(ac); + ldb_debug(ldb, LDB_DEBUG_FATAL, + "samldb_modify: ldb_msg_copy_shallow failed!\n"); + return ldb_operr(ldb); + } + + ret = samldb_check_sensitive_attributes(ac); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + + if (is_undelete == NULL) { + el = ldb_msg_find_element(ac->msg, "primaryGroupID"); + if (el != NULL) { + ret = samldb_prim_group_trigger(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } + + el = ldb_msg_find_element(ac->msg, "userAccountControl"); + if (el != NULL) { + modified = true; + ret = samldb_user_account_control_change(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + el = ldb_msg_find_element(ac->msg, "pwdLastSet"); + if (el != NULL) { + modified = true; + ret = samldb_pwd_last_set_change(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + el = ldb_msg_find_element(ac->msg, "lockoutTime"); + if (el != NULL) { + modified = true; + ret = samldb_lockout_time(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + el = ldb_msg_find_element(ac->msg, "groupType"); + if (el != NULL) { + modified = true; + ret = samldb_group_type_change(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + el = ldb_msg_find_element(ac->msg, "sAMAccountName"); + if (el != NULL) { + uint32_t user_account_control; + struct ldb_result *res = NULL; + const char * const attrs[] = { "userAccountControl", + "objectclass", + NULL }; + ret = dsdb_module_search_dn(ac->module, + ac, + &res, + ac->msg->dn, + attrs, + DSDB_FLAG_NEXT_MODULE | DSDB_SEARCH_SHOW_DELETED, + ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + user_account_control + = ldb_msg_find_attr_as_uint(res->msgs[0], + "userAccountControl", + 0); + + if ((user_account_control + & UF_TRUST_ACCOUNT_MASK) != 0) { + ac->need_trailing_dollar = true; + + } else if (samdb_find_attribute(ldb, + res->msgs[0], + "objectclass", + "computer") + != NULL) { + ac->need_trailing_dollar = true; + } + + ret = samldb_sam_accountname_valid_check(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + el = ldb_msg_find_element(ac->msg, "userPrincipalName"); + if (el != NULL) { + ret = samldb_sam_account_upn_clash(ac); + if (ret != LDB_SUCCESS) { + talloc_free(ac); + return ret; + } + } + + el = ldb_msg_find_element(ac->msg, "ldapDisplayName"); + if (el != NULL) { + ret = samldb_schema_ldapdisplayname_valid_check(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + el = ldb_msg_find_element(ac->msg, "attributeID"); + if (el != NULL) { + ldb_asprintf_errstring(ldb_module_get_ctx(ac->module), + "Once set, attributeID values may not be modified"); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + el = ldb_msg_find_element(ac->msg, "governsID"); + if (el != NULL) { + ldb_asprintf_errstring(ldb_module_get_ctx(ac->module), + "Once set, governsID values may not be modified"); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + el = ldb_msg_find_element(ac->msg, "member"); + if (el != NULL) { + struct ldb_control *fix_link_sid_ctrl = NULL; + + fix_link_sid_ctrl = ldb_request_get_control(ac->req, + DSDB_CONTROL_DBCHECK_FIX_LINK_DN_SID); + if (fix_link_sid_ctrl == NULL) { + ret = samldb_member_check(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } + + el = ldb_msg_find_element(ac->msg, "description"); + if (el != NULL) { + ret = samldb_description_check(ac, &modified); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + el = ldb_msg_find_element(ac->msg, "dNSHostName"); + el2 = ldb_msg_find_element(ac->msg, "sAMAccountName"); + if ((el != NULL) || (el2 != NULL)) { + modified = true; + /* + * samldb_service_principal_names_change() might add SPN + * changes to the request, so this must come before the SPN + * uniqueness check below. + * + * Note we ALSO have to do the SPN uniqueness check inside + * samldb_service_principal_names_change(), because it does a + * subrequest to do requested SPN modifications *before* its + * automatic ones are added. + */ + ret = samldb_service_principal_names_change(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + el = ldb_msg_find_element(ac->msg, "servicePrincipalName"); + if ((el != NULL)) { + /* + * We need to check whether the SPN collides with an existing + * one (anywhere) including via aliases. + */ + modified = true; + ret = samldb_spn_uniqueness_check(ac, el); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + el = ldb_msg_find_element(ac->msg, "fSMORoleOwner"); + if (el != NULL) { + ret = samldb_fsmo_role_owner_check(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + if (modified) { + struct ldb_request *child_req; + + /* Now perform the real modifications as a child request */ + ret = ldb_build_mod_req(&child_req, ldb, ac, + ac->msg, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(child_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, child_req); + } + + talloc_free(ac); + + /* no change which interests us, go on */ + return ldb_next_request(module, req); +} + +/* delete */ + +static int samldb_prim_group_users_check(struct samldb_ctx *ac) +{ + struct ldb_context *ldb; + struct dom_sid *sid; + uint32_t rid; + NTSTATUS status; + int ret; + struct ldb_result *res = NULL; + struct ldb_result *res_users = NULL; + const char * const attrs[] = { "objectSid", "isDeleted", NULL }; + const char * const noattrs[] = { NULL }; + + ldb = ldb_module_get_ctx(ac->module); + + /* Finds out the SID/RID of the SAM object */ + ret = dsdb_module_search_dn(ac->module, ac, &res, ac->req->op.del.dn, + attrs, + DSDB_FLAG_NEXT_MODULE | DSDB_SEARCH_SHOW_DELETED, + ac->req); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (ldb_msg_check_string_attribute(res->msgs[0], "isDeleted", "TRUE")) { + return LDB_SUCCESS; + } + + sid = samdb_result_dom_sid(ac, res->msgs[0], "objectSid"); + if (sid == NULL) { + /* No SID - it might not be a SAM object - therefore ok */ + return LDB_SUCCESS; + } + status = dom_sid_split_rid(ac, sid, NULL, &rid); + if (!NT_STATUS_IS_OK(status)) { + return ldb_operr(ldb); + } + if (rid == 0) { + /* Special object (security principal?) */ + return LDB_SUCCESS; + } + /* do not allow deletion of well-known sids */ + if (rid < DSDB_SAMDB_MINIMUM_ALLOWED_RID && + (ldb_request_get_control(ac->req, LDB_CONTROL_RELAX_OID) == NULL)) { + return LDB_ERR_OTHER; + } + + /* Deny delete requests from groups which are primary ones */ + ret = dsdb_module_search(ac->module, ac, &res_users, + ldb_get_default_basedn(ldb), + LDB_SCOPE_SUBTREE, noattrs, + DSDB_FLAG_NEXT_MODULE, + ac->req, + "(&(primaryGroupID=%u)(objectClass=user))", rid); + if (ret != LDB_SUCCESS) { + return ret; + } + if (res_users->count > 0) { + ldb_asprintf_errstring(ldb_module_get_ctx(ac->module), + "Refusing to delete %s, as it " + "is still the primaryGroupID " + "for %u users", + ldb_dn_get_linearized(res->msgs[0]->dn), + res_users->count); + + /* + * Yes, this seems very wrong, but we have a test + * for this exact error code in sam.py + */ + return LDB_ERR_ENTRY_ALREADY_EXISTS; + } + + return LDB_SUCCESS; +} + +static int samldb_delete(struct ldb_module *module, struct ldb_request *req) +{ + struct samldb_ctx *ac; + char *referral = NULL; + int ret; + struct ldb_context *ldb; + + if (ldb_dn_is_special(req->op.del.dn)) { + /* do not manipulate our control entries */ + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + referral = refer_if_rodc(ldb, req, req->op.del.dn); + if (referral != NULL) { + ret = ldb_module_send_referral(req, referral); + return ret; + } + + ac = samldb_ctx_init(module, req); + if (ac == NULL) { + return ldb_operr(ldb_module_get_ctx(module)); + } + + ret = samldb_prim_group_users_check(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + + talloc_free(ac); + + return ldb_next_request(module, req); +} + +/* rename */ + +static int check_rename_constraints(struct ldb_message *msg, + struct samldb_ctx *ac, + struct ldb_dn *olddn, struct ldb_dn *newdn) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + struct ldb_dn *dn1, *dn2, *nc_root; + int32_t systemFlags; + bool move_op = false; + bool rename_op = false; + int ret; + + /* Skip the checks if old and new DN are the same, or if we have the + * relax control specified or if the returned objects is already + * deleted and needs only to be moved for consistency. */ + + if (ldb_dn_compare(olddn, newdn) == 0) { + return LDB_SUCCESS; + } + if (ldb_request_get_control(ac->req, LDB_CONTROL_RELAX_OID) != NULL) { + return LDB_SUCCESS; + } + + if (ldb_msg_find_attr_as_bool(msg, "isDeleted", false)) { + /* + * check originating request if we are supposed + * to "see" this record in first place. + */ + if (ldb_request_get_control(ac->req, LDB_CONTROL_SHOW_DELETED_OID) == NULL) { + return LDB_ERR_NO_SUCH_OBJECT; + } + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* Objects under CN=System */ + + dn1 = samdb_system_container_dn(ldb, ac); + if (dn1 == NULL) return ldb_oom(ldb); + + if ((ldb_dn_compare_base(dn1, olddn) == 0) && + (ldb_dn_compare_base(dn1, newdn) != 0)) { + talloc_free(dn1); + ldb_asprintf_errstring(ldb, + "subtree_rename: Cannot move/rename %s. Objects under CN=System have to stay under it!", + ldb_dn_get_linearized(olddn)); + return LDB_ERR_OTHER; + } + + talloc_free(dn1); + + /* LSA objects */ + + if ((samdb_find_attribute(ldb, msg, "objectClass", "secret") != NULL) || + (samdb_find_attribute(ldb, msg, "objectClass", "trustedDomain") != NULL)) { + ldb_asprintf_errstring(ldb, + "subtree_rename: Cannot move/rename %s. It's an LSA-specific object!", + ldb_dn_get_linearized(olddn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* subnet objects */ + if (samdb_find_attribute(ldb, msg, "objectclass", "subnet") != NULL) { + ret = samldb_verify_subnet(ac, newdn); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + /* systemFlags */ + + dn1 = ldb_dn_get_parent(ac, olddn); + if (dn1 == NULL) return ldb_oom(ldb); + dn2 = ldb_dn_get_parent(ac, newdn); + if (dn2 == NULL) return ldb_oom(ldb); + + if (ldb_dn_compare(dn1, dn2) == 0) { + rename_op = true; + } else { + move_op = true; + } + + talloc_free(dn1); + talloc_free(dn2); + + systemFlags = ldb_msg_find_attr_as_int(msg, "systemFlags", 0); + + /* Fetch name context */ + + ret = dsdb_find_nc_root(ldb, ac, olddn, &nc_root); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (ldb_dn_compare(nc_root, ldb_get_schema_basedn(ldb)) == 0) { + if (move_op) { + ldb_asprintf_errstring(ldb, + "subtree_rename: Cannot move %s within schema partition", + ldb_dn_get_linearized(olddn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + if (rename_op && + (systemFlags & SYSTEM_FLAG_SCHEMA_BASE_OBJECT) != 0) { + ldb_asprintf_errstring(ldb, + "subtree_rename: Cannot rename %s within schema partition", + ldb_dn_get_linearized(olddn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } else if (ldb_dn_compare(nc_root, ldb_get_config_basedn(ldb)) == 0) { + if (move_op && + (systemFlags & SYSTEM_FLAG_CONFIG_ALLOW_MOVE) == 0) { + /* Here we have to do more: control the + * "ALLOW_LIMITED_MOVE" flag. This means that the + * grand-grand-parents of two objects have to be equal + * in order to perform the move (this is used for + * moving "server" objects in the "sites" container). */ + bool limited_move = + systemFlags & SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE; + + if (limited_move) { + dn1 = ldb_dn_copy(ac, olddn); + if (dn1 == NULL) return ldb_oom(ldb); + dn2 = ldb_dn_copy(ac, newdn); + if (dn2 == NULL) return ldb_oom(ldb); + + limited_move &= ldb_dn_remove_child_components(dn1, 3); + limited_move &= ldb_dn_remove_child_components(dn2, 3); + limited_move &= ldb_dn_compare(dn1, dn2) == 0; + + talloc_free(dn1); + talloc_free(dn2); + } + + if (!limited_move + && ldb_request_get_control(ac->req, DSDB_CONTROL_RESTORE_TOMBSTONE_OID) == NULL) { + ldb_asprintf_errstring(ldb, + "subtree_rename: Cannot move %s to %s in config partition", + ldb_dn_get_linearized(olddn), ldb_dn_get_linearized(newdn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + if (rename_op && + (systemFlags & SYSTEM_FLAG_CONFIG_ALLOW_RENAME) == 0) { + ldb_asprintf_errstring(ldb, + "subtree_rename: Cannot rename %s to %s within config partition", + ldb_dn_get_linearized(olddn), ldb_dn_get_linearized(newdn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } else if (ldb_dn_compare(nc_root, ldb_get_default_basedn(ldb)) == 0) { + if (move_op && + (systemFlags & SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE) != 0) { + ldb_asprintf_errstring(ldb, + "subtree_rename: Cannot move %s to %s - DISALLOW_MOVE set", + ldb_dn_get_linearized(olddn), ldb_dn_get_linearized(newdn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + if (rename_op && + (systemFlags & SYSTEM_FLAG_DOMAIN_DISALLOW_RENAME) != 0) { + ldb_asprintf_errstring(ldb, + "subtree_rename: Cannot rename %s to %s - DISALLOW_RENAME set", + ldb_dn_get_linearized(olddn), ldb_dn_get_linearized(newdn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + + talloc_free(nc_root); + + return LDB_SUCCESS; +} + + +static int samldb_rename_search_base_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct samldb_ctx *ac; + int ret; + + ac = talloc_get_type(req->context, struct samldb_ctx); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + /* + * This is the root entry of the originating move + * respectively rename request. It has been already + * stored in the list using "subtree_rename_search()". + * Only this one is subject to constraint checking. + */ + ret = check_rename_constraints(ares->message, ac, + ac->req->op.rename.olddn, + ac->req->op.rename.newdn); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, + ret); + } + break; + + case LDB_REPLY_REFERRAL: + /* ignore */ + break; + + case LDB_REPLY_DONE: + + /* + * Great, no problem with the rename, so go ahead as + * if we never were here + */ + ret = ldb_next_request(ac->module, ac->req); + talloc_free(ares); + return ret; + } + + talloc_free(ares); + return LDB_SUCCESS; +} + + +/* rename */ +static int samldb_rename(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + static const char * const attrs[] = { "objectClass", "systemFlags", + "isDeleted", NULL }; + struct ldb_request *search_req; + struct samldb_ctx *ac; + int ret; + + if (ldb_dn_is_special(req->op.rename.olddn)) { /* do not manipulate our control entries */ + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + ac = samldb_ctx_init(module, req); + if (!ac) { + return ldb_oom(ldb); + } + + ret = ldb_build_search_req(&search_req, ldb, ac, + req->op.rename.olddn, + LDB_SCOPE_BASE, + "(objectClass=*)", + attrs, + NULL, + ac, + samldb_rename_search_base_callback, + req); + LDB_REQ_SET_LOCATION(search_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_request_add_control(search_req, LDB_CONTROL_SHOW_RECYCLED_OID, + true, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(ac->module, search_req); +} + +/* extended */ + +static int samldb_extended_allocate_rid_pool(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct dsdb_fsmo_extended_op *exop; + int ret; + + exop = talloc_get_type(req->op.extended.data, + struct dsdb_fsmo_extended_op); + if (!exop) { + ldb_set_errstring(ldb, + "samldb_extended_allocate_rid_pool: invalid extended data"); + return LDB_ERR_PROTOCOL_ERROR; + } + + ret = ridalloc_allocate_rid_pool_fsmo(module, exop, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS); +} + +static int samldb_extended_allocate_rid(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct dsdb_extended_allocate_rid *exop; + int ret; + + exop = talloc_get_type(req->op.extended.data, + struct dsdb_extended_allocate_rid); + if (!exop) { + ldb_set_errstring(ldb, + "samldb_extended_allocate_rid: invalid extended data"); + return LDB_ERR_PROTOCOL_ERROR; + } + + ret = ridalloc_allocate_rid(module, &exop->rid, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS); +} + +static int samldb_extended_create_own_rid_set(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + int ret; + struct ldb_dn *dn; + + if (req->op.extended.data != NULL) { + ldb_set_errstring(ldb, + "samldb_extended_create_own_rid_set: invalid extended data (should be NULL)"); + return LDB_ERR_PROTOCOL_ERROR; + } + + ret = ridalloc_create_own_rid_set(module, req, + &dn, req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS); +} + +static int samldb_extended(struct ldb_module *module, struct ldb_request *req) +{ + if (strcmp(req->op.extended.oid, DSDB_EXTENDED_ALLOCATE_RID_POOL) == 0) { + return samldb_extended_allocate_rid_pool(module, req); + } + + if (strcmp(req->op.extended.oid, DSDB_EXTENDED_ALLOCATE_RID) == 0) { + return samldb_extended_allocate_rid(module, req); + } + + if (strcmp(req->op.extended.oid, DSDB_EXTENDED_CREATE_OWN_RID_SET) == 0) { + return samldb_extended_create_own_rid_set(module, req); + } + + return ldb_next_request(module, req); +} + + +static const struct ldb_module_ops ldb_samldb_module_ops = { + .name = "samldb", + .add = samldb_add, + .modify = samldb_modify, + .del = samldb_delete, + .rename = samldb_rename, + .extended = samldb_extended +}; + + +int ldb_samldb_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_samldb_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/schema_data.c b/source4/dsdb/samdb/ldb_modules/schema_data.c new file mode 100644 index 0000000..697ce21 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/schema_data.c @@ -0,0 +1,691 @@ +/* + Unix SMB/CIFS Implementation. + + The module that handles the Schema checkings and dynamic attributes + + Copyright (C) Stefan Metzmacher <metze@samba.org> 2007 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 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/>. + +*/ + +#include "includes.h" +#include "ldb_module.h" +#include "dsdb/samdb/samdb.h" +#include "librpc/gen_ndr/ndr_misc.h" +#include "librpc/gen_ndr/ndr_drsuapi.h" +#include "librpc/gen_ndr/ndr_drsblobs.h" +#include "param/param.h" +#include "dsdb/samdb/ldb_modules/util.h" + +#undef strcasecmp + +static int generate_objectClasses(struct ldb_context *ldb, struct ldb_message *msg, + const struct dsdb_schema *schema); +static int generate_attributeTypes(struct ldb_context *ldb, struct ldb_message *msg, + const struct dsdb_schema *schema); +static int generate_dITContentRules(struct ldb_context *ldb, struct ldb_message *msg, + const struct dsdb_schema *schema); +static int generate_extendedAttributeInfo(struct ldb_context *ldb, struct ldb_message *msg, + const struct dsdb_schema *schema); +static int generate_extendedClassInfo(struct ldb_context *ldb, struct ldb_message *msg, + const struct dsdb_schema *schema); +static int generate_possibleInferiors(struct ldb_context *ldb, struct ldb_message *msg, + const struct dsdb_schema *schema); + +static const struct { + const char *attr; + int (*fn)(struct ldb_context *, struct ldb_message *, const struct dsdb_schema *); + bool aggregate; +} generated_attrs[] = { + { + .attr = "objectClasses", + .fn = generate_objectClasses, + .aggregate = true, + }, + { + .attr = "attributeTypes", + .fn = generate_attributeTypes, + .aggregate = true, + }, + { + .attr = "dITContentRules", + .fn = generate_dITContentRules, + .aggregate = true, + }, + { + .attr = "extendedAttributeInfo", + .fn = generate_extendedAttributeInfo, + .aggregate = true, + }, + { + .attr = "extendedClassInfo", + .fn = generate_extendedClassInfo, + .aggregate = true, + }, + { + .attr = "possibleInferiors", + .fn = generate_possibleInferiors, + .aggregate = false, + } +}; + +struct schema_data_private_data { + struct ldb_dn *aggregate_dn; + struct ldb_dn *schema_dn; +}; + +struct schema_data_search_data { + struct ldb_module *module; + struct ldb_request *req; + + const struct dsdb_schema *schema; +}; + +static int schema_data_init(struct ldb_module *module) +{ + struct ldb_context *ldb; + struct ldb_dn *schema_dn; + int ret; + struct schema_data_private_data *data; + + ret = ldb_next_init(module); + if (ret != LDB_SUCCESS) { + return ret; + } + + ldb = ldb_module_get_ctx(module); + schema_dn = ldb_get_schema_basedn(ldb); + if (!schema_dn) { + ldb_reset_err_string(ldb); + ldb_debug(ldb, LDB_DEBUG_WARNING, + "schema_data_init: no schema dn present: (skip schema loading)\n"); + return LDB_SUCCESS; + } + + data = talloc(module, struct schema_data_private_data); + if (data == NULL) { + return ldb_oom(ldb); + } + + data->schema_dn = schema_dn; + + /* Used to check to see if this is a result on the CN=Aggregate schema */ + data->aggregate_dn = samdb_aggregate_schema_dn(ldb, data); + if (!data->aggregate_dn) { + ldb_asprintf_errstring(ldb, "schema_data_init: Could not build aggregate schema DN for schema in %s", ldb_dn_get_linearized(schema_dn)); + return LDB_ERR_OPERATIONS_ERROR; + } + + ldb_module_set_private(module, data); + return LDB_SUCCESS; +} + +static int schema_data_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct dsdb_schema *schema; + const struct ldb_val *attributeID = NULL; + const struct ldb_val *governsID = NULL; + const char *oid_attr = NULL; + const char *oid = NULL; + struct ldb_dn *parent_dn = NULL; + int cmp; + WERROR status; + bool rodc = false; + int ret; + struct schema_data_private_data *mc; + mc = talloc_get_type(ldb_module_get_private(module), struct schema_data_private_data); + + ldb = ldb_module_get_ctx(module); + + /* special objects should always go through */ + if (ldb_dn_is_special(req->op.add.message->dn)) { + return ldb_next_request(module, req); + } + + /* replicated update should always go through */ + if (ldb_request_get_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID)) { + return ldb_next_request(module, req); + } + + schema = dsdb_get_schema(ldb, req); + if (!schema) { + return ldb_next_request(module, req); + } + + ret = samdb_rodc(ldb, &rodc); + if (ret != LDB_SUCCESS) { + DEBUG(4, (__location__ ": unable to tell if we are an RODC \n")); + } + + if (!schema->fsmo.we_are_master && !rodc) { + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + "schema_data_add: we are not master: reject add request\n"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + if (!schema->fsmo.update_allowed && !rodc) { + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + "schema_data_add: updates are not allowed: reject add request\n"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + if (ldb_request_get_control(req, LDB_CONTROL_RELAX_OID)) { + /* + * the provision code needs to create + * the schema root object. + */ + cmp = ldb_dn_compare(req->op.add.message->dn, mc->schema_dn); + if (cmp == 0) { + return ldb_next_request(module, req); + } + } + + parent_dn = ldb_dn_get_parent(req, req->op.add.message->dn); + if (!parent_dn) { + return ldb_oom(ldb); + } + + cmp = ldb_dn_compare(parent_dn, mc->schema_dn); + if (cmp != 0) { + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + "schema_data_add: no direct child :%s\n", + ldb_dn_get_linearized(req->op.add.message->dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + attributeID = ldb_msg_find_ldb_val(req->op.add.message, "attributeID"); + governsID = ldb_msg_find_ldb_val(req->op.add.message, "governsID"); + + if (attributeID) { + oid_attr = "attributeID"; + oid = talloc_strndup(req, (const char *)attributeID->data, attributeID->length); + } else if (governsID) { + oid_attr = "governsID"; + oid = talloc_strndup(req, (const char *)governsID->data, governsID->length); + } else { + return ldb_next_request(module, req); + } + + if (!oid) { + return ldb_oom(ldb); + } + + status = dsdb_schema_pfm_find_oid(schema->prefixmap, oid, NULL); + if (!W_ERROR_IS_OK(status)) { + /* check for internal errors */ + if (!W_ERROR_EQUAL(status, WERR_NOT_FOUND)) { + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + "schema_data_add: failed to map %s[%s]: %s\n", + oid_attr, oid, win_errstr(status)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* Update prefixMap and save it */ + status = dsdb_create_prefix_mapping(ldb, schema, oid); + if (!W_ERROR_IS_OK(status)) { + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + "schema_data_add: failed to create prefix mapping for %s[%s]: %s\n", + oid_attr, oid, win_errstr(status)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + + return ldb_next_request(module, req); +} + +static int schema_data_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct dsdb_schema *schema; + int cmp; + bool rodc = false; + int ret; + struct ldb_control *sd_propagation_control; + struct schema_data_private_data *mc; + mc = talloc_get_type(ldb_module_get_private(module), struct schema_data_private_data); + + ldb = ldb_module_get_ctx(module); + + /* special objects should always go through */ + if (ldb_dn_is_special(req->op.mod.message->dn)) { + return ldb_next_request(module, req); + } + + /* replicated update should always go through */ + if (ldb_request_get_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID)) { + return ldb_next_request(module, req); + } + + /* dbcheck should be able to fix things */ + if (ldb_request_get_control(req, DSDB_CONTROL_DBCHECK)) { + return ldb_next_request(module, req); + } + + sd_propagation_control = ldb_request_get_control(req, + DSDB_CONTROL_SEC_DESC_PROPAGATION_OID); + if (sd_propagation_control != NULL) { + if (req->op.mod.message->num_elements != 1) { + return ldb_module_operr(module); + } + ret = strcmp(req->op.mod.message->elements[0].name, + "nTSecurityDescriptor"); + if (ret != 0) { + return ldb_module_operr(module); + } + + return ldb_next_request(module, req); + } + + schema = dsdb_get_schema(ldb, req); + if (!schema) { + return ldb_next_request(module, req); + } + + cmp = ldb_dn_compare(req->op.mod.message->dn, mc->schema_dn); + if (cmp == 0) { + static const char * const constrained_attrs[] = { + "schemaInfo", + "prefixMap", + "msDs-Schema-Extensions", + "msDS-IntId", + NULL + }; + size_t i; + struct ldb_message_element *el; + + if (ldb_request_get_control(req, LDB_CONTROL_AS_SYSTEM_OID)) { + return ldb_next_request(module, req); + } + + for (i=0; constrained_attrs[i]; i++) { + el = ldb_msg_find_element(req->op.mod.message, + constrained_attrs[i]); + if (el == NULL) { + continue; + } + + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + "schema_data_modify: reject update " + "of attribute[%s]\n", + constrained_attrs[i]); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + return ldb_next_request(module, req); + } + + ret = samdb_rodc(ldb, &rodc); + if (ret != LDB_SUCCESS) { + DEBUG(4, (__location__ ": unable to tell if we are an RODC \n")); + } + + if (!schema->fsmo.we_are_master && !rodc) { + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + "schema_data_modify: we are not master: reject modify request\n"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + if (!schema->fsmo.update_allowed && !rodc) { + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + "schema_data_modify: updates are not allowed: reject modify request\n"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + return ldb_next_request(module, req); +} + +static int schema_data_del(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct dsdb_schema *schema; + bool rodc = false; + int ret; + + ldb = ldb_module_get_ctx(module); + + /* special objects should always go through */ + if (ldb_dn_is_special(req->op.del.dn)) { + return ldb_next_request(module, req); + } + + /* replicated update should always go through */ + if (ldb_request_get_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID)) { + return ldb_next_request(module, req); + } + + /* dbcheck should be able to fix things */ + if (ldb_request_get_control(req, DSDB_CONTROL_DBCHECK)) { + return ldb_next_request(module, req); + } + + schema = dsdb_get_schema(ldb, req); + if (!schema) { + return ldb_next_request(module, req); + } + + ret = samdb_rodc(ldb, &rodc); + if (ret != LDB_SUCCESS) { + DEBUG(4, (__location__ ": unable to tell if we are an RODC \n")); + } + + if (!schema->fsmo.we_are_master && !rodc) { + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + "schema_data_modify: we are not master: reject request\n"); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + /* + * normaly the DACL will prevent delete + * with LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS + * above us. + */ + ldb_debug_set(ldb, LDB_DEBUG_ERROR, + "schema_data_del: delete is not allowed in the schema\n"); + return LDB_ERR_UNWILLING_TO_PERFORM; +} + +static int generate_objectClasses(struct ldb_context *ldb, struct ldb_message *msg, + const struct dsdb_schema *schema) +{ + const struct dsdb_class *sclass; + int ret; + + for (sclass = schema->classes; sclass; sclass = sclass->next) { + char *v = schema_class_to_description(msg, sclass); + if (v == NULL) { + return ldb_oom(ldb); + } + ret = ldb_msg_add_steal_string(msg, "objectClasses", v); + if (ret != LDB_SUCCESS) { + return ret; + } + } + return LDB_SUCCESS; +} +static int generate_attributeTypes(struct ldb_context *ldb, struct ldb_message *msg, + const struct dsdb_schema *schema) +{ + const struct dsdb_attribute *attribute; + int ret; + + for (attribute = schema->attributes; attribute; attribute = attribute->next) { + char *v = schema_attribute_to_description(msg, attribute); + if (v == NULL) { + return ldb_oom(ldb); + } + ret = ldb_msg_add_steal_string(msg, "attributeTypes", v); + if (ret != LDB_SUCCESS) { + return ret; + } + } + return LDB_SUCCESS; +} + +static int generate_dITContentRules(struct ldb_context *ldb, struct ldb_message *msg, + const struct dsdb_schema *schema) +{ + const struct dsdb_class *sclass; + int ret; + + for (sclass = schema->classes; sclass; sclass = sclass->next) { + if (sclass->auxiliaryClass || sclass->systemAuxiliaryClass) { + char *ditcontentrule = schema_class_to_dITContentRule(msg, sclass, schema); + if (!ditcontentrule) { + return ldb_oom(ldb); + } + ret = ldb_msg_add_steal_string(msg, "dITContentRules", ditcontentrule); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } + return LDB_SUCCESS; +} + +static int generate_extendedAttributeInfo(struct ldb_context *ldb, + struct ldb_message *msg, + const struct dsdb_schema *schema) +{ + const struct dsdb_attribute *attribute; + int ret; + + for (attribute = schema->attributes; attribute; attribute = attribute->next) { + char *val = schema_attribute_to_extendedInfo(msg, attribute); + if (!val) { + return ldb_oom(ldb); + } + + ret = ldb_msg_add_steal_string(msg, "extendedAttributeInfo", val); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + return LDB_SUCCESS; +} + +static int generate_extendedClassInfo(struct ldb_context *ldb, + struct ldb_message *msg, + const struct dsdb_schema *schema) +{ + const struct dsdb_class *sclass; + int ret; + + for (sclass = schema->classes; sclass; sclass = sclass->next) { + char *val = schema_class_to_extendedInfo(msg, sclass); + if (!val) { + return ldb_oom(ldb); + } + + ret = ldb_msg_add_steal_string(msg, "extendedClassInfo", val); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + return LDB_SUCCESS; +} + + +static int generate_possibleInferiors(struct ldb_context *ldb, struct ldb_message *msg, + const struct dsdb_schema *schema) +{ + struct ldb_dn *dn = msg->dn; + unsigned int i; + int ret; + const char *first_component_name = ldb_dn_get_component_name(dn, 0); + const struct ldb_val *first_component_val; + const struct dsdb_class *schema_class; + const char **possibleInferiors; + + if (strcasecmp(first_component_name, "cn") != 0) { + return LDB_SUCCESS; + } + + first_component_val = ldb_dn_get_component_val(dn, 0); + + schema_class = dsdb_class_by_cn_ldb_val(schema, first_component_val); + if (schema_class == NULL) { + return LDB_SUCCESS; + } + + possibleInferiors = schema_class->possibleInferiors; + if (possibleInferiors == NULL) { + return LDB_SUCCESS; + } + + for (i=0;possibleInferiors[i];i++) { + char *v = talloc_strdup(msg, possibleInferiors[i]); + if (v == NULL) { + return ldb_oom(ldb); + } + ret = ldb_msg_add_steal_string(msg, "possibleInferiors", v); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + return LDB_SUCCESS; +} + + +/* Add objectClasses, attributeTypes and dITContentRules from the + schema object (they are not stored in the database) + */ +static int schema_data_search_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct schema_data_search_data *ac; + struct schema_data_private_data *mc; + unsigned int i; + int ret; + + ac = talloc_get_type(req->context, struct schema_data_search_data); + mc = talloc_get_type(ldb_module_get_private(ac->module), struct schema_data_private_data); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + /* Only entries are interesting, and we handle the case of the parent seperatly */ + + switch (ares->type) { + case LDB_REPLY_ENTRY: + + if (ldb_dn_compare(ares->message->dn, mc->aggregate_dn) == 0) { + for (i=0; i < ARRAY_SIZE(generated_attrs); i++) { + if (generated_attrs[i].aggregate && + ldb_attr_in_list(ac->req->op.search.attrs, generated_attrs[i].attr)) { + ret = generated_attrs[i].fn(ldb, ares->message, ac->schema); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } + } else if ((ldb_dn_compare_base(mc->schema_dn, ares->message->dn) == 0) + && (ldb_dn_compare(mc->schema_dn, ares->message->dn) != 0)) { + for (i=0; i < ARRAY_SIZE(generated_attrs); i++) { + if (!generated_attrs[i].aggregate && + ldb_attr_in_list(ac->req->op.search.attrs, generated_attrs[i].attr)) { + ret = generated_attrs[i].fn(ldb, ares->message, ac->schema); + if (ret != LDB_SUCCESS) { + return ret; + } + } + } + } + + + return ldb_module_send_entry(ac->req, ares->message, ares->controls); + + case LDB_REPLY_REFERRAL: + + return ldb_module_send_referral(ac->req, ares->referral); + + case LDB_REPLY_DONE: + + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + return LDB_SUCCESS; +} + +/* search */ +static int schema_data_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + unsigned int i; + int ret; + struct schema_data_search_data *search_context; + struct ldb_request *down_req; + const struct dsdb_schema *schema; + if (!ldb_module_get_private(module)) { + /* If there is no module data, there is little we can do */ + return ldb_next_request(module, req); + } + + /* The schema manipulation does not apply to special DNs */ + if (ldb_dn_is_special(req->op.search.base)) { + return ldb_next_request(module, req); + } + + for (i=0; i < ARRAY_SIZE(generated_attrs); i++) { + if (ldb_attr_in_list(req->op.search.attrs, generated_attrs[i].attr)) { + break; + } + } + if (i == ARRAY_SIZE(generated_attrs)) { + /* No request for a generated attr found, nothing to + * see here, move along... */ + return ldb_next_request(module, req); + } + + schema = dsdb_get_schema(ldb, NULL); + if (!schema || !ldb_module_get_private(module)) { + /* If there is no schema, there is little we can do */ + return ldb_next_request(module, req); + } + + search_context = talloc(req, struct schema_data_search_data); + if (!search_context) { + return ldb_oom(ldb); + } + + search_context->module = module; + search_context->req = req; + search_context->schema = talloc_reference(search_context, schema); + if (!search_context->schema) { + return ldb_oom(ldb); + } + + ret = ldb_build_search_req_ex(&down_req, ldb, search_context, + req->op.search.base, + req->op.search.scope, + req->op.search.tree, + req->op.search.attrs, + req->controls, + search_context, schema_data_search_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + + return ldb_next_request(module, down_req); +} + + +static const struct ldb_module_ops ldb_schema_data_module_ops = { + .name = "schema_data", + .init_context = schema_data_init, + .add = schema_data_add, + .modify = schema_data_modify, + .del = schema_data_del, + .search = schema_data_search +}; + +int ldb_schema_data_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_schema_data_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/schema_load.c b/source4/dsdb/samdb/ldb_modules/schema_load.c new file mode 100644 index 0000000..13bad83 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/schema_load.c @@ -0,0 +1,657 @@ +/* + Unix SMB/CIFS Implementation. + + The module that handles the Schema FSMO Role Owner + checkings, it also loads the dsdb_schema. + + Copyright (C) Stefan Metzmacher <metze@samba.org> 2007 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2009-2010 + + 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/>. + +*/ + +#include "includes.h" +#include "ldb_module.h" +#include "dsdb/samdb/samdb.h" +#include "librpc/gen_ndr/ndr_misc.h" +#include "librpc/gen_ndr/ndr_drsuapi.h" +#include "librpc/gen_ndr/ndr_drsblobs.h" +#include "param/param.h" +#include <tdb.h> +#include "lib/tdb_wrap/tdb_wrap.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "lib/ldb-samba/ldb_wrap.h" +#include "lib/util/smb_strtox.h" + +#include "system/filesys.h" +struct schema_load_private_data { + struct ldb_module *module; + uint64_t in_transaction; + uint64_t in_read_transaction; + struct tdb_wrap *metadata; + uint64_t schema_seq_num_cache; + int tdb_seqnum; + + /* + * Please write out the updated schema on the next transaction + * start + */ + bool need_write; +}; + +static int dsdb_schema_from_db(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + uint64_t schema_seq_num, + struct dsdb_schema **schema); + +/* + * Open sam.ldb.d/metadata.tdb. + */ +static int schema_metadata_open(struct ldb_module *module) +{ + struct schema_load_private_data *data = talloc_get_type(ldb_module_get_private(module), struct schema_load_private_data); + struct ldb_context *ldb = ldb_module_get_ctx(module); + TALLOC_CTX *tmp_ctx; + struct loadparm_context *lp_ctx; + char *filename; + int open_flags; + struct stat statbuf; + + if (!data) { + return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR, + "schema_load: metadata not initialized"); + } + data->metadata = NULL; + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + return ldb_module_oom(module); + } + + filename = ldb_relative_path(ldb, + tmp_ctx, + "sam.ldb.d/metadata.tdb"); + if (filename == NULL) { + talloc_free(tmp_ctx); + return ldb_module_oom(module); + } + + open_flags = O_RDWR; + if (stat(filename, &statbuf) != 0) { + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + lp_ctx = talloc_get_type_abort(ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + + data->metadata = tdb_wrap_open(data, filename, 10, + lpcfg_tdb_flags(lp_ctx, TDB_DEFAULT|TDB_SEQNUM), + open_flags, 0660); + if (data->metadata == NULL) { + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + +static int schema_metadata_get_uint64(struct schema_load_private_data *data, + const char *key, uint64_t *value, + uint64_t default_value) +{ + struct tdb_context *tdb; + TDB_DATA tdb_key, tdb_data; + char *value_str; + TALLOC_CTX *tmp_ctx; + int tdb_seqnum; + int error = 0; + + if (!data) { + *value = default_value; + return LDB_SUCCESS; + } + + if (!data->metadata) { + return LDB_ERR_OPERATIONS_ERROR; + } + + tdb_seqnum = tdb_get_seqnum(data->metadata->tdb); + if (tdb_seqnum == data->tdb_seqnum) { + *value = data->schema_seq_num_cache; + return LDB_SUCCESS; + } + + tmp_ctx = talloc_new(NULL); + if (tmp_ctx == NULL) { + return ldb_module_oom(data->module); + } + + tdb = data->metadata->tdb; + + tdb_key.dptr = (uint8_t *)discard_const_p(char, key); + tdb_key.dsize = strlen(key); + + tdb_data = tdb_fetch(tdb, tdb_key); + if (!tdb_data.dptr) { + if (tdb_error(tdb) == TDB_ERR_NOEXIST) { + *value = default_value; + talloc_free(tmp_ctx); + return LDB_SUCCESS; + } else { + talloc_free(tmp_ctx); + return ldb_module_error(data->module, LDB_ERR_OPERATIONS_ERROR, + tdb_errorstr(tdb)); + } + } + + value_str = talloc_strndup(tmp_ctx, (char *)tdb_data.dptr, tdb_data.dsize); + if (value_str == NULL) { + SAFE_FREE(tdb_data.dptr); + talloc_free(tmp_ctx); + return ldb_module_oom(data->module); + } + + /* + * Now store it in the cache. We don't mind that tdb_seqnum + * may be stale now, that just means the cache won't be used + * next time + */ + data->tdb_seqnum = tdb_seqnum; + data->schema_seq_num_cache = smb_strtoull(value_str, + NULL, + 10, + &error, + SMB_STR_STANDARD); + if (error != 0) { + talloc_free(tmp_ctx); + return ldb_module_error(data->module, LDB_ERR_OPERATIONS_ERROR, + "Failed to convert value"); + } + + *value = data->schema_seq_num_cache; + + SAFE_FREE(tdb_data.dptr); + talloc_free(tmp_ctx); + + return LDB_SUCCESS; +} + +static struct dsdb_schema *dsdb_schema_refresh(struct ldb_module *module, struct tevent_context *ev, + struct dsdb_schema *schema, bool is_global_schema) +{ + TALLOC_CTX *mem_ctx; + uint64_t schema_seq_num = 0; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct dsdb_schema *new_schema; + + struct schema_load_private_data *private_data = talloc_get_type(ldb_module_get_private(module), struct schema_load_private_data); + if (!private_data) { + /* We can't refresh until the init function has run */ + return schema; + } + + if (schema != NULL) { + /* + * If we have a schema already (not in the startup) + * and we are in a read or write transaction, then + * avoid a schema reload, it can't have changed + */ + if (private_data->in_transaction > 0 + || private_data->in_read_transaction > 0 ) { + /* + * If the refresh is not an expected part of a + * larger transaction, then we don't allow a + * schema reload during a transaction. This + * stops others from modifying our schema + * behind our backs + */ + if (ldb_get_opaque(ldb, + "dsdb_schema_refresh_expected") + != (void *)1) { + return schema; + } + } + } + + SMB_ASSERT(ev == ldb_get_event_context(ldb)); + + mem_ctx = talloc_new(module); + if (mem_ctx == NULL) { + return NULL; + } + + /* + * We update right now the last refresh timestamp so that if + * the schema partition hasn't change we don't keep on retrying. + * Otherwise if the timestamp was update only when the schema has + * actually changed (and therefor completely reloaded) we would + * continue to hit the database to get the highest USN. + */ + + ret = schema_metadata_get_uint64(private_data, + DSDB_METADATA_SCHEMA_SEQ_NUM, + &schema_seq_num, 0); + + if (schema != NULL) { + if (ret == LDB_SUCCESS) { + if (schema->metadata_usn == schema_seq_num) { + TALLOC_FREE(mem_ctx); + return schema; + } else { + DEBUG(3, ("Schema refresh needed %lld != %lld\n", + (unsigned long long)schema->metadata_usn, + (unsigned long long)schema_seq_num)); + } + } else { + /* From an old provision it can happen that the tdb didn't exists yet */ + DEBUG(0, ("Error while searching for the schema usn in the metadata ignoring: %d:%s:%s\n", + ret, ldb_strerror(ret), ldb_errstring(ldb))); + TALLOC_FREE(mem_ctx); + return schema; + } + } else { + DEBUG(10, ("Initial schema load needed, as we have no existing schema, seq_num: %lld\n", + (unsigned long long)schema_seq_num)); + } + + ret = dsdb_schema_from_db(module, mem_ctx, schema_seq_num, &new_schema); + if (ret != LDB_SUCCESS) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "dsdb_schema_from_db() failed: %d:%s: %s", + ret, ldb_strerror(ret), ldb_errstring(ldb)); + TALLOC_FREE(mem_ctx); + return schema; + } + + ret = dsdb_set_schema(ldb, new_schema, SCHEMA_MEMORY_ONLY); + if (ret != LDB_SUCCESS) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "dsdb_set_schema() failed: %d:%s: %s", + ret, ldb_strerror(ret), ldb_errstring(ldb)); + TALLOC_FREE(mem_ctx); + return schema; + } + if (is_global_schema) { + dsdb_make_schema_global(ldb, new_schema); + } + TALLOC_FREE(mem_ctx); + return new_schema; +} + + +/* + Given an LDB module (pointing at the schema DB), and the DN, set the populated schema +*/ + +static int dsdb_schema_from_db(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + uint64_t schema_seq_num, + struct dsdb_schema **schema) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + TALLOC_CTX *tmp_ctx; + char *error_string; + int ret, i; + struct ldb_dn *schema_dn = ldb_get_schema_basedn(ldb); + struct ldb_result *res; + struct ldb_message *schema_msg = NULL; + static const char *schema_attrs[] = { + DSDB_SCHEMA_COMMON_ATTRS, + DSDB_SCHEMA_ATTR_ATTRS, + DSDB_SCHEMA_CLASS_ATTRS, + "prefixMap", + "schemaInfo", + "fSMORoleOwner", + NULL + }; + unsigned flags; + + tmp_ctx = talloc_new(module); + if (!tmp_ctx) { + return ldb_oom(ldb); + } + + /* we don't want to trace the schema load */ + flags = ldb_get_flags(ldb); + ldb_set_flags(ldb, flags & ~LDB_FLG_ENABLE_TRACING); + + /* + * Load the attribute and class definitions, as well as + * the schema object. We do this in one search and then + * split it so that there isn't a race condition when + * the schema is changed between two searches. + */ + ret = dsdb_module_search(module, tmp_ctx, &res, + schema_dn, LDB_SCOPE_SUBTREE, + schema_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + NULL, + "(|(objectClass=attributeSchema)" + "(objectClass=classSchema)" + "(objectClass=dMD))"); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "dsdb_schema: failed to search attributeSchema and classSchema objects: %s", + ldb_errstring(ldb)); + goto failed; + } + + /* + * Separate the schema object from the attribute and + * class objects. + */ + for (i = 0; i < res->count; i++) { + if (ldb_msg_find_element(res->msgs[i], "prefixMap")) { + schema_msg = res->msgs[i]; + break; + } + } + + if (schema_msg == NULL) { + ldb_asprintf_errstring(ldb, + "dsdb_schema load failed: failed to find prefixMap"); + ret = LDB_ERR_NO_SUCH_ATTRIBUTE; + goto failed; + } + + ret = dsdb_schema_from_ldb_results(tmp_ctx, ldb, + schema_msg, res, schema, &error_string); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "dsdb_schema load failed: %s", + error_string); + goto failed; + } + + (*schema)->metadata_usn = schema_seq_num; + + talloc_steal(mem_ctx, *schema); + +failed: + if (flags & LDB_FLG_ENABLE_TRACING) { + flags = ldb_get_flags(ldb); + ldb_set_flags(ldb, flags | LDB_FLG_ENABLE_TRACING); + } + talloc_free(tmp_ctx); + return ret; +} + +static int schema_load(struct ldb_context *ldb, + struct ldb_module *module, + bool *need_write) +{ + struct dsdb_schema *schema; + int ret, metadata_ret; + TALLOC_CTX *frame = talloc_stackframe(); + + schema = dsdb_get_schema(ldb, frame); + + metadata_ret = schema_metadata_open(module); + + /* We might already have a schema */ + if (schema != NULL) { + /* If we have the metadata.tdb, then hook up the refresh function */ + if (metadata_ret == LDB_SUCCESS && dsdb_uses_global_schema(ldb)) { + ret = dsdb_set_schema_refresh_function(ldb, dsdb_schema_refresh, module); + + if (ret != LDB_SUCCESS) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "schema_load_init: dsdb_set_schema_refresh_fns() failed: %d:%s: %s", + ret, ldb_strerror(ret), ldb_errstring(ldb)); + TALLOC_FREE(frame); + return ret; + } + } + + TALLOC_FREE(frame); + return LDB_SUCCESS; + } + + if (metadata_ret == LDB_SUCCESS) { + ret = dsdb_set_schema_refresh_function(ldb, dsdb_schema_refresh, module); + + if (ret != LDB_SUCCESS) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "schema_load_init: dsdb_set_schema_refresh_fns() failed: %d:%s: %s", + ret, ldb_strerror(ret), ldb_errstring(ldb)); + TALLOC_FREE(frame); + return ret; + } + } else { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "schema_load_init: failed to open metadata.tdb"); + TALLOC_FREE(frame); + return metadata_ret; + } + + schema = dsdb_get_schema(ldb, frame); + + /* We do this, invoking the refresh handler, so we know that it works */ + if (schema == NULL) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "schema_load_init: dsdb_get_schema failed"); + TALLOC_FREE(frame); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* Now check the @INDEXLIST is correct, or fix it up */ + ret = dsdb_schema_set_indices_and_attributes(ldb, schema, + SCHEMA_COMPARE); + if (ret == LDB_ERR_BUSY) { + *need_write = true; + ret = LDB_SUCCESS; + } else { + *need_write = false; + } + + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to update " + "@INDEXLIST and @ATTRIBUTES " + "records to match database schema: %s", + ldb_errstring(ldb)); + TALLOC_FREE(frame); + return ret; + } + + TALLOC_FREE(frame); + return LDB_SUCCESS; +} + +static int schema_load_init(struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct schema_load_private_data *private_data = + talloc_get_type_abort(ldb_module_get_private(module), + struct schema_load_private_data); + int ret; + + ret = ldb_next_init(module); + if (ret != LDB_SUCCESS) { + return ret; + } + + return schema_load(ldb, module, &private_data->need_write); +} + +static int schema_load_start_transaction(struct ldb_module *module) +{ + struct schema_load_private_data *private_data = + talloc_get_type_abort(ldb_module_get_private(module), + struct schema_load_private_data); + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct dsdb_schema *schema; + int ret; + + ret = ldb_next_start_trans(module); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* Try the schema refresh now */ + schema = dsdb_get_schema(ldb, NULL); + if (schema == NULL) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "schema_load_init: dsdb_get_schema failed"); + return LDB_ERR_OPERATIONS_ERROR; + } + + if (private_data->need_write) { + ret = dsdb_schema_set_indices_and_attributes(ldb, + schema, + SCHEMA_WRITE); + private_data->need_write = false; + } + + private_data->in_transaction++; + + return ret; +} + +static int schema_load_end_transaction(struct ldb_module *module) +{ + struct schema_load_private_data *private_data = + talloc_get_type_abort(ldb_module_get_private(module), + struct schema_load_private_data); + struct ldb_context *ldb = ldb_module_get_ctx(module); + + if (private_data->in_transaction == 0) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "schema_load_end_transaction: transaction mismatch"); + return LDB_ERR_OPERATIONS_ERROR; + } + private_data->in_transaction--; + + return ldb_next_end_trans(module); +} + +static int schema_load_del_transaction(struct ldb_module *module) +{ + struct schema_load_private_data *private_data = + talloc_get_type(ldb_module_get_private(module), struct schema_load_private_data); + struct ldb_context *ldb = ldb_module_get_ctx(module); + + if (private_data->in_transaction == 0) { + ldb_debug_set(ldb, LDB_DEBUG_FATAL, + "schema_load_del_transaction: transaction mismatch"); + return LDB_ERR_OPERATIONS_ERROR; + } + private_data->in_transaction--; + + return ldb_next_del_trans(module); +} + +/* This is called in a transaction held by the callers */ +static int schema_load_extended(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct dsdb_schema *schema; + int ret; + + if (strcmp(req->op.extended.oid, DSDB_EXTENDED_SCHEMA_LOAD) == 0) { + + ret = dsdb_schema_from_db(module, req, 0, &schema); + if (ret == LDB_SUCCESS) { + return ldb_module_done(req, NULL, NULL, LDB_SUCCESS); + } + return ret; + + } else if (strcmp(req->op.extended.oid, DSDB_EXTENDED_SCHEMA_UPDATE_NOW_OID) == 0) { + /* Force a refresh */ + schema = dsdb_get_schema(ldb, NULL); + + ret = dsdb_schema_set_indices_and_attributes(ldb, + schema, + SCHEMA_WRITE); + + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to write new " + "@INDEXLIST and @ATTRIBUTES " + "records for updated schema: %s", + ldb_errstring(ldb)); + return ret; + } + + return ldb_next_request(module, req); + } else { + /* Pass to next module, the partition one should finish the chain */ + return ldb_next_request(module, req); + } +} + +static int schema_read_lock(struct ldb_module *module) +{ + struct schema_load_private_data *private_data = + talloc_get_type(ldb_module_get_private(module), struct schema_load_private_data); + int ret; + + if (private_data == NULL) { + private_data = talloc_zero(module, struct schema_load_private_data); + if (private_data == NULL) { + return ldb_module_oom(module); + } + + private_data->module = module; + + ldb_module_set_private(module, private_data); + } + + ret = ldb_next_read_lock(module); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (private_data->in_transaction == 0 && + private_data->in_read_transaction == 0) { + /* Try the schema refresh now */ + dsdb_get_schema(ldb_module_get_ctx(module), NULL); + } + + private_data->in_read_transaction++; + + return LDB_SUCCESS; +} + +static int schema_read_unlock(struct ldb_module *module) +{ + struct schema_load_private_data *private_data = + talloc_get_type_abort(ldb_module_get_private(module), + struct schema_load_private_data); + + private_data->in_read_transaction--; + + return ldb_next_read_unlock(module); +} + + +static const struct ldb_module_ops ldb_schema_load_module_ops = { + .name = "schema_load", + .init_context = schema_load_init, + .extended = schema_load_extended, + .start_transaction = schema_load_start_transaction, + .end_transaction = schema_load_end_transaction, + .del_transaction = schema_load_del_transaction, + .read_lock = schema_read_lock, + .read_unlock = schema_read_unlock, +}; + +int ldb_schema_load_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_schema_load_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/schema_util.c b/source4/dsdb/samdb/ldb_modules/schema_util.c new file mode 100644 index 0000000..47d2411 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/schema_util.c @@ -0,0 +1,345 @@ +/* + Unix SMB/CIFS implementation. + + dsdb module schema utility functions + + Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2010 + Copyright (C) Andrew Tridgell 2010 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2010 + + 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/>. +*/ + +#include "includes.h" +#include "dsdb/common/util.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include <ldb_module.h> +#include "librpc/gen_ndr/ndr_drsuapi.h" +#include "librpc/gen_ndr/ndr_drsblobs.h" +#include "param/param.h" + +/** + * Reads schema_info structure from schemaInfo + * attribute on SCHEMA partition + * + * @param dsdb_flags DSDB_FLAG_... flag of 0 + */ +int dsdb_module_schema_info_blob_read(struct ldb_module *ldb_module, + uint32_t dsdb_flags, + TALLOC_CTX *mem_ctx, + struct ldb_val *schema_info_blob, + struct ldb_request *parent) +{ + int ldb_err; + const struct ldb_val *blob_val; + struct ldb_dn *schema_dn; + struct ldb_result *schema_res = NULL; + static const char *schema_attrs[] = { + "schemaInfo", + NULL + }; + + schema_dn = ldb_get_schema_basedn(ldb_module_get_ctx(ldb_module)); + if (!schema_dn) { + DEBUG(0,("dsdb_module_schema_info_blob_read: no schema dn present!\n")); + return ldb_operr(ldb_module_get_ctx(ldb_module)); + } + + ldb_err = dsdb_module_search(ldb_module, mem_ctx, &schema_res, schema_dn, + LDB_SCOPE_BASE, schema_attrs, dsdb_flags, parent, + NULL); + if (ldb_err == LDB_ERR_NO_SUCH_OBJECT) { + DEBUG(0,("dsdb_module_schema_info_blob_read: Schema DN not found!\n")); + talloc_free(schema_res); + return ldb_err; + } else if (ldb_err != LDB_SUCCESS) { + DEBUG(0,("dsdb_module_schema_info_blob_read: failed to find schemaInfo attribute\n")); + talloc_free(schema_res); + return ldb_err; + } + + blob_val = ldb_msg_find_ldb_val(schema_res->msgs[0], "schemaInfo"); + if (!blob_val) { + ldb_asprintf_errstring(ldb_module_get_ctx(ldb_module), + "dsdb_module_schema_info_blob_read: no schemaInfo attribute found"); + talloc_free(schema_res); + return LDB_ERR_NO_SUCH_ATTRIBUTE; + } + + /* transfer .data ownership to mem_ctx */ + schema_info_blob->length = blob_val->length; + schema_info_blob->data = talloc_steal(mem_ctx, blob_val->data); + + talloc_free(schema_res); + + return LDB_SUCCESS; +} + +/** + * Prepares ldb_msg to be used for updating schemaInfo value in DB + */ +static int dsdb_schema_info_write_prepare(struct ldb_context *ldb, + struct ldb_val *schema_info_blob, + TALLOC_CTX *mem_ctx, + struct ldb_message **_msg) +{ + int ldb_err; + struct ldb_message *msg; + struct ldb_dn *schema_dn; + struct ldb_message_element *return_el; + + schema_dn = ldb_get_schema_basedn(ldb); + if (!schema_dn) { + DEBUG(0,("dsdb_schema_info_write_prepare: no schema dn present\n")); + return ldb_operr(ldb); + } + + /* prepare ldb_msg to update schemaInfo */ + msg = ldb_msg_new(mem_ctx); + if (msg == NULL) { + return ldb_oom(ldb); + } + + msg->dn = schema_dn; + ldb_err = ldb_msg_add_value(msg, "schemaInfo", schema_info_blob, &return_el); + if (ldb_err != 0) { + ldb_asprintf_errstring(ldb, "dsdb_schema_info_write_prepare: ldb_msg_add_value failed - %s\n", + ldb_strerror(ldb_err)); + talloc_free(msg); + return ldb_err; + } + + /* mark schemaInfo element for replacement */ + return_el->flags = LDB_FLAG_MOD_REPLACE; + + *_msg = msg; + + return LDB_SUCCESS; +} + + + +/** + * Writes schema_info structure into schemaInfo + * attribute on SCHEMA partition + * + * @param dsdb_flags DSDB_FLAG_... flag of 0 + */ +int dsdb_module_schema_info_blob_write(struct ldb_module *ldb_module, + uint32_t dsdb_flags, + struct ldb_val *schema_info_blob, + struct ldb_request *parent) +{ + int ldb_err; + struct ldb_message *msg = NULL; + TALLOC_CTX *temp_ctx; + + temp_ctx = talloc_new(ldb_module); + if (temp_ctx == NULL) { + return ldb_module_oom(ldb_module); + } + + /* write serialized schemaInfo into LDB */ + ldb_err = dsdb_schema_info_write_prepare(ldb_module_get_ctx(ldb_module), + schema_info_blob, + temp_ctx, &msg); + if (ldb_err != LDB_SUCCESS) { + talloc_free(temp_ctx); + return ldb_err; + } + + + ldb_err = dsdb_module_modify(ldb_module, msg, dsdb_flags, parent); + + talloc_free(temp_ctx); + + if (ldb_err != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(ldb_module), + "dsdb_module_schema_info_blob_write: dsdb_replace failed: %s (%s)\n", + ldb_strerror(ldb_err), + ldb_errstring(ldb_module_get_ctx(ldb_module))); + return ldb_err; + } + + return LDB_SUCCESS; +} + + +/** + * Reads schema_info structure from schemaInfo + * attribute on SCHEMA partition + */ +static int dsdb_module_schema_info_read(struct ldb_module *ldb_module, + uint32_t dsdb_flags, + TALLOC_CTX *mem_ctx, + struct dsdb_schema_info **_schema_info, + struct ldb_request *parent) +{ + int ret; + DATA_BLOB ndr_blob; + TALLOC_CTX *temp_ctx; + WERROR werr; + + temp_ctx = talloc_new(mem_ctx); + if (temp_ctx == NULL) { + return ldb_module_oom(ldb_module); + } + + /* read serialized schemaInfo from LDB */ + ret = dsdb_module_schema_info_blob_read(ldb_module, dsdb_flags, temp_ctx, &ndr_blob, parent); + if (ret != LDB_SUCCESS) { + talloc_free(temp_ctx); + return ret; + } + + /* convert NDR blob to dsdb_schema_info object */ + werr = dsdb_schema_info_from_blob(&ndr_blob, + mem_ctx, + _schema_info); + talloc_free(temp_ctx); + + if (W_ERROR_EQUAL(werr, WERR_DS_NO_ATTRIBUTE_OR_VALUE)) { + return LDB_ERR_NO_SUCH_ATTRIBUTE; + } + + if (!W_ERROR_IS_OK(werr)) { + ldb_asprintf_errstring(ldb_module_get_ctx(ldb_module), __location__ ": failed to get schema_info"); + return ldb_operr(ldb_module_get_ctx(ldb_module)); + } + + return LDB_SUCCESS; +} + +/** + * Writes schema_info structure into schemaInfo + * attribute on SCHEMA partition + * + * @param dsdb_flags DSDB_FLAG_... flag of 0 + */ +static int dsdb_module_schema_info_write(struct ldb_module *ldb_module, + uint32_t dsdb_flags, + const struct dsdb_schema_info *schema_info, + struct ldb_request *parent) +{ + WERROR werr; + int ret; + DATA_BLOB ndr_blob; + TALLOC_CTX *temp_ctx; + + temp_ctx = talloc_new(ldb_module); + if (temp_ctx == NULL) { + return ldb_module_oom(ldb_module); + } + + /* convert schema_info to a blob */ + werr = dsdb_blob_from_schema_info(schema_info, temp_ctx, &ndr_blob); + if (!W_ERROR_IS_OK(werr)) { + talloc_free(temp_ctx); + ldb_asprintf_errstring(ldb_module_get_ctx(ldb_module), __location__ ": failed to get schema_info"); + return ldb_operr(ldb_module_get_ctx(ldb_module)); + } + + /* write serialized schemaInfo into LDB */ + ret = dsdb_module_schema_info_blob_write(ldb_module, dsdb_flags, &ndr_blob, parent); + + talloc_free(temp_ctx); + + return ret; +} + + +/** + * Increments schemaInfo revision and save it to DB + * setting our invocationID in the process + * NOTE: this function should be called in a transaction + * much in the same way prefixMap update function is called + * + * @param ldb_module current module + * @param schema schema cache + * @param dsdb_flags DSDB_FLAG_... flag of 0 + */ +int dsdb_module_schema_info_update(struct ldb_module *ldb_module, + struct dsdb_schema *schema, + int dsdb_flags, struct ldb_request *parent) +{ + int ret; + const struct GUID *invocation_id; + struct dsdb_schema_info *schema_info; + WERROR werr; + TALLOC_CTX *temp_ctx = talloc_new(schema); + if (temp_ctx == NULL) { + return ldb_module_oom(ldb_module); + } + + invocation_id = samdb_ntds_invocation_id(ldb_module_get_ctx(ldb_module)); + if (!invocation_id) { + talloc_free(temp_ctx); + return ldb_operr(ldb_module_get_ctx(ldb_module)); + } + + /* read serialized schemaInfo from LDB */ + ret = dsdb_module_schema_info_read(ldb_module, dsdb_flags, temp_ctx, &schema_info, parent); + if (ret == LDB_ERR_NO_SUCH_ATTRIBUTE) { + /* make default value in case + * we have no schemaInfo value yet */ + werr = dsdb_schema_info_new(temp_ctx, &schema_info); + if (!W_ERROR_IS_OK(werr)) { + talloc_free(temp_ctx); + return ldb_module_oom(ldb_module); + } + ret = LDB_SUCCESS; + } + if (ret != LDB_SUCCESS) { + talloc_free(temp_ctx); + return ret; + } + + /* update schemaInfo */ + schema_info->revision++; + schema_info->invocation_id = *invocation_id; + + ret = dsdb_module_schema_info_write(ldb_module, dsdb_flags, schema_info, parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(ldb_module), + "dsdb_module_schema_info_update: failed to save schemaInfo - %s\n", + ldb_strerror(ret)); + talloc_free(temp_ctx); + return ret; + } + + /* + * We don't update the schema->schema_info! + * as that would not represent the other information + * in schema->* + * + * We're not sure if the current transaction will go through! + * E.g. schema changes are only allowed on the schema master, + * otherwise they result in a UNWILLING_TO_PERFORM and a + * + * Note that schema might a global variable shared between + * multiple ldb_contexts. With process model "single" it + * means the drsuapi server also uses it. + * + * We keep it simple and just try to update the + * stored value. + * + * The next schema reload will pick it up, which + * then works for originating and replicated changes + * in the same way. + */ + + talloc_free(temp_ctx); + return LDB_SUCCESS; +} diff --git a/source4/dsdb/samdb/ldb_modules/secrets_tdb_sync.c b/source4/dsdb/samdb/ldb_modules/secrets_tdb_sync.c new file mode 100644 index 0000000..52c8aad --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/secrets_tdb_sync.c @@ -0,0 +1,532 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2007-2012 + + 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 secrets_tdb_sync module + * + * Description: Update secrets.tdb whenever the matching secret record changes + * + * Author: Andrew Bartlett + */ + +#include "includes.h" +#include "ldb_module.h" +#include "lib/util/dlinklist.h" +#include "auth/credentials/credentials.h" +#include "auth/credentials/credentials_krb5.h" +#include "system/kerberos.h" +#include "auth/kerberos/kerberos.h" +#include "auth/kerberos/kerberos_srv_keytab.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "param/secrets.h" +#include "source3/include/secrets.h" +#include "lib/dbwrap/dbwrap.h" +#include "dsdb/samdb/samdb.h" + +struct dn_list { + struct ldb_message *msg; + bool do_delete; + struct dn_list *prev, *next; +}; + +struct secrets_tdb_sync_private { + struct dn_list *changed_dns; + struct db_context *secrets_tdb; +}; + +struct secrets_tdb_sync_ctx { + struct ldb_module *module; + struct ldb_request *req; + + struct ldb_dn *dn; + bool do_delete; + + struct ldb_reply *op_reply; + bool found; +}; + +static struct secrets_tdb_sync_ctx *secrets_tdb_sync_ctx_init(struct ldb_module *module, + struct ldb_request *req) +{ + struct secrets_tdb_sync_ctx *ac; + + ac = talloc_zero(req, struct secrets_tdb_sync_ctx); + if (ac == NULL) { + ldb_oom(ldb_module_get_ctx(module)); + return NULL; + } + + ac->module = module; + ac->req = req; + + return ac; +} + +/* FIXME: too many semi-async searches here for my taste, direct and indirect as + * cli_credentials_set_secrets() performs a sync ldb search. + * Just hope we are lucky and nothing breaks (using the tdb backend masks a lot + * of async issues). -SSS + */ +static int add_modified(struct ldb_module *module, struct ldb_dn *dn, bool do_delete, + struct ldb_request *parent) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct secrets_tdb_sync_private *data = talloc_get_type(ldb_module_get_private(module), struct secrets_tdb_sync_private); + struct dn_list *item; + char *filter; + struct ldb_result *res; + int ret; + + filter = talloc_asprintf(data, + "(&(objectClass=primaryDomain)(flatname=*))"); + if (!filter) { + return ldb_oom(ldb); + } + + ret = dsdb_module_search(module, data, &res, + dn, LDB_SCOPE_BASE, NULL, + DSDB_FLAG_NEXT_MODULE, parent, + "%s", filter); + talloc_free(filter); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (res->count != 1) { + /* if it's not a primaryDomain then we don't have anything to update */ + talloc_free(res); + return LDB_SUCCESS; + } + + item = talloc(data->changed_dns? (void *)data->changed_dns: (void *)data, struct dn_list); + if (!item) { + talloc_free(res); + return ldb_oom(ldb); + } + + item->msg = talloc_steal(item, res->msgs[0]); + item->do_delete = do_delete; + talloc_free(res); + + DLIST_ADD_END(data->changed_dns, item); + return LDB_SUCCESS; +} + +static int ust_search_modified(struct secrets_tdb_sync_ctx *ac); + +static int secrets_tdb_sync_op_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct secrets_tdb_sync_ctx *ac; + int ret; + + ac = talloc_get_type(req->context, struct secrets_tdb_sync_ctx); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + if (ares->type != LDB_REPLY_DONE) { + ldb_set_errstring(ldb, "Invalid request type!\n"); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + if (ac->do_delete) { + return ldb_module_done(ac->req, ares->controls, + ares->response, LDB_SUCCESS); + } + + ac->op_reply = talloc_steal(ac, ares); + + ret = ust_search_modified(ac); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + return LDB_SUCCESS; +} + +static int ust_del_op(struct secrets_tdb_sync_ctx *ac) +{ + struct ldb_context *ldb; + struct ldb_request *down_req; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + ret = ldb_build_del_req(&down_req, ldb, ac, + ac->dn, + ac->req->controls, + ac, secrets_tdb_sync_op_callback, + ac->req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(ac->module, down_req); +} + +static int ust_search_modified_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct secrets_tdb_sync_ctx *ac; + int ret; + + ac = talloc_get_type(req->context, struct secrets_tdb_sync_ctx); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + + ac->found = true; + break; + + case LDB_REPLY_REFERRAL: + /* ignore */ + break; + + case LDB_REPLY_DONE: + + if (ac->found) { + /* do the dirty sync job here :/ */ + ret = add_modified(ac->module, ac->dn, ac->do_delete, ac->req); + } + + if (ac->do_delete) { + ret = ust_del_op(ac); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, + NULL, NULL, ret); + } + break; + } + + return ldb_module_done(ac->req, ac->op_reply->controls, + ac->op_reply->response, LDB_SUCCESS); + } + + talloc_free(ares); + return LDB_SUCCESS; +} + +static int ust_search_modified(struct secrets_tdb_sync_ctx *ac) +{ + struct ldb_context *ldb; + static const char * const no_attrs[] = { NULL }; + struct ldb_request *search_req; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + ret = ldb_build_search_req(&search_req, ldb, ac, + ac->dn, LDB_SCOPE_BASE, + "(&(objectClass=kerberosSecret)" + "(privateKeytab=*))", no_attrs, + NULL, + ac, ust_search_modified_callback, + ac->req); + LDB_REQ_SET_LOCATION(search_req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(ac->module, search_req); +} + + +/* add */ +static int secrets_tdb_sync_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct secrets_tdb_sync_ctx *ac; + struct ldb_request *down_req; + int ret; + + ldb = ldb_module_get_ctx(module); + + ac = secrets_tdb_sync_ctx_init(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + ac->dn = req->op.add.message->dn; + + ret = ldb_build_add_req(&down_req, ldb, ac, + req->op.add.message, + req->controls, + ac, secrets_tdb_sync_op_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, down_req); +} + +/* modify */ +static int secrets_tdb_sync_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct secrets_tdb_sync_ctx *ac; + struct ldb_request *down_req; + int ret; + + ldb = ldb_module_get_ctx(module); + + ac = secrets_tdb_sync_ctx_init(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + ac->dn = req->op.mod.message->dn; + + ret = ldb_build_mod_req(&down_req, ldb, ac, + req->op.mod.message, + req->controls, + ac, secrets_tdb_sync_op_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, down_req); +} + +/* delete */ +static int secrets_tdb_sync_delete(struct ldb_module *module, struct ldb_request *req) +{ + struct secrets_tdb_sync_ctx *ac; + + ac = secrets_tdb_sync_ctx_init(module, req); + if (ac == NULL) { + return ldb_operr(ldb_module_get_ctx(module)); + } + + ac->dn = req->op.del.dn; + ac->do_delete = true; + + return ust_search_modified(ac); +} + +/* rename */ +static int secrets_tdb_sync_rename(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct secrets_tdb_sync_ctx *ac; + struct ldb_request *down_req; + int ret; + + ldb = ldb_module_get_ctx(module); + + ac = secrets_tdb_sync_ctx_init(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + ac->dn = req->op.rename.newdn; + + ret = ldb_build_rename_req(&down_req, ldb, ac, + req->op.rename.olddn, + req->op.rename.newdn, + req->controls, + ac, secrets_tdb_sync_op_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, down_req); +} + +/* prepare for a commit */ +static int secrets_tdb_sync_prepare_commit(struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct secrets_tdb_sync_private *data = talloc_get_type(ldb_module_get_private(module), + struct secrets_tdb_sync_private); + struct dn_list *p; + TALLOC_CTX *tmp_ctx; + + tmp_ctx = talloc_new(data); + if (!tmp_ctx) { + ldb_oom(ldb); + goto fail; + } + + for (p=data->changed_dns; p; p = p->next) { + const struct ldb_val *whenChanged = ldb_msg_find_ldb_val(p->msg, "whenChanged"); + time_t lct = 0; + bool ret; + + if (whenChanged) { + ldb_val_to_time(whenChanged, &lct); + } + + ret = secrets_store_machine_pw_sync(ldb_msg_find_attr_as_string(p->msg, "secret", NULL), + ldb_msg_find_attr_as_string(p->msg, "priorSecret", NULL), + + ldb_msg_find_attr_as_string(p->msg, "flatname", NULL), + ldb_msg_find_attr_as_string(p->msg, "realm", NULL), + ldb_msg_find_attr_as_string(p->msg, "saltPrincipal", NULL), + (uint32_t)ldb_msg_find_attr_as_int(p->msg, "msDS-SupportedEncryptionTypes", ENC_ALL_TYPES), + samdb_result_dom_sid(tmp_ctx, p->msg, "objectSid"), + + lct, + (uint32_t)ldb_msg_find_attr_as_int(p->msg, "secureChannelType", 0), + p->do_delete); + if (ret == false) { + ldb_asprintf_errstring(ldb, "Failed to update secrets.tdb from entry %s in %s", + ldb_dn_get_linearized(p->msg->dn), + (const char *)ldb_get_opaque(ldb, "ldb_url")); + goto fail; + } + } + + talloc_free(data->changed_dns); + data->changed_dns = NULL; + talloc_free(tmp_ctx); + + return ldb_next_prepare_commit(module); + +fail: + dbwrap_transaction_cancel(data->secrets_tdb); + talloc_free(data->changed_dns); + data->changed_dns = NULL; + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; +} + +/* start a transaction */ +static int secrets_tdb_sync_start_transaction(struct ldb_module *module) +{ + struct secrets_tdb_sync_private *data = talloc_get_type(ldb_module_get_private(module), struct secrets_tdb_sync_private); + + if (dbwrap_transaction_start(data->secrets_tdb) != 0) { + return ldb_module_operr(module); + } + + return ldb_next_start_trans(module); +} + +/* end a transaction */ +static int secrets_tdb_sync_end_transaction(struct ldb_module *module) +{ + struct secrets_tdb_sync_private *data = talloc_get_type(ldb_module_get_private(module), struct secrets_tdb_sync_private); + + if (dbwrap_transaction_commit(data->secrets_tdb) != 0) { + return ldb_module_operr(module); + } + + return ldb_next_end_trans(module); +} + +/* abandon a transaction */ +static int secrets_tdb_sync_del_transaction(struct ldb_module *module) +{ + struct secrets_tdb_sync_private *data = talloc_get_type(ldb_module_get_private(module), struct secrets_tdb_sync_private); + + talloc_free(data->changed_dns); + data->changed_dns = NULL; + if (dbwrap_transaction_cancel(data->secrets_tdb) != 0) { + return ldb_module_operr(module); + } + + return ldb_next_del_trans(module); +} + +static int secrets_tdb_sync_init(struct ldb_module *module) +{ + struct ldb_context *ldb; + struct secrets_tdb_sync_private *data; + char *private_dir, *p; + const char *secrets_ldb; + + ldb = ldb_module_get_ctx(module); + + data = talloc(module, struct secrets_tdb_sync_private); + if (data == NULL) { + return ldb_oom(ldb); + } + + data->changed_dns = NULL; + + ldb_module_set_private(module, data); + + secrets_ldb = (const char *)ldb_get_opaque(ldb, "ldb_url"); + if (!secrets_ldb) { + return ldb_operr(ldb); + } + if (strncmp("tdb://", secrets_ldb, 6) == 0) { + secrets_ldb += 6; + } + private_dir = talloc_strdup(data, secrets_ldb); + p = strrchr(private_dir, '/'); + if (p) { + *p = '\0'; + } else { + private_dir = talloc_strdup(data, "."); + } + + secrets_init_path(private_dir); + + TALLOC_FREE(private_dir); + + data->secrets_tdb = secrets_db_ctx(); + + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_secrets_tdb_sync_module_ops = { + .name = "secrets_tdb_sync", + .init_context = secrets_tdb_sync_init, + .add = secrets_tdb_sync_add, + .modify = secrets_tdb_sync_modify, + .rename = secrets_tdb_sync_rename, + .del = secrets_tdb_sync_delete, + .start_transaction = secrets_tdb_sync_start_transaction, + .prepare_commit = secrets_tdb_sync_prepare_commit, + .end_transaction = secrets_tdb_sync_end_transaction, + .del_transaction = secrets_tdb_sync_del_transaction, +}; + +int ldb_secrets_tdb_sync_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_secrets_tdb_sync_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/show_deleted.c b/source4/dsdb/samdb/ldb_modules/show_deleted.c new file mode 100644 index 0000000..e3dcad5 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/show_deleted.c @@ -0,0 +1,220 @@ +/* + ldb database library + + Copyright (C) Simo Sorce 2005 + Copyright (C) Stefan Metzmacher <metze@samba.org> 2007 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2009 + Copyright (C) Matthias Dieter Wallnöfer 2010 + + 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 deleted objects control module + * + * Description: this module hides deleted and recylced objects, and returns + * them if the right control is there + * + * Author: Stefan Metzmacher + */ + +#include "includes.h" +#include <ldb_module.h> +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" + +struct show_deleted_state { + bool need_refresh; + bool recycle_bin_enabled; +}; + +static int show_deleted_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct ldb_control *show_del, *show_rec; + struct ldb_request *down_req; + struct ldb_parse_tree *new_tree = req->op.search.tree; + struct show_deleted_state *state; + int ret; + const char *exclude_filter = NULL; + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.search.base)) { + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + /* This is the logic from MS-ADTS 3.1.1.3.4.1.14 that + determines if objects are visible + + Extended control name Deleted-objects Tombstones Recycled-objects + LDAP_SERVER_SHOW_DELETED_OID Visible Visible Not Visible + LDAP_SERVER_SHOW_RECYCLED_OID Visible Visible Visible + + Note that if the recycle bin is disabled, then the + isRecycled attribute is ignored, and objects are either + "normal" or "tombstone". + + When the recycle bin is enabled, then objects are in one of + 3 states, "normal", "deleted" or "recycled" + */ + + /* check if there's a show deleted control */ + show_del = ldb_request_get_control(req, LDB_CONTROL_SHOW_DELETED_OID); + /* check if there's a show recycled control */ + show_rec = ldb_request_get_control(req, LDB_CONTROL_SHOW_RECYCLED_OID); + + /* + * When recycle bin is not enabled, then all we look + * at is the isDeleted attribute. We hide objects with this + * attribute set to TRUE when the client has not specified either + * SHOW_DELETED or SHOW_RECYCLED + */ + if (show_rec == NULL && show_del == NULL) { + /* We don't want deleted or recycled objects, + * which we get by filtering on isDeleted */ + exclude_filter = "isDeleted"; + } else { + state = talloc_get_type(ldb_module_get_private(module), struct show_deleted_state); + + /* Note that state may be NULL during initialisation */ + if (state != NULL && state->need_refresh) { + /* Do not move this assignment, it can cause recursion loops! */ + state->need_refresh = false; + ret = dsdb_recyclebin_enabled(module, &state->recycle_bin_enabled); + if (ret != LDB_SUCCESS) { + state->recycle_bin_enabled = false; + /* + * We can fail to find the feature object + * during provision. Ignore any such error and + * assume the recycle bin cannot be enabled at + * this point in time. + */ + if (ret != LDB_ERR_NO_SUCH_OBJECT) { + state->need_refresh = true; + return LDB_ERR_UNWILLING_TO_PERFORM; + } + } + } + + if (state != NULL && state->recycle_bin_enabled) { + /* + * The recycle bin is enabled, so we want deleted not + * recycled. + */ + if (show_rec == NULL) { + exclude_filter = "isRecycled"; + } + } + } + + if (exclude_filter != NULL) { + new_tree = talloc(req, struct ldb_parse_tree); + if (!new_tree) { + return ldb_oom(ldb); + } + new_tree->operation = LDB_OP_AND; + new_tree->u.list.num_elements = 2; + new_tree->u.list.elements = talloc_array(new_tree, struct ldb_parse_tree *, 2); + if (!new_tree->u.list.elements) { + return ldb_oom(ldb); + } + + new_tree->u.list.elements[0] = talloc(new_tree->u.list.elements, struct ldb_parse_tree); + new_tree->u.list.elements[0]->operation = LDB_OP_NOT; + new_tree->u.list.elements[0]->u.isnot.child = + talloc(new_tree->u.list.elements, struct ldb_parse_tree); + if (!new_tree->u.list.elements[0]->u.isnot.child) { + return ldb_oom(ldb); + } + new_tree->u.list.elements[0]->u.isnot.child->operation = LDB_OP_EQUALITY; + new_tree->u.list.elements[0]->u.isnot.child->u.equality.attr = exclude_filter; + new_tree->u.list.elements[0]->u.isnot.child->u.equality.value = data_blob_string_const("TRUE"); + new_tree->u.list.elements[1] = req->op.search.tree; + } + + ret = ldb_build_search_req_ex(&down_req, ldb, req, + req->op.search.base, + req->op.search.scope, + new_tree, + req->op.search.attrs, + req->controls, + req, dsdb_next_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* mark the controls as done */ + if (show_del != NULL) { + show_del->critical = 0; + } + if (show_rec != NULL) { + show_rec->critical = 0; + } + + /* perform the search */ + return ldb_next_request(module, down_req); +} + +static int show_deleted_init(struct ldb_module *module) +{ + struct ldb_context *ldb; + int ret; + struct show_deleted_state *state; + + state = talloc_zero(module, struct show_deleted_state); + if (state == NULL) { + return ldb_module_oom(module); + } + state->need_refresh = true; + + ldb = ldb_module_get_ctx(module); + + ret = ldb_mod_register_control(module, LDB_CONTROL_SHOW_DELETED_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "show_deleted: Unable to register control with rootdse!\n"); + return ldb_operr(ldb); + } + + ret = ldb_mod_register_control(module, LDB_CONTROL_SHOW_RECYCLED_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "show_deleted: Unable to register control with rootdse!\n"); + return ldb_operr(ldb); + } + + ret = ldb_next_init(module); + + ldb_module_set_private(module, state); + + return ret; +} + +static const struct ldb_module_ops ldb_show_deleted_module_ops = { + .name = "show_deleted", + .search = show_deleted_search, + .init_context = show_deleted_init +}; + +int ldb_show_deleted_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_show_deleted_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/subtree_delete.c b/source4/dsdb/samdb/ldb_modules/subtree_delete.c new file mode 100644 index 0000000..24211b6 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/subtree_delete.c @@ -0,0 +1,142 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2006-2007 + Copyright (C) Andrew Tridgell <tridge@samba.org> 2009 + Copyright (C) Stefan Metzmacher <metze@samba.org> 2007 + Copyright (C) Simo Sorce <idra@samba.org> 2008 + Copyright (C) Matthias Dieter Wallnöfer 2010 + + 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 subtree delete module + * + * Description: Delete of a subtree in LDB + * + * Author: Andrew Bartlett + */ + +#include "includes.h" +#include <ldb.h> +#include <ldb_module.h> +#include "dsdb/samdb/ldb_modules/util.h" +#include "dsdb/common/util.h" + + +static int subtree_delete(struct ldb_module *module, struct ldb_request *req) +{ + static const char * const attrs[] = { NULL }; + struct ldb_result *res = NULL; + uint32_t flags; + unsigned int i; + int ret; + + if (ldb_dn_is_special(req->op.del.dn)) { + /* do not manipulate our control entries */ + return ldb_next_request(module, req); + } + + /* see if we have any children */ + ret = dsdb_module_search(module, req, &res, req->op.del.dn, + LDB_SCOPE_ONELEVEL, attrs, + DSDB_FLAG_NEXT_MODULE, + req, + "(objectClass=*)"); + if (ret != LDB_SUCCESS) { + talloc_free(res); + return ret; + } + if (res->count == 0) { + talloc_free(res); + return ldb_next_request(module, req); + } + + if (ldb_request_get_control(req, LDB_CONTROL_TREE_DELETE_OID) == NULL) { + /* Do not add any DN outputs to this error string! + * Some MMC consoles (eg release 2000) have a strange + * bug and prevent subtree deletes afterwards. */ + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "subtree_delete: Unable to " + "delete a non-leaf node " + "(it has %u children)!", + res->count); + talloc_free(res); + return LDB_ERR_NOT_ALLOWED_ON_NON_LEAF; + } + + /* + * we need to start from the top since other LDB modules could + * enforce constraints (eg "objectclass" and "samldb" do so). + * + * We pass DSDB_FLAG_AS_SYSTEM as the acl module above us + * has already checked for SEC_ADS_DELETE_TREE. + */ + flags = DSDB_FLAG_TOP_MODULE | + DSDB_FLAG_AS_SYSTEM | + DSDB_FLAG_TRUSTED | + DSDB_TREE_DELETE; + if (ldb_request_get_control(req, LDB_CONTROL_RELAX_OID) != NULL) { + flags |= DSDB_MODIFY_RELAX; + } + + /* + * The net result of this code is that the leaf nodes are + * deleted first, as the parent is only deleted once these + * calls (and the delete calls recursive within these) + * complete. + */ + for (i = 0; i < res->count; i++) { + ret = dsdb_module_del(module, res->msgs[i]->dn, flags, req); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + talloc_free(res); + + return ldb_next_request(module, req); +} + +static int subtree_delete_init(struct ldb_module *module) +{ + struct ldb_context *ldb; + int ret; + + ldb = ldb_module_get_ctx(module); + + ret = ldb_mod_register_control(module, LDB_CONTROL_TREE_DELETE_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, + "subtree_delete: Unable to register control with rootdse!\n"); + return ldb_operr(ldb); + } + + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_subtree_delete_module_ops = { + .name = "subtree_delete", + .init_context = subtree_delete_init, + .del = subtree_delete +}; + +int ldb_subtree_delete_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_subtree_delete_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/subtree_rename.c b/source4/dsdb/samdb/ldb_modules/subtree_rename.c new file mode 100644 index 0000000..be02e92 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/subtree_rename.c @@ -0,0 +1,202 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2006-2007 + Copyright (C) Stefan Metzmacher <metze@samba.org> 2007 + Copyright (C) Matthias Dieter Wallnöfer <mdw@samba.org> 2010-2011 + + 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 subtree rename module + * + * Description: Rename a subtree in LDB + * + * Author: Andrew Bartlett + */ + +#include "includes.h" +#include <ldb.h> +#include <ldb_module.h> +#include "libds/common/flags.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" + +struct subtree_rename_context { + struct ldb_module *module; + struct ldb_request *req; + bool base_renamed; +}; + +static struct subtree_rename_context *subren_ctx_init(struct ldb_module *module, + struct ldb_request *req) +{ + struct subtree_rename_context *ac; + + + ac = talloc_zero(req, struct subtree_rename_context); + if (ac == NULL) { + return NULL; + } + + ac->module = module; + ac->req = req; + ac->base_renamed = false; + + return ac; +} + +static int subtree_rename_search_onelevel_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct subtree_rename_context *ac; + int ret; + + ac = talloc_get_type(req->context, struct subtree_rename_context); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + if (ac->base_renamed == false) { + ac->base_renamed = true; + + ret = dsdb_module_rename(ac->module, + ac->req->op.rename.olddn, + ac->req->op.rename.newdn, + DSDB_FLAG_NEXT_MODULE, req); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + { + struct ldb_dn *old_dn = NULL; + struct ldb_dn *new_dn = NULL; + + old_dn = ares->message->dn; + new_dn = ldb_dn_copy(ares, old_dn); + if (!new_dn) { + return ldb_module_oom(ac->module); + } + + if ( ! ldb_dn_remove_base_components(new_dn, + ldb_dn_get_comp_num(ac->req->op.rename.olddn))) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + if ( ! ldb_dn_add_base(new_dn, ac->req->op.rename.newdn)) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + ret = dsdb_module_rename(ac->module, old_dn, new_dn, DSDB_FLAG_OWN_MODULE, req); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + talloc_free(ares); + + return LDB_SUCCESS; + } + case LDB_REPLY_REFERRAL: + /* ignore */ + break; + case LDB_REPLY_DONE: + talloc_free(ares); + return ldb_module_done(ac->req, NULL, NULL, LDB_SUCCESS); + default: + { + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + + ldb_asprintf_errstring(ldb, "Invalid LDB reply type %d", ares->type); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + } + + return LDB_SUCCESS; +} + +/* rename */ +static int subtree_rename(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + static const char * const no_attrs[] = {NULL}; + struct ldb_request *search_req; + struct subtree_rename_context *ac; + int ret; + + if (ldb_dn_is_special(req->op.rename.olddn)) { /* do not manipulate our control entries */ + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + /* + * This gets complex: We need to: + * - Do a search for all entries under this entry + * - Wait for these results to appear + * - Do our own rename (in first callback) + * - In the callback for each result, issue a dsdb_module_rename() + */ + + ac = subren_ctx_init(module, req); + if (!ac) { + return ldb_oom(ldb); + } + + ret = ldb_build_search_req(&search_req, ldb_module_get_ctx(ac->module), ac, + ac->req->op.rename.olddn, + LDB_SCOPE_ONELEVEL, + "(objectClass=*)", + no_attrs, + NULL, + ac, + subtree_rename_search_onelevel_callback, + req); + LDB_REQ_SET_LOCATION(search_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_request_add_control(search_req, LDB_CONTROL_SHOW_RECYCLED_OID, + true, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(ac->module, search_req); +} + +static const struct ldb_module_ops ldb_subtree_rename_module_ops = { + .name = "subtree_rename", + .rename = subtree_rename +}; + +int ldb_subtree_rename_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_subtree_rename_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/tests/possibleinferiors.py b/source4/dsdb/samdb/ldb_modules/tests/possibleinferiors.py new file mode 100755 index 0000000..d28be8f --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/tests/possibleinferiors.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 + +# Unix SMB/CIFS implementation. +# Copyright (C) Andrew Tridgell 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/>. +# + +"""Tests the possibleInferiors generation in the schema_fsmo ldb module""" + +import optparse +import sys + + +# Find right directory when running from source tree +sys.path.insert(0, "bin/python") + +from samba import getopt as options, Ldb +import ldb + +parser = optparse.OptionParser("possibleinferiors.py <URL> [<CLASS>]") +sambaopts = options.SambaOptions(parser) +parser.add_option_group(sambaopts) +credopts = options.CredentialsOptions(parser) +parser.add_option_group(credopts) +parser.add_option_group(options.VersionOptions(parser)) +parser.add_option("--wspp", action="store_true") + +opts, args = parser.parse_args() + +if len(args) < 1: + parser.print_usage() + sys.exit(1) + +url = args[0] +if (len(args) > 1): + objectclass = args[1] +else: + objectclass = None + + +def uniq_list(alist): + """return a unique list""" + set = {} + return [set.setdefault(e, e) for e in alist if e not in set] + + +lp_ctx = sambaopts.get_loadparm() + +creds = credopts.get_credentials(lp_ctx) + +ldb_options = [] +# use 'paged_search' module when connecting remotely +if url.lower().startswith("ldap://"): + ldb_options = ["modules:paged_searches"] + +db = Ldb(url, credentials=creds, lp=lp_ctx, options=ldb_options) + +# get the rootDSE +res = db.search(base="", expression="", + scope=ldb.SCOPE_BASE, + attrs=["schemaNamingContext"]) +rootDse = res[0] + +schema_base = rootDse["schemaNamingContext"][0] + + +def possible_inferiors_search(db, oc): + """return the possible inferiors via a search for the possibleInferiors attribute""" + res = db.search(base=schema_base, + expression=("ldapDisplayName=%s" % oc), + attrs=["possibleInferiors"]) + + poss = [] + if len(res) == 0 or res[0].get("possibleInferiors") is None: + return poss + for item in res[0]["possibleInferiors"]: + poss.append(str(item)) + poss = uniq_list(poss) + poss.sort() + return poss + + +# see [MS-ADTS] section 3.1.1.4.5.21 +# and section 3.1.1.4.2 for this algorithm + +# !systemOnly=TRUE +# !objectClassCategory=2 +# !objectClassCategory=3 + +def supclasses(classinfo, oc): + list = [] + if oc == "top": + return list + if classinfo[oc].get("SUPCLASSES") is not None: + return classinfo[oc]["SUPCLASSES"] + res = classinfo[oc]["subClassOf"] + for r in res: + list.append(r) + list.extend(supclasses(classinfo, r)) + classinfo[oc]["SUPCLASSES"] = list + return list + + +def auxclasses(classinfo, oclist): + list = [] + if oclist == []: + return list + for oc in oclist: + if classinfo[oc].get("AUXCLASSES") is not None: + list.extend(classinfo[oc]["AUXCLASSES"]) + else: + list2 = [] + list2.extend(classinfo[oc]["systemAuxiliaryClass"]) + list2.extend(auxclasses(classinfo, classinfo[oc]["systemAuxiliaryClass"])) + list2.extend(classinfo[oc]["auxiliaryClass"]) + list2.extend(auxclasses(classinfo, classinfo[oc]["auxiliaryClass"])) + list2.extend(auxclasses(classinfo, supclasses(classinfo, oc))) + classinfo[oc]["AUXCLASSES"] = list2 + list.extend(list2) + return list + + +def subclasses(classinfo, oclist): + list = [] + for oc in oclist: + list.extend(classinfo[oc]["SUBCLASSES"]) + return list + + +def posssuperiors(classinfo, oclist): + list = [] + for oc in oclist: + if classinfo[oc].get("POSSSUPERIORS") is not None: + list.extend(classinfo[oc]["POSSSUPERIORS"]) + else: + list2 = [] + list2.extend(classinfo[oc]["systemPossSuperiors"]) + list2.extend(classinfo[oc]["possSuperiors"]) + list2.extend(posssuperiors(classinfo, supclasses(classinfo, oc))) + if opts.wspp: + # the WSPP docs suggest we should do this: + list2.extend(posssuperiors(classinfo, auxclasses(classinfo, [oc]))) + else: + # but testing against w2k3 and w2k8 shows that we need to do this instead + list2.extend(subclasses(classinfo, list2)) + classinfo[oc]["POSSSUPERIORS"] = list2 + list.extend(list2) + return list + + +def pull_classinfo(db): + """At startup we build a classinfo[] dictionary that holds all the information needed to construct the possible inferiors""" + classinfo = {} + res = db.search(base=schema_base, + expression="objectclass=classSchema", + attrs=["ldapDisplayName", "systemOnly", "objectClassCategory", + "possSuperiors", "systemPossSuperiors", + "auxiliaryClass", "systemAuxiliaryClass", "subClassOf"]) + for r in res: + name = str(r["ldapDisplayName"][0]) + classinfo[name] = {} + if str(r["systemOnly"]) == "TRUE": + classinfo[name]["systemOnly"] = True + else: + classinfo[name]["systemOnly"] = False + if r.get("objectClassCategory"): + classinfo[name]["objectClassCategory"] = int(r["objectClassCategory"][0]) + else: + classinfo[name]["objectClassCategory"] = 0 + for a in ["possSuperiors", "systemPossSuperiors", + "auxiliaryClass", "systemAuxiliaryClass", + "subClassOf"]: + classinfo[name][a] = [] + if r.get(a): + for i in r[a]: + classinfo[name][a].append(str(i)) + + # build a list of subclasses for each class + def subclasses_recurse(subclasses, oc): + list = subclasses[oc] + for c in list: + list.extend(subclasses_recurse(subclasses, c)) + return list + + subclasses = {} + for oc in classinfo: + subclasses[oc] = [] + for oc in classinfo: + for c in classinfo[oc]["subClassOf"]: + if not c == oc: + subclasses[c].append(oc) + for oc in classinfo: + classinfo[oc]["SUBCLASSES"] = uniq_list(subclasses_recurse(subclasses, oc)) + + return classinfo + + +def is_in_list(list, c): + for a in list: + if c == a: + return True + return False + + +def possible_inferiors_constructed(db, classinfo, c): + list = [] + for oc in classinfo: + superiors = posssuperiors(classinfo, [oc]) + if (is_in_list(superiors, c) and + classinfo[oc]["systemOnly"] == False and + classinfo[oc]["objectClassCategory"] != 2 and + classinfo[oc]["objectClassCategory"] != 3): + list.append(oc) + list = uniq_list(list) + list.sort() + return list + + +def test_class(db, classinfo, oc): + """test to see if one objectclass returns the correct possibleInferiors""" + print("test: objectClass.%s" % oc) + poss1 = possible_inferiors_search(db, oc) + poss2 = possible_inferiors_constructed(db, classinfo, oc) + if poss1 != poss2: + print("failure: objectClass.%s [" % oc) + print("Returned incorrect list for objectclass %s" % oc) + print("search: %s" % poss1) + print("constructed: %s" % poss2) + for i in range(0, min(len(poss1), len(poss2))): + print("%30s %30s" % (poss1[i], poss2[i])) + print("]") + sys.exit(1) + else: + print("success: objectClass.%s" % oc) + + +def get_object_classes(db): + """return a list of all object classes""" + list = [] + for item in classinfo: + list.append(item) + return list + + +classinfo = pull_classinfo(db) + +if objectclass is None: + for oc in get_object_classes(db): + test_class(db, classinfo, oc) +else: + test_class(db, classinfo, objectclass) + +print("Lists match OK") diff --git a/source4/dsdb/samdb/ldb_modules/tests/test_audit_log.c b/source4/dsdb/samdb/ldb_modules/tests/test_audit_log.c new file mode 100644 index 0000000..885248e --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/tests/test_audit_log.c @@ -0,0 +1,2305 @@ +/* + Unit tests for the dsdb audit logging code code in audit_log.c + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018 + + 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/>. +*/ + +#include <stdarg.h> +#include <stddef.h> +#include <setjmp.h> +#include <unistd.h> +#include <cmocka.h> + +int ldb_audit_log_module_init(const char *version); +#include "../audit_log.c" + +#include "lib/ldb/include/ldb_private.h" +#include <regex.h> +#include <float.h> + +/* + * Test helper to check ISO 8601 timestamps for validity + */ +static void check_timestamp(time_t before, const char* timestamp) +{ + int rc; + int usec, tz; + char c[2]; + struct tm tm; + time_t after; + time_t actual; + struct timeval tv; + + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + after = tv.tv_sec; + + /* + * Convert the ISO 8601 timestamp into a time_t + * Note for convenience we ignore the value of the microsecond + * part of the time stamp. + */ + rc = sscanf( + timestamp, + "%4d-%2d-%2dT%2d:%2d:%2d.%6d%1c%4d", + &tm.tm_year, + &tm.tm_mon, + &tm.tm_mday, + &tm.tm_hour, + &tm.tm_min, + &tm.tm_sec, + &usec, + c, + &tz); + assert_int_equal(9, rc); + tm.tm_year = tm.tm_year - 1900; + tm.tm_mon = tm.tm_mon - 1; + tm.tm_isdst = -1; + actual = mktime(&tm); + + /* + * The timestamp should be before <= actual <= after + */ + assert_in_range(actual, before, after); +} + +static void test_has_password_changed(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_message *msg = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + /* + * Empty message + */ + msg = ldb_msg_new(ldb); + assert_false(has_password_changed(msg)); + TALLOC_FREE(msg); + + /* + * No password attributes + */ + msg = ldb_msg_new(ldb); + ldb_msg_add_string(msg, "attr01", "value01"); + assert_false(has_password_changed(msg)); + TALLOC_FREE(msg); + + /* + * No password attributes >1 entries + */ + msg = ldb_msg_new(ldb); + ldb_msg_add_string(msg, "attr01", "value01"); + ldb_msg_add_string(msg, "attr02", "value03"); + ldb_msg_add_string(msg, "attr03", "value03"); + assert_false(has_password_changed(msg)); + TALLOC_FREE(msg); + + /* + * userPassword set + */ + msg = ldb_msg_new(ldb); + ldb_msg_add_string(msg, "userPassword", "value01"); + assert_true(has_password_changed(msg)); + TALLOC_FREE(msg); + + /* + * clearTextPassword set + */ + msg = ldb_msg_new(ldb); + ldb_msg_add_string(msg, "clearTextPassword", "value01"); + assert_true(has_password_changed(msg)); + TALLOC_FREE(msg); + + /* + * unicodePwd set + */ + msg = ldb_msg_new(ldb); + ldb_msg_add_string(msg, "unicodePwd", "value01"); + assert_true(has_password_changed(msg)); + TALLOC_FREE(msg); + + /* + * dBCSPwd set + */ + msg = ldb_msg_new(ldb); + ldb_msg_add_string(msg, "dBCSPwd", "value01"); + assert_true(has_password_changed(msg)); + TALLOC_FREE(msg); + + /* + * All attributes set + */ + msg = ldb_msg_new(ldb); + ldb_msg_add_string(msg, "userPassword", "value01"); + ldb_msg_add_string(msg, "clearTextPassword", "value02"); + ldb_msg_add_string(msg, "unicodePwd", "value03"); + ldb_msg_add_string(msg, "dBCSPwd", "value04"); + assert_true(has_password_changed(msg)); + TALLOC_FREE(msg); + + /* + * first attribute is a password attribute + */ + msg = ldb_msg_new(ldb); + ldb_msg_add_string(msg, "userPassword", "value01"); + ldb_msg_add_string(msg, "attr02", "value02"); + ldb_msg_add_string(msg, "attr03", "value03"); + ldb_msg_add_string(msg, "attr04", "value04"); + assert_true(has_password_changed(msg)); + TALLOC_FREE(msg); + + /* + * last attribute is a password attribute + */ + msg = ldb_msg_new(ldb); + ldb_msg_add_string(msg, "attr01", "value01"); + ldb_msg_add_string(msg, "attr02", "value02"); + ldb_msg_add_string(msg, "attr03", "value03"); + ldb_msg_add_string(msg, "clearTextPassword", "value04"); + assert_true(has_password_changed(msg)); + TALLOC_FREE(msg); + + /* + * middle attribute is a password attribute + */ + msg = ldb_msg_new(ldb); + ldb_msg_add_string(msg, "attr01", "value01"); + ldb_msg_add_string(msg, "attr02", "value02"); + ldb_msg_add_string(msg, "unicodePwd", "pwd"); + ldb_msg_add_string(msg, "attr03", "value03"); + ldb_msg_add_string(msg, "attr04", "value04"); + assert_true(has_password_changed(msg)); + TALLOC_FREE(msg); + + TALLOC_FREE(ctx); +} + +static void test_get_password_action(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct dsdb_control_password_acl_validation *pav = NULL; + int ret; + + TALLOC_CTX *ctx = talloc_new(NULL); + ldb = ldb_init(ctx, NULL); + + /* + * Add request, will always be a reset + */ + ldb_build_add_req(&req, ldb, ctx, NULL, NULL, NULL, NULL, NULL); + reply = talloc_zero(ctx, struct ldb_reply); + assert_string_equal("Reset", get_password_action(req, reply)); + TALLOC_FREE(req); + TALLOC_FREE(reply); + + /* + * No password control acl, expect "Reset" + */ + ldb_build_mod_req(&req, ldb, ctx, NULL, NULL, NULL, NULL, NULL); + reply = talloc_zero(ctx, struct ldb_reply); + assert_string_equal("Reset", get_password_action(req, reply)); + TALLOC_FREE(req); + TALLOC_FREE(reply); + + /* + * dsdb_control_password_acl_validation reset = false, expect "Change" + */ + ret = ldb_build_mod_req(&req, ldb, ctx, NULL, NULL, NULL, NULL, NULL); + assert_int_equal(ret, LDB_SUCCESS); + reply = talloc_zero(ctx, struct ldb_reply); + pav = talloc_zero(req, struct dsdb_control_password_acl_validation); + + ldb_reply_add_control( + reply, + DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID, + false, + pav); + assert_string_equal("Change", get_password_action(req, reply)); + TALLOC_FREE(req); + TALLOC_FREE(reply); + + /* + * dsdb_control_password_acl_validation reset = true, expect "Reset" + */ + ldb_build_mod_req(&req, ldb, ctx, NULL, NULL, NULL, NULL, NULL); + reply = talloc_zero(ctx, struct ldb_reply); + pav = talloc_zero(req, struct dsdb_control_password_acl_validation); + pav->pwd_reset = true; + + ldb_reply_add_control( + reply, + DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID, + false, + pav); + assert_string_equal("Reset", get_password_action(req, reply)); + TALLOC_FREE(req); + TALLOC_FREE(reply); + + TALLOC_FREE(ctx); +} + +/* + * Test helper to validate a version object. + */ +static void check_version(struct json_t *version, int major, int minor) +{ + struct json_t *v = NULL; + + assert_true(json_is_object(version)); + assert_int_equal(2, json_object_size(version)); + + v = json_object_get(version, "major"); + assert_non_null(v); + assert_int_equal(major, json_integer_value(v)); + + v = json_object_get(version, "minor"); + assert_non_null(v); + assert_int_equal(minor, json_integer_value(v)); +} + +/* + * minimal unit test of operation_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_operation_json_empty(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + + struct json_object json; + json_t *audit = NULL; + json_t *v = NULL; + json_t *o = NULL; + time_t before; + struct timeval tv; + int rc; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + audit_private = talloc_zero(ctx, struct audit_private); + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + req = talloc_zero(ctx, struct ldb_request); + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_SUCCESS; + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + before = tv.tv_sec; + json = operation_json(module, req, reply); + assert_int_equal(3, json_object_size(json.root)); + + + v = json_object_get(json.root, "type"); + assert_non_null(v); + assert_string_equal("dsdbChange", json_string_value(v)); + + v = json_object_get(json.root, "timestamp"); + assert_non_null(v); + assert_true(json_is_string(v)); + check_timestamp(before, json_string_value(v)); + + audit = json_object_get(json.root, "dsdbChange"); + assert_non_null(audit); + assert_true(json_is_object(audit)); + assert_int_equal(10, json_object_size(audit)); + + o = json_object_get(audit, "version"); + assert_non_null(o); + check_version(o, OPERATION_MAJOR, OPERATION_MINOR); + + v = json_object_get(audit, "statusCode"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(LDB_SUCCESS, json_integer_value(v)); + + v = json_object_get(audit, "status"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("Success", json_string_value(v)); + + v = json_object_get(audit, "operation"); + assert_non_null(v); + assert_true(json_is_string(v)); + /* + * Search operation constant is zero + */ + assert_string_equal("Search", json_string_value(v)); + + v = json_object_get(audit, "remoteAddress"); + assert_non_null(v); + assert_true(json_is_null(v)); + + v = json_object_get(audit, "userSid"); + assert_non_null(v); + assert_true(json_is_null(v)); + + v = json_object_get(audit, "performedAsSystem"); + assert_non_null(v); + assert_true(json_is_boolean(v)); + assert_true(json_is_false(v)); + + + v = json_object_get(audit, "dn"); + assert_non_null(v); + assert_true(json_is_null(v)); + + v = json_object_get(audit, "transactionId"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal( + "00000000-0000-0000-0000-000000000000", + json_string_value(v)); + + v = json_object_get(audit, "sessionId"); + assert_non_null(v); + assert_true(json_is_null(v)); + + json_free(&json); + TALLOC_FREE(ctx); + +} + +/* + * unit test of operation_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_operation_json(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + + struct tsocket_address *ts = NULL; + + struct auth_session_info *sess = NULL; + struct security_token *token = NULL; + struct dom_sid sid; + const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + struct GUID session_id; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct ldb_dn *dn = NULL; + const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG"; + + struct ldb_message *msg = NULL; + + struct json_object json; + json_t *audit = NULL; + json_t *v = NULL; + json_t *o = NULL; + json_t *a = NULL; + json_t *b = NULL; + json_t *c = NULL; + json_t *d = NULL; + json_t *e = NULL; + json_t *f = NULL; + json_t *g = NULL; + time_t before; + struct timeval tv; + int rc; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + audit_private = talloc_zero(ctx, struct audit_private); + GUID_from_string(TRANSACTION, &transaction_id); + audit_private->transaction_guid = transaction_id; + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(ldb, "remoteAddress", ts); + + sess = talloc_zero(ctx, struct auth_session_info); + token = talloc_zero(ctx, struct security_token); + string_to_sid(&sid, SID); + token->num_sids = 1; + token->sids = &sid; + sess->security_token = token; + GUID_from_string(SESSION, &session_id); + sess->unique_session_token = session_id; + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sess); + + msg = talloc_zero(ctx, struct ldb_message); + dn = ldb_dn_new(ctx, ldb, DN); + msg->dn = dn; + ldb_msg_add_string(msg, "attribute", "the-value"); + + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + req->op.add.message = msg; + + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_ERR_OPERATIONS_ERROR; + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + before = tv.tv_sec; + json = operation_json(module, req, reply); + assert_int_equal(3, json_object_size(json.root)); + + v = json_object_get(json.root, "type"); + assert_non_null(v); + assert_string_equal("dsdbChange", json_string_value(v)); + + v = json_object_get(json.root, "timestamp"); + assert_non_null(v); + assert_true(json_is_string(v)); + check_timestamp(before, json_string_value(v)); + + audit = json_object_get(json.root, "dsdbChange"); + assert_non_null(audit); + assert_true(json_is_object(audit)); + assert_int_equal(11, json_object_size(audit)); + + o = json_object_get(audit, "version"); + assert_non_null(o); + check_version(o, OPERATION_MAJOR, OPERATION_MINOR); + + v = json_object_get(audit, "statusCode"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(LDB_ERR_OPERATIONS_ERROR, json_integer_value(v)); + + v = json_object_get(audit, "status"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("Operations error", json_string_value(v)); + + v = json_object_get(audit, "operation"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("Add", json_string_value(v)); + + v = json_object_get(audit, "remoteAddress"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("ipv4:127.0.0.1:0", json_string_value(v)); + + v = json_object_get(audit, "userSid"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(SID, json_string_value(v)); + + v = json_object_get(audit, "performedAsSystem"); + assert_non_null(v); + assert_true(json_is_boolean(v)); + assert_true(json_is_false(v)); + + v = json_object_get(audit, "dn"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(DN, json_string_value(v)); + + v = json_object_get(audit, "transactionId"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(TRANSACTION, json_string_value(v)); + + v = json_object_get(audit, "sessionId"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(SESSION, json_string_value(v)); + + o = json_object_get(audit, "attributes"); + assert_non_null(v); + assert_true(json_is_object(o)); + assert_int_equal(1, json_object_size(o)); + + a = json_object_get(o, "attribute"); + assert_non_null(a); + assert_true(json_is_object(a)); + + b = json_object_get(a, "actions"); + assert_non_null(b); + assert_true(json_is_array(b)); + assert_int_equal(1, json_array_size(b)); + + c = json_array_get(b, 0); + assert_non_null(c); + assert_true(json_is_object(c)); + + d = json_object_get(c, "action"); + assert_non_null(d); + assert_true(json_is_string(d)); + assert_string_equal("add", json_string_value(d)); + + e = json_object_get(c, "values"); + assert_non_null(b); + assert_true(json_is_array(e)); + assert_int_equal(1, json_array_size(e)); + + f = json_array_get(e, 0); + assert_non_null(f); + assert_true(json_is_object(f)); + assert_int_equal(1, json_object_size(f)); + + g = json_object_get(f, "value"); + assert_non_null(g); + assert_true(json_is_string(g)); + assert_string_equal("the-value", json_string_value(g)); + + json_free(&json); + TALLOC_FREE(ctx); + +} + +/* + * unit test of operation_json, that ensures that all the expected + * attributes and objects are in the json object. + * In this case for an operation performed as the system user. + */ +static void test_as_system_operation_json(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + + struct tsocket_address *ts = NULL; + + struct auth_session_info *sess = NULL; + struct auth_session_info *sys_sess = NULL; + struct security_token *token = NULL; + struct security_token *sys_token = NULL; + struct dom_sid sid; + const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const SYS_SESSION = "7130cb06-2062-6a1b-409e-3514c26b1998"; + struct GUID session_id; + struct GUID sys_session_id; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct ldb_dn *dn = NULL; + const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG"; + + struct ldb_message *msg = NULL; + + struct json_object json; + json_t *audit = NULL; + json_t *v = NULL; + json_t *o = NULL; + json_t *a = NULL; + json_t *b = NULL; + json_t *c = NULL; + json_t *d = NULL; + json_t *e = NULL; + json_t *f = NULL; + json_t *g = NULL; + time_t before; + struct timeval tv; + int rc; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + audit_private = talloc_zero(ctx, struct audit_private); + GUID_from_string(TRANSACTION, &transaction_id); + audit_private->transaction_guid = transaction_id; + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(ldb, "remoteAddress", ts); + + sess = talloc_zero(ctx, struct auth_session_info); + token = talloc_zero(ctx, struct security_token); + string_to_sid(&sid, SID); + token->num_sids = 1; + token->sids = &sid; + sess->security_token = token; + GUID_from_string(SESSION, &session_id); + sess->unique_session_token = session_id; + ldb_set_opaque(ldb, DSDB_NETWORK_SESSION_INFO, sess); + + sys_sess = talloc_zero(ctx, struct auth_session_info); + sys_token = talloc_zero(ctx, struct security_token); + sys_token->num_sids = 1; + sys_token->sids = discard_const(&global_sid_System); + sys_sess->security_token = sys_token; + GUID_from_string(SYS_SESSION, &sys_session_id); + sess->unique_session_token = sys_session_id; + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sys_sess); + + msg = talloc_zero(ctx, struct ldb_message); + dn = ldb_dn_new(ctx, ldb, DN); + msg->dn = dn; + ldb_msg_add_string(msg, "attribute", "the-value"); + + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + req->op.add.message = msg; + + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_ERR_OPERATIONS_ERROR; + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + before = tv.tv_sec; + json = operation_json(module, req, reply); + assert_int_equal(3, json_object_size(json.root)); + + v = json_object_get(json.root, "type"); + assert_non_null(v); + assert_string_equal("dsdbChange", json_string_value(v)); + + v = json_object_get(json.root, "timestamp"); + assert_non_null(v); + assert_true(json_is_string(v)); + check_timestamp(before, json_string_value(v)); + + audit = json_object_get(json.root, "dsdbChange"); + assert_non_null(audit); + assert_true(json_is_object(audit)); + assert_int_equal(11, json_object_size(audit)); + + o = json_object_get(audit, "version"); + assert_non_null(o); + check_version(o, OPERATION_MAJOR, OPERATION_MINOR); + + v = json_object_get(audit, "statusCode"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(LDB_ERR_OPERATIONS_ERROR, json_integer_value(v)); + + v = json_object_get(audit, "status"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("Operations error", json_string_value(v)); + + v = json_object_get(audit, "operation"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("Add", json_string_value(v)); + + v = json_object_get(audit, "remoteAddress"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("ipv4:127.0.0.1:0", json_string_value(v)); + + v = json_object_get(audit, "userSid"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(SID, json_string_value(v)); + + v = json_object_get(audit, "performedAsSystem"); + assert_non_null(v); + assert_true(json_is_boolean(v)); + assert_true(json_is_true(v)); + + v = json_object_get(audit, "dn"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(DN, json_string_value(v)); + + v = json_object_get(audit, "transactionId"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(TRANSACTION, json_string_value(v)); + + v = json_object_get(audit, "sessionId"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(SYS_SESSION, json_string_value(v)); + + o = json_object_get(audit, "attributes"); + assert_non_null(v); + assert_true(json_is_object(o)); + assert_int_equal(1, json_object_size(o)); + + a = json_object_get(o, "attribute"); + assert_non_null(a); + assert_true(json_is_object(a)); + + b = json_object_get(a, "actions"); + assert_non_null(b); + assert_true(json_is_array(b)); + assert_int_equal(1, json_array_size(b)); + + c = json_array_get(b, 0); + assert_non_null(c); + assert_true(json_is_object(c)); + + d = json_object_get(c, "action"); + assert_non_null(d); + assert_true(json_is_string(d)); + assert_string_equal("add", json_string_value(d)); + + e = json_object_get(c, "values"); + assert_non_null(b); + assert_true(json_is_array(e)); + assert_int_equal(1, json_array_size(e)); + + f = json_array_get(e, 0); + assert_non_null(f); + assert_true(json_is_object(f)); + assert_int_equal(1, json_object_size(f)); + + g = json_object_get(f, "value"); + assert_non_null(g); + assert_true(json_is_string(g)); + assert_string_equal("the-value", json_string_value(g)); + + json_free(&json); + TALLOC_FREE(ctx); + +} + +/* + * minimal unit test of password_change_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_password_change_json_empty(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + + struct json_object json; + json_t *audit = NULL; + json_t *v = NULL; + json_t *o = NULL; + time_t before; + struct timeval tv; + int rc; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + audit_private = talloc_zero(ctx, struct audit_private); + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + req = talloc_zero(ctx, struct ldb_request); + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_SUCCESS; + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + before = tv.tv_sec; + json = password_change_json(module, req, reply); + assert_int_equal(3, json_object_size(json.root)); + + + v = json_object_get(json.root, "type"); + assert_non_null(v); + assert_string_equal("passwordChange", json_string_value(v)); + + v = json_object_get(json.root, "timestamp"); + assert_non_null(v); + assert_true(json_is_string(v)); + check_timestamp(before, json_string_value(v)); + + audit = json_object_get(json.root, "passwordChange"); + assert_non_null(audit); + assert_true(json_is_object(audit)); + assert_int_equal(10, json_object_size(audit)); + + o = json_object_get(audit, "version"); + assert_non_null(o); + + v = json_object_get(audit, "eventId"); + assert_non_null(v); + + v = json_object_get(audit, "statusCode"); + assert_non_null(v); + + v = json_object_get(audit, "status"); + assert_non_null(v); + + v = json_object_get(audit, "remoteAddress"); + assert_non_null(v); + + v = json_object_get(audit, "userSid"); + assert_non_null(v); + + v = json_object_get(audit, "dn"); + assert_non_null(v); + + v = json_object_get(audit, "transactionId"); + assert_non_null(v); + + v = json_object_get(audit, "sessionId"); + assert_non_null(v); + + v = json_object_get(audit, "action"); + assert_non_null(v); + + json_free(&json); + TALLOC_FREE(ctx); + +} + +/* + * minimal unit test of password_change_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_password_change_json(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + + struct tsocket_address *ts = NULL; + + struct auth_session_info *sess = NULL; + struct security_token *token = NULL; + struct dom_sid sid; + const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + struct GUID session_id; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct ldb_dn *dn = NULL; + const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG"; + + struct ldb_message *msg = NULL; + + struct json_object json; + json_t *audit = NULL; + json_t *v = NULL; + json_t *o = NULL; + time_t before; + struct timeval tv; + int rc; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + audit_private = talloc_zero(ctx, struct audit_private); + GUID_from_string(TRANSACTION, &transaction_id); + audit_private->transaction_guid = transaction_id; + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(ldb, "remoteAddress", ts); + + sess = talloc_zero(ctx, struct auth_session_info); + token = talloc_zero(ctx, struct security_token); + string_to_sid(&sid, SID); + token->num_sids = 1; + token->sids = &sid; + sess->security_token = token; + GUID_from_string(SESSION, &session_id); + sess->unique_session_token = session_id; + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sess); + + msg = talloc_zero(ctx, struct ldb_message); + dn = ldb_dn_new(ctx, ldb, DN); + msg->dn = dn; + ldb_msg_add_string(msg, "planTextPassword", "super-secret"); + + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + req->op.add.message = msg; + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_SUCCESS; + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + before = tv.tv_sec; + json = password_change_json(module, req, reply); + assert_int_equal(3, json_object_size(json.root)); + + + v = json_object_get(json.root, "type"); + assert_non_null(v); + assert_string_equal("passwordChange", json_string_value(v)); + + v = json_object_get(json.root, "timestamp"); + assert_non_null(v); + assert_true(json_is_string(v)); + check_timestamp(before, json_string_value(v)); + + audit = json_object_get(json.root, "passwordChange"); + assert_non_null(audit); + assert_true(json_is_object(audit)); + assert_int_equal(10, json_object_size(audit)); + + o = json_object_get(audit, "version"); + assert_non_null(o); + check_version(o, PASSWORD_MAJOR,PASSWORD_MINOR); + + v = json_object_get(audit, "eventId"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(EVT_ID_PASSWORD_RESET, json_integer_value(v)); + + v = json_object_get(audit, "statusCode"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(LDB_SUCCESS, json_integer_value(v)); + + v = json_object_get(audit, "status"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("Success", json_string_value(v)); + + v = json_object_get(audit, "remoteAddress"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("ipv4:127.0.0.1:0", json_string_value(v)); + + v = json_object_get(audit, "userSid"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(SID, json_string_value(v)); + + v = json_object_get(audit, "dn"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(DN, json_string_value(v)); + + v = json_object_get(audit, "transactionId"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(TRANSACTION, json_string_value(v)); + + v = json_object_get(audit, "sessionId"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(SESSION, json_string_value(v)); + + v = json_object_get(audit, "action"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("Reset", json_string_value(v)); + + json_free(&json); + TALLOC_FREE(ctx); + +} + + +/* + * minimal unit test of transaction_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_transaction_json(void **state) +{ + + struct GUID guid; + const char * const GUID = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct json_object json; + json_t *audit = NULL; + json_t *v = NULL; + json_t *o = NULL; + time_t before; + struct timeval tv; + int rc; + + GUID_from_string(GUID, &guid); + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + before = tv.tv_sec; + json = transaction_json("delete", &guid, 10000099); + + assert_int_equal(3, json_object_size(json.root)); + + + v = json_object_get(json.root, "type"); + assert_non_null(v); + assert_string_equal("dsdbTransaction", json_string_value(v)); + + v = json_object_get(json.root, "timestamp"); + assert_non_null(v); + assert_true(json_is_string(v)); + check_timestamp(before, json_string_value(v)); + + audit = json_object_get(json.root, "dsdbTransaction"); + assert_non_null(audit); + assert_true(json_is_object(audit)); + assert_int_equal(4, json_object_size(audit)); + + o = json_object_get(audit, "version"); + assert_non_null(o); + check_version(o, TRANSACTION_MAJOR, TRANSACTION_MINOR); + + v = json_object_get(audit, "transactionId"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(GUID, json_string_value(v)); + + v = json_object_get(audit, "action"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("delete", json_string_value(v)); + + v = json_object_get(audit, "duration"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(10000099, json_integer_value(v)); + + json_free(&json); + +} + +/* + * minimal unit test of commit_failure_json, that ensures that all the + * expected attributes and objects are in the json object. + */ +static void test_commit_failure_json(void **state) +{ + + struct GUID guid; + const char * const GUID = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct json_object json; + json_t *audit = NULL; + json_t *v = NULL; + json_t *o = NULL; + time_t before; + struct timeval tv; + int rc; + + GUID_from_string(GUID, &guid); + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + before = tv.tv_sec; + json = commit_failure_json( + "prepare", + 987876, + LDB_ERR_OPERATIONS_ERROR, + "because", + &guid); + + assert_int_equal(3, json_object_size(json.root)); + + + v = json_object_get(json.root, "type"); + assert_non_null(v); + assert_string_equal("dsdbTransaction", json_string_value(v)); + + v = json_object_get(json.root, "timestamp"); + assert_non_null(v); + assert_true(json_is_string(v)); + check_timestamp(before, json_string_value(v)); + + audit = json_object_get(json.root, "dsdbTransaction"); + assert_non_null(audit); + assert_true(json_is_object(audit)); + assert_int_equal(7, json_object_size(audit)); + + o = json_object_get(audit, "version"); + assert_non_null(o); + check_version(o, TRANSACTION_MAJOR, TRANSACTION_MINOR); + + v = json_object_get(audit, "transactionId"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(GUID, json_string_value(v)); + + v = json_object_get(audit, "action"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("prepare", json_string_value(v)); + + v = json_object_get(audit, "statusCode"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(LDB_ERR_OPERATIONS_ERROR, json_integer_value(v)); + + v = json_object_get(audit, "status"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("Operations error", json_string_value(v)); + v = json_object_get(audit, "status"); + assert_non_null(v); + + v = json_object_get(audit, "reason"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("because", json_string_value(v)); + + v = json_object_get(audit, "duration"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(987876, json_integer_value(v)); + + json_free(&json); + +} + +/* + * minimal unit test of replicated_update_json, that ensures that all the + * expected attributes and objects are in the json object. + */ +static void test_replicated_update_json_empty(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + struct dsdb_extended_replicated_objects *ro = NULL; + struct repsFromTo1 *source_dsa = NULL; + + struct json_object json; + json_t *audit = NULL; + json_t *v = NULL; + json_t *o = NULL; + time_t before; + struct timeval tv; + int rc; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + audit_private = talloc_zero(ctx, struct audit_private); + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + source_dsa = talloc_zero(ctx, struct repsFromTo1); + ro = talloc_zero(ctx, struct dsdb_extended_replicated_objects); + ro->source_dsa = source_dsa; + req = talloc_zero(ctx, struct ldb_request); + req->op.extended.data = ro; + req->operation = LDB_EXTENDED; + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_SUCCESS; + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + before = tv.tv_sec; + json = replicated_update_json(module, req, reply); + assert_int_equal(3, json_object_size(json.root)); + + + v = json_object_get(json.root, "type"); + assert_non_null(v); + assert_string_equal("replicatedUpdate", json_string_value(v)); + + v = json_object_get(json.root, "timestamp"); + assert_non_null(v); + assert_true(json_is_string(v)); + check_timestamp(before, json_string_value(v)); + + audit = json_object_get(json.root, "replicatedUpdate"); + assert_non_null(audit); + assert_true(json_is_object(audit)); + assert_int_equal(11, json_object_size(audit)); + + o = json_object_get(audit, "version"); + assert_non_null(o); + check_version(o, REPLICATION_MAJOR, REPLICATION_MINOR); + + v = json_object_get(audit, "statusCode"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(LDB_SUCCESS, json_integer_value(v)); + + v = json_object_get(audit, "status"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("Success", json_string_value(v)); + + v = json_object_get(audit, "transactionId"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal( + "00000000-0000-0000-0000-000000000000", + json_string_value(v)); + + v = json_object_get(audit, "objectCount"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(0, json_integer_value(v)); + + v = json_object_get(audit, "linkCount"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(0, json_integer_value(v)); + + v = json_object_get(audit, "partitionDN"); + assert_non_null(v); + assert_true(json_is_null(v)); + + v = json_object_get(audit, "error"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal( + "The operation completed successfully.", + json_string_value(v)); + + v = json_object_get(audit, "errorCode"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(0, json_integer_value(v)); + + v = json_object_get(audit, "sourceDsa"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal( + "00000000-0000-0000-0000-000000000000", + json_string_value(v)); + + v = json_object_get(audit, "invocationId"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal( + "00000000-0000-0000-0000-000000000000", + json_string_value(v)); + + json_free(&json); + TALLOC_FREE(ctx); + +} + +/* + * unit test of replicated_update_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_replicated_update_json(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + struct dsdb_extended_replicated_objects *ro = NULL; + struct repsFromTo1 *source_dsa = NULL; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct ldb_dn *dn = NULL; + const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG"; + + struct GUID source_dsa_obj_guid; + const char *const SOURCE_DSA = "7130cb06-2062-6a1b-409e-3514c26b1793"; + + struct GUID invocation_id; + const char *const INVOCATION_ID = + "7130cb06-2062-6a1b-409e-3514c26b1893"; + struct json_object json; + json_t *audit = NULL; + json_t *v = NULL; + json_t *o = NULL; + time_t before; + struct timeval tv; + int rc; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + audit_private = talloc_zero(ctx, struct audit_private); + GUID_from_string(TRANSACTION, &transaction_id); + audit_private->transaction_guid = transaction_id; + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + dn = ldb_dn_new(ctx, ldb, DN); + GUID_from_string(SOURCE_DSA, &source_dsa_obj_guid); + GUID_from_string(INVOCATION_ID, &invocation_id); + source_dsa = talloc_zero(ctx, struct repsFromTo1); + source_dsa->source_dsa_obj_guid = source_dsa_obj_guid; + source_dsa->source_dsa_invocation_id = invocation_id; + + ro = talloc_zero(ctx, struct dsdb_extended_replicated_objects); + ro->source_dsa = source_dsa; + ro->num_objects = 808; + ro->linked_attributes_count = 2910; + ro->partition_dn = dn; + ro->error = WERR_NOT_SUPPORTED; + + + req = talloc_zero(ctx, struct ldb_request); + req->op.extended.data = ro; + req->operation = LDB_EXTENDED; + + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_ERR_NO_SUCH_OBJECT; + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + before = tv.tv_sec; + json = replicated_update_json(module, req, reply); + assert_int_equal(3, json_object_size(json.root)); + + + v = json_object_get(json.root, "type"); + assert_non_null(v); + assert_string_equal("replicatedUpdate", json_string_value(v)); + + v = json_object_get(json.root, "timestamp"); + assert_non_null(v); + assert_true(json_is_string(v)); + check_timestamp(before, json_string_value(v)); + + audit = json_object_get(json.root, "replicatedUpdate"); + assert_non_null(audit); + assert_true(json_is_object(audit)); + assert_int_equal(11, json_object_size(audit)); + + o = json_object_get(audit, "version"); + assert_non_null(o); + check_version(o, REPLICATION_MAJOR, REPLICATION_MINOR); + + v = json_object_get(audit, "statusCode"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(LDB_ERR_NO_SUCH_OBJECT, json_integer_value(v)); + + v = json_object_get(audit, "status"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("No such object", json_string_value(v)); + + v = json_object_get(audit, "transactionId"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(TRANSACTION, json_string_value(v)); + + v = json_object_get(audit, "objectCount"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(808, json_integer_value(v)); + + v = json_object_get(audit, "linkCount"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(2910, json_integer_value(v)); + + v = json_object_get(audit, "partitionDN"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(DN, json_string_value(v)); + + v = json_object_get(audit, "error"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal( + "The request is not supported.", + json_string_value(v)); + + v = json_object_get(audit, "errorCode"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(W_ERROR_V(WERR_NOT_SUPPORTED), json_integer_value(v)); + + v = json_object_get(audit, "sourceDsa"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(SOURCE_DSA, json_string_value(v)); + + v = json_object_get(audit, "invocationId"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal(INVOCATION_ID, json_string_value(v)); + + json_free(&json); + TALLOC_FREE(ctx); + +} + +/* + * minimal unit test of operation_human_readable, that ensures that all the + * expected attributes and objects are in the json object. + */ +static void test_operation_hr_empty(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + + char *line = NULL; + const char *rs = NULL; + regex_t regex; + + int ret; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + audit_private = talloc_zero(ctx, struct audit_private); + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + req = talloc_zero(ctx, struct ldb_request); + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_SUCCESS; + + line = operation_human_readable(ctx, module, req, reply); + assert_non_null(line); + + /* + * We ignore the timestamp to make this test a little easier + * to write. + */ + rs = "\\[Search] at \\[" + "[^[]*" + "\\] status \\[Success\\] remote host \\[Unknown\\]" + " SID \\[(NULL SID)\\] DN \\[(null)\\]"; + + ret = regcomp(®ex, rs, 0); + assert_int_equal(0, ret); + + ret = regexec(®ex, line, 0, NULL, 0); + assert_int_equal(0, ret); + + regfree(®ex); + TALLOC_FREE(ctx); + +} + +/* + * unit test of operation_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_operation_hr(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + + struct tsocket_address *ts = NULL; + + struct auth_session_info *sess = NULL; + struct security_token *token = NULL; + struct dom_sid sid; + const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + struct GUID session_id; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct ldb_dn *dn = NULL; + const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG"; + + struct ldb_message *msg = NULL; + + char *line = NULL; + const char *rs = NULL; + regex_t regex; + + int ret; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + audit_private = talloc_zero(ctx, struct audit_private); + GUID_from_string(TRANSACTION, &transaction_id); + audit_private->transaction_guid = transaction_id; + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(ldb, "remoteAddress", ts); + + sess = talloc_zero(ctx, struct auth_session_info); + token = talloc_zero(ctx, struct security_token); + string_to_sid(&sid, SID); + token->num_sids = 1; + token->sids = &sid; + sess->security_token = token; + GUID_from_string(SESSION, &session_id); + sess->unique_session_token = session_id; + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sess); + + msg = talloc_zero(ctx, struct ldb_message); + dn = ldb_dn_new(ctx, ldb, DN); + msg->dn = dn; + ldb_msg_add_string(msg, "attribute", "the-value"); + + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + req->op.add.message = msg; + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_SUCCESS; + + line = operation_human_readable(ctx, module, req, reply); + assert_non_null(line); + + /* + * We ignore the timestamp to make this test a little easier + * to write. + */ + rs = "\\[Add\\] at \\[" + "[^]]*" + "\\] status \\[Success\\] " + "remote host \\[ipv4:127.0.0.1:0\\] " + "SID \\[S-1-5-21-2470180966-3899876309-2637894779\\] " + "DN \\[dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG\\] " + "attributes \\[attribute \\[the-value\\]\\]"; + + ret = regcomp(®ex, rs, 0); + assert_int_equal(0, ret); + + ret = regexec(®ex, line, 0, NULL, 0); + assert_int_equal(0, ret); + + regfree(®ex); + TALLOC_FREE(ctx); +} + +/* + * unit test of operation_json, that ensures that all the expected + * attributes and objects are in the json object. + * In this case the operation is being performed in a system session. + */ +static void test_as_system_operation_hr(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + + struct tsocket_address *ts = NULL; + + struct auth_session_info *sess = NULL; + struct auth_session_info *sys_sess = NULL; + struct security_token *token = NULL; + struct security_token *sys_token = NULL; + struct dom_sid sid; + const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const SYS_SESSION = "7130cb06-2062-6a1b-409e-3514c26b1999"; + struct GUID session_id; + struct GUID sys_session_id; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct ldb_dn *dn = NULL; + const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG"; + + struct ldb_message *msg = NULL; + + char *line = NULL; + const char *rs = NULL; + regex_t regex; + + int ret; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + audit_private = talloc_zero(ctx, struct audit_private); + GUID_from_string(TRANSACTION, &transaction_id); + audit_private->transaction_guid = transaction_id; + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(ldb, "remoteAddress", ts); + + sess = talloc_zero(ctx, struct auth_session_info); + token = talloc_zero(ctx, struct security_token); + string_to_sid(&sid, SID); + token->num_sids = 1; + token->sids = &sid; + sess->security_token = token; + GUID_from_string(SESSION, &session_id); + sess->unique_session_token = session_id; + ldb_set_opaque(ldb, DSDB_NETWORK_SESSION_INFO, sess); + + sys_sess = talloc_zero(ctx, struct auth_session_info); + sys_token = talloc_zero(ctx, struct security_token); + sys_token->num_sids = 1; + sys_token->sids = discard_const(&global_sid_System); + sys_sess->security_token = sys_token; + GUID_from_string(SYS_SESSION, &sys_session_id); + sess->unique_session_token = sys_session_id; + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sys_sess); + + msg = talloc_zero(ctx, struct ldb_message); + dn = ldb_dn_new(ctx, ldb, DN); + msg->dn = dn; + ldb_msg_add_string(msg, "attribute", "the-value"); + + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + req->op.add.message = msg; + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_SUCCESS; + + line = operation_human_readable(ctx, module, req, reply); + assert_non_null(line); + + /* + * We ignore the timestamp to make this test a little easier + * to write. + */ + rs = "\\[Add\\] at \\[" + "[^]]*" + "\\] status \\[Success\\] " + "remote host \\[ipv4:127.0.0.1:0\\] " + "SID \\[S-1-5-21-2470180966-3899876309-2637894779\\] " + "DN \\[dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG\\] " + "attributes \\[attribute \\[the-value\\]\\]"; + + ret = regcomp(®ex, rs, 0); + assert_int_equal(0, ret); + + ret = regexec(®ex, line, 0, NULL, 0); + assert_int_equal(0, ret); + + regfree(®ex); + TALLOC_FREE(ctx); +} + +/* + * minimal unit test of password_change_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_password_change_hr_empty(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + + char *line = NULL; + const char *rs = NULL; + regex_t regex; + int ret; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + audit_private = talloc_zero(ctx, struct audit_private); + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + req = talloc_zero(ctx, struct ldb_request); + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_SUCCESS; + + line = password_change_human_readable(ctx, module, req, reply); + assert_non_null(line); + + /* + * We ignore the timestamp to make this test a little easier + * to write. + */ + rs = "\\[Reset] at \\[" + "[^[]*" + "\\] status \\[Success\\] remote host \\[Unknown\\]" + " SID \\[(NULL SID)\\] DN \\[(null)\\]"; + + ret = regcomp(®ex, rs, 0); + assert_int_equal(0, ret); + + ret = regexec(®ex, line, 0, NULL, 0); + assert_int_equal(0, ret); + + regfree(®ex); + TALLOC_FREE(ctx); +} + +/* + * minimal unit test of password_change_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_password_change_hr(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + + struct tsocket_address *ts = NULL; + + struct auth_session_info *sess = NULL; + struct security_token *token = NULL; + struct dom_sid sid; + const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + struct GUID session_id; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct ldb_dn *dn = NULL; + const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG"; + + struct ldb_message *msg = NULL; + + char *line = NULL; + const char *rs = NULL; + regex_t regex; + int ret; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + audit_private = talloc_zero(ctx, struct audit_private); + GUID_from_string(TRANSACTION, &transaction_id); + audit_private->transaction_guid = transaction_id; + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(ldb, "remoteAddress", ts); + + sess = talloc_zero(ctx, struct auth_session_info); + token = talloc_zero(ctx, struct security_token); + string_to_sid(&sid, SID); + token->num_sids = 1; + token->sids = &sid; + sess->security_token = token; + GUID_from_string(SESSION, &session_id); + sess->unique_session_token = session_id; + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sess); + + msg = talloc_zero(ctx, struct ldb_message); + dn = ldb_dn_new(ctx, ldb, DN); + msg->dn = dn; + ldb_msg_add_string(msg, "planTextPassword", "super-secret"); + + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + req->op.add.message = msg; + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_SUCCESS; + + line = password_change_human_readable(ctx, module, req, reply); + assert_non_null(line); + + /* + * We ignore the timestamp to make this test a little easier + * to write. + */ + rs = "\\[Reset\\] at \\[" + "[^[]*" + "\\] status \\[Success\\] " + "remote host \\[ipv4:127.0.0.1:0\\] " + "SID \\[S-1-5-21-2470180966-3899876309-2637894779\\] " + "DN \\[dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG\\]"; + + ret = regcomp(®ex, rs, 0); + assert_int_equal(0, ret); + + ret = regexec(®ex, line, 0, NULL, 0); + assert_int_equal(0, ret); + + regfree(®ex); + TALLOC_FREE(ctx); + +} + +/* + * minimal unit test of transaction_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_transaction_hr(void **state) +{ + + struct GUID guid; + const char * const GUID = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + char *line = NULL; + const char *rs = NULL; + regex_t regex; + int ret; + + TALLOC_CTX *ctx = talloc_new(NULL); + + GUID_from_string(GUID, &guid); + + line = transaction_human_readable(ctx, "delete", 23); + assert_non_null(line); + + /* + * We ignore the timestamp to make this test a little easier + * to write. + */ + rs = "\\[delete] at \\[[^[]*\\] duration \\[23\\]"; + + ret = regcomp(®ex, rs, 0); + assert_int_equal(0, ret); + + ret = regexec(®ex, line, 0, NULL, 0); + assert_int_equal(0, ret); + + regfree(®ex); + TALLOC_FREE(ctx); + +} + +/* + * minimal unit test of commit_failure_hr, that ensures + * that all the expected conten is in the log entry. + */ +static void test_commit_failure_hr(void **state) +{ + + struct GUID guid; + const char * const GUID = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + char *line = NULL; + const char *rs = NULL; + regex_t regex; + int ret; + + TALLOC_CTX *ctx = talloc_new(NULL); + + GUID_from_string(GUID, &guid); + + line = commit_failure_human_readable( + ctx, + "commit", + 789345, + LDB_ERR_OPERATIONS_ERROR, + "because"); + + assert_non_null(line); + + /* + * We ignore the timestamp to make this test a little easier + * to write. + */ + rs = "\\[commit\\] at \\[[^[]*\\] duration \\[789345\\] " + "status \\[1\\] reason \\[because\\]"; + + ret = regcomp(®ex, rs, 0); + assert_int_equal(0, ret); + + ret = regexec(®ex, line, 0, NULL, 0); + assert_int_equal(0, ret); + + regfree(®ex); + TALLOC_FREE(ctx); +} + +static void test_add_transaction_id(void **state) +{ + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct audit_private *audit_private = NULL; + struct GUID guid; + const char * const GUID = "7130cb06-2062-6a1b-409e-3514c26b1773"; + struct ldb_control * control = NULL; + int status; + + TALLOC_CTX *ctx = talloc_new(NULL); + + audit_private = talloc_zero(ctx, struct audit_private); + GUID_from_string(GUID, &guid); + audit_private->transaction_guid = guid; + + module = talloc_zero(ctx, struct ldb_module); + ldb_module_set_private(module, audit_private); + + req = talloc_zero(ctx, struct ldb_request); + + status = add_transaction_id(module, req); + assert_int_equal(LDB_SUCCESS, status); + + control = ldb_request_get_control( + req, + DSDB_CONTROL_TRANSACTION_IDENTIFIER_OID); + assert_non_null(control); + assert_memory_equal( + &audit_private->transaction_guid, + control->data, + sizeof(struct GUID)); + + TALLOC_FREE(ctx); +} + +static void test_log_attributes(void **state) +{ + struct ldb_message *msg = NULL; + + char *buf = NULL; + char *str = NULL; + char lv[MAX_LENGTH+2]; + char ex[MAX_LENGTH+80]; + + TALLOC_CTX *ctx = talloc_new(NULL); + + + /* + * Test an empty message + * Should get empty attributes representation. + */ + buf = talloc_zero(ctx, char); + msg = talloc_zero(ctx, struct ldb_message); + + str = log_attributes(ctx, buf, LDB_ADD, msg); + assert_string_equal("", str); + + TALLOC_FREE(str); + TALLOC_FREE(msg); + + /* + * Test a message with a single secret attribute + */ + buf = talloc_zero(ctx, char); + msg = talloc_zero(ctx, struct ldb_message); + ldb_msg_add_string(msg, "clearTextPassword", "secret"); + + str = log_attributes(ctx, buf, LDB_ADD, msg); + assert_string_equal( + "clearTextPassword [REDACTED SECRET ATTRIBUTE]", + str); + TALLOC_FREE(str); + /* + * Test as a modify message, should add an action + * action will be unknown as there are no ACL's set + */ + buf = talloc_zero(ctx, char); + str = log_attributes(ctx, buf, LDB_MODIFY, msg); + assert_string_equal( + "unknown: clearTextPassword [REDACTED SECRET ATTRIBUTE]", + str); + + TALLOC_FREE(str); + TALLOC_FREE(msg); + + /* + * Test a message with a single attribute, single valued attribute + */ + buf = talloc_zero(ctx, char); + msg = talloc_zero(ctx, struct ldb_message); + ldb_msg_add_string(msg, "attribute", "value"); + + str = log_attributes(ctx, buf, LDB_ADD, msg); + assert_string_equal( + "attribute [value]", + str); + + TALLOC_FREE(str); + TALLOC_FREE(msg); + + /* + * Test a message with a single attribute, single valued attribute + * And as a modify + */ + buf = talloc_zero(ctx, char); + msg = talloc_zero(ctx, struct ldb_message); + ldb_msg_add_string(msg, "attribute", "value"); + + str = log_attributes(ctx, buf, LDB_MODIFY, msg); + assert_string_equal( + "unknown: attribute [value]", + str); + + TALLOC_FREE(str); + TALLOC_FREE(msg); + + /* + * Test a message with multiple attributes and a multi-valued attribute + * + */ + buf = talloc_zero(ctx, char); + msg = talloc_zero(ctx, struct ldb_message); + ldb_msg_add_string(msg, "attribute01", "value01"); + ldb_msg_add_string(msg, "attribute02", "value02"); + ldb_msg_add_string(msg, "attribute02", "value03"); + + str = log_attributes(ctx, buf, LDB_MODIFY, msg); + assert_string_equal( + "unknown: attribute01 [value01] " + "unknown: attribute02 [value02] [value03]", + str); + + TALLOC_FREE(str); + TALLOC_FREE(msg); + + /* + * Test a message with a single attribute, single valued attribute + * with a non printable character. Should be base64 encoded + */ + buf = talloc_zero(ctx, char); + msg = talloc_zero(ctx, struct ldb_message); + ldb_msg_add_string(msg, "attribute", "value\n"); + + str = log_attributes(ctx, buf, LDB_ADD, msg); + assert_string_equal("attribute {dmFsdWUK}", str); + + TALLOC_FREE(str); + TALLOC_FREE(msg); + + /* + * Test a message with a single valued attribute + * with more than MAX_LENGTH characters, should be truncated with + * trailing ... + */ + buf = talloc_zero(ctx, char); + msg = talloc_zero(ctx, struct ldb_message); + memset(lv, '\0', sizeof(lv)); + memset(lv, 'x', MAX_LENGTH+1); + ldb_msg_add_string(msg, "attribute", lv); + + str = log_attributes(ctx, buf, LDB_ADD, msg); + snprintf(ex, sizeof(ex), "attribute [%.*s...]", MAX_LENGTH, lv); + assert_string_equal(ex, str); + + TALLOC_FREE(str); + TALLOC_FREE(msg); + + TALLOC_FREE(ctx); +} + +/* + * minimal unit test of replicated_update_human_readable + */ +static void test_replicated_update_hr_empty(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + struct dsdb_extended_replicated_objects *ro = NULL; + struct repsFromTo1 *source_dsa = NULL; + + const char* line = NULL; + const char *rs = NULL; + regex_t regex; + int ret; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + audit_private = talloc_zero(ctx, struct audit_private); + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + source_dsa = talloc_zero(ctx, struct repsFromTo1); + ro = talloc_zero(ctx, struct dsdb_extended_replicated_objects); + ro->source_dsa = source_dsa; + req = talloc_zero(ctx, struct ldb_request); + req->op.extended.data = ro; + req->operation = LDB_EXTENDED; + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_SUCCESS; + + line = replicated_update_human_readable(ctx, module, req, reply); + assert_non_null(line); + /* + * We ignore the timestamp to make this test a little easier + * to write. + */ + rs = "at \\[[^[]*\\] " + "status \\[Success\\] " + "error \\[The operation completed successfully.\\] " + "partition \\[(null)\\] objects \\[0\\] links \\[0\\] " + "object \\[00000000-0000-0000-0000-000000000000\\] " + "invocation \\[00000000-0000-0000-0000-000000000000\\]"; + + ret = regcomp(®ex, rs, 0); + assert_int_equal(0, ret); + + ret = regexec(®ex, line, 0, NULL, 0); + assert_int_equal(0, ret); + + regfree(®ex); + TALLOC_FREE(ctx); + +} + +/* + * unit test of replicated_update_human_readable + */ +static void test_replicated_update_hr(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + struct dsdb_extended_replicated_objects *ro = NULL; + struct repsFromTo1 *source_dsa = NULL; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct ldb_dn *dn = NULL; + const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG"; + + struct GUID source_dsa_obj_guid; + const char *const SOURCE_DSA = "7130cb06-2062-6a1b-409e-3514c26b1793"; + + struct GUID invocation_id; + const char *const INVOCATION_ID = + "7130cb06-2062-6a1b-409e-3514c26b1893"; + + const char* line = NULL; + const char *rs = NULL; + regex_t regex; + int ret; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + audit_private = talloc_zero(ctx, struct audit_private); + GUID_from_string(TRANSACTION, &transaction_id); + audit_private->transaction_guid = transaction_id; + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + dn = ldb_dn_new(ctx, ldb, DN); + GUID_from_string(SOURCE_DSA, &source_dsa_obj_guid); + GUID_from_string(INVOCATION_ID, &invocation_id); + source_dsa = talloc_zero(ctx, struct repsFromTo1); + source_dsa->source_dsa_obj_guid = source_dsa_obj_guid; + source_dsa->source_dsa_invocation_id = invocation_id; + + ro = talloc_zero(ctx, struct dsdb_extended_replicated_objects); + ro->source_dsa = source_dsa; + ro->num_objects = 808; + ro->linked_attributes_count = 2910; + ro->partition_dn = dn; + ro->error = WERR_NOT_SUPPORTED; + + + req = talloc_zero(ctx, struct ldb_request); + req->op.extended.data = ro; + req->operation = LDB_EXTENDED; + + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_ERR_NO_SUCH_OBJECT; + + line = replicated_update_human_readable(ctx, module, req, reply); + assert_non_null(line); + + /* + * We ignore the timestamp to make this test a little easier + * to write. + */ + rs = "at \\[[^[]*\\] " + "status \\[No such object\\] " + "error \\[The request is not supported.\\] " + "partition \\[dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG\\] " + "objects \\[808\\] links \\[2910\\] " + "object \\[7130cb06-2062-6a1b-409e-3514c26b1793\\] " + "invocation \\[7130cb06-2062-6a1b-409e-3514c26b1893\\]"; + + ret = regcomp(®ex, rs, 0); + assert_int_equal(0, ret); + + ret = regexec(®ex, line, 0, NULL, 0); + assert_int_equal(0, ret); + + regfree(®ex); + TALLOC_FREE(ctx); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_has_password_changed), + cmocka_unit_test(test_get_password_action), + cmocka_unit_test(test_operation_json_empty), + cmocka_unit_test(test_operation_json), + cmocka_unit_test(test_as_system_operation_json), + cmocka_unit_test(test_password_change_json_empty), + cmocka_unit_test(test_password_change_json), + cmocka_unit_test(test_transaction_json), + cmocka_unit_test(test_commit_failure_json), + cmocka_unit_test(test_replicated_update_json_empty), + cmocka_unit_test(test_replicated_update_json), + cmocka_unit_test(test_add_transaction_id), + cmocka_unit_test(test_operation_hr_empty), + cmocka_unit_test(test_operation_hr), + cmocka_unit_test(test_as_system_operation_hr), + cmocka_unit_test(test_password_change_hr_empty), + cmocka_unit_test(test_password_change_hr), + cmocka_unit_test(test_transaction_hr), + cmocka_unit_test(test_commit_failure_hr), + cmocka_unit_test(test_log_attributes), + cmocka_unit_test(test_replicated_update_hr_empty), + cmocka_unit_test(test_replicated_update_hr), + }; + + cmocka_set_message_output(CM_OUTPUT_SUBUNIT); + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/source4/dsdb/samdb/ldb_modules/tests/test_audit_log_errors.c b/source4/dsdb/samdb/ldb_modules/tests/test_audit_log_errors.c new file mode 100644 index 0000000..2931768 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/tests/test_audit_log_errors.c @@ -0,0 +1,639 @@ +/* + Unit tests for the dsdb audit logging code code in audit_log.c + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018 + + 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/>. +*/ + +/* + * These tests exercise the error handling code + */ + +#include <stdarg.h> +#include <stddef.h> +#include <setjmp.h> +#include <unistd.h> +#include <cmocka.h> + +int ldb_audit_log_module_init(const char *version); +#include "../audit_log.c" +#include "lib/ldb/include/ldb_private.h" + +/* + * cmocka wrappers for json_new_object + */ +struct json_object __wrap_json_new_object(void); +struct json_object __real_json_new_object(void); +struct json_object __wrap_json_new_object(void) +{ + + bool use_real = (bool)mock(); + if (!use_real) { + return json_empty_object; + } + return __real_json_new_object(); +} + +/* + * cmocka wrappers for json_add_version + */ +int __wrap_json_add_version(struct json_object *object, int major, int minor); +int __real_json_add_version(struct json_object *object, int major, int minor); +int __wrap_json_add_version(struct json_object *object, int major, int minor) +{ + + int ret = (int)mock(); + if (ret) { + return ret; + } + return __real_json_add_version(object, major, minor); +} + +/* + * cmocka wrappers for json_add_version + */ +int __wrap_json_add_timestamp(struct json_object *object); +int __real_json_add_timestamp(struct json_object *object); +int __wrap_json_add_timestamp(struct json_object *object) +{ + + int ret = (int)mock(); + if (ret) { + return ret; + } + return __real_json_add_timestamp(object); +} +/* + * unit test of operation_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_operation_json(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + + struct tsocket_address *ts = NULL; + + struct auth_session_info *sess = NULL; + struct security_token *token = NULL; + struct dom_sid sid; + const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + struct GUID session_id; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct ldb_dn *dn = NULL; + const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG"; + + struct ldb_message *msg = NULL; + + struct json_object json; + + + /* + * Test setup + */ + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + audit_private = talloc_zero(ctx, struct audit_private); + GUID_from_string(TRANSACTION, &transaction_id); + audit_private->transaction_guid = transaction_id; + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(ldb, "remoteAddress", ts); + + sess = talloc_zero(ctx, struct auth_session_info); + token = talloc_zero(ctx, struct security_token); + string_to_sid(&sid, SID); + token->num_sids = 1; + token->sids = &sid; + sess->security_token = token; + GUID_from_string(SESSION, &session_id); + sess->unique_session_token = session_id; + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sess); + + msg = talloc_zero(ctx, struct ldb_message); + dn = ldb_dn_new(ctx, ldb, DN); + msg->dn = dn; + ldb_msg_add_string(msg, "attribute", "the-value"); + + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + req->op.add.message = msg; + + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_ERR_OPERATIONS_ERROR; + + /* + * Fail on the creation of the audit json object + */ + + will_return(__wrap_json_new_object, false); + + json = operation_json(module, req, reply); + assert_true(json_is_invalid(&json)); + + /* + * Fail adding the version object . + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, JSON_ERROR); + + json = operation_json(module, req, reply); + assert_true(json_is_invalid(&json)); + + /* + * Fail on creation of the wrapper. + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_new_object, false); + + json = operation_json(module, req, reply); + assert_true(json_is_invalid(&json)); + + /* + * Fail adding the timestamp to the wrapper object. + */ + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_timestamp, JSON_ERROR); + + json = operation_json(module, req, reply); + assert_true(json_is_invalid(&json)); + + /* + * Now test the happy path + */ + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_timestamp, 0); + + json = operation_json(module, req, reply); + assert_false(json_is_invalid(&json)); + json_free(&json); + + TALLOC_FREE(ctx); + +} + +/* + * minimal unit test of password_change_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_password_change_json(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + + struct tsocket_address *ts = NULL; + + struct auth_session_info *sess = NULL; + struct security_token *token = NULL; + struct dom_sid sid; + const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + struct GUID session_id; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct ldb_dn *dn = NULL; + const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG"; + + struct ldb_message *msg = NULL; + + struct json_object json; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + audit_private = talloc_zero(ctx, struct audit_private); + GUID_from_string(TRANSACTION, &transaction_id); + audit_private->transaction_guid = transaction_id; + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(ldb, "remoteAddress", ts); + + sess = talloc_zero(ctx, struct auth_session_info); + token = talloc_zero(ctx, struct security_token); + string_to_sid(&sid, SID); + token->num_sids = 1; + token->sids = &sid; + sess->security_token = token; + GUID_from_string(SESSION, &session_id); + sess->unique_session_token = session_id; + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sess); + + msg = talloc_zero(ctx, struct ldb_message); + dn = ldb_dn_new(ctx, ldb, DN); + msg->dn = dn; + ldb_msg_add_string(msg, "planTextPassword", "super-secret"); + + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + req->op.add.message = msg; + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_SUCCESS; + + + /* + * Fail on the creation of the audit json object + */ + + will_return(__wrap_json_new_object, false); + json = password_change_json(module, req, reply); + + assert_true(json_is_invalid(&json)); + + /* + * Fail adding the version object . + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, JSON_ERROR); + + json = password_change_json(module, req, reply); + assert_true(json_is_invalid(&json)); + + /* + * Fail on creation of the wrapper. + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, false); + + json = password_change_json(module, req, reply); + assert_true(json_is_invalid(&json)); + + /* + * Fail on creation of the time stamp. + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_timestamp, JSON_ERROR); + + json = password_change_json(module, req, reply); + assert_true(json_is_invalid(&json)); + + /* + * Now test the happy path + */ + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_timestamp, 0); + + json = password_change_json(module, req, reply); + assert_false(json_is_invalid(&json)); + json_free(&json); + + TALLOC_FREE(ctx); +} + + +/* + * minimal unit test of transaction_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_transaction_json(void **state) +{ + + struct GUID guid; + const char * const GUID = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct json_object json; + + GUID_from_string(GUID, &guid); + + + /* + * Fail on the creation of the audit json object + */ + + will_return(__wrap_json_new_object, false); + + json = transaction_json("delete", &guid, 10000099); + assert_true(json_is_invalid(&json)); + + /* + * Fail adding the version object . + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, JSON_ERROR); + + json = transaction_json("delete", &guid, 10000099); + assert_true(json_is_invalid(&json)); + + /* + * Fail on creation of the wrapper. + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, false); + + json = transaction_json("delete", &guid, 10000099); + assert_true(json_is_invalid(&json)); + + /* + * Fail on creation of the time stamp. + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_timestamp, JSON_ERROR); + + json = transaction_json("delete", &guid, 10000099); + assert_true(json_is_invalid(&json)); + + /* + * Now test the happy path + */ + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_timestamp, 0); + + json = transaction_json("delete", &guid, 10000099); + assert_false(json_is_invalid(&json)); + json_free(&json); +} + +/* + * minimal unit test of commit_failure_json, that ensures that all the + * expected attributes and objects are in the json object. + */ +static void test_commit_failure_json(void **state) +{ + + struct GUID guid; + const char * const GUID = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct json_object json; + + GUID_from_string(GUID, &guid); + + + /* + * Fail on the creation of the audit json object + */ + + will_return(__wrap_json_new_object, false); + + json = commit_failure_json( + "prepare", + 987876, + LDB_ERR_OPERATIONS_ERROR, + "because", + &guid); + assert_true(json_is_invalid(&json)); + + /* + * Fail adding the version object . + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, JSON_ERROR); + + json = commit_failure_json( + "prepare", + 987876, + LDB_ERR_OPERATIONS_ERROR, + "because", + &guid); + assert_true(json_is_invalid(&json)); + + /* + * Fail on creation of the wrapper. + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, false); + + json = commit_failure_json( + "prepare", + 987876, + LDB_ERR_OPERATIONS_ERROR, + "because", + &guid); + assert_true(json_is_invalid(&json)); + + /* + * Fail on creation of the time stamp. + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_timestamp, JSON_ERROR); + + json = commit_failure_json( + "prepare", + 987876, + LDB_ERR_OPERATIONS_ERROR, + "because", + &guid); + assert_true(json_is_invalid(&json)); + + /* + * Now test the happy path + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_timestamp, 0); + + json = commit_failure_json( + "prepare", + 987876, + LDB_ERR_OPERATIONS_ERROR, + "because", + &guid); + assert_false(json_is_invalid(&json)); + json_free(&json); +} + +/* + * unit test of replicated_update_json, that ensures that all the expected + * attributes and objects are in the json object. + */ +static void test_replicated_update_json(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + struct ldb_reply *reply = NULL; + struct audit_private *audit_private = NULL; + struct dsdb_extended_replicated_objects *ro = NULL; + struct repsFromTo1 *source_dsa = NULL; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct ldb_dn *dn = NULL; + const char *const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG"; + + struct GUID source_dsa_obj_guid; + const char *const SOURCE_DSA = "7130cb06-2062-6a1b-409e-3514c26b1793"; + + struct GUID invocation_id; + const char *const INVOCATION_ID = + "7130cb06-2062-6a1b-409e-3514c26b1893"; + struct json_object json; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + audit_private = talloc_zero(ctx, struct audit_private); + GUID_from_string(TRANSACTION, &transaction_id); + audit_private->transaction_guid = transaction_id; + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + ldb_module_set_private(module, audit_private); + + dn = ldb_dn_new(ctx, ldb, DN); + GUID_from_string(SOURCE_DSA, &source_dsa_obj_guid); + GUID_from_string(INVOCATION_ID, &invocation_id); + source_dsa = talloc_zero(ctx, struct repsFromTo1); + source_dsa->source_dsa_obj_guid = source_dsa_obj_guid; + source_dsa->source_dsa_invocation_id = invocation_id; + + ro = talloc_zero(ctx, struct dsdb_extended_replicated_objects); + ro->source_dsa = source_dsa; + ro->num_objects = 808; + ro->linked_attributes_count = 2910; + ro->partition_dn = dn; + ro->error = WERR_NOT_SUPPORTED; + + + req = talloc_zero(ctx, struct ldb_request); + req->op.extended.data = ro; + req->operation = LDB_EXTENDED; + + reply = talloc_zero(ctx, struct ldb_reply); + reply->error = LDB_ERR_NO_SUCH_OBJECT; + + + /* + * Fail on the creation of the audit json object + */ + + will_return(__wrap_json_new_object, false); + + json = replicated_update_json(module, req, reply); + assert_true(json_is_invalid(&json)); + + /* + * Fail adding the version object . + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, JSON_ERROR); + + json = replicated_update_json(module, req, reply); + assert_true(json_is_invalid(&json)); + + /* + * Fail on creation of the wrapper. + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, false); + + json = replicated_update_json(module, req, reply); + assert_true(json_is_invalid(&json)); + + /* + * Fail on creation of the time stamp. + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_timestamp, JSON_ERROR); + + json = replicated_update_json(module, req, reply); + assert_true(json_is_invalid(&json)); + + /* + * Now test the happy path. + */ + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_timestamp, 0); + + json = replicated_update_json(module, req, reply); + assert_false(json_is_invalid(&json)); + json_free(&json); + + TALLOC_FREE(ctx); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_operation_json), + cmocka_unit_test(test_password_change_json), + cmocka_unit_test(test_transaction_json), + cmocka_unit_test(test_commit_failure_json), + cmocka_unit_test(test_replicated_update_json), + }; + + cmocka_set_message_output(CM_OUTPUT_SUBUNIT); + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/source4/dsdb/samdb/ldb_modules/tests/test_audit_util.c b/source4/dsdb/samdb/ldb_modules/tests/test_audit_util.c new file mode 100644 index 0000000..cf2546d --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/tests/test_audit_util.c @@ -0,0 +1,1260 @@ +/* + Unit tests for the dsdb audit logging utility code code in audit_util.c + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018 + + 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/>. +*/ + +#include <stdarg.h> +#include <stddef.h> +#include <setjmp.h> +#include <unistd.h> +#include <cmocka.h> + +#include "../audit_util.c" + +#include "lib/ldb/include/ldb_private.h" + +static void test_dsdb_audit_add_ldb_value(void **state) +{ + struct json_object object; + struct json_object array; + struct ldb_val val = data_blob_null; + struct json_t *el = NULL; + struct json_t *atr = NULL; + char* base64 = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + /* + * Test a non array object + */ + object = json_new_object(); + assert_false(json_is_invalid(&object)); + dsdb_audit_add_ldb_value(&object, val); + assert_true(json_is_invalid(&object)); + json_free(&object); + + array = json_new_array(); + /* + * Test a data_blob_null, should encode as a JSON null value. + */ + val = data_blob_null; + dsdb_audit_add_ldb_value(&array, val); + el = json_array_get(array.root, 0); + assert_true(json_is_null(el)); + + /* + * Test a +ve length but a null data ptr, should encode as a null. + */ + val = data_blob_null; + val.length = 1; + dsdb_audit_add_ldb_value(&array, val); + el = json_array_get(array.root, 1); + assert_true(json_is_null(el)); + + /* + * Test a zero length but a non null data ptr, should encode as a null. + */ + val = data_blob_null; + val.data = discard_const("Data on the stack"); + dsdb_audit_add_ldb_value(&array, val); + el = json_array_get(array.root, 2); + assert_true(json_is_null(el)); + + /* + * Test a printable value. + * value should not be encoded + * truncated and base64 should be missing + */ + val = data_blob_string_const("A value of interest"); + dsdb_audit_add_ldb_value(&array, val); + el = json_array_get(array.root, 3); + assert_true(json_is_object(el)); + atr = json_object_get(el, "value"); + assert_true(json_is_string(atr)); + assert_string_equal("A value of interest", json_string_value(atr)); + assert_null(json_object_get(el, "truncated")); + assert_null(json_object_get(el, "base64")); + + /* + * Test non printable value, should be base64 encoded. + * truncated should be missing and base64 should be set. + */ + val = data_blob_string_const("A value of interest\n"); + dsdb_audit_add_ldb_value(&array, val); + el = json_array_get(array.root, 4); + assert_true(json_is_object(el)); + atr = json_object_get(el, "value"); + assert_true(json_is_string(atr)); + assert_string_equal( + "QSB2YWx1ZSBvZiBpbnRlcmVzdAo=", + json_string_value(atr)); + atr = json_object_get(el, "base64"); + assert_true(json_is_boolean(atr)); + assert_true(json_boolean(atr)); + assert_null(json_object_get(el, "truncated")); + + /* + * test a printable value exactly max bytes long + * should not be truncated or encoded. + */ + val = data_blob_null; + val.length = MAX_LENGTH; + val.data = (unsigned char *)generate_random_str_list( + ctx, + MAX_LENGTH, + "abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + "1234567890!@#$%^&*()"); + + dsdb_audit_add_ldb_value(&array, val); + + el = json_array_get(array.root, 5); + assert_true(json_is_object(el)); + atr = json_object_get(el, "value"); + assert_true(json_is_string(atr)); + assert_int_equal(MAX_LENGTH, strlen(json_string_value(atr))); + assert_memory_equal(val.data, json_string_value(atr), MAX_LENGTH); + + assert_null(json_object_get(el, "base64")); + assert_null(json_object_get(el, "truncated")); + + + /* + * test a printable value exactly max + 1 bytes long + * should be truncated and not encoded. + */ + val = data_blob_null; + val.length = MAX_LENGTH + 1; + val.data = (unsigned char *)generate_random_str_list( + ctx, + MAX_LENGTH + 1, + "abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + "1234567890!@#$%^&*()"); + + dsdb_audit_add_ldb_value(&array, val); + + el = json_array_get(array.root, 6); + assert_true(json_is_object(el)); + atr = json_object_get(el, "value"); + assert_true(json_is_string(atr)); + assert_int_equal(MAX_LENGTH, strlen(json_string_value(atr))); + assert_memory_equal(val.data, json_string_value(atr), MAX_LENGTH); + + atr = json_object_get(el, "truncated"); + assert_true(json_is_boolean(atr)); + assert_true(json_boolean(atr)); + + assert_null(json_object_get(el, "base64")); + + TALLOC_FREE(val.data); + + /* + * test a non-printable value exactly max bytes long + * should not be truncated but should be encoded. + */ + val = data_blob_null; + val.length = MAX_LENGTH; + val.data = (unsigned char *)generate_random_str_list( + ctx, + MAX_LENGTH, + "abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + "1234567890!@#$%^&*()"); + + val.data[0] = 0x03; + dsdb_audit_add_ldb_value(&array, val); + base64 = ldb_base64_encode(ctx, (char*) val.data, MAX_LENGTH); + + el = json_array_get(array.root, 7); + assert_true(json_is_object(el)); + atr = json_object_get(el, "value"); + assert_true(json_is_string(atr)); + assert_int_equal(strlen(base64), strlen(json_string_value(atr))); + assert_string_equal(base64, json_string_value(atr)); + + atr = json_object_get(el, "base64"); + assert_true(json_is_boolean(atr)); + assert_true(json_boolean(atr)); + + assert_null(json_object_get(el, "truncated")); + TALLOC_FREE(base64); + TALLOC_FREE(val.data); + + /* + * test a non-printable value exactly max + 1 bytes long + * should be truncated and encoded. + */ + val = data_blob_null; + val.length = MAX_LENGTH + 1; + val.data = (unsigned char *)generate_random_str_list( + ctx, + MAX_LENGTH + 1, + "abcdefghijklmnopqrstuvwzyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + "1234567890!@#$%^&*()"); + + val.data[0] = 0x03; + dsdb_audit_add_ldb_value(&array, val); + /* + * The data is truncated before it is base 64 encoded + */ + base64 = ldb_base64_encode(ctx, (char*) val.data, MAX_LENGTH); + + el = json_array_get(array.root, 8); + assert_true(json_is_object(el)); + atr = json_object_get(el, "value"); + assert_true(json_is_string(atr)); + assert_int_equal(strlen(base64), strlen(json_string_value(atr))); + assert_string_equal(base64, json_string_value(atr)); + + atr = json_object_get(el, "base64"); + assert_true(json_is_boolean(atr)); + assert_true(json_boolean(atr)); + + atr = json_object_get(el, "truncated"); + assert_true(json_is_boolean(atr)); + assert_true(json_boolean(atr)); + + TALLOC_FREE(base64); + TALLOC_FREE(val.data); + + json_free(&array); + TALLOC_FREE(ctx); +} + +static void test_dsdb_audit_attributes_json(void **state) +{ + struct ldb_message *msg = NULL; + + struct json_object o; + json_t *a = NULL; + json_t *v = NULL; + json_t *x = NULL; + json_t *y = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + + /* + * Test an empty message + * Should get an empty attributes object + */ + msg = talloc_zero(ctx, struct ldb_message); + + o = dsdb_audit_attributes_json(LDB_ADD, msg); + assert_true(json_is_object(o.root)); + assert_int_equal(0, json_object_size(o.root)); + json_free(&o); + + o = dsdb_audit_attributes_json(LDB_MODIFY, msg); + assert_true(json_is_object(o.root)); + assert_int_equal(0, json_object_size(o.root)); + json_free(&o); + + /* + * Test a message with a single secret attribute + * should only have that object and it should have no value + * attribute and redacted should be set. + */ + msg = talloc_zero(ctx, struct ldb_message); + ldb_msg_add_string(msg, "clearTextPassword", "secret"); + + o = dsdb_audit_attributes_json(LDB_ADD, msg); + assert_true(json_is_object(o.root)); + assert_int_equal(1, json_object_size(o.root)); + + a = json_object_get(o.root, "clearTextPassword"); + assert_int_equal(1, json_object_size(a)); + + v = json_object_get(a, "actions"); + assert_true(json_is_array(v)); + assert_int_equal(1, json_array_size(v)); + + a = json_array_get(v, 0); + v = json_object_get(a, "redacted"); + assert_true(json_is_boolean(v)); + assert_true(json_boolean(v)); + + json_free(&o); + + /* + * Test as a modify message, should add an action attribute + */ + o = dsdb_audit_attributes_json(LDB_MODIFY, msg); + assert_true(json_is_object(o.root)); + assert_int_equal(1, json_object_size(o.root)); + + a = json_object_get(o.root, "clearTextPassword"); + assert_true(json_is_object(a)); + assert_int_equal(1, json_object_size(a)); + + v = json_object_get(a, "actions"); + assert_true(json_is_array(v)); + assert_int_equal(1, json_array_size(v)); + + a = json_array_get(v, 0); + v = json_object_get(a, "redacted"); + assert_true(json_is_boolean(v)); + assert_true(json_boolean(v)); + + v = json_object_get(a, "action"); + assert_true(json_is_string(v)); + assert_string_equal("unknown", json_string_value(v)); + + json_free(&o); + TALLOC_FREE(msg); + + /* + * Test a message with a single attribute, single valued attribute + */ + msg = talloc_zero(ctx, struct ldb_message); + ldb_msg_add_string(msg, "attribute", "value"); + + o = dsdb_audit_attributes_json(LDB_ADD, msg); + assert_true(json_is_object(o.root)); + assert_int_equal(1, json_object_size(o.root)); + + a = json_object_get(o.root, "attribute"); + assert_true(json_is_object(a)); + assert_int_equal(1, json_object_size(a)); + + v = json_object_get(a, "actions"); + assert_true(json_is_array(v)); + assert_int_equal(1, json_array_size(v)); + + x = json_array_get(v, 0); + assert_int_equal(2, json_object_size(x)); + y = json_object_get(x, "action"); + assert_string_equal("add", json_string_value(y)); + + y = json_object_get(x, "values"); + assert_true(json_is_array(y)); + assert_int_equal(1, json_array_size(y)); + + x = json_array_get(y, 0); + assert_true(json_is_object(x)); + assert_int_equal(1, json_object_size(x)); + y = json_object_get(x, "value"); + assert_string_equal("value", json_string_value(y)); + + json_free(&o); + TALLOC_FREE(msg); + + /* + * Test a message with a single attribute, single valued attribute + * And as a modify + */ + msg = talloc_zero(ctx, struct ldb_message); + ldb_msg_add_string(msg, "attribute", "value"); + + o = dsdb_audit_attributes_json(LDB_MODIFY, msg); + assert_true(json_is_object(o.root)); + assert_int_equal(1, json_object_size(o.root)); + + a = json_object_get(o.root, "attribute"); + assert_true(json_is_object(a)); + assert_int_equal(1, json_object_size(a)); + + v = json_object_get(a, "actions"); + assert_true(json_is_array(v)); + assert_int_equal(1, json_array_size(v)); + + x = json_array_get(v, 0); + assert_int_equal(2, json_object_size(x)); + y = json_object_get(x, "action"); + assert_string_equal("unknown", json_string_value(y)); + + y = json_object_get(x, "values"); + assert_true(json_is_array(y)); + assert_int_equal(1, json_array_size(y)); + + x = json_array_get(y, 0); + assert_true(json_is_object(x)); + assert_int_equal(1, json_object_size(x)); + y = json_object_get(x, "value"); + assert_string_equal("value", json_string_value(y)); + + json_free(&o); + TALLOC_FREE(msg); + + /* + * Test a message with a multivalues attributres + */ + msg = talloc_zero(ctx, struct ldb_message); + ldb_msg_add_string(msg, "attribute01", "value01"); + ldb_msg_add_string(msg, "attribute02", "value02"); + ldb_msg_add_string(msg, "attribute02", "value03"); + + o = dsdb_audit_attributes_json(LDB_ADD, msg); + assert_true(json_is_object(o.root)); + assert_int_equal(2, json_object_size(o.root)); + + a = json_object_get(o.root, "attribute01"); + assert_true(json_is_object(a)); + assert_int_equal(1, json_object_size(a)); + + v = json_object_get(a, "actions"); + assert_true(json_is_array(v)); + assert_int_equal(1, json_array_size(v)); + + x = json_array_get(v, 0); + assert_int_equal(2, json_object_size(x)); + y = json_object_get(x, "action"); + assert_string_equal("add", json_string_value(y)); + + y = json_object_get(x, "values"); + assert_true(json_is_array(y)); + assert_int_equal(1, json_array_size(y)); + + x = json_array_get(y, 0); + assert_true(json_is_object(x)); + assert_int_equal(1, json_object_size(x)); + y = json_object_get(x, "value"); + assert_string_equal("value01", json_string_value(y)); + + a = json_object_get(o.root, "attribute02"); + assert_true(json_is_object(a)); + assert_int_equal(1, json_object_size(a)); + + v = json_object_get(a, "actions"); + assert_true(json_is_array(v)); + assert_int_equal(1, json_array_size(v)); + + x = json_array_get(v, 0); + assert_int_equal(2, json_object_size(x)); + y = json_object_get(x, "action"); + assert_string_equal("add", json_string_value(y)); + + y = json_object_get(x, "values"); + assert_true(json_is_array(y)); + assert_int_equal(2, json_array_size(y)); + + x = json_array_get(y, 0); + assert_true(json_is_object(x)); + assert_int_equal(1, json_object_size(x)); + v = json_object_get(x, "value"); + assert_string_equal("value02", json_string_value(v)); + + x = json_array_get(y, 1); + assert_true(json_is_object(x)); + assert_int_equal(1, json_object_size(x)); + v = json_object_get(x, "value"); + assert_string_equal("value03", json_string_value(v)); + + json_free(&o); + TALLOC_FREE(msg); + + TALLOC_FREE(ctx); +} + +static void test_dsdb_audit_get_remote_address(void **state) +{ + struct ldb_context *ldb = NULL; + const struct tsocket_address *ts = NULL; + struct tsocket_address *in = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + /* + * Test a freshly initialized ldb + * should return NULL + */ + ldb = ldb_init(ctx, NULL); + ts = dsdb_audit_get_remote_address(ldb); + assert_null(ts); + + /* + * opaque set to null, should return NULL + */ + ldb_set_opaque(ldb, "remoteAddress", NULL); + ts = dsdb_audit_get_remote_address(ldb); + assert_null(ts); + + /* + * Ensure that the value set is returned + */ + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &in); + ldb_set_opaque(ldb, "remoteAddress", in); + ts = dsdb_audit_get_remote_address(ldb); + assert_non_null(ts); + assert_ptr_equal(in, ts); + + TALLOC_FREE(ldb); + TALLOC_FREE(ctx); + +} + +static void test_dsdb_audit_get_ldb_error_string(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + const char *s = NULL; + const char * const text = "Custom reason"; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + + /* + * No ldb error string set should get the default error description for + * the status code + */ + s = dsdb_audit_get_ldb_error_string(module, LDB_ERR_OPERATIONS_ERROR); + assert_string_equal("Operations error", s); + + /* + * Set the error string that should now be returned instead of the + * default description. + */ + ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, text); + s = dsdb_audit_get_ldb_error_string(module, LDB_ERR_OPERATIONS_ERROR); + /* + * Only test the start of the string as ldb_error adds location data. + */ + assert_int_equal(0, strncmp(text, s, strlen(text))); + + TALLOC_FREE(ctx); +} + +static void test_dsdb_audit_get_user_sid(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + const struct dom_sid *sid = NULL; + struct auth_session_info *sess = NULL; + struct security_token *token = NULL; + struct dom_sid sids[2]; + const char * const SID0 = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SID1 = "S-1-5-21-4284042908-2889457889-3672286761"; + struct dom_sid_buf sid_buf; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + + /* + * Freshly initialised structures, will be no session data + * so expect NULL + */ + sid = dsdb_audit_get_user_sid(module); + assert_null(sid); + + /* + * Now add a NULL session info + */ + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sess); + sid = dsdb_audit_get_user_sid(module); + assert_null(sid); + + /* + * Now add a session info with no user sid + */ + sess = talloc_zero(ctx, struct auth_session_info); + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sess); + sid = dsdb_audit_get_user_sid(module); + assert_null(sid); + + /* + * Now add an empty security token. + */ + token = talloc_zero(ctx, struct security_token); + sess->security_token = token; + sid = dsdb_audit_get_user_sid(module); + assert_null(sid); + + /* + * Add a single SID + */ + string_to_sid(&sids[0], SID0); + token->num_sids = 1; + token->sids = sids; + sid = dsdb_audit_get_user_sid(module); + assert_non_null(sid); + dom_sid_str_buf(sid, &sid_buf); + assert_string_equal(SID0, sid_buf.buf); + + /* + * Add a second SID, should still use the first SID + */ + string_to_sid(&sids[1], SID1); + token->num_sids = 2; + sid = dsdb_audit_get_user_sid(module); + assert_non_null(sid); + dom_sid_str_buf(sid, &sid_buf); + assert_string_equal(SID0, sid_buf.buf); + + + /* + * Now test a null sid in the first position + */ + token->num_sids = 1; + token->sids = NULL; + sid = dsdb_audit_get_user_sid(module); + assert_null(sid); + + TALLOC_FREE(ctx); +} + +static void test_dsdb_audit_get_actual_sid(void **state) +{ + struct ldb_context *ldb = NULL; + const struct dom_sid *sid = NULL; + struct auth_session_info *sess = NULL; + struct security_token *token = NULL; + struct dom_sid sids[2]; + const char * const SID0 = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SID1 = "S-1-5-21-4284042908-2889457889-3672286761"; + struct dom_sid_buf sid_buf; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + /* + * Freshly initialised structures, will be no session data + * so expect NULL + */ + sid = dsdb_audit_get_actual_sid(ldb); + assert_null(sid); + + /* + * Now add a NULL session info + */ + ldb_set_opaque(ldb, DSDB_NETWORK_SESSION_INFO, NULL); + sid = dsdb_audit_get_actual_sid(ldb); + assert_null(sid); + + /* + * Now add a session info with no user sid + */ + sess = talloc_zero(ctx, struct auth_session_info); + ldb_set_opaque(ldb, DSDB_NETWORK_SESSION_INFO, sess); + sid = dsdb_audit_get_actual_sid(ldb); + assert_null(sid); + + /* + * Now add an empty security token. + */ + token = talloc_zero(ctx, struct security_token); + sess->security_token = token; + sid = dsdb_audit_get_actual_sid(ldb); + assert_null(sid); + + /* + * Add a single SID + */ + string_to_sid(&sids[0], SID0); + token->num_sids = 1; + token->sids = sids; + sid = dsdb_audit_get_actual_sid(ldb); + assert_non_null(sid); + dom_sid_str_buf(sid, &sid_buf); + assert_string_equal(SID0, sid_buf.buf); + + /* + * Add a second SID, should still use the first SID + */ + string_to_sid(&sids[1], SID1); + token->num_sids = 2; + sid = dsdb_audit_get_actual_sid(ldb); + assert_non_null(sid); + dom_sid_str_buf(sid, &sid_buf); + assert_string_equal(SID0, sid_buf.buf); + + + /* + * Now test a null sid in the first position + */ + token->num_sids = 1; + token->sids = NULL; + sid = dsdb_audit_get_actual_sid(ldb); + assert_null(sid); + + TALLOC_FREE(ctx); +} + +static void test_dsdb_audit_is_system_session(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + const struct dom_sid *sid = NULL; + struct auth_session_info *sess = NULL; + struct security_token *token = NULL; + struct dom_sid sids[2]; + const char * const SID0 = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SID1 = "S-1-5-21-4284042908-2889457889-3672286761"; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + + /* + * Freshly initialised structures, will be no session data + * so expect NULL + */ + assert_false(dsdb_audit_is_system_session(module)); + + /* + * Now add a NULL session info + */ + ldb_set_opaque(ldb, DSDB_SESSION_INFO, NULL); + assert_false(dsdb_audit_is_system_session(module)); + + /* + * Now add a session info with no user sid + */ + sess = talloc_zero(ctx, struct auth_session_info); + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sess); + assert_false(dsdb_audit_is_system_session(module)); + + /* + * Now add an empty security token. + */ + token = talloc_zero(ctx, struct security_token); + sess->security_token = token; + assert_false(dsdb_audit_is_system_session(module)); + + /* + * Add a single SID, non system sid + */ + string_to_sid(&sids[0], SID0); + token->num_sids = 1; + token->sids = sids; + assert_false(dsdb_audit_is_system_session(module)); + + /* + * Add the system SID to the second position, + * this should be ignored. + */ + token->num_sids = 2; + sids[1] = global_sid_System; + assert_false(dsdb_audit_is_system_session(module)); + + /* + * Add a single SID, system sid + */ + token->num_sids = 1; + sids[0] = global_sid_System; + token->sids = sids; + assert_true(dsdb_audit_is_system_session(module)); + + /* + * Add a non system SID to position 2 + */ + sids[0] = global_sid_System; + string_to_sid(&sids[1], SID1); + token->num_sids = 2; + token->sids = sids; + assert_true(dsdb_audit_is_system_session(module)); + + /* + * Now test a null sid in the first position + */ + token->num_sids = 1; + token->sids = NULL; + sid = dsdb_audit_get_user_sid(module); + assert_null(sid); + + TALLOC_FREE(ctx); +} + +static void test_dsdb_audit_get_unique_session_token(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct auth_session_info *sess = NULL; + const struct GUID *guid; + const char * const GUID_S = "7130cb06-2062-6a1b-409e-3514c26b1773"; + struct GUID in; + char *guid_str; + struct GUID_txt_buf guid_buff; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + + /* + * Test a freshly initialized ldb + * should return NULL + */ + guid = dsdb_audit_get_unique_session_token(module); + assert_null(guid); + + /* + * Now add a NULL session info + */ + ldb_set_opaque(ldb, DSDB_SESSION_INFO, NULL); + guid = dsdb_audit_get_unique_session_token(module); + assert_null(guid); + + /* + * Now add a session info with no session id + * Note if the memory has not been zeroed correctly all bets are + * probably off. + */ + sess = talloc_zero(ctx, struct auth_session_info); + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sess); + guid = dsdb_audit_get_unique_session_token(module); + /* + * We will get a GUID, but it's contents will be undefined + */ + assert_non_null(guid); + + /* + * Now set the session id and confirm that we get it back. + */ + GUID_from_string(GUID_S, &in); + sess->unique_session_token = in; + guid = dsdb_audit_get_unique_session_token(module); + assert_non_null(guid); + guid_str = GUID_buf_string(guid, &guid_buff); + assert_string_equal(GUID_S, guid_str); + + TALLOC_FREE(ctx); + +} + +static void test_dsdb_audit_get_actual_unique_session_token(void **state) +{ + struct ldb_context *ldb = NULL; + struct auth_session_info *sess = NULL; + const struct GUID *guid; + const char * const GUID_S = "7130cb06-2062-6a1b-409e-3514c26b1773"; + struct GUID in; + char *guid_str; + struct GUID_txt_buf guid_buff; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + /* + * Test a freshly initialized ldb + * should return NULL + */ + guid = dsdb_audit_get_actual_unique_session_token(ldb); + assert_null(guid); + + /* + * Now add a NULL session info + */ + ldb_set_opaque(ldb, DSDB_NETWORK_SESSION_INFO, NULL); + guid = dsdb_audit_get_actual_unique_session_token(ldb); + assert_null(guid); + + /* + * Now add a session info with no session id + * Note if the memory has not been zeroed correctly all bets are + * probably off. + */ + sess = talloc_zero(ctx, struct auth_session_info); + ldb_set_opaque(ldb, DSDB_NETWORK_SESSION_INFO, sess); + guid = dsdb_audit_get_actual_unique_session_token(ldb); + /* + * We will get a GUID, but it's contents will be undefined + */ + assert_non_null(guid); + + /* + * Now set the session id and confirm that we get it back. + */ + GUID_from_string(GUID_S, &in); + sess->unique_session_token = in; + guid = dsdb_audit_get_actual_unique_session_token(ldb); + assert_non_null(guid); + guid_str = GUID_buf_string(guid, &guid_buff); + assert_string_equal(GUID_S, guid_str); + + TALLOC_FREE(ctx); + +} + +static void test_dsdb_audit_get_remote_host(void **state) +{ + struct ldb_context *ldb = NULL; + char *rh = NULL; + struct tsocket_address *in = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + /* + * Test a freshly initialized ldb + * should return "Unknown" + */ + rh = dsdb_audit_get_remote_host(ldb, ctx); + assert_string_equal("Unknown", rh); + TALLOC_FREE(rh); + + /* + * opaque set to null, should return NULL + */ + ldb_set_opaque(ldb, "remoteAddress", NULL); + rh = dsdb_audit_get_remote_host(ldb, ctx); + assert_string_equal("Unknown", rh); + TALLOC_FREE(rh); + + /* + * Ensure that the value set is returned + */ + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 42, &in); + ldb_set_opaque(ldb, "remoteAddress", in); + rh = dsdb_audit_get_remote_host(ldb, ctx); + assert_string_equal("ipv4:127.0.0.1:42", rh); + TALLOC_FREE(rh); + + TALLOC_FREE(ctx); + +} + +static void test_dsdb_audit_get_primary_dn(void **state) +{ + struct ldb_request *req = NULL; + struct ldb_message *msg = NULL; + struct ldb_context *ldb = NULL; + + struct ldb_dn *dn = NULL; + + const char * const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG"; + const char *s = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + req = talloc_zero(ctx, struct ldb_request); + msg = talloc_zero(ctx, struct ldb_message); + ldb = ldb_init(ctx, NULL); + dn = ldb_dn_new(ctx, ldb, DN); + + /* + * Try an empty request. + */ + s = dsdb_audit_get_primary_dn(req); + assert_null(s); + + /* + * Now try an add with a null message. + */ + req->operation = LDB_ADD; + req->op.add.message = NULL; + s = dsdb_audit_get_primary_dn(req); + assert_null(s); + + /* + * Now try an mod with a null message. + */ + req->operation = LDB_MODIFY; + req->op.mod.message = NULL; + s = dsdb_audit_get_primary_dn(req); + assert_null(s); + + /* + * Now try an add with a missing dn + */ + req->operation = LDB_ADD; + req->op.add.message = msg; + s = dsdb_audit_get_primary_dn(req); + assert_null(s); + + /* + * Now try a mod with a messing dn + */ + req->operation = LDB_ADD; + req->op.mod.message = msg; + s = dsdb_audit_get_primary_dn(req); + assert_null(s); + + /* + * Add a dn to the message + */ + msg->dn = dn; + + /* + * Now try an add with a dn + */ + req->operation = LDB_ADD; + req->op.add.message = msg; + s = dsdb_audit_get_primary_dn(req); + assert_non_null(s); + assert_string_equal(DN, s); + + /* + * Now try a mod with a dn + */ + req->operation = LDB_MODIFY; + req->op.mod.message = msg; + s = dsdb_audit_get_primary_dn(req); + assert_non_null(s); + assert_string_equal(DN, s); + + /* + * Try a delete without a dn + */ + req->operation = LDB_DELETE; + req->op.del.dn = NULL; + s = dsdb_audit_get_primary_dn(req); + assert_null(s); + + /* + * Try a delete with a dn + */ + req->operation = LDB_DELETE; + req->op.del.dn = dn; + s = dsdb_audit_get_primary_dn(req); + assert_non_null(s); + assert_string_equal(DN, s); + + /* + * Try a rename without a dn + */ + req->operation = LDB_RENAME; + req->op.rename.olddn = NULL; + s = dsdb_audit_get_primary_dn(req); + assert_null(s); + + /* + * Try a rename with a dn + */ + req->operation = LDB_RENAME; + req->op.rename.olddn = dn; + s = dsdb_audit_get_primary_dn(req); + assert_non_null(s); + assert_string_equal(DN, s); + + /* + * Try an extended operation, i.e. one that does not have a DN + * associated with it for logging purposes. + */ + req->operation = LDB_EXTENDED; + s = dsdb_audit_get_primary_dn(req); + assert_null(s); + + TALLOC_FREE(ctx); +} + +static void test_dsdb_audit_get_message(void **state) +{ + struct ldb_request *req = NULL; + struct ldb_message *msg = NULL; + const struct ldb_message *r = NULL; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + req = talloc_zero(ctx, struct ldb_request); + msg = talloc_zero(ctx, struct ldb_message); + + /* + * Test an empty message + */ + r = dsdb_audit_get_message(req); + assert_null(r); + + /* + * Test an add message + */ + req->operation = LDB_ADD; + req->op.add.message = msg; + r = dsdb_audit_get_message(req); + assert_ptr_equal(msg, r); + + /* + * Test a modify message + */ + req->operation = LDB_MODIFY; + req->op.mod.message = msg; + r = dsdb_audit_get_message(req); + assert_ptr_equal(msg, r); + + /* + * Test a Delete message, i.e. trigger the default case + */ + req->operation = LDB_DELETE; + r = dsdb_audit_get_message(req); + assert_null(r); + + TALLOC_FREE(ctx); +} + +static void test_dsdb_audit_get_secondary_dn(void **state) +{ + struct ldb_request *req = NULL; + struct ldb_context *ldb = NULL; + + struct ldb_dn *dn = NULL; + + const char * const DN = "dn=CN=USER,CN=Users,DC=SAMBA,DC=ORG"; + const char *s = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + req = talloc_zero(ctx, struct ldb_request); + ldb = ldb_init(ctx, NULL); + dn = ldb_dn_new(ctx, ldb, DN); + + /* + * Try an empty request. + */ + s = dsdb_audit_get_secondary_dn(req); + assert_null(s); + + /* + * Try a rename without a dn + */ + req->operation = LDB_RENAME; + req->op.rename.newdn = NULL; + s = dsdb_audit_get_secondary_dn(req); + assert_null(s); + + /* + * Try a rename with a dn + */ + req->operation = LDB_RENAME; + req->op.rename.newdn = dn; + s = dsdb_audit_get_secondary_dn(req); + assert_non_null(s); + assert_string_equal(DN, s); + + /* + * Try an extended operation, i.e. one that does not have a DN + * associated with it for logging purposes. + */ + req->operation = LDB_EXTENDED; + s = dsdb_audit_get_primary_dn(req); + assert_null(s); + + TALLOC_FREE(ctx); +} + +static void test_dsdb_audit_get_operation_name(void **state) +{ + struct ldb_request *req = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + req = talloc_zero(ctx, struct ldb_request); + + req->operation = LDB_SEARCH; + assert_string_equal("Search", dsdb_audit_get_operation_name(req)); + + req->operation = LDB_ADD; + assert_string_equal("Add", dsdb_audit_get_operation_name(req)); + + req->operation = LDB_MODIFY; + assert_string_equal("Modify", dsdb_audit_get_operation_name(req)); + + req->operation = LDB_DELETE; + assert_string_equal("Delete", dsdb_audit_get_operation_name(req)); + + req->operation = LDB_RENAME; + assert_string_equal("Rename", dsdb_audit_get_operation_name(req)); + + req->operation = LDB_EXTENDED; + assert_string_equal("Extended", dsdb_audit_get_operation_name(req)); + + req->operation = LDB_REQ_REGISTER_CONTROL; + assert_string_equal( + "Register Control", + dsdb_audit_get_operation_name(req)); + + req->operation = LDB_REQ_REGISTER_PARTITION; + assert_string_equal( + "Register Partition", + dsdb_audit_get_operation_name(req)); + + /* + * Trigger the default case + */ + req->operation = -1; + assert_string_equal("Unknown", dsdb_audit_get_operation_name(req)); + + TALLOC_FREE(ctx); +} + +static void test_dsdb_audit_get_modification_action(void **state) +{ + assert_string_equal( + "add", + dsdb_audit_get_modification_action(LDB_FLAG_MOD_ADD)); + assert_string_equal( + "delete", + dsdb_audit_get_modification_action(LDB_FLAG_MOD_DELETE)); + assert_string_equal( + "replace", + dsdb_audit_get_modification_action(LDB_FLAG_MOD_REPLACE)); + /* + * Trigger the default case + */ + assert_string_equal( + "unknown", + dsdb_audit_get_modification_action(0)); +} + +static void test_dsdb_audit_is_password_attribute(void **state) +{ + assert_true(dsdb_audit_is_password_attribute("userPassword")); + assert_true(dsdb_audit_is_password_attribute("clearTextPassword")); + assert_true(dsdb_audit_is_password_attribute("unicodePwd")); + assert_true(dsdb_audit_is_password_attribute("dBCSPwd")); + + assert_false(dsdb_audit_is_password_attribute("xserPassword")); +} + +static void test_dsdb_audit_redact_attribute(void **state) +{ + assert_true(dsdb_audit_redact_attribute("userPassword")); + + assert_true(dsdb_audit_redact_attribute("pekList")); + assert_true(dsdb_audit_redact_attribute("clearTextPassword")); + assert_true(dsdb_audit_redact_attribute("initialAuthIncoming")); + + assert_false(dsdb_audit_redact_attribute("supaskrt")); +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_dsdb_audit_add_ldb_value), + cmocka_unit_test(test_dsdb_audit_attributes_json), + cmocka_unit_test(test_dsdb_audit_get_remote_address), + cmocka_unit_test(test_dsdb_audit_get_ldb_error_string), + cmocka_unit_test(test_dsdb_audit_get_user_sid), + cmocka_unit_test(test_dsdb_audit_get_actual_sid), + cmocka_unit_test(test_dsdb_audit_is_system_session), + cmocka_unit_test(test_dsdb_audit_get_unique_session_token), + cmocka_unit_test(test_dsdb_audit_get_actual_unique_session_token), + cmocka_unit_test(test_dsdb_audit_get_remote_host), + cmocka_unit_test(test_dsdb_audit_get_primary_dn), + cmocka_unit_test(test_dsdb_audit_get_message), + cmocka_unit_test(test_dsdb_audit_get_secondary_dn), + cmocka_unit_test(test_dsdb_audit_get_operation_name), + cmocka_unit_test(test_dsdb_audit_get_modification_action), + cmocka_unit_test(test_dsdb_audit_is_password_attribute), + cmocka_unit_test(test_dsdb_audit_redact_attribute), + }; + + cmocka_set_message_output(CM_OUTPUT_SUBUNIT); + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/source4/dsdb/samdb/ldb_modules/tests/test_encrypted_secrets.c b/source4/dsdb/samdb/ldb_modules/tests/test_encrypted_secrets.c new file mode 100644 index 0000000..e639d4c --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/tests/test_encrypted_secrets.c @@ -0,0 +1,973 @@ +/* + Unit tests for the encrypted secrets code in encrypted_secrets.c + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017 + + 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/>. +*/ + +#include <stdarg.h> +#include <stddef.h> +#include <setjmp.h> +#include <unistd.h> +#include <cmocka.h> + +int ldb_encrypted_secrets_module_init(const char *version); +#define TEST_ENCRYPTED_SECRETS +#include "../encrypted_secrets.c" + +struct ldbtest_ctx { + struct tevent_context *ev; + struct ldb_context *ldb; + struct ldb_module *module; + + const char *dbfile; + const char *lockfile; /* lockfile is separate */ + const char *keyfile; + + const char *dbpath; +}; + +/* -------------------------------------------------------------------------- */ +/* + * Replace the dsdb helper routines used by the operational_init function + * + */ +int dsdb_module_search_dn( + struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_result **_res, + struct ldb_dn *basedn, + const char * const *attrs, + uint32_t dsdb_flags, + struct ldb_request *parent) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_message *msg = ldb_msg_new(ldb); + struct ldb_result *res = talloc_zero(mem_ctx, struct ldb_result); + + msg->dn = ldb_dn_new(msg, ldb, "@SAMBA_DSDB"); + ldb_msg_add_string( + msg, + SAMBA_REQUIRED_FEATURES_ATTR, + SAMBA_ENCRYPTED_SECRETS_FEATURE); + + res->msgs = talloc_array(mem_ctx, struct ldb_message*, 1); + res->msgs[0] = msg; + *_res = res; + return LDB_SUCCESS; +} + +int dsdb_module_reference_dn( + struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_dn *base, + const char *attribute, + struct ldb_dn **dn, + struct ldb_request *parent) +{ + return LDB_SUCCESS; +} +/* -------------------------------------------------------------------------- */ + +static void unlink_old_db(struct ldbtest_ctx *test_ctx) +{ + int ret; + + errno = 0; + ret = unlink(test_ctx->lockfile); + if (ret == -1 && errno != ENOENT) { + fail(); + } + + errno = 0; + ret = unlink(test_ctx->dbfile); + if (ret == -1 && errno != ENOENT) { + fail(); + } + + errno = 0; + ret = unlink(test_ctx->keyfile); + if (ret == -1 && errno != ENOENT) { + fail(); + } +} + +static void write_key(void **state, DATA_BLOB key) { + + struct ldbtest_ctx *test_ctx = talloc_get_type_abort(*state, + struct ldbtest_ctx); + FILE *fp = NULL; + int written = 0; + + fp = fopen(test_ctx->keyfile, "wb"); + assert_non_null(fp); + + written = fwrite(key.data, 1, key.length, fp); + assert_int_equal(written, key.length); + fclose(fp); +} + +static const struct ldb_module_ops eol_ops = { + .name = "eol", + .search = NULL, + .add = NULL, + .modify = NULL, + .del = NULL, + .rename = NULL, + .init_context = NULL +}; + +static int setup(void **state) +{ + struct ldbtest_ctx *test_ctx = NULL; + struct ldb_module *eol = NULL; + int rc; + + test_ctx = talloc_zero(NULL, struct ldbtest_ctx); + assert_non_null(test_ctx); + + test_ctx->ev = tevent_context_init(test_ctx); + assert_non_null(test_ctx->ev); + + test_ctx->ldb = ldb_init(test_ctx, test_ctx->ev); + assert_non_null(test_ctx->ldb); + + + + test_ctx->module = ldb_module_new( + test_ctx, + test_ctx->ldb, + "encrypted_secrets", + &ldb_encrypted_secrets_module_ops); + assert_non_null(test_ctx->module); + eol = ldb_module_new(test_ctx, test_ctx->ldb, "eol", &eol_ops); + assert_non_null(eol); + ldb_module_set_next(test_ctx->module, eol); + + test_ctx->dbfile = talloc_strdup(test_ctx, "apitest.ldb"); + assert_non_null(test_ctx->dbfile); + + test_ctx->lockfile = talloc_asprintf(test_ctx, "%s-lock", + test_ctx->dbfile); + assert_non_null(test_ctx->lockfile); + + test_ctx->keyfile = talloc_strdup(test_ctx, SECRETS_KEY_FILE); + assert_non_null(test_ctx->keyfile); + + test_ctx->dbpath = talloc_asprintf(test_ctx, + TEST_BE"://%s", test_ctx->dbfile); + assert_non_null(test_ctx->dbpath); + + unlink_old_db(test_ctx); + + rc = ldb_connect(test_ctx->ldb, test_ctx->dbpath, 0, NULL); + assert_int_equal(rc, 0); + *state = test_ctx; + return 0; +} + +static int setup_with_key(void **state) +{ + struct ldbtest_ctx *test_ctx = NULL; + DATA_BLOB key = data_blob_null; + uint8_t key_data[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; + int rc; + + setup(state); + key.data = key_data; + key.length = sizeof(key_data); + + write_key(state, key); + + test_ctx = talloc_get_type_abort(*state, struct ldbtest_ctx); + { + struct ldb_message *msg = ldb_msg_new(test_ctx->ldb); + msg->dn = ldb_dn_new(msg, test_ctx->ldb, "@SAMBA_DSDB"); + ldb_msg_add_string( + msg, + SAMBA_REQUIRED_FEATURES_ATTR, + SAMBA_ENCRYPTED_SECRETS_FEATURE); + ldb_add(test_ctx->ldb, msg); + } + + rc = es_init(test_ctx->module); + assert_int_equal(rc, LDB_SUCCESS); + + return 0; +} + +static int teardown(void **state) +{ + struct ldbtest_ctx *test_ctx = talloc_get_type_abort(*state, + struct ldbtest_ctx); + + unlink_old_db(test_ctx); + talloc_free(test_ctx); + return 0; +} +/* + * No key file present. + * + * The key should be empty and encrypt_secrets should be false. + */ +static void test_no_key_file(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct es_data *data = NULL; + + int rc; + + rc = es_init(test_ctx->module); + assert_int_equal(rc, LDB_SUCCESS); + + data = talloc_get_type(ldb_module_get_private(test_ctx->module), + struct es_data); + + assert_false(data->encrypt_secrets); + assert_int_equal(0, data->keys[0].length); + +} + +/* + * Key file present. + * + * The key should be loaded and encrypt secrets should be true; + */ +static void test_key_file(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct es_data *data = NULL; + int rc; + DATA_BLOB key = data_blob_null; + uint8_t key_data[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; + + key.data = key_data; + key.length = sizeof(key_data); + + write_key(state, key); + + + rc = es_init(test_ctx->module); + assert_int_equal(rc, LDB_SUCCESS); + + data = talloc_get_type(ldb_module_get_private(test_ctx->module), + struct es_data); + + assert_true(data->encrypt_secrets); + assert_int_equal(16, data->keys[0].length); + assert_int_equal(0, data_blob_cmp(&key, &data->keys[0])); + +} + +/* + * Key file present, short key. + * + * The key should be not be loaded and an error returned. + */ +static void test_key_file_short_key(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + int rc; + DATA_BLOB key = data_blob_null; + uint8_t key_data[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e}; + + key.data = key_data; + key.length = sizeof(key_data); + + write_key(state, key); + + + rc = es_init(test_ctx->module); + assert_int_equal(rc, LDB_ERR_OPERATIONS_ERROR); +} + +/* + * Key file present, long key. + * + * Only the first 16 bytes of the key should be loaded. + */ +static void test_key_file_long_key(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct es_data *data = NULL; + int rc; + DATA_BLOB key = data_blob_null; + uint8_t key_data[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf, + 0x10}; + + key.data = key_data; + key.length = sizeof(key_data); + + write_key(state, key); + + rc = es_init(test_ctx->module); + assert_int_equal(rc, LDB_SUCCESS); + + data = talloc_get_type(ldb_module_get_private(test_ctx->module), + struct es_data); + + assert_true(data->encrypt_secrets); + assert_int_equal(16, data->keys[0].length); + + /* + * Should have only read the first 16 bytes of the written key + */ + key.length = 16; + assert_int_equal(0, data_blob_cmp(&key, &data->keys[0])); +} + +/* + * Test gnutls_encryption and decryption. + */ +static void test_gnutls_value_decryption(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + const struct ldb_val plain_text = + data_blob_string_const("A text value"); + unsigned char iv_data[] = { + 0xe7, 0xa3, 0x85, 0x17, 0x45, 0x73, 0xf4, 0x25, + 0xa5, 0x56, 0xde, 0x4c, + }; + unsigned char encrypted_data[] = { + 0xac, 0x13, 0x86, 0x94, 0x3b, 0xed, 0xf2, 0x51, + 0xec, 0x85, 0x4d, 0x00, 0x37, 0x81, 0x46, 0x15, + 0x42, 0x13, 0xb1, 0x69, 0x49, 0x10, 0xe7, 0x9e, + 0x15, 0xbd, 0x95, 0x75, 0x6b, 0x0c, 0xc0, 0xa4, + }; + struct EncryptedSecret es = { + .iv = { + .data = iv_data, + .length = sizeof(iv_data), + }, + .header = { + .magic = ENCRYPTED_SECRET_MAGIC_VALUE, + .version = SECRET_ATTRIBUTE_VERSION, + .algorithm = ENC_SECRET_AES_128_AEAD, + }, + .encrypted = { + .data = encrypted_data, + .length = sizeof(encrypted_data), + } + }; + unsigned char es_keys_blob[] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + }; + struct es_data data = { + .encrypt_secrets = true, + .keys[0] = { + .data = es_keys_blob, + .length = sizeof(es_keys_blob), + }, + .encryption_algorithm = GNUTLS_CIPHER_AES_128_GCM, + }; + struct PlaintextSecret *decrypted = + talloc_zero(test_ctx, struct PlaintextSecret); + int err = LDB_SUCCESS; + + gnutls_decrypt_aead(&err, + test_ctx, + test_ctx->ldb, + &es, + decrypted, + &data); + assert_int_equal(LDB_SUCCESS, err); + assert_int_equal(plain_text.length, decrypted->cleartext.length); + assert_int_equal(0, data_blob_cmp(&decrypted->cleartext, &plain_text)); +} + +/* + * Test gnutls_encryption and decryption. + */ +static void test_gnutls_value_encryption(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct ldb_val plain_text = data_blob_null; + struct ldb_val cipher_text = data_blob_null; + struct EncryptedSecret es; + + struct es_data *data = talloc_get_type( + ldb_module_get_private(test_ctx->module), + struct es_data); + int err = LDB_SUCCESS; + int rc; + + plain_text = data_blob_string_const("A text value"); + cipher_text = gnutls_encrypt_aead( + &err, + test_ctx, + test_ctx->ldb, + plain_text, + data); + assert_int_equal(LDB_SUCCESS, err); + + rc = ndr_pull_struct_blob( + &cipher_text, + test_ctx, + &es, + (ndr_pull_flags_fn_t) ndr_pull_EncryptedSecret); + assert_true(NDR_ERR_CODE_IS_SUCCESS(rc)); + assert_true(check_header(&es)); + + { + struct PlaintextSecret *decrypted = + talloc_zero(test_ctx, struct PlaintextSecret); + gnutls_decrypt_aead( + &err, + test_ctx, + test_ctx->ldb, + &es, + decrypted, + data); + assert_int_equal(LDB_SUCCESS, err); + assert_int_equal( + plain_text.length, + decrypted->cleartext.length); + assert_int_equal(0, + data_blob_cmp( + &decrypted->cleartext, + &plain_text)); + } +} + +static void test_gnutls_altered_header(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct ldb_val plain_text = data_blob_null; + struct ldb_val cipher_text = data_blob_null; + struct EncryptedSecret es; + + struct es_data *data = talloc_get_type( + ldb_module_get_private(test_ctx->module), + struct es_data); + int err = LDB_SUCCESS; + int rc; + + plain_text = data_blob_string_const("A text value"); + cipher_text = gnutls_encrypt_aead( + &err, + test_ctx, + test_ctx->ldb, + plain_text, + data); + assert_int_equal(LDB_SUCCESS, err); + + rc = ndr_pull_struct_blob( + &cipher_text, + test_ctx, + &es, + (ndr_pull_flags_fn_t) ndr_pull_EncryptedSecret); + assert_true(NDR_ERR_CODE_IS_SUCCESS(rc)); + assert_true(check_header(&es)); + + { + struct PlaintextSecret *decrypted = + talloc_zero(test_ctx, struct PlaintextSecret); + gnutls_decrypt_aead( + &err, + test_ctx, + test_ctx->ldb, + &es, + decrypted, + data); + assert_int_equal(LDB_SUCCESS, err); + assert_int_equal( + plain_text.length, + decrypted->cleartext.length); + assert_int_equal(0, + data_blob_cmp( + &decrypted->cleartext, + &plain_text)); + } + es.header.flags = es.header.flags ^ 0xffffffff; + { + struct PlaintextSecret *decrypted = + talloc_zero(test_ctx, struct PlaintextSecret); + gnutls_decrypt_aead( + &err, + test_ctx, + test_ctx->ldb, + &es, + decrypted, + data); + assert_int_equal(LDB_ERR_OPERATIONS_ERROR, err); + } +} + +static void test_gnutls_altered_data(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct ldb_val plain_text = data_blob_null; + struct ldb_val cipher_text = data_blob_null; + struct EncryptedSecret es; + + struct es_data *data = talloc_get_type( + ldb_module_get_private(test_ctx->module), + struct es_data); + int err = LDB_SUCCESS; + int rc; + + plain_text = data_blob_string_const("A text value"); + cipher_text = gnutls_encrypt_aead( + &err, + test_ctx, + test_ctx->ldb, + plain_text, + data); + assert_int_equal(LDB_SUCCESS, err); + + rc = ndr_pull_struct_blob( + &cipher_text, + test_ctx, + &es, + (ndr_pull_flags_fn_t) ndr_pull_EncryptedSecret); + assert_true(NDR_ERR_CODE_IS_SUCCESS(rc)); + assert_true(check_header(&es)); + + { + struct PlaintextSecret *decrypted = + talloc_zero(test_ctx, struct PlaintextSecret); + gnutls_decrypt_aead( + &err, + test_ctx, + test_ctx->ldb, + &es, + decrypted, + data); + assert_int_equal(LDB_SUCCESS, err); + assert_int_equal( + plain_text.length, + decrypted->cleartext.length); + assert_int_equal(0, + data_blob_cmp( + &decrypted->cleartext, + &plain_text)); + } + es.encrypted.data[0] = es.encrypted.data[0] ^ 0xff; + { + struct PlaintextSecret *decrypted = + talloc_zero(test_ctx, struct PlaintextSecret); + gnutls_decrypt_aead( + &err, + test_ctx, + test_ctx->ldb, + &es, + decrypted, + data); + assert_int_equal(LDB_ERR_OPERATIONS_ERROR, err); + } +} + +static void test_gnutls_altered_iv(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct ldb_val plain_text = data_blob_null; + struct ldb_val cipher_text = data_blob_null; + struct EncryptedSecret es; + + struct es_data *data = talloc_get_type( + ldb_module_get_private(test_ctx->module), + struct es_data); + int err = LDB_SUCCESS; + int rc; + + plain_text = data_blob_string_const("A text value"); + cipher_text = gnutls_encrypt_aead( + &err, + test_ctx, + test_ctx->ldb, + plain_text, + data); + assert_int_equal(LDB_SUCCESS, err); + + rc = ndr_pull_struct_blob( + &cipher_text, + test_ctx, + &es, + (ndr_pull_flags_fn_t) ndr_pull_EncryptedSecret); + assert_true(NDR_ERR_CODE_IS_SUCCESS(rc)); + assert_true(check_header(&es)); + + { + struct PlaintextSecret *decrypted = + talloc_zero(test_ctx, struct PlaintextSecret); + gnutls_decrypt_aead( + &err, + test_ctx, + test_ctx->ldb, + &es, + decrypted, + data); + assert_int_equal(LDB_SUCCESS, err); + assert_int_equal( + plain_text.length, + decrypted->cleartext.length); + assert_int_equal(0, + data_blob_cmp( + &decrypted->cleartext, + &plain_text)); + } + es.iv.data[0] = es.iv.data[0] ^ 0xff; + { + struct PlaintextSecret *decrypted = + talloc_zero(test_ctx, struct PlaintextSecret); + gnutls_decrypt_aead( + &err, + test_ctx, + test_ctx->ldb, + &es, + decrypted, + data); + assert_int_equal(LDB_ERR_OPERATIONS_ERROR, err); + } +} + +/* + * Test samba encryption and decryption and decryption. + */ + +/* + * Test message encryption. + * Test the secret attributes of a message are encrypted and decrypted. + * Test that the non secret attributes are not encrypted. + * + */ +static void test_message_encryption_decryption(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct ldb_context *ldb = test_ctx->ldb; + const char * const secrets[] = {DSDB_SECRET_ATTRIBUTES}; + const size_t num_secrets + = (sizeof(secrets)/sizeof(secrets[0])); + struct ldb_message *msg = ldb_msg_new(ldb); + const struct ldb_message *encrypted_msg = NULL; + struct es_data *data = talloc_get_type( + ldb_module_get_private(test_ctx->module), + struct es_data); + struct ldb_message_element *el = NULL; + int ret = LDB_SUCCESS; + size_t i; + unsigned int j; + + msg->dn = ldb_dn_new(msg, ldb, "dc=test"); + ldb_msg_add_string(msg, "cmocka_test_name01", "value01"); + for (i=0; i < num_secrets; i++) { + ldb_msg_add_string( + msg, + secrets[i], + secrets[i]); + } + ldb_msg_add_string(msg, "cmocka_test_name02", "value02"); + + encrypted_msg = encrypt_secret_attributes( + &ret, + test_ctx, + test_ctx->ldb, + msg, + data); + assert_int_equal(LDB_SUCCESS, ret); + + /* + * Check that all the secret attributes have been encrypted + * + */ + for (i=0; i < num_secrets; i++) { + el = ldb_msg_find_element(encrypted_msg, secrets[i]); + assert_non_null(el); + for (j = 0; j < el->num_values; j++) { + int rc = LDB_SUCCESS; + struct ldb_val dc = decrypt_value( + &rc, + test_ctx, + test_ctx->ldb, + el->values[j], + data); + assert_int_equal(LDB_SUCCESS, rc); + assert_memory_equal( + secrets[i], + dc.data, + dc.length); + TALLOC_FREE(dc.data); + } + } + + /* + * Check that the normal attributes have not been encrypted + */ + el = ldb_msg_find_element(encrypted_msg, "cmocka_test_name01"); + assert_non_null(el); + assert_memory_equal( + "value01", + el->values[0].data, + el->values[0].length); + + el = ldb_msg_find_element(encrypted_msg, "cmocka_test_name02"); + assert_non_null(el); + assert_memory_equal( + "value02", + el->values[0].data, + el->values[0].length); + + /* + * Now decrypt the message + */ + ret = decrypt_secret_attributes(test_ctx->ldb, + discard_const(encrypted_msg), + data); + assert_int_equal(LDB_SUCCESS, ret); + + /* + * Check that all the secret attributes have been decrypted + */ + for (i=0; i < num_secrets; i++) { + el = ldb_msg_find_element(encrypted_msg, secrets[i]); + assert_non_null(el); + for (j = 0; j < el->num_values; j++) { + assert_memory_equal( + secrets[i], + el->values[j].data, + el->values[j].length); + } + } + + /* + * Check that the normal attributes are intact + */ + el = ldb_msg_find_element(msg, "cmocka_test_name01"); + assert_non_null(el); + assert_memory_equal( + "value01", + el->values[0].data, + el->values[0].length); + + el = ldb_msg_find_element(msg, "cmocka_test_name02"); + assert_non_null(el); + assert_memory_equal( + "value02", + el->values[0].data, + el->values[0].length); + +} + +static void test_check_header(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + + struct ldb_val enc = data_blob_null; + struct EncryptedSecret *es = NULL; + int rc; + + /* + * Valid EncryptedSecret + */ + es = makeEncryptedSecret(test_ctx->ldb, test_ctx); + rc = ndr_push_struct_blob( + &enc, + test_ctx, + es, + (ndr_push_flags_fn_t) ndr_push_EncryptedSecret); + assert_true(NDR_ERR_CODE_IS_SUCCESS(rc)); + assert_true(check_header(es)); + TALLOC_FREE(enc.data); + TALLOC_FREE(es); + + /* + * invalid magic value + */ + es = makeEncryptedSecret(test_ctx->ldb, test_ctx); + es->header.magic = 0xca5cadee; + rc = ndr_push_struct_blob( + &enc, + test_ctx, + es, + (ndr_push_flags_fn_t) ndr_push_EncryptedSecret); + assert_true(NDR_ERR_CODE_IS_SUCCESS(rc)); + assert_false(check_header(es)); + TALLOC_FREE(enc.data); + TALLOC_FREE(es); + + /* + * invalid version + */ + es = makeEncryptedSecret(test_ctx->ldb, test_ctx); + es->header.version = SECRET_ATTRIBUTE_VERSION + 1; + rc = ndr_push_struct_blob( + &enc, + test_ctx, + es, + (ndr_push_flags_fn_t) ndr_push_EncryptedSecret); + assert_true(NDR_ERR_CODE_IS_SUCCESS(rc)); + assert_false(check_header(es)); + TALLOC_FREE(enc.data); + TALLOC_FREE(es); + + /* + * invalid algorithm + */ + es = makeEncryptedSecret(test_ctx->ldb, test_ctx); + es->header.algorithm = SECRET_ENCRYPTION_ALGORITHM + 1; + rc = ndr_push_struct_blob( + &enc, + test_ctx, + es, + (ndr_push_flags_fn_t) ndr_push_EncryptedSecret); + assert_true(NDR_ERR_CODE_IS_SUCCESS(rc)); + assert_false(check_header(es)); + TALLOC_FREE(enc.data); + TALLOC_FREE(es); +} + +/* + * Attempt to decrypt a message containing an unencrypted secret attribute + * this should fail + */ +static void test_unencrypted_secret(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct ldb_context *ldb = test_ctx->ldb; + struct ldb_message *msg = ldb_msg_new(ldb); + struct es_data *data = talloc_get_type( + ldb_module_get_private(test_ctx->module), + struct es_data); + int ret = LDB_SUCCESS; + + msg->dn = ldb_dn_new(msg, ldb, "dc=test"); + ldb_msg_add_string(msg, "unicodePwd", "value01"); + + ret = decrypt_secret_attributes(test_ctx->ldb, msg, data); + assert_int_equal(LDB_ERR_OPERATIONS_ERROR, ret); +} + +/* + * Test full decryption of a static value with static key + */ +static void test_record_decryption(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + unsigned char plain_data[] = { + 0xe6, 0xa6, 0xb8, 0xff, 0xdf, 0x06, 0x6c, 0xe3, + 0xea, 0xd0, 0x94, 0xbb, 0x79, 0xbd, 0x0a, 0x24 + }; + unsigned char encrypted_data[] = { + 0x0c, 0x00, 0x00, 0x00, 0x33, 0x91, 0x74, 0x25, + 0x26, 0xcc, 0x0b, 0x8c, 0x21, 0xc1, 0x13, 0xe2, + 0xed, 0xad, 0x5c, 0xca, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1a, 0xdc, 0xc9, 0x11, 0x08, 0xca, 0x2c, 0xfb, + 0xc8, 0x32, 0x6b, 0x1b, 0x25, 0x7f, 0x52, 0xbb, + 0xae, 0x9b, 0x88, 0x52, 0xb0, 0x18, 0x6d, 0x9d, + 0x9b, 0xdd, 0xcd, 0x1b, 0x5f, 0x4a, 0x5c, 0x29, + 0xca, 0x0b, 0x36, 0xaa + }; + struct ldb_val cipher_text + = data_blob_const(encrypted_data, + sizeof(encrypted_data)); + unsigned char es_keys_blob[] = { + 0x1d, 0xae, 0xf5, 0xaa, 0xa3, 0x85, 0x0d, 0x0a, + 0x8c, 0x24, 0x5c, 0x4c, 0xa7, 0x0f, 0x81, 0x79 + }; + struct es_data data = { + .encrypt_secrets = true, + .keys[0] = { + .data = es_keys_blob, + .length = sizeof(es_keys_blob), + }, + .encryption_algorithm = GNUTLS_CIPHER_AES_128_GCM, + }; + int err = LDB_SUCCESS; + struct ldb_val dec = decrypt_value(&err, test_ctx, test_ctx->ldb, cipher_text, + &data); + assert_int_equal(LDB_SUCCESS, err); + assert_int_equal(sizeof(plain_data), dec.length); + assert_memory_equal(dec.data, plain_data, sizeof(plain_data)); +} + + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown( + test_no_key_file, + setup, + teardown), + cmocka_unit_test_setup_teardown( + test_key_file, + setup, + teardown), + cmocka_unit_test_setup_teardown( + test_key_file_short_key, + setup, + teardown), + cmocka_unit_test_setup_teardown( + test_key_file_long_key, + setup, + teardown), + cmocka_unit_test_setup_teardown( + test_check_header, + setup, + teardown), + cmocka_unit_test_setup_teardown( + test_gnutls_value_decryption, + setup_with_key, + teardown), + cmocka_unit_test_setup_teardown( + test_gnutls_value_encryption, + setup_with_key, + teardown), + cmocka_unit_test_setup_teardown( + test_gnutls_altered_header, + setup_with_key, + teardown), + cmocka_unit_test_setup_teardown( + test_gnutls_altered_data, + setup_with_key, + teardown), + cmocka_unit_test_setup_teardown( + test_gnutls_altered_iv, + setup_with_key, + teardown), + cmocka_unit_test_setup_teardown( + test_message_encryption_decryption, + setup_with_key, + teardown), + cmocka_unit_test_setup_teardown( + test_unencrypted_secret, + setup_with_key, + teardown), + cmocka_unit_test_setup_teardown( + test_record_decryption, + setup_with_key, + teardown), + }; + + cmocka_set_message_output(CM_OUTPUT_SUBUNIT); + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/source4/dsdb/samdb/ldb_modules/tests/test_group_audit.c b/source4/dsdb/samdb/ldb_modules/tests/test_group_audit.c new file mode 100644 index 0000000..f7075f3 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/tests/test_group_audit.c @@ -0,0 +1,2022 @@ +/* + Unit tests for the dsdb group auditing code in group_audit.c + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018 + + 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/>. +*/ + +#include <stdarg.h> +#include <stddef.h> +#include <setjmp.h> +#include <unistd.h> +#include <cmocka.h> + +int ldb_group_audit_log_module_init(const char *version); +#include "../group_audit.c" + +#include "lib/ldb/include/ldb_private.h" +#include <regex.h> + +/* + * Mock version of dsdb_search_one + */ +struct ldb_dn *g_basedn = NULL; +enum ldb_scope g_scope; +const char * const *g_attrs = NULL; +uint32_t g_dsdb_flags; +const char *g_exp_fmt; +const char *g_dn = NULL; +int g_status = LDB_SUCCESS; +struct ldb_result *g_result = NULL; + +int dsdb_search_one(struct ldb_context *ldb, + TALLOC_CTX *mem_ctx, + struct ldb_message **msg, + struct ldb_dn *basedn, + enum ldb_scope scope, + const char * const *attrs, + uint32_t dsdb_flags, + const char *exp_fmt, ...) _PRINTF_ATTRIBUTE(8, 9) +{ + struct ldb_dn *dn = ldb_dn_new(mem_ctx, ldb, g_dn); + struct ldb_message *m = talloc_zero(mem_ctx, struct ldb_message); + m->dn = dn; + *msg = m; + + g_basedn = basedn; + g_scope = scope; + g_attrs = attrs; + g_dsdb_flags = dsdb_flags; + g_exp_fmt = exp_fmt; + + return g_status; +} + +int dsdb_module_search_dn( + struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_result **res, + struct ldb_dn *basedn, + const char * const *attrs, + uint32_t dsdb_flags, + struct ldb_request *parent) +{ + + g_basedn = basedn; + g_attrs = attrs; + g_dsdb_flags = dsdb_flags; + + *res = g_result; + + return g_status; +} +/* + * Mock version of audit_log_json + */ + +#define MAX_EXPECTED_MESSAGES 16 +static struct json_object messages[MAX_EXPECTED_MESSAGES]; +static size_t messages_sent = 0; + +void audit_message_send( + struct imessaging_context *msg_ctx, + const char *server_name, + uint32_t message_type, + struct json_object *message) +{ + messages[messages_sent].root = json_deep_copy(message->root); + messages[messages_sent].valid = message->valid; + messages_sent++; +} + +#define check_group_change_message(m, u, a, e) \ + _check_group_change_message(m, u, a, e, __FILE__, __LINE__); +/* + * declare the internal cmocka cm_print_error so that we can output messages + * in sub unit format + */ +void cm_print_error(const char * const format, ...); + +/* + * Validate a group change JSON audit message + * + * It should contain 3 elements. + * Have a type of "groupChange" + * Have a groupChange element + * + * The group change element should have 10 elements. + * + * There should be a user element matching the expected value + * There should be an action matching the expected value + */ +static void _check_group_change_message(const int message, + const char *user, + const char *action, + enum event_id_type event_id, + const char *file, + const int line) +{ + struct json_object json; + json_t *audit = NULL; + json_t *v = NULL; + const char* value; + int int_value; + int cmp; + + json = messages[message]; + + /* + * Validate the root JSON element + * check the number of elements + */ + if (json_object_size(json.root) != 3) { + cm_print_error( + "Unexpected number of elements in root %zu != %d\n", + json_object_size(json.root), + 3); + _fail(file, line); + } + + /* + * Check the type element + */ + v = json_object_get(json.root, "type"); + if (v == NULL) { + cm_print_error( "No \"type\" element\n"); + _fail(file, line); + } + + value = json_string_value(v); + cmp = strcmp("groupChange", value); + if (cmp != 0) { + cm_print_error( + "Unexpected type \"%s\" != \"groupChange\"\n", + value); + _fail(file, line); + } + + + audit = json_object_get(json.root, "groupChange"); + if (audit == NULL) { + cm_print_error("No groupChange element\n"); + _fail(file, line); + } + + /* + * Validate the groupChange element + */ + if ((event_id == EVT_ID_NONE && json_object_size(audit) != 10) || + (event_id != EVT_ID_NONE && json_object_size(audit) != 11)) { + cm_print_error("Unexpected number of elements in groupChange " + "%zu != %d\n", + json_object_size(audit), + 11); + _fail(file, line); + } + /* + * Validate the user element + */ + v = json_object_get(audit, "user"); + if (v == NULL) { + cm_print_error( "No user element\n"); + _fail(file, line); + } + + value = json_string_value(v); + cmp = strcmp(user, value); + if (cmp != 0) { + cm_print_error( + "Unexpected user name \"%s\" != \"%s\"\n", + value, + user); + _fail(file, line); + } + /* + * Validate the action element + */ + v = json_object_get(audit, "action"); + if (v == NULL) { + cm_print_error( "No action element\n"); + _fail(file, line); + } + + value = json_string_value(v); + cmp = strcmp(action, value); + if (cmp != 0) { + print_error( + "Unexpected action \"%s\" != \"%s\"\n", + value, + action); + _fail(file, line); + } + + /* + * Validate the eventId element + */ + v = json_object_get(audit, "eventId"); + if (event_id == EVT_ID_NONE) { + if (v != NULL) { + int_value = json_integer_value(v); + cm_print_error("Unexpected eventId \"%d\", it should " + "NOT be present", + int_value); + _fail(file, line); + } + } + else { + if (v == NULL) { + cm_print_error("No eventId element\n"); + _fail(file, line); + } + + int_value = json_integer_value(v); + if (int_value != event_id) { + cm_print_error("Unexpected eventId \"%d\" != \"%d\"\n", + int_value, + event_id); + _fail(file, line); + } + } +} + +#define check_timestamp(b, t)\ + _check_timestamp(b, t, __FILE__, __LINE__); +/* + * Test helper to check ISO 8601 timestamps for validity + */ +static void _check_timestamp( + time_t before, + const char *timestamp, + const char *file, + const int line) +{ + int rc; + int usec, tz; + char c[2]; + struct tm tm; + time_t after; + time_t actual; + struct timeval tv; + + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + after = tv.tv_sec; + + /* + * Convert the ISO 8601 timestamp into a time_t + * Note for convenience we ignore the value of the microsecond + * part of the time stamp. + */ + rc = sscanf( + timestamp, + "%4d-%2d-%2dT%2d:%2d:%2d.%6d%1c%4d", + &tm.tm_year, + &tm.tm_mon, + &tm.tm_mday, + &tm.tm_hour, + &tm.tm_min, + &tm.tm_sec, + &usec, + c, + &tz); + assert_int_equal(9, rc); + tm.tm_year = tm.tm_year - 1900; + tm.tm_mon = tm.tm_mon - 1; + tm.tm_isdst = -1; + actual = mktime(&tm); + + /* + * The time stamp should be before <= actual <= after + */ + if (difftime(actual, before) < 0) { + char buffer[40]; + strftime(buffer, + sizeof(buffer)-1, + "%Y-%m-%dT%T", + localtime(&before)); + cm_print_error( + "time stamp \"%s\" is before start time \"%s\"\n", + timestamp, + buffer); + _fail(file, line); + } + if (difftime(after, actual) < 0) { + char buffer[40]; + strftime(buffer, + sizeof(buffer)-1, + "%Y-%m-%dT%T", + localtime(&after)); + cm_print_error( + "time stamp \"%s\" is after finish time \"%s\"\n", + timestamp, + buffer); + _fail(file, line); + } +} + +#define check_version(v, m, n)\ + _check_version(v, m, n, __FILE__, __LINE__); +/* + * Test helper to validate a version object. + */ +static void _check_version( + struct json_t *version, + int major, + int minor, + const char* file, + const int line) +{ + struct json_t *v = NULL; + int value; + + if (!json_is_object(version)) { + cm_print_error("version is not a JSON object\n"); + _fail(file, line); + } + + if (json_object_size(version) != 2) { + cm_print_error( + "Unexpected number of elements in version %zu != %d\n", + json_object_size(version), + 2); + _fail(file, line); + } + + /* + * Validate the major version number element + */ + v = json_object_get(version, "major"); + if (v == NULL) { + cm_print_error( "No major element\n"); + _fail(file, line); + } + + value = json_integer_value(v); + if (value != major) { + print_error( + "Unexpected major version number \"%d\" != \"%d\"\n", + value, + major); + _fail(file, line); + } + + /* + * Validate the minor version number element + */ + v = json_object_get(version, "minor"); + if (v == NULL) { + cm_print_error( "No minor element\n"); + _fail(file, line); + } + + value = json_integer_value(v); + if (value != minor) { + print_error( + "Unexpected minor version number \"%d\" != \"%d\"\n", + value, + minor); + _fail(file, line); + } +} + +/* + * Test helper to insert a transaction_id into a request. + */ +static void add_transaction_id(struct ldb_request *req, const char *id) +{ + struct GUID guid; + struct dsdb_control_transaction_identifier *transaction_id = NULL; + + transaction_id = talloc_zero( + req, + struct dsdb_control_transaction_identifier); + assert_non_null(transaction_id); + GUID_from_string(id, &guid); + transaction_id->transaction_guid = guid; + ldb_request_add_control( + req, + DSDB_CONTROL_TRANSACTION_IDENTIFIER_OID, + false, + transaction_id); +} + +/* + * Test helper to add a session id and user SID + */ +static void add_session_data( + TALLOC_CTX *ctx, + struct ldb_context *ldb, + const char *session, + const char *user_sid) +{ + struct auth_session_info *sess = NULL; + struct security_token *token = NULL; + struct dom_sid *sid = NULL; + struct GUID session_id; + bool ok; + + sess = talloc_zero(ctx, struct auth_session_info); + token = talloc_zero(ctx, struct security_token); + sid = talloc_zero(ctx, struct dom_sid); + ok = string_to_sid(sid, user_sid); + assert_true(ok); + token->sids = sid; + sess->security_token = token; + GUID_from_string(session, &session_id); + sess->unique_session_token = session_id; + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sess); +} + +static void test_get_transaction_id(void **state) +{ + struct ldb_request *req = NULL; + struct GUID *guid; + const char * const ID = "7130cb06-2062-6a1b-409e-3514c26b1773"; + char *guid_str = NULL; + struct GUID_txt_buf guid_buff; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + + /* + * No transaction id, should return a zero guid + */ + req = talloc_zero(ctx, struct ldb_request); + guid = get_transaction_id(req); + assert_null(guid); + TALLOC_FREE(req); + + /* + * And now test with the transaction_id set + */ + req = talloc_zero(ctx, struct ldb_request); + assert_non_null(req); + add_transaction_id(req, ID); + + guid = get_transaction_id(req); + guid_str = GUID_buf_string(guid, &guid_buff); + assert_string_equal(ID, guid_str); + TALLOC_FREE(req); + + TALLOC_FREE(ctx); +} + +static void test_audit_group_hr(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + + struct tsocket_address *ts = NULL; + + const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + + char *line = NULL; + const char *rs = NULL; + regex_t regex; + int ret; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + GUID_from_string(TRANSACTION, &transaction_id); + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(ldb, "remoteAddress", ts); + + add_session_data(ctx, ldb, SESSION, SID); + + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + add_transaction_id(req, TRANSACTION); + + line = audit_group_human_readable( + ctx, + module, + req, + "the-action", + "the-user-name", + "the-group-name", + LDB_ERR_OPERATIONS_ERROR); + assert_non_null(line); + + rs = "\\[the-action\\] at \\[" + "[^]]*" + "\\] status \\[Operations error\\] " + "Remote host \\[ipv4:127.0.0.1:0\\] " + "SID \\[S-1-5-21-2470180966-3899876309-2637894779\\] " + "Group \\[the-group-name\\] " + "User \\[the-user-name\\]"; + + ret = regcomp(®ex, rs, 0); + assert_int_equal(0, ret); + + ret = regexec(®ex, line, 0, NULL, 0); + assert_int_equal(0, ret); + + regfree(®ex); + TALLOC_FREE(ctx); + +} + +/* + * test get_parsed_dns + * For this test we assume Valgrind or Address Sanitizer will detect any over + * runs. Also we don't care that the values are DN's only that the value in the + * element is copied to the parsed_dns. + */ +static void test_get_parsed_dns(void **state) +{ + struct ldb_message_element *el = NULL; + struct parsed_dn *dns = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + el = talloc_zero(ctx, struct ldb_message_element); + + /* + * empty element, zero dns + */ + dns = get_parsed_dns(ctx, el); + assert_null(dns); + + /* + * one entry + */ + el->num_values = 1; + el->values = talloc_zero_array(ctx, DATA_BLOB, 1); + el->values[0] = data_blob_string_const("The first value"); + + dns = get_parsed_dns(ctx, el); + + assert_ptr_equal(el->values[0].data, dns[0].v->data); + assert_int_equal(el->values[0].length, dns[0].v->length); + + TALLOC_FREE(dns); + TALLOC_FREE(el); + + + /* + * Multiple values + */ + el = talloc_zero(ctx, struct ldb_message_element); + el->num_values = 2; + el->values = talloc_zero_array(ctx, DATA_BLOB, 2); + el->values[0] = data_blob_string_const("The first value"); + el->values[0] = data_blob_string_const("The second value"); + + dns = get_parsed_dns(ctx, el); + + assert_ptr_equal(el->values[0].data, dns[0].v->data); + assert_int_equal(el->values[0].length, dns[0].v->length); + + assert_ptr_equal(el->values[1].data, dns[1].v->data); + assert_int_equal(el->values[1].length, dns[1].v->length); + + TALLOC_FREE(ctx); +} + +static void test_dn_compare(void **state) +{ + + struct ldb_context *ldb = NULL; + struct parsed_dn *a; + DATA_BLOB ab; + + struct parsed_dn *b; + DATA_BLOB bb; + + int res; + + TALLOC_CTX *ctx = talloc_new(NULL); + const struct GUID *ZERO_GUID = talloc_zero(ctx, struct GUID); + + ldb = ldb_init(ctx, NULL); + ldb_register_samba_handlers(ldb); + + + /* + * Identical binary DN's + */ + ab = data_blob_string_const( + "<GUID=fbee08fd-6f75-4bd4-af3f-e4f063a6379e>;" + "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=org"); + a = talloc_zero(ctx, struct parsed_dn); + a->v = &ab; + + bb = data_blob_string_const( + "<GUID=fbee08fd-6f75-4bd4-af3f-e4f063a6379e>;" + "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=org"); + b = talloc_zero(ctx, struct parsed_dn); + b->v = &bb; + + res = dn_compare(ctx, ldb, a, b); + assert_int_equal(BINARY_EQUAL, res); + /* + * DN's should not have been parsed + */ + assert_null(a->dsdb_dn); + assert_memory_equal(ZERO_GUID, &a->guid, sizeof(struct GUID)); + assert_null(b->dsdb_dn); + assert_memory_equal(ZERO_GUID, &b->guid, sizeof(struct GUID)); + + TALLOC_FREE(a); + TALLOC_FREE(b); + + /* + * differing binary DN's but equal GUID's + */ + ab = data_blob_string_const( + "<GUID=efdc91e5-5a5a-493e-9606-166ed0c2651e>;" + "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=com"); + a = talloc_zero(ctx, struct parsed_dn); + a->v = &ab; + + bb = data_blob_string_const( + "<GUID=efdc91e5-5a5a-493e-9606-166ed0c2651e>;" + "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=org"); + b = talloc_zero(ctx, struct parsed_dn); + b->v = &bb; + + res = dn_compare(ctx, ldb, a, b); + assert_int_equal(EQUAL, res); + /* + * DN's should have been parsed + */ + assert_non_null(a->dsdb_dn); + assert_memory_not_equal(ZERO_GUID, &a->guid, sizeof(struct GUID)); + assert_non_null(b->dsdb_dn); + assert_memory_not_equal(ZERO_GUID, &b->guid, sizeof(struct GUID)); + + TALLOC_FREE(a); + TALLOC_FREE(b); + + /* + * differing binary DN's but and second guid greater + */ + ab = data_blob_string_const( + "<GUID=efdc91e5-5a5a-493e-9606-166ed0c2651d>;" + "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=com"); + a = talloc_zero(ctx, struct parsed_dn); + a->v = &ab; + + bb = data_blob_string_const( + "<GUID=efdc91e5-5a5a-493e-9606-166ed0c2651e>;" + "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=org"); + b = talloc_zero(ctx, struct parsed_dn); + b->v = &bb; + + res = dn_compare(ctx, ldb, a, b); + assert_int_equal(LESS_THAN, res); + /* + * DN's should have been parsed + */ + assert_non_null(a->dsdb_dn); + assert_memory_not_equal(ZERO_GUID, &a->guid, sizeof(struct GUID)); + assert_non_null(b->dsdb_dn); + assert_memory_not_equal(ZERO_GUID, &b->guid, sizeof(struct GUID)); + + TALLOC_FREE(a); + TALLOC_FREE(b); + + /* + * differing binary DN's but and second guid less + */ + ab = data_blob_string_const( + "<GUID=efdc91e5-5a5a-493e-9606-166ed0c2651d>;" + "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=com"); + a = talloc_zero(ctx, struct parsed_dn); + a->v = &ab; + + bb = data_blob_string_const( + "<GUID=efdc91e5-5a5a-493e-9606-166ed0c2651c>;" + "OU=Domain Controllers,DC=ad,DC=testing,DC=samba,DC=org"); + b = talloc_zero(ctx, struct parsed_dn); + b->v = &bb; + + res = dn_compare(ctx, ldb, a, b); + assert_int_equal(GREATER_THAN, res); + /* + * DN's should have been parsed + */ + assert_non_null(a->dsdb_dn); + assert_memory_not_equal(ZERO_GUID, &a->guid, sizeof(struct GUID)); + assert_non_null(b->dsdb_dn); + assert_memory_not_equal(ZERO_GUID, &b->guid, sizeof(struct GUID)); + + TALLOC_FREE(a); + TALLOC_FREE(b); + + TALLOC_FREE(ctx); +} + +static void test_get_primary_group_dn(void **state) +{ + + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + const uint32_t RID = 71; + struct dom_sid sid; + const char *SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char *DN = "OU=Things,DC=ad,DC=testing,DC=samba,DC=org"; + const char *dn; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + ldb_register_samba_handlers(ldb); + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + + /* + * Pass an empty dom sid this will cause dom_sid_split_rid to fail; + * assign to sid.num_auths to suppress a valgrind warning. + */ + sid.num_auths = 0; + dn = get_primary_group_dn(ctx, module, &sid, RID); + assert_null(dn); + + /* + * A valid dom sid + */ + assert_true(string_to_sid(&sid, SID)); + g_dn = DN; + dn = get_primary_group_dn(ctx, module, &sid, RID); + assert_non_null(dn); + assert_string_equal(DN, dn); + assert_int_equal(LDB_SCOPE_BASE, g_scope); + assert_int_equal(0, g_dsdb_flags); + assert_null(g_attrs); + assert_null(g_exp_fmt); + assert_string_equal + ("<SID=S-1-5-21-2470180966-3899876309-71>", + ldb_dn_get_extended_linearized(ctx, g_basedn, 1)); + + /* + * Test dsdb search failure + */ + g_status = LDB_ERR_NO_SUCH_OBJECT; + dn = get_primary_group_dn(ctx, module, &sid, RID); + assert_null(dn); + + TALLOC_FREE(ldb); + TALLOC_FREE(ctx); +} + +static void test_audit_group_json(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + + struct tsocket_address *ts = NULL; + + const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + enum event_id_type event_id = EVT_ID_USER_ADDED_TO_GLOBAL_SEC_GROUP; + + struct json_object json; + json_t *audit = NULL; + json_t *v = NULL; + json_t *o = NULL; + time_t before; + struct timeval tv; + int rc; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + GUID_from_string(TRANSACTION, &transaction_id); + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(ldb, "remoteAddress", ts); + + add_session_data(ctx, ldb, SESSION, SID); + + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + add_transaction_id(req, TRANSACTION); + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + before = tv.tv_sec; + json = audit_group_json(module, + req, + "the-action", + "the-user-name", + "the-group-name", + event_id, + LDB_SUCCESS); + assert_int_equal(3, json_object_size(json.root)); + + v = json_object_get(json.root, "type"); + assert_non_null(v); + assert_string_equal("groupChange", json_string_value(v)); + + v = json_object_get(json.root, "timestamp"); + assert_non_null(v); + assert_true(json_is_string(v)); + check_timestamp(before, json_string_value(v)); + + audit = json_object_get(json.root, "groupChange"); + assert_non_null(audit); + assert_true(json_is_object(audit)); + assert_int_equal(11, json_object_size(audit)); + + o = json_object_get(audit, "version"); + assert_non_null(o); + check_version(o, AUDIT_MAJOR, AUDIT_MINOR); + + v = json_object_get(audit, "eventId"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(EVT_ID_USER_ADDED_TO_GLOBAL_SEC_GROUP, + json_integer_value(v)); + + v = json_object_get(audit, "statusCode"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(LDB_SUCCESS, json_integer_value(v)); + + v = json_object_get(audit, "status"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("Success", json_string_value(v)); + + v = json_object_get(audit, "user"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("the-user-name", json_string_value(v)); + + v = json_object_get(audit, "group"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("the-group-name", json_string_value(v)); + + v = json_object_get(audit, "action"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("the-action", json_string_value(v)); + + json_free(&json); + TALLOC_FREE(ctx); +} + +static void test_audit_group_json_error(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + + struct tsocket_address *ts = NULL; + + const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + enum event_id_type event_id = EVT_ID_USER_ADDED_TO_GLOBAL_SEC_GROUP; + + struct json_object json; + json_t *audit = NULL; + json_t *v = NULL; + json_t *o = NULL; + time_t before; + struct timeval tv; + int rc; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + GUID_from_string(TRANSACTION, &transaction_id); + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(ldb, "remoteAddress", ts); + + add_session_data(ctx, ldb, SESSION, SID); + + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + add_transaction_id(req, TRANSACTION); + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + before = tv.tv_sec; + json = audit_group_json(module, + req, + "the-action", + "the-user-name", + "the-group-name", + event_id, + LDB_ERR_OPERATIONS_ERROR); + assert_int_equal(3, json_object_size(json.root)); + + v = json_object_get(json.root, "type"); + assert_non_null(v); + assert_string_equal("groupChange", json_string_value(v)); + + v = json_object_get(json.root, "timestamp"); + assert_non_null(v); + assert_true(json_is_string(v)); + check_timestamp(before, json_string_value(v)); + + audit = json_object_get(json.root, "groupChange"); + assert_non_null(audit); + assert_true(json_is_object(audit)); + assert_int_equal(11, json_object_size(audit)); + + o = json_object_get(audit, "version"); + assert_non_null(o); + check_version(o, AUDIT_MAJOR, AUDIT_MINOR); + + v = json_object_get(audit, "eventId"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal( + EVT_ID_USER_ADDED_TO_GLOBAL_SEC_GROUP, + json_integer_value(v)); + + v = json_object_get(audit, "statusCode"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(LDB_ERR_OPERATIONS_ERROR, json_integer_value(v)); + + v = json_object_get(audit, "status"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("Operations error", json_string_value(v)); + + v = json_object_get(audit, "user"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("the-user-name", json_string_value(v)); + + v = json_object_get(audit, "group"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("the-group-name", json_string_value(v)); + + v = json_object_get(audit, "action"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("the-action", json_string_value(v)); + + json_free(&json); + TALLOC_FREE(ctx); +} + +static void test_audit_group_json_no_event(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + + struct tsocket_address *ts = NULL; + + const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + enum event_id_type event_id = EVT_ID_NONE; + + struct json_object json; + json_t *audit = NULL; + json_t *v = NULL; + json_t *o = NULL; + time_t before; + struct timeval tv; + int rc; + + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + GUID_from_string(TRANSACTION, &transaction_id); + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(ldb, "remoteAddress", ts); + + add_session_data(ctx, ldb, SESSION, SID); + + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + add_transaction_id(req, TRANSACTION); + + rc = gettimeofday(&tv, NULL); + assert_return_code(rc, errno); + before = tv.tv_sec; + json = audit_group_json(module, + req, + "the-action", + "the-user-name", + "the-group-name", + event_id, + LDB_SUCCESS); + assert_int_equal(3, json_object_size(json.root)); + + v = json_object_get(json.root, "type"); + assert_non_null(v); + assert_string_equal("groupChange", json_string_value(v)); + + v = json_object_get(json.root, "timestamp"); + assert_non_null(v); + assert_true(json_is_string(v)); + check_timestamp(before, json_string_value(v)); + + audit = json_object_get(json.root, "groupChange"); + assert_non_null(audit); + assert_true(json_is_object(audit)); + assert_int_equal(10, json_object_size(audit)); + + o = json_object_get(audit, "version"); + assert_non_null(o); + check_version(o, AUDIT_MAJOR, AUDIT_MINOR); + + v = json_object_get(audit, "eventId"); + assert_null(v); + + v = json_object_get(audit, "statusCode"); + assert_non_null(v); + assert_true(json_is_integer(v)); + assert_int_equal(LDB_SUCCESS, json_integer_value(v)); + + v = json_object_get(audit, "status"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("Success", json_string_value(v)); + + v = json_object_get(audit, "user"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("the-user-name", json_string_value(v)); + + v = json_object_get(audit, "group"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("the-group-name", json_string_value(v)); + + v = json_object_get(audit, "action"); + assert_non_null(v); + assert_true(json_is_string(v)); + assert_string_equal("the-action", json_string_value(v)); + + json_free(&json); + TALLOC_FREE(ctx); +} +static void setup_ldb( + TALLOC_CTX *ctx, + struct ldb_context **ldb, + struct ldb_module **module, + const char *ip, + const char *session, + const char *sid) +{ + struct tsocket_address *ts = NULL; + struct audit_context *context = NULL; + + *ldb = ldb_init(ctx, NULL); + ldb_register_samba_handlers(*ldb); + + + *module = talloc_zero(ctx, struct ldb_module); + (*module)->ldb = *ldb; + + context = talloc_zero(*module, struct audit_context); + context->send_events = true; + context->msg_ctx = (struct imessaging_context *) 0x01; + + ldb_module_set_private(*module, context); + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(*ldb, "remoteAddress", ts); + + add_session_data(ctx, *ldb, session, sid); +} + +/* + * Test the removal of a user from a group. + * + * The new element contains one group member + * The old element contains two group member + * + * Expect to see the removed entry logged. + * + * This test confirms bug 13664 + * https://bugzilla.samba.org/show_bug.cgi?id=13664 + */ +static void test_log_membership_changes_removed(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + const char * const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const IP = "127.0.0.1"; + struct ldb_request *req = NULL; + struct ldb_message_element *new_el = NULL; + struct ldb_message_element *old_el = NULL; + uint32_t group_type = GTYPE_SECURITY_GLOBAL_GROUP; + int status = 0; + TALLOC_CTX *ctx = talloc_new(NULL); + + setup_ldb(ctx, &ldb, &module, IP, SESSION, SID); + + /* + * Build the ldb_request + */ + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + add_transaction_id(req, TRANSACTION); + + /* + * Populate the new elements, containing one entry. + * Indicating that one element has been removed + */ + new_el = talloc_zero(ctx, struct ldb_message_element); + new_el->num_values = 1; + new_el->values = talloc_zero_array(ctx, DATA_BLOB, 1); + new_el->values[0] = data_blob_string_const( + "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;" + "CN=testuser131953,CN=Users,DC=addom,DC=samba," + "DC=example,DC=com"); + + /* + * Populate the old elements, with two elements + * The first is the same as the one in new elements. + */ + old_el = talloc_zero(ctx, struct ldb_message_element); + old_el->num_values = 2; + old_el->values = talloc_zero_array(ctx, DATA_BLOB, 2); + old_el->values[0] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;" + "cn=grpadttstuser01,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + old_el->values[1] = data_blob_string_const( + "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;" + "CN=testuser131953,CN=Users,DC=addom,DC=samba," + "DC=example,DC=com"); + + /* + * call log_membership_changes + */ + messages_sent = 0; + log_membership_changes(module, req, new_el, old_el, group_type, status); + + /* + * Check the results + */ + assert_int_equal(1, messages_sent); + + check_group_change_message( + 0, + "cn=grpadttstuser01,cn=users,DC=addom,DC=samba,DC=example,DC=com", + "Removed", + EVT_ID_USER_REMOVED_FROM_GLOBAL_SEC_GROUP); + + /* + * Clean up + */ + json_free(&messages[0]); + TALLOC_FREE(ctx); +} + +/* test log_membership_changes + * + * old contains 2 user dn's + * new contains 0 user dn's + * + * Expect to see both dn's logged as deleted. + */ +static void test_log_membership_changes_remove_all(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + const char * const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const IP = "127.0.0.1"; + struct ldb_request *req = NULL; + struct ldb_message_element *new_el = NULL; + struct ldb_message_element *old_el = NULL; + int status = 0; + uint32_t group_type = GTYPE_SECURITY_BUILTIN_LOCAL_GROUP; + TALLOC_CTX *ctx = talloc_new(NULL); + + setup_ldb(ctx, &ldb, &module, IP, SESSION, SID); + + /* + * Build the ldb_request + */ + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + add_transaction_id(req, TRANSACTION); + + /* + * Populate the new elements, containing no entries. + * Indicating that all elements have been removed + */ + new_el = talloc_zero(ctx, struct ldb_message_element); + new_el->num_values = 0; + new_el->values = NULL; + + /* + * Populate the old elements, with two elements + */ + old_el = talloc_zero(ctx, struct ldb_message_element); + old_el->num_values = 2; + old_el->values = talloc_zero_array(ctx, DATA_BLOB, 2); + old_el->values[0] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;" + "cn=grpadttstuser01,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + old_el->values[1] = data_blob_string_const( + "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;" + "CN=testuser131953,CN=Users,DC=addom,DC=samba," + "DC=example,DC=com"); + + /* + * call log_membership_changes + */ + messages_sent = 0; + log_membership_changes(module, req, new_el, old_el, group_type, status); + + /* + * Check the results + */ + assert_int_equal(2, messages_sent); + + check_group_change_message( + 0, + "cn=grpadttstuser01,cn=users,DC=addom,DC=samba,DC=example,DC=com", + "Removed", + EVT_ID_USER_REMOVED_FROM_LOCAL_SEC_GROUP); + + check_group_change_message( + 1, + "CN=testuser131953,CN=Users,DC=addom,DC=samba,DC=example,DC=com", + "Removed", + EVT_ID_USER_REMOVED_FROM_LOCAL_SEC_GROUP); + + /* + * Clean up + */ + json_free(&messages[0]); + json_free(&messages[1]); + TALLOC_FREE(ctx); +} + +/* test log_membership_changes + * + * Add an entry. + * + * Old entries contains a single user dn + * New entries contains 2 user dn's, one matching the dn in old entries + * + * Should see a single new entry logged. + */ +static void test_log_membership_changes_added(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + const char * const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const IP = "127.0.0.1"; + struct ldb_request *req = NULL; + struct ldb_message_element *new_el = NULL; + struct ldb_message_element *old_el = NULL; + uint32_t group_type = GTYPE_SECURITY_DOMAIN_LOCAL_GROUP; + int status = 0; + TALLOC_CTX *ctx = talloc_new(NULL); + + setup_ldb(ctx, &ldb, &module, IP, SESSION, SID); + + /* + * Build the ldb_request + */ + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + add_transaction_id(req, TRANSACTION); + + /* + * Populate the old elements adding a single entry. + */ + old_el = talloc_zero(ctx, struct ldb_message_element); + old_el->num_values = 1; + old_el->values = talloc_zero_array(ctx, DATA_BLOB, 1); + old_el->values[0] = data_blob_string_const( + "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;" + "CN=testuser131953,CN=Users,DC=addom,DC=samba," + "DC=example,DC=com"); + + /* + * Populate the new elements adding two entries. One matches the entry + * in old elements. We expect to see the other element logged as Added + */ + new_el = talloc_zero(ctx, struct ldb_message_element); + new_el->num_values = 2; + new_el->values = talloc_zero_array(ctx, DATA_BLOB, 2); + new_el->values[0] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;" + "cn=grpadttstuser01,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + new_el->values[1] = data_blob_string_const( + "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;" + "CN=testuser131953,CN=Users,DC=addom,DC=samba," + "DC=example,DC=com"); + + /* + * call log_membership_changes + */ + messages_sent = 0; + log_membership_changes(module, req, new_el, old_el, group_type, status); + + /* + * Check the results + */ + assert_int_equal(1, messages_sent); + + check_group_change_message( + 0, + "cn=grpadttstuser01,cn=users,DC=addom,DC=samba,DC=example,DC=com", + "Added", + EVT_ID_USER_ADDED_TO_LOCAL_SEC_GROUP); + + /* + * Clean up + */ + json_free(&messages[0]); + TALLOC_FREE(ctx); +} + +/* + * test log_membership_changes. + * + * Old entries is empty + * New entries contains 2 user dn's + * + * Expect to see log messages for two added users + */ +static void test_log_membership_changes_add_to_empty(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + const char * const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const IP = "127.0.0.1"; + struct ldb_request *req = NULL; + struct ldb_message_element *new_el = NULL; + struct ldb_message_element *old_el = NULL; + uint32_t group_type = GTYPE_SECURITY_UNIVERSAL_GROUP; + int status = 0; + TALLOC_CTX *ctx = talloc_new(NULL); + + /* + * Set up the ldb and module structures + */ + setup_ldb(ctx, &ldb, &module, IP, SESSION, SID); + + /* + * Build the request structure + */ + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + add_transaction_id(req, TRANSACTION); + + /* + * Build the element containing the old values + */ + old_el = talloc_zero(ctx, struct ldb_message_element); + old_el->num_values = 0; + old_el->values = NULL; + + /* + * Build the element containing the new values + */ + new_el = talloc_zero(ctx, struct ldb_message_element); + new_el->num_values = 2; + new_el->values = talloc_zero_array(ctx, DATA_BLOB, 2); + new_el->values[0] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;" + "cn=grpadttstuser01,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + new_el->values[1] = data_blob_string_const( + "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;" + "CN=testuser131953,CN=Users,DC=addom,DC=samba," + "DC=example,DC=com"); + + /* + * Run log membership changes + */ + messages_sent = 0; + log_membership_changes(module, req, new_el, old_el, group_type, status); + assert_int_equal(2, messages_sent); + + check_group_change_message( + 0, + "cn=grpadttstuser01,cn=users,DC=addom,DC=samba,DC=example,DC=com", + "Added", + EVT_ID_USER_ADDED_TO_UNIVERSAL_SEC_GROUP); + + check_group_change_message( + 1, + "CN=testuser131953,CN=Users,DC=addom,DC=samba,DC=example,DC=com", + "Added", + EVT_ID_USER_ADDED_TO_UNIVERSAL_SEC_GROUP); + + json_free(&messages[0]); + json_free(&messages[1]); + TALLOC_FREE(ctx); +} + +/* test log_membership_changes + * + * Test Replication Meta Data flag handling. + * + * 4 entries in old and new entries with their RMD_FLAGS set as below: + * old new + * 1) 0 0 Not logged + * 2) 1 1 Both deleted, no change not logged + * 3) 0 1 New tagged as deleted, log as deleted + * 4) 1 0 Has been undeleted, log as an add + * + * Should see a single new entry logged. + */ +static void test_log_membership_changes_rmd_flags(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + const char * const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const IP = "127.0.0.1"; + struct ldb_request *req = NULL; + struct ldb_message_element *new_el = NULL; + struct ldb_message_element *old_el = NULL; + uint32_t group_type = GTYPE_SECURITY_GLOBAL_GROUP; + int status = 0; + TALLOC_CTX *ctx = talloc_new(NULL); + + setup_ldb(ctx, &ldb, &module, IP, SESSION, SID); + + /* + * Build the ldb_request + */ + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + add_transaction_id(req, TRANSACTION); + + /* + * Populate the old elements. + */ + old_el = talloc_zero(ctx, struct ldb_message_element); + old_el->num_values = 4; + old_el->values = talloc_zero_array(ctx, DATA_BLOB, 4); + old_el->values[0] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;" + "<RMD_FLAGS=0>;" + "cn=grpadttstuser01,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + old_el->values[1] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681c>;" + "<RMD_FLAGS=1>;" + "cn=grpadttstuser02,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + old_el->values[2] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681d>;" + "<RMD_FLAGS=0>;" + "cn=grpadttstuser03,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + old_el->values[3] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681e>;" + "<RMD_FLAGS=1>;" + "cn=grpadttstuser04,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + + /* + * Populate the new elements. + */ + new_el = talloc_zero(ctx, struct ldb_message_element); + new_el->num_values = 4; + new_el->values = talloc_zero_array(ctx, DATA_BLOB, 4); + new_el->values[0] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;" + "<RMD_FLAGS=0>;" + "cn=grpadttstuser01,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + new_el->values[1] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681c>;" + "<RMD_FLAGS=1>;" + "cn=grpadttstuser02,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + new_el->values[2] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681d>;" + "<RMD_FLAGS=1>;" + "cn=grpadttstuser03,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + new_el->values[3] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681e>;" + "<RMD_FLAGS=0>;" + "cn=grpadttstuser04,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + + /* + * call log_membership_changes + */ + messages_sent = 0; + log_membership_changes(module, req, new_el, old_el, group_type, status); + + /* + * Check the results + */ + assert_int_equal(2, messages_sent); + + check_group_change_message( + 0, + "cn=grpadttstuser03,cn=users,DC=addom,DC=samba,DC=example,DC=com", + "Removed", + EVT_ID_USER_REMOVED_FROM_GLOBAL_SEC_GROUP); + check_group_change_message( + 1, + "cn=grpadttstuser04,cn=users,DC=addom,DC=samba,DC=example,DC=com", + "Added", + EVT_ID_USER_ADDED_TO_GLOBAL_SEC_GROUP); + + /* + * Clean up + */ + json_free(&messages[0]); + json_free(&messages[1]); + TALLOC_FREE(ctx); +} + +static void test_get_add_member_event(void **state) +{ + assert_int_equal( + EVT_ID_USER_ADDED_TO_LOCAL_SEC_GROUP, + get_add_member_event(GTYPE_SECURITY_BUILTIN_LOCAL_GROUP)); + + assert_int_equal(EVT_ID_USER_ADDED_TO_GLOBAL_SEC_GROUP, + get_add_member_event(GTYPE_SECURITY_GLOBAL_GROUP)); + + assert_int_equal( + EVT_ID_USER_ADDED_TO_LOCAL_SEC_GROUP, + get_add_member_event(GTYPE_SECURITY_DOMAIN_LOCAL_GROUP)); + + assert_int_equal(EVT_ID_USER_ADDED_TO_UNIVERSAL_SEC_GROUP, + get_add_member_event(GTYPE_SECURITY_UNIVERSAL_GROUP)); + + assert_int_equal(EVT_ID_USER_ADDED_TO_GLOBAL_GROUP, + get_add_member_event(GTYPE_DISTRIBUTION_GLOBAL_GROUP)); + + assert_int_equal( + EVT_ID_USER_ADDED_TO_LOCAL_GROUP, + get_add_member_event(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)); + + assert_int_equal( + EVT_ID_USER_ADDED_TO_UNIVERSAL_GROUP, + get_add_member_event(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP)); + + assert_int_equal(EVT_ID_NONE, get_add_member_event(0)); + + assert_int_equal(EVT_ID_NONE, get_add_member_event(UINT32_MAX)); +} + +static void test_get_remove_member_event(void **state) +{ + assert_int_equal( + EVT_ID_USER_REMOVED_FROM_LOCAL_SEC_GROUP, + get_remove_member_event(GTYPE_SECURITY_BUILTIN_LOCAL_GROUP)); + + assert_int_equal(EVT_ID_USER_REMOVED_FROM_GLOBAL_SEC_GROUP, + get_remove_member_event(GTYPE_SECURITY_GLOBAL_GROUP)); + + assert_int_equal( + EVT_ID_USER_REMOVED_FROM_LOCAL_SEC_GROUP, + get_remove_member_event(GTYPE_SECURITY_DOMAIN_LOCAL_GROUP)); + + assert_int_equal( + EVT_ID_USER_REMOVED_FROM_UNIVERSAL_SEC_GROUP, + get_remove_member_event(GTYPE_SECURITY_UNIVERSAL_GROUP)); + + assert_int_equal( + EVT_ID_USER_REMOVED_FROM_GLOBAL_GROUP, + get_remove_member_event(GTYPE_DISTRIBUTION_GLOBAL_GROUP)); + + assert_int_equal( + EVT_ID_USER_REMOVED_FROM_LOCAL_GROUP, + get_remove_member_event(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)); + + assert_int_equal( + EVT_ID_USER_REMOVED_FROM_UNIVERSAL_GROUP, + get_remove_member_event(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP)); + + assert_int_equal(EVT_ID_NONE, get_remove_member_event(0)); + + assert_int_equal(EVT_ID_NONE, get_remove_member_event(UINT32_MAX)); +} + +/* test log_group_membership_changes + * + * Happy path test case + * + */ +static void test_log_group_membership_changes(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + const char * const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const IP = "127.0.0.1"; + struct ldb_request *req = NULL; + struct ldb_message *msg = NULL; + struct ldb_message_element *el = NULL; + struct audit_callback_context *acc = NULL; + struct ldb_result *res = NULL; + struct ldb_message *new_msg = NULL; + struct ldb_message_element *group_type = NULL; + const char *group_type_str = NULL; + struct ldb_message_element *new_el = NULL; + struct ldb_message_element *old_el = NULL; + int status = 0; + TALLOC_CTX *ctx = talloc_new(NULL); + + setup_ldb(ctx, &ldb, &module, IP, SESSION, SID); + + /* + * Build the ldb message + */ + msg = talloc_zero(ctx, struct ldb_message); + + /* + * Populate message elements, adding a new entry to the membership list + * + */ + + el = talloc_zero(ctx, struct ldb_message_element); + el->name = "member"; + el->num_values = 1; + el->values = talloc_zero_array(ctx, DATA_BLOB, 1); + el->values[0] = data_blob_string_const( + "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;" + "CN=testuser131953,CN=Users,DC=addom,DC=samba," + "DC=example,DC=com"); + msg->elements = el; + msg->num_elements = 1; + + /* + * Build the ldb_request + */ + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + req->op.add.message = msg; + add_transaction_id(req, TRANSACTION); + + /* + * Build the initial state of the database + */ + old_el = talloc_zero(ctx, struct ldb_message_element); + old_el->name = "member"; + old_el->num_values = 1; + old_el->values = talloc_zero_array(ctx, DATA_BLOB, 1); + old_el->values[0] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;" + "cn=grpadttstuser01,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + + /* + * Build the updated state of the database + */ + res = talloc_zero(ctx, struct ldb_result); + new_msg = talloc_zero(ctx, struct ldb_message); + new_el = talloc_zero(ctx, struct ldb_message_element); + new_el->name = "member"; + new_el->num_values = 2; + new_el->values = talloc_zero_array(ctx, DATA_BLOB, 2); + new_el->values[0] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;" + "cn=grpadttstuser01,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + new_el->values[1] = data_blob_string_const( + "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;" + "CN=testuser131953,CN=Users,DC=addom,DC=samba," + "DC=example,DC=com"); + + group_type = talloc_zero(ctx, struct ldb_message_element); + group_type->name = "groupType"; + group_type->num_values = 1; + group_type->values = talloc_zero_array(ctx, DATA_BLOB, 1); + group_type_str = talloc_asprintf(ctx, "%u", GTYPE_SECURITY_GLOBAL_GROUP); + group_type->values[0] = data_blob_string_const(group_type_str); + + + new_msg->elements = talloc_zero_array(ctx, struct ldb_message_element, 2); + new_msg->num_elements = 2; + new_msg->elements[0] = *new_el; + new_msg->elements[1] = *group_type; + + res->count = 1; + res->msgs = &new_msg; + + acc = talloc_zero(ctx, struct audit_callback_context); + acc->request = req; + acc->module = module; + acc->members = old_el; + /* + * call log_membership_changes + */ + messages_sent = 0; + g_result = res; + g_status = LDB_SUCCESS; + log_group_membership_changes(acc, status); + g_result = NULL; + + /* + * Check the results + */ + assert_int_equal(1, messages_sent); + + check_group_change_message( + 0, + "CN=testuser131953,CN=Users,DC=addom,DC=samba,DC=example,DC=com", + "Added", + EVT_ID_USER_ADDED_TO_GLOBAL_SEC_GROUP); + + /* + * Clean up + */ + json_free(&messages[0]); + TALLOC_FREE(ctx); +} + +/* test log_group_membership_changes + * + * The ldb query to retrieve the new values failed. + * + * Should generate group membership change Failure message. + * + */ +static void test_log_group_membership_changes_read_new_failure(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + const char * const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const IP = "127.0.0.1"; + struct ldb_request *req = NULL; + struct ldb_message *msg = NULL; + struct ldb_message_element *el = NULL; + struct audit_callback_context *acc = NULL; + struct ldb_message_element *old_el = NULL; + int status = 0; + TALLOC_CTX *ctx = talloc_new(NULL); + + setup_ldb(ctx, &ldb, &module, IP, SESSION, SID); + + /* + * Build the ldb message + */ + msg = talloc_zero(ctx, struct ldb_message); + + /* + * Populate message elements, adding a new entry to the membership list + * + */ + + el = talloc_zero(ctx, struct ldb_message_element); + el->name = "member"; + el->num_values = 1; + el->values = talloc_zero_array(ctx, DATA_BLOB, 1); + el->values[0] = data_blob_string_const( + "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;" + "CN=testuser131953,CN=Users,DC=addom,DC=samba," + "DC=example,DC=com"); + msg->elements = el; + msg->num_elements = 1; + + /* + * Build the ldb_request + */ + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + req->op.add.message = msg; + add_transaction_id(req, TRANSACTION); + + /* + * Build the initial state of the database + */ + old_el = talloc_zero(ctx, struct ldb_message_element); + old_el->name = "member"; + old_el->num_values = 1; + old_el->values = talloc_zero_array(ctx, DATA_BLOB, 1); + old_el->values[0] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;" + "cn=grpadttstuser01,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + + acc = talloc_zero(ctx, struct audit_callback_context); + acc->request = req; + acc->module = module; + acc->members = old_el; + /* + * call log_membership_changes + */ + messages_sent = 0; + g_result = NULL; + g_status = LDB_ERR_NO_SUCH_OBJECT; + log_group_membership_changes(acc, status); + + /* + * Check the results + */ + assert_int_equal(1, messages_sent); + + check_group_change_message( + 0, + "", + "Failure", + EVT_ID_NONE); + + /* + * Clean up + */ + json_free(&messages[0]); + TALLOC_FREE(ctx); +} + +/* test log_group_membership_changes + * + * The operation failed. + * + * Should generate group membership change Failure message. + * + */ +static void test_log_group_membership_changes_error(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + const char * const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + const char * const IP = "127.0.0.1"; + struct ldb_request *req = NULL; + struct ldb_message *msg = NULL; + struct ldb_message_element *el = NULL; + struct ldb_message_element *old_el = NULL; + struct audit_callback_context *acc = NULL; + int status = LDB_ERR_OPERATIONS_ERROR; + TALLOC_CTX *ctx = talloc_new(NULL); + + setup_ldb(ctx, &ldb, &module, IP, SESSION, SID); + + /* + * Build the ldb message + */ + msg = talloc_zero(ctx, struct ldb_message); + + /* + * Populate message elements, adding a new entry to the membership list + * + */ + + el = talloc_zero(ctx, struct ldb_message_element); + el->name = "member"; + el->num_values = 1; + el->values = talloc_zero_array(ctx, DATA_BLOB, 1); + el->values[0] = data_blob_string_const( + "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;" + "CN=testuser131953,CN=Users,DC=addom,DC=samba," + "DC=example,DC=com"); + msg->elements = el; + msg->num_elements = 1; + + /* + * Build the ldb_request + */ + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + req->op.add.message = msg; + add_transaction_id(req, TRANSACTION); + + /* + * Build the initial state of the database + */ + old_el = talloc_zero(ctx, struct ldb_message_element); + old_el->name = "member"; + old_el->num_values = 1; + old_el->values = talloc_zero_array(ctx, DATA_BLOB, 1); + old_el->values[0] = data_blob_string_const( + "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;" + "cn=grpadttstuser01,cn=users,DC=addom," + "DC=samba,DC=example,DC=com"); + + + acc = talloc_zero(ctx, struct audit_callback_context); + acc->request = req; + acc->module = module; + acc->members = old_el; + /* + * call log_membership_changes + */ + messages_sent = 0; + log_group_membership_changes(acc, status); + + /* + * Check the results + */ + assert_int_equal(1, messages_sent); + + check_group_change_message( + 0, + "", + "Failure", + EVT_ID_NONE); + + /* + * Clean up + */ + json_free(&messages[0]); + TALLOC_FREE(ctx); +} + +/* + * Note: to run under valgrind us: + * valgrind --suppressions=test_group_audit.valgrind bin/test_group_audit + * This suppresses the errors generated because the ldb_modules are not + * de-registered. + * + */ +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_audit_group_json), + cmocka_unit_test(test_audit_group_json_error), + cmocka_unit_test(test_audit_group_json_no_event), + cmocka_unit_test(test_get_transaction_id), + cmocka_unit_test(test_audit_group_hr), + cmocka_unit_test(test_get_parsed_dns), + cmocka_unit_test(test_dn_compare), + cmocka_unit_test(test_get_primary_group_dn), + cmocka_unit_test(test_log_membership_changes_removed), + cmocka_unit_test(test_log_membership_changes_remove_all), + cmocka_unit_test(test_log_membership_changes_added), + cmocka_unit_test(test_log_membership_changes_add_to_empty), + cmocka_unit_test(test_log_membership_changes_rmd_flags), + cmocka_unit_test(test_get_add_member_event), + cmocka_unit_test(test_get_remove_member_event), + cmocka_unit_test(test_log_group_membership_changes), + cmocka_unit_test(test_log_group_membership_changes_read_new_failure), + cmocka_unit_test(test_log_group_membership_changes_error), + }; + + cmocka_set_message_output(CM_OUTPUT_SUBUNIT); + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/source4/dsdb/samdb/ldb_modules/tests/test_group_audit.valgrind b/source4/dsdb/samdb/ldb_modules/tests/test_group_audit.valgrind new file mode 100644 index 0000000..1cf2b4e --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/tests/test_group_audit.valgrind @@ -0,0 +1,19 @@ +{ + ldb_modules_load modules not are freed + Memcheck:Leak + match-leak-kinds: possible + fun:malloc + fun:__talloc_with_prefix + fun:__talloc + fun:_talloc_named_const + fun:talloc_named_const + fun:ldb_register_module + fun:ldb_init_module + fun:ldb_modules_load_path + fun:ldb_modules_load_dir + fun:ldb_modules_load_path + fun:ldb_modules_load + fun:ldb_init +} + + diff --git a/source4/dsdb/samdb/ldb_modules/tests/test_group_audit_errors.c b/source4/dsdb/samdb/ldb_modules/tests/test_group_audit_errors.c new file mode 100644 index 0000000..ea9f2b7 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/tests/test_group_audit_errors.c @@ -0,0 +1,266 @@ +/* + Unit tests for the dsdb group auditing code in group_audit.c + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018 + + 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/>. +*/ + +/* + * These tests exercise the error handling routines. + */ + +#include <stdarg.h> +#include <stddef.h> +#include <setjmp.h> +#include <unistd.h> +#include <cmocka.h> + +int ldb_group_audit_log_module_init(const char *version); +#include "../group_audit.c" + +#include "lib/ldb/include/ldb_private.h" + +/* + * cmocka wrappers for json_new_object + */ +struct json_object __wrap_json_new_object(void); +struct json_object __real_json_new_object(void); +struct json_object __wrap_json_new_object(void) +{ + + bool use_real = (bool)mock(); + if (!use_real) { + return json_empty_object; + } + return __real_json_new_object(); +} + +/* + * cmocka wrappers for json_add_version + */ +int __wrap_json_add_version(struct json_object *object, int major, int minor); +int __real_json_add_version(struct json_object *object, int major, int minor); +int __wrap_json_add_version(struct json_object *object, int major, int minor) +{ + + int ret = (int)mock(); + if (ret) { + return ret; + } + return __real_json_add_version(object, major, minor); +} + +/* + * cmocka wrappers for json_add_version + */ +int __wrap_json_add_timestamp(struct json_object *object); +int __real_json_add_timestamp(struct json_object *object); +int __wrap_json_add_timestamp(struct json_object *object) +{ + + int ret = (int)mock(); + if (ret) { + return ret; + } + return __real_json_add_timestamp(object); +} + +/* + * Test helper to add a session id and user SID + */ +static void add_session_data( + TALLOC_CTX *ctx, + struct ldb_context *ldb, + const char *session, + const char *user_sid) +{ + struct auth_session_info *sess = NULL; + struct security_token *token = NULL; + struct dom_sid *sid = NULL; + struct GUID session_id; + bool ok; + + sess = talloc_zero(ctx, struct auth_session_info); + token = talloc_zero(ctx, struct security_token); + sid = talloc_zero(ctx, struct dom_sid); + ok = string_to_sid(sid, user_sid); + assert_true(ok); + token->sids = sid; + sess->security_token = token; + GUID_from_string(session, &session_id); + sess->unique_session_token = session_id; + ldb_set_opaque(ldb, DSDB_SESSION_INFO, sess); +} + +/* + * Test helper to insert a transaction_id into a request. + */ +static void add_transaction_id(struct ldb_request *req, const char *id) +{ + struct GUID guid; + struct dsdb_control_transaction_identifier *transaction_id = NULL; + + transaction_id = talloc_zero( + req, + struct dsdb_control_transaction_identifier); + assert_non_null(transaction_id); + GUID_from_string(id, &guid); + transaction_id->transaction_guid = guid; + ldb_request_add_control( + req, + DSDB_CONTROL_TRANSACTION_IDENTIFIER_OID, + false, + transaction_id); +} + +static void test_audit_group_json(void **state) +{ + struct ldb_context *ldb = NULL; + struct ldb_module *module = NULL; + struct ldb_request *req = NULL; + + struct tsocket_address *ts = NULL; + + const char *const SID = "S-1-5-21-2470180966-3899876309-2637894779"; + const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + struct GUID transaction_id; + const char *const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773"; + + enum event_id_type event_id = EVT_ID_USER_REMOVED_FROM_GLOBAL_SEC_GROUP; + + struct json_object json; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_init(ctx, NULL); + + GUID_from_string(TRANSACTION, &transaction_id); + + module = talloc_zero(ctx, struct ldb_module); + module->ldb = ldb; + + tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts); + ldb_set_opaque(ldb, "remoteAddress", ts); + + add_session_data(ctx, ldb, SESSION, SID); + + req = talloc_zero(ctx, struct ldb_request); + req->operation = LDB_ADD; + add_transaction_id(req, TRANSACTION); + + /* + * Fail on the creation of the audit json object + */ + + will_return(__wrap_json_new_object, false); + + json = audit_group_json(module, + req, + "the-action", + "the-user-name", + "the-group-name", + event_id, + LDB_ERR_OPERATIONS_ERROR); + assert_true(json_is_invalid(&json)); + + /* + * Fail adding the version object . + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, JSON_ERROR); + + json = audit_group_json(module, + req, + "the-action", + "the-user-name", + "the-group-name", + event_id, + LDB_ERR_OPERATIONS_ERROR); + assert_true(json_is_invalid(&json)); + + /* + * Fail on creation of the wrapper. + */ + + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, false); + + json = audit_group_json(module, + req, + "the-action", + "the-user-name", + "the-group-name", + event_id, + LDB_ERR_OPERATIONS_ERROR); + assert_true(json_is_invalid(&json)); + + /* + * Fail adding the timestamp to the wrapper object. + */ + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_timestamp, JSON_ERROR); + + json = audit_group_json(module, + req, + "the-action", + "the-user-name", + "the-group-name", + event_id, + LDB_ERR_OPERATIONS_ERROR); + assert_true(json_is_invalid(&json)); + + + /* + * Now test the happy path + */ + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_version, 0); + will_return(__wrap_json_new_object, true); + will_return(__wrap_json_add_timestamp, 0); + + json = audit_group_json(module, + req, + "the-action", + "the-user-name", + "the-group-name", + event_id, + LDB_ERR_OPERATIONS_ERROR); + assert_false(json_is_invalid(&json)); + + json_free(&json); + TALLOC_FREE(ctx); + +} + +/* + * Note: to run under valgrind us: + * valgrind --suppressions=test_group_audit.valgrind bin/test_group_audit + * This suppresses the errors generated because the ldb_modules are not + * de-registered. + * + */ +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(test_audit_group_json), + }; + + cmocka_set_message_output(CM_OUTPUT_SUBUNIT); + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/source4/dsdb/samdb/ldb_modules/tests/test_unique_object_sids.c b/source4/dsdb/samdb/ldb_modules/tests/test_unique_object_sids.c new file mode 100644 index 0000000..f9065e4 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/tests/test_unique_object_sids.c @@ -0,0 +1,514 @@ +/* + Unit tests for the unique objectSID code in unique_object_sids.c + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017 + + 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/>. +*/ + +#include <stdarg.h> +#include <stddef.h> +#include <setjmp.h> +#include <unistd.h> +#include <cmocka.h> + +int ldb_unique_object_sids_init(const char *version); +#include "../unique_object_sids.c" + +#include "../libcli/security/dom_sid.h" +#include "librpc/gen_ndr/ndr_security.h" + +#define TEST_BE "tdb" + +#define DOMAIN_SID "S-1-5-21-2470180966-3899876309-2637894779" +#define LOCAL_SID "S-1-5-21-2470180966-3899876309-2637894779-1000" +#define FOREIGN_SID "S-1-5-21-2470180966-3899876309-2637894778-1000" + +static struct ldb_request *last_request; + +/* + * ldb_next_request mock, records the request passed in last_request + * so it can be examined in the test cases. + */ +int ldb_next_request( + struct ldb_module *module, + struct ldb_request *request) +{ + last_request = request; + return ldb_module_done(request, NULL, NULL, LDB_SUCCESS); +} + +/* + * Test context + */ +struct ldbtest_ctx { + struct tevent_context *ev; + struct ldb_context *ldb; + struct ldb_module *module; + + const char *dbfile; + const char *lockfile; /* lockfile is separate */ + + const char *dbpath; + struct dom_sid *domain_sid; +}; + +/* + * Remove any database files created by the tests + */ +static void unlink_old_db(struct ldbtest_ctx *test_ctx) +{ + int ret; + + errno = 0; + ret = unlink(test_ctx->lockfile); + if (ret == -1 && errno != ENOENT) { + fail(); + } + + errno = 0; + ret = unlink(test_ctx->dbfile); + if (ret == -1 && errno != ENOENT) { + fail(); + } +} + +/* + * Empty module to signal the end of the module list + */ +static const struct ldb_module_ops eol_ops = { + .name = "eol", + .search = NULL, + .add = NULL, + .modify = NULL, + .del = NULL, + .rename = NULL, + .init_context = NULL +}; + +/* + * Test set up + */ +static int setup(void **state) +{ + struct ldbtest_ctx *test_ctx = NULL; + struct ldb_module *eol = NULL; + int rc; + + test_ctx = talloc_zero(NULL, struct ldbtest_ctx); + assert_non_null(test_ctx); + + test_ctx->ev = tevent_context_init(test_ctx); + assert_non_null(test_ctx->ev); + + test_ctx->ldb = ldb_init(test_ctx, test_ctx->ev); + assert_non_null(test_ctx->ldb); + + test_ctx->domain_sid = talloc_zero(test_ctx, struct dom_sid); + assert_non_null(test_ctx->domain_sid); + assert_true(string_to_sid(test_ctx->domain_sid, DOMAIN_SID)); + ldb_set_opaque(test_ctx->ldb, "cache.domain_sid", test_ctx->domain_sid); + + test_ctx->module = ldb_module_new( + test_ctx, + test_ctx->ldb, + "unique_object_sids", + &ldb_unique_object_sids_module_ops); + assert_non_null(test_ctx->module); + eol = ldb_module_new(test_ctx, test_ctx->ldb, "eol", &eol_ops); + assert_non_null(eol); + ldb_module_set_next(test_ctx->module, eol); + + test_ctx->dbfile = talloc_strdup(test_ctx, "duptest.ldb"); + assert_non_null(test_ctx->dbfile); + + test_ctx->lockfile = talloc_asprintf(test_ctx, "%s-lock", + test_ctx->dbfile); + assert_non_null(test_ctx->lockfile); + + test_ctx->dbpath = talloc_asprintf(test_ctx, + TEST_BE"://%s", test_ctx->dbfile); + assert_non_null(test_ctx->dbpath); + + unlink_old_db(test_ctx); + + rc = ldb_connect(test_ctx->ldb, test_ctx->dbpath, 0, NULL); + assert_int_equal(rc, LDB_SUCCESS); + + rc = unique_object_sids_init(test_ctx->module); + assert_int_equal(rc, LDB_SUCCESS); + + *state = test_ctx; + + last_request = NULL; + return 0; +} + +/* + * Test clean up + */ +static int teardown(void **state) +{ + struct ldbtest_ctx *test_ctx = talloc_get_type_abort(*state, + struct ldbtest_ctx); + + unlink_old_db(test_ctx); + talloc_free(test_ctx); + return 0; +} + +/* + * Add an objectSID in string form to the supplied message + * + * + */ +static void add_sid( + struct ldb_message *msg, + const char *sid_str) +{ + struct ldb_val v; + enum ndr_err_code ndr_err; + struct dom_sid *sid = NULL; + + sid = talloc_zero(msg, struct dom_sid); + assert_non_null(sid); + assert_true(string_to_sid(sid, sid_str)); + ndr_err = ndr_push_struct_blob(&v, msg, sid, + (ndr_push_flags_fn_t)ndr_push_dom_sid); + assert_true(NDR_ERR_CODE_IS_SUCCESS(ndr_err)); + assert_int_equal(0, ldb_msg_add_value(msg, "objectSID", &v, NULL)); +} + +/* + * The object is in the current local domain so it should have + * DB_FLAG_INTERNAL_UNIQUE_VALUE set + */ +static void test_objectSID_in_domain(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct ldb_context *ldb = test_ctx->ldb; + struct ldb_message *msg = ldb_msg_new(test_ctx); + struct ldb_message_element *el = NULL; + struct ldb_request *request = NULL; + struct ldb_request *original_request = NULL; + int rc; + + msg->dn = ldb_dn_new(msg, ldb, "dc=test"); + add_sid(msg, LOCAL_SID); + + rc = ldb_build_add_req( + &request, + test_ctx->ldb, + test_ctx, + msg, + NULL, + NULL, + ldb_op_default_callback, + NULL); + + assert_int_equal(rc, LDB_SUCCESS); + assert_non_null(request); + original_request = request; + + rc = unique_object_sids_add(test_ctx->module, request); + assert_int_equal(rc, LDB_SUCCESS); + + /* + * Check that a copy of the request was passed to the next module + * and not the original request + */ + assert_ptr_not_equal(last_request, original_request); + + /* + * Check the flag was set on the request passed to the next + * module + */ + el = ldb_msg_find_element(last_request->op.add.message, "objectSID"); + assert_non_null(el); + assert_true(el->flags & LDB_FLAG_INTERNAL_FORCE_UNIQUE_INDEX); + + /* + * Check the flag was not set on the original request + */ + el = ldb_msg_find_element(request->op.add.message, "objectSID"); + assert_non_null(el); + assert_false(el->flags & LDB_FLAG_INTERNAL_FORCE_UNIQUE_INDEX); + +} + +/* + * The object is not in the current local domain so it should NOT have + * DB_FLAG_INTERNAL_UNIQUE_VALUE set + */ +static void test_objectSID_not_in_domain(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct ldb_context *ldb = test_ctx->ldb; + struct ldb_message *msg = ldb_msg_new(test_ctx); + struct ldb_message_element *el = NULL; + struct ldb_request *request = NULL; + struct ldb_request *original_request = NULL; + int rc; + + msg->dn = ldb_dn_new(msg, ldb, "dc=test"); + add_sid(msg, FOREIGN_SID); + + rc = ldb_build_add_req( + &request, + test_ctx->ldb, + test_ctx, + msg, + NULL, + NULL, + ldb_op_default_callback, + NULL); + + assert_int_equal(rc, LDB_SUCCESS); + assert_non_null(request); + original_request = request; + + rc = unique_object_sids_add(test_ctx->module, request); + assert_int_equal(rc, LDB_SUCCESS); + + /* + * Check that the original request was passed to the next module + * and not a copy + */ + assert_ptr_equal(last_request, original_request); + + /* + * Check that the flag was not set on the objectSID element + */ + el = ldb_msg_find_element(msg, "objectSID"); + assert_non_null(el); + assert_false(el->flags & LDB_FLAG_INTERNAL_FORCE_UNIQUE_INDEX); +} + +/* + * No objectSID on the record so it should pass through the module untouched + * + */ +static void test_no_objectSID(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct ldb_context *ldb = test_ctx->ldb; + struct ldb_message *msg = ldb_msg_new(test_ctx); + struct ldb_request *request = NULL; + struct ldb_request *original_request = NULL; + int rc; + + msg->dn = ldb_dn_new(msg, ldb, "dc=test"); + assert_int_equal(LDB_SUCCESS, ldb_msg_add_string(msg, "cn", "test")); + + rc = ldb_build_add_req( + &request, + test_ctx->ldb, + test_ctx, + msg, + NULL, + NULL, + ldb_op_default_callback, + NULL); + + assert_int_equal(rc, LDB_SUCCESS); + assert_non_null(request); + original_request = request; + + rc = unique_object_sids_add(test_ctx->module, request); + assert_int_equal(rc, LDB_SUCCESS); + + /* + * Check that the original request was passed to the next module + * and not a copy + */ + assert_ptr_equal(last_request, original_request); + +} + +/* + * Attempt to modify an objectSID DSDB_CONTROL_REPLICATED_UPDATE_OID not set + * this should fail with LDB_ERR_UNWILLING_TO_PERFORM + */ +static void test_modify_of_objectSID_not_replicated(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct ldb_context *ldb = test_ctx->ldb; + struct ldb_message *msg = ldb_msg_new(test_ctx); + struct ldb_request *request = NULL; + int rc; + + msg->dn = ldb_dn_new(msg, ldb, "dc=test"); + add_sid(msg, LOCAL_SID); + + rc = ldb_build_mod_req( + &request, + test_ctx->ldb, + test_ctx, + msg, + NULL, + NULL, + ldb_op_default_callback, + NULL); + + assert_int_equal(rc, LDB_SUCCESS); + assert_non_null(request); + + rc = unique_object_sids_modify(test_ctx->module, request); + + assert_int_equal(rc, LDB_ERR_UNWILLING_TO_PERFORM); +} + + +/* + * Attempt to modify an objectSID DSDB_CONTROL_REPLICATED_UPDATE_OID set + * this should succeed + */ +static void test_modify_of_objectSID_replicated(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct ldb_context *ldb = test_ctx->ldb; + struct ldb_message *msg = ldb_msg_new(test_ctx); + struct ldb_message_element *el = NULL; + struct ldb_request *request = NULL; + struct ldb_request *original_request = NULL; + int rc; + + msg->dn = ldb_dn_new(msg, ldb, "dc=test"); + add_sid(msg, LOCAL_SID); + + rc = ldb_build_mod_req( + &request, + test_ctx->ldb, + test_ctx, + msg, + NULL, + NULL, + ldb_op_default_callback, + NULL); + assert_int_equal(rc, LDB_SUCCESS); + assert_non_null(request); + original_request = request; + + rc = ldb_request_add_control( + request, + DSDB_CONTROL_REPLICATED_UPDATE_OID, + false, + NULL); + assert_int_equal(rc, LDB_SUCCESS); + + rc = unique_object_sids_modify(test_ctx->module, request); + + assert_int_equal(rc, LDB_SUCCESS); + + /* + * Check that a copy of the request was passed to the next module + * and not the original request + */ + assert_ptr_not_equal(last_request, original_request); + + /* + * Check the flag was set on the request passed to the next + * module + */ + el = ldb_msg_find_element(last_request->op.add.message, "objectSID"); + assert_non_null(el); + assert_true(el->flags & LDB_FLAG_INTERNAL_FORCE_UNIQUE_INDEX); + + /* + * Check the flag was not set on the original request + */ + el = ldb_msg_find_element(request->op.add.message, "objectSID"); + assert_non_null(el); + assert_false(el->flags & LDB_FLAG_INTERNAL_FORCE_UNIQUE_INDEX); + +} + +/* + * Test the a modify with no object SID is passed through correctly + * + */ +static void test_modify_no_objectSID(void **state) +{ + struct ldbtest_ctx *test_ctx = + talloc_get_type_abort(*state, struct ldbtest_ctx); + struct ldb_context *ldb = test_ctx->ldb; + struct ldb_message *msg = ldb_msg_new(test_ctx); + struct ldb_request *request = NULL; + struct ldb_request *original_request = NULL; + int rc; + + msg->dn = ldb_dn_new(msg, ldb, "dc=test"); + assert_int_equal(LDB_SUCCESS, ldb_msg_add_string(msg, "cn", "test")); + + rc = ldb_build_mod_req( + &request, + test_ctx->ldb, + test_ctx, + msg, + NULL, + NULL, + ldb_op_default_callback, + NULL); + + assert_int_equal(rc, LDB_SUCCESS); + assert_non_null(request); + original_request = request; + + rc = unique_object_sids_modify(test_ctx->module, request); + assert_int_equal(rc, LDB_SUCCESS); + + /* + * Check that the original request was passed to the next module + * and not a copy + */ + assert_ptr_equal(last_request, original_request); + +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown( + test_objectSID_in_domain, + setup, + teardown), + cmocka_unit_test_setup_teardown( + test_objectSID_not_in_domain, + setup, + teardown), + cmocka_unit_test_setup_teardown( + test_no_objectSID, + setup, + teardown), + cmocka_unit_test_setup_teardown( + test_modify_no_objectSID, + setup, + teardown), + cmocka_unit_test_setup_teardown( + test_modify_of_objectSID_not_replicated, + setup, + teardown), + cmocka_unit_test_setup_teardown( + test_modify_of_objectSID_replicated, + setup, + teardown), + }; + + cmocka_set_message_output(CM_OUTPUT_SUBUNIT); + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/source4/dsdb/samdb/ldb_modules/tombstone_reanimate.c b/source4/dsdb/samdb/ldb_modules/tombstone_reanimate.c new file mode 100644 index 0000000..99c5955 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/tombstone_reanimate.c @@ -0,0 +1,423 @@ +/* + ldb database library + + Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2014 + + 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: tombstone_reanimate + * + * Component: Handle Tombstone reanimation requests + * + * Description: + * Tombstone reanimation requests are plain ldap modify request like: + * dn: CN=tombi 1\0ADEL:e6e17ff7-8986-4cdd-87ad-afb683ccbb89,CN=Deleted Objects,DC=samba4,DC=devel + * changetype: modify + * delete: isDeleted + * - + * replace: distinguishedName + * distinguishedName: CN=Tombi 1,CN=Users,DC=samba4,DC=devel + * - + * + * Usually we don't allow distinguishedName modifications (see rdn_name.c) + * Reanimating Tombstones is described here: + * - http://msdn.microsoft.com/en-us/library/cc223467.aspx + * + * Author: Kamen Mazdrashki + */ + + +#include "includes.h" +#include "ldb_module.h" +#include "dsdb/samdb/samdb.h" +#include "librpc/ndr/libndr.h" +#include "librpc/gen_ndr/ndr_security.h" +#include "libcli/security/security.h" +#include "auth/auth.h" +#include "param/param.h" +#include "../libds/common/flags.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "libds/common/flag_mapping.h" + +struct tr_context { + struct ldb_module *module; + + struct ldb_request *req; + const struct ldb_message *req_msg; + + struct ldb_result *search_res; + const struct ldb_message *search_msg; + + struct ldb_message *mod_msg; + struct ldb_result *mod_res; + struct ldb_request *mod_req; + + struct ldb_dn *rename_dn; + struct ldb_result *rename_res; + struct ldb_request *rename_req; + + const struct dsdb_schema *schema; +}; + +static struct tr_context *tr_init_context(struct ldb_module *module, + struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct tr_context *ac; + + ac = talloc_zero(req, struct tr_context); + if (ac == NULL) { + ldb_oom(ldb); + return NULL; + } + + ac->module = module; + ac->req = req; + ac->req_msg = req->op.mod.message; + ac->schema = dsdb_get_schema(ldb, ac); + + return ac; +} + + +static bool is_tombstone_reanimate_request(struct ldb_request *req, + const struct ldb_message_element **pel_dn) +{ + struct ldb_message_element *el_dn; + struct ldb_message_element *el_deleted; + + /* check distinguishedName requirement */ + el_dn = ldb_msg_find_element(req->op.mod.message, "distinguishedName"); + if (el_dn == NULL) { + return false; + } + if (LDB_FLAG_MOD_TYPE(el_dn->flags) != LDB_FLAG_MOD_REPLACE) { + return false; + } + if (el_dn->num_values != 1) { + return false; + } + + /* check isDeleted requirement */ + el_deleted = ldb_msg_find_element(req->op.mod.message, "isDeleted"); + if (el_deleted == NULL) { + return false; + } + + if (LDB_FLAG_MOD_TYPE(el_deleted->flags) != LDB_FLAG_MOD_DELETE) { + return false; + } + + *pel_dn = el_dn; + return true; +} + +/** + * Local rename implementation based on dsdb_module_rename() + * so we could fine tune it and add more controls + */ +static int tr_prepare_rename(struct tr_context *ac, + const struct ldb_message_element *new_dn) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + int ret; + + ac->rename_dn = ldb_dn_from_ldb_val(ac, ldb, &new_dn->values[0]); + if (ac->rename_dn == NULL) { + return ldb_module_oom(ac->module); + } + + ac->rename_res = talloc_zero(ac, struct ldb_result); + if (ac->rename_res == NULL) { + return ldb_module_oom(ac->module); + } + + ret = ldb_build_rename_req(&ac->rename_req, ldb, ac, + ac->req_msg->dn, + ac->rename_dn, + NULL, + ac->rename_res, + ldb_modify_default_callback, + ac->req); + LDB_REQ_SET_LOCATION(ac->rename_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ret; +} + +/** + * Local rename implementation based on dsdb_module_modify() + * so we could fine tune it and add more controls + */ +static int tr_do_down_req(struct tr_context *ac, struct ldb_request *down_req) +{ + int ret; + + /* We need this since object is 'delete' atm */ + ret = ldb_request_add_control(down_req, + LDB_CONTROL_SHOW_DELETED_OID, + false, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* mark request as part of Tombstone reanimation */ + ret = ldb_request_add_control(down_req, + DSDB_CONTROL_RESTORE_TOMBSTONE_OID, + false, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* Run request from Next module */ + ret = ldb_next_request(ac->module, down_req); + if (ret == LDB_SUCCESS) { + ret = ldb_wait(down_req->handle, LDB_WAIT_ALL); + } + + return ret; +} + +static int tr_prepare_attributes(struct tr_context *ac) +{ + struct ldb_context *ldb = ldb_module_get_ctx(ac->module); + int ret; + struct ldb_message_element *el = NULL; + uint32_t account_type, user_account_control; + struct ldb_dn *objectcategory = NULL; + + ac->mod_msg = ldb_msg_copy_shallow(ac, ac->req_msg); + if (ac->mod_msg == NULL) { + return ldb_oom(ldb); + } + + ac->mod_res = talloc_zero(ac, struct ldb_result); + if (ac->mod_res == NULL) { + return ldb_oom(ldb); + } + + ret = ldb_build_mod_req(&ac->mod_req, ldb, ac, + ac->mod_msg, + NULL, + ac->mod_res, + ldb_modify_default_callback, + ac->req); + LDB_REQ_SET_LOCATION(ac->mod_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* - remove distinguishedName - we don't need it */ + ldb_msg_remove_attr(ac->mod_msg, "distinguishedName"); + + /* remove isRecycled */ + ret = ldb_msg_add_empty(ac->mod_msg, "isRecycled", + LDB_FLAG_MOD_DELETE, NULL); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Failed to reset isRecycled attribute: %s", ldb_strerror(ret)); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* objectClass is USER */ + if (samdb_find_attribute(ldb, ac->search_msg, "objectclass", "user") != NULL) { + uint32_t primary_group_rid; + /* restoring 'user' instance attribute is heavily borrowed from samldb.c */ + + /* Default values */ + ret = dsdb_user_obj_set_defaults(ldb, ac->mod_msg, ac->mod_req); + if (ret != LDB_SUCCESS) return ret; + + /* "userAccountControl" must exists on deleted object */ + user_account_control = ldb_msg_find_attr_as_uint(ac->search_msg, + "userAccountControl", + (uint32_t)-1); + if (user_account_control == (uint32_t)-1) { + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "reanimate: No 'userAccountControl' attribute found!"); + } + + /* restore "sAMAccountType" */ + ret = dsdb_user_obj_set_account_type(ldb, ac->mod_msg, + user_account_control, NULL); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* "userAccountControl" -> "primaryGroupID" mapping */ + ret = dsdb_user_obj_set_primary_group_id(ldb, ac->mod_msg, + user_account_control, + &primary_group_rid); + if (ret != LDB_SUCCESS) { + return ret; + } + /* + * Older AD deployments don't know about the + * RODC group + */ + if (primary_group_rid == DOMAIN_RID_READONLY_DCS) { + /* TODO: check group exists */ + } + + } + + /* objectClass is GROUP */ + if (samdb_find_attribute(ldb, ac->search_msg, "objectclass", "group") != NULL) { + /* "groupType" -> "sAMAccountType" */ + uint32_t group_type; + + el = ldb_msg_find_element(ac->search_msg, "groupType"); + if (el == NULL) { + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "reanimate: Unexpected: missing groupType attribute."); + } + + group_type = ldb_msg_find_attr_as_uint(ac->search_msg, + "groupType", 0); + + account_type = ds_gtype2atype(group_type); + if (account_type == 0) { + return ldb_error(ldb, LDB_ERR_UNWILLING_TO_PERFORM, + "reanimate: Unrecognized account type!"); + } + ret = samdb_msg_append_uint(ldb, ac->mod_msg, ac->mod_msg, + "sAMAccountType", account_type, + LDB_FLAG_MOD_REPLACE); + if (ret != LDB_SUCCESS) { + return ldb_error(ldb, LDB_ERR_OPERATIONS_ERROR, + "reanimate: Failed to add sAMAccountType to restored object."); + } + + /* Default values set by Windows */ + ret = samdb_find_or_add_attribute(ldb, ac->mod_msg, + "adminCount", "0"); + if (ret != LDB_SUCCESS) return ret; + ret = samdb_find_or_add_attribute(ldb, ac->mod_msg, + "operatorCount", "0"); + if (ret != LDB_SUCCESS) return ret; + } + + /* - restore objectCategory if not present */ + objectcategory = ldb_msg_find_attr_as_dn(ldb, ac, ac->search_msg, + "objectCategory"); + if (objectcategory == NULL) { + const char *value; + + ret = dsdb_make_object_category(ldb, ac->schema, ac->search_msg, + ac->mod_msg, &value); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_msg_append_string(ac->mod_msg, "objectCategory", value, + LDB_FLAG_MOD_ADD); + if (ret != LDB_SUCCESS) { + return ret; + } + } + + return LDB_SUCCESS; +} + +/** + * Handle special LDAP modify request to restore deleted objects + */ +static int tombstone_reanimate_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + const struct ldb_message_element *el_dn = NULL; + struct tr_context *ac = NULL; + int ret; + + ldb_debug(ldb, LDB_DEBUG_TRACE, "%s\n", __PRETTY_FUNCTION__); + + /* do not manipulate our control entries */ + if (ldb_dn_is_special(req->op.mod.message->dn)) { + return ldb_next_request(module, req); + } + + /* Check if this is a reanimate request */ + if (!is_tombstone_reanimate_request(req, &el_dn)) { + return ldb_next_request(module, req); + } + + ac = tr_init_context(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + /* Load original object */ + ret = dsdb_module_search_dn(module, ac, &ac->search_res, + ac->req_msg->dn, NULL, + DSDB_FLAG_TOP_MODULE | + DSDB_SEARCH_SHOW_DELETED, + req); + if (ret != LDB_SUCCESS) { + return ldb_operr(ldb); + } + ac->search_msg = ac->search_res->msgs[0]; + + /* check if it a Deleted Object */ + if (!ldb_msg_find_attr_as_bool(ac->search_msg, "isDeleted", false)) { + return ldb_error(ldb, LDB_ERR_UNWILLING_TO_PERFORM, "Trying to restore not deleted object\n"); + } + + /* Simple implementation */ + + /* prepare attributed depending on objectClass */ + ret = tr_prepare_attributes(ac); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* Rename request to modify distinguishedName */ + ret = tr_prepare_rename(ac, el_dn); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* restore attributed depending on objectClass */ + ret = tr_do_down_req(ac, ac->mod_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + /* Rename request to modify distinguishedName */ + ret = tr_do_down_req(ac, ac->rename_req); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_ERROR, "Renaming object to %s has failed with %s\n", el_dn->values[0].data, ldb_strerror(ret)); + if (ret != LDB_ERR_ENTRY_ALREADY_EXISTS && ret != LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS ) { + /* Windows returns Operations Error in case we can't rename the object */ + return LDB_ERR_OPERATIONS_ERROR; + } + return ret; + } + + return ldb_module_done(ac->req, NULL, NULL, LDB_SUCCESS); +} + + +static const struct ldb_module_ops ldb_reanimate_module_ops = { + .name = "tombstone_reanimate", + .modify = tombstone_reanimate_modify, +}; + +int ldb_tombstone_reanimate_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_reanimate_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/unique_object_sids.c b/source4/dsdb/samdb/ldb_modules/unique_object_sids.c new file mode 100644 index 0000000..f8427bc --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/unique_object_sids.c @@ -0,0 +1,262 @@ +/* + ldb database module to enforce unique local objectSIDs + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017 + + 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/>. +*/ + +/* + + Duplicate ObjectSIDs are possible on foreign security principals and + replication conflict records. However a duplicate objectSID within + the local domainSID is an error. + + As the uniqueness requirement depends on the source domain it is not possible + to enforce this with a unique index. + + This module sets the LDB_FLAG_FORCE_UNIQUE_INDEX for objectSIDs in the + local domain. +*/ + +#include "includes.h" +#include "ldb_module.h" +#include "dsdb/samdb/samdb.h" +#include "libcli/security/dom_sid.h" +#include "dsdb/samdb/ldb_modules/util.h" + +struct private_data { + const struct dom_sid *domain_sid; +}; + + +/* + * Does the add request contain a local objectSID + */ +static bool message_contains_local_objectSID( + struct ldb_module *module, + const struct ldb_message *msg) +{ + struct dom_sid *objectSID = NULL; + + struct private_data *data = + talloc_get_type( + ldb_module_get_private(module), + struct private_data); + + TALLOC_CTX *frame = talloc_stackframe(); + + objectSID = samdb_result_dom_sid(frame, msg, "objectSID"); + if (objectSID == NULL) { + TALLOC_FREE(frame); + return false; + } + + /* + * data->domain_sid can be NULL but dom_sid_in_domain handles this + * case correctly. See unique_object_sids_init for more details. + */ + if (!dom_sid_in_domain(data->domain_sid, objectSID)) { + TALLOC_FREE(frame); + return false; + } + TALLOC_FREE(frame); + return true; +} + +static int flag_objectSID( + struct ldb_module *module, + struct ldb_request *req, + const struct ldb_message *msg, + struct ldb_message **new_msg) +{ + struct ldb_message_element *el = NULL; + + *new_msg = ldb_msg_copy_shallow(req, msg); + if (!*new_msg) { + return ldb_module_oom(module); + } + + el = ldb_msg_find_element(*new_msg, "objectSID"); + if (el == NULL) { + struct ldb_context *ldb = NULL; + ldb = ldb_module_get_ctx(module); + ldb_asprintf_errstring( + ldb, + "Unable to locate objectSID in copied request\n"); + return LDB_ERR_OPERATIONS_ERROR; + } + el->flags |= LDB_FLAG_INTERNAL_FORCE_UNIQUE_INDEX; + return LDB_SUCCESS; +} + +/* add */ +static int unique_object_sids_add( + struct ldb_module *module, + struct ldb_request *req) +{ + const struct ldb_message *msg = req->op.add.message; + struct ldb_message *new_msg = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + int rc; + + if (!message_contains_local_objectSID(module, msg)) { + /* + * Request does not contain a local objectSID so chain the + * next module + */ + return ldb_next_request(module, req); + } + + /* + * The add request contains an objectSID for the local domain + */ + + rc = flag_objectSID(module, req, msg, &new_msg); + if (rc != LDB_SUCCESS) { + return rc; + } + + ldb = ldb_module_get_ctx(module); + rc = ldb_build_add_req( + &new_req, + ldb, + req, + new_msg, + req->controls, + req, + dsdb_next_callback, + req); + if (rc != LDB_SUCCESS) { + return rc; + } + + return ldb_next_request(module, new_req); +} + +/* modify */ +static int unique_object_sids_modify( + struct ldb_module *module, + struct ldb_request *req) +{ + + const struct ldb_message *msg = req->op.mod.message; + struct ldb_message *new_msg = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + int rc; + + if (!message_contains_local_objectSID(module, msg)) { + /* + * Request does not contain a local objectSID so chain the + * next module + */ + return ldb_next_request(module, req); + } + + ldb = ldb_module_get_ctx(module); + + /* + * If DSDB_CONTROL_REPLICATED_UPDATE_OID replicated is set we know + * that the modify request is well formed and objectSID only appears + * once. + * + * Enforcing this assumption simplifies the subsequent code. + * + */ + if(!ldb_request_get_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID)) { + ldb_asprintf_errstring( + ldb, + "Modify of %s rejected, " + "as it is modifying an objectSID\n", + ldb_dn_get_linearized(msg->dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + + rc = flag_objectSID(module, req, msg, &new_msg); + if (rc != LDB_SUCCESS) { + return rc; + } + + ldb = ldb_module_get_ctx(module); + rc = ldb_build_mod_req( + &new_req, + ldb, + req, + new_msg, + req->controls, + req, + dsdb_next_callback, + req); + if (rc != LDB_SUCCESS) { + return rc; + } + + return ldb_next_request(module, new_req); +} + +/* init */ +static int unique_object_sids_init( + struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct private_data *data = NULL; + int ret; + + ret = ldb_next_init(module); + + if (ret != LDB_SUCCESS) { + return ret; + } + + data = talloc_zero(module, struct private_data); + if (!data) { + return ldb_module_oom(module); + } + + data->domain_sid = samdb_domain_sid(ldb); + if (data->domain_sid == NULL) { + /* + * Unable to determine the domainSID, this normally occurs + * when provisioning. As there is no easy way to detect + * that we are provisioning. We currently just log this as a + * warning. + */ + ldb_debug( + ldb, + LDB_DEBUG_WARNING, + "Unable to determine the DomainSID, " + "can not enforce uniqueness constraint on local " + "domainSIDs\n"); + } + + ldb_module_set_private(module, data); + + return LDB_SUCCESS; +} + +static const struct ldb_module_ops ldb_unique_object_sids_module_ops = { + .name = "unique_object_sids", + .init_context = unique_object_sids_init, + .add = unique_object_sids_add, + .modify = unique_object_sids_modify, +}; + +int ldb_unique_object_sids_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_unique_object_sids_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/update_keytab.c b/source4/dsdb/samdb/ldb_modules/update_keytab.c new file mode 100644 index 0000000..780eb81 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/update_keytab.c @@ -0,0 +1,510 @@ +/* + ldb database library + + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2007 + + 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 update_keytabs module + * + * Description: Update keytabs whenever their matching secret record changes + * + * Author: Andrew Bartlett + */ + +#include "includes.h" +#include "ldb_module.h" +#include "lib/util/dlinklist.h" +#include "auth/credentials/credentials.h" +#include "auth/credentials/credentials_krb5.h" +#include "system/kerberos.h" +#include "auth/kerberos/kerberos.h" +#include "auth/kerberos/kerberos_srv_keytab.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "param/secrets.h" + +struct dn_list { + struct ldb_message *msg; + bool do_delete; + struct dn_list *prev, *next; +}; + +struct update_kt_private { + struct dn_list *changed_dns; +}; + +struct update_kt_ctx { + struct ldb_module *module; + struct ldb_request *req; + + struct ldb_dn *dn; + bool do_delete; + + struct ldb_reply *op_reply; + bool found; +}; + +static struct update_kt_ctx *update_kt_ctx_init(struct ldb_module *module, + struct ldb_request *req) +{ + struct update_kt_ctx *ac; + + ac = talloc_zero(req, struct update_kt_ctx); + if (ac == NULL) { + ldb_oom(ldb_module_get_ctx(module)); + return NULL; + } + + ac->module = module; + ac->req = req; + + return ac; +} + +/* FIXME: too many semi-async searches here for my taste, direct and indirect as + * cli_credentials_set_secrets() performs a sync ldb search. + * Just hope we are lucky and nothing breaks (using the tdb backend masks a lot + * of async issues). -SSS + */ +static int add_modified(struct ldb_module *module, struct ldb_dn *dn, bool do_delete, + struct ldb_request *parent) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct update_kt_private *data = talloc_get_type(ldb_module_get_private(module), struct update_kt_private); + struct dn_list *item; + char *filter; + struct ldb_result *res; + int ret; + + filter = talloc_asprintf(data, + "(&(objectClass=kerberosSecret)(privateKeytab=*))"); + if (!filter) { + return ldb_oom(ldb); + } + + ret = dsdb_module_search(module, data, &res, + dn, LDB_SCOPE_BASE, NULL, + DSDB_FLAG_NEXT_MODULE, parent, + "%s", filter); + talloc_free(filter); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (res->count != 1) { + /* if it's not a kerberosSecret then we don't have anything to update */ + talloc_free(res); + return LDB_SUCCESS; + } + + item = talloc(data->changed_dns? (void *)data->changed_dns: (void *)data, struct dn_list); + if (!item) { + talloc_free(res); + return ldb_oom(ldb); + } + + item->msg = talloc_steal(item, res->msgs[0]); + item->do_delete = do_delete; + talloc_free(res); + + DLIST_ADD_END(data->changed_dns, item); + return LDB_SUCCESS; +} + +static int ukt_search_modified(struct update_kt_ctx *ac); + +static int update_kt_op_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct ldb_context *ldb; + struct update_kt_ctx *ac; + int ret; + + ac = talloc_get_type(req->context, struct update_kt_ctx); + ldb = ldb_module_get_ctx(ac->module); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + if (ares->type != LDB_REPLY_DONE) { + ldb_set_errstring(ldb, "Invalid request type!\n"); + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + if (ac->do_delete) { + return ldb_module_done(ac->req, ares->controls, + ares->response, LDB_SUCCESS); + } + + ac->op_reply = talloc_steal(ac, ares); + + ret = ukt_search_modified(ac); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + + return LDB_SUCCESS; +} + +static int ukt_del_op(struct update_kt_ctx *ac) +{ + struct ldb_context *ldb; + struct ldb_request *down_req; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + ret = ldb_build_del_req(&down_req, ldb, ac, + ac->dn, + ac->req->controls, + ac, update_kt_op_callback, + ac->req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(ac->module, down_req); +} + +static int ukt_search_modified_callback(struct ldb_request *req, + struct ldb_reply *ares) +{ + struct update_kt_ctx *ac; + int ret; + + ac = talloc_get_type(req->context, struct update_kt_ctx); + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + + ac->found = true; + break; + + case LDB_REPLY_REFERRAL: + /* ignore */ + break; + + case LDB_REPLY_DONE: + + if (ac->found) { + /* do the dirty sync job here :/ */ + ret = add_modified(ac->module, ac->dn, ac->do_delete, ac->req); + } + + if (ac->do_delete) { + ret = ukt_del_op(ac); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, + NULL, NULL, ret); + } + break; + } + + return ldb_module_done(ac->req, ac->op_reply->controls, + ac->op_reply->response, LDB_SUCCESS); + } + + talloc_free(ares); + return LDB_SUCCESS; +} + +static int ukt_search_modified(struct update_kt_ctx *ac) +{ + struct ldb_context *ldb; + static const char * const no_attrs[] = { NULL }; + struct ldb_request *search_req; + int ret; + + ldb = ldb_module_get_ctx(ac->module); + + ret = ldb_build_search_req(&search_req, ldb, ac, + ac->dn, LDB_SCOPE_BASE, + "(&(objectClass=kerberosSecret)" + "(privateKeytab=*))", no_attrs, + NULL, + ac, ukt_search_modified_callback, + ac->req); + LDB_REQ_SET_LOCATION(search_req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(ac->module, search_req); +} + + +/* add */ +static int update_kt_add(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct update_kt_ctx *ac; + struct ldb_request *down_req; + int ret; + + ldb = ldb_module_get_ctx(module); + + ac = update_kt_ctx_init(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + ac->dn = req->op.add.message->dn; + + ret = ldb_build_add_req(&down_req, ldb, ac, + req->op.add.message, + req->controls, + ac, update_kt_op_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, down_req); +} + +/* modify */ +static int update_kt_modify(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct update_kt_ctx *ac; + struct ldb_request *down_req; + int ret; + + ldb = ldb_module_get_ctx(module); + + ac = update_kt_ctx_init(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + ac->dn = req->op.mod.message->dn; + + ret = ldb_build_mod_req(&down_req, ldb, ac, + req->op.mod.message, + req->controls, + ac, update_kt_op_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, down_req); +} + +/* delete */ +static int update_kt_delete(struct ldb_module *module, struct ldb_request *req) +{ + struct update_kt_ctx *ac; + + ac = update_kt_ctx_init(module, req); + if (ac == NULL) { + return ldb_operr(ldb_module_get_ctx(module)); + } + + ac->dn = req->op.del.dn; + ac->do_delete = true; + + return ukt_search_modified(ac); +} + +/* rename */ +static int update_kt_rename(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct update_kt_ctx *ac; + struct ldb_request *down_req; + int ret; + + ldb = ldb_module_get_ctx(module); + + ac = update_kt_ctx_init(module, req); + if (ac == NULL) { + return ldb_operr(ldb); + } + + ac->dn = req->op.rename.newdn; + + ret = ldb_build_rename_req(&down_req, ldb, ac, + req->op.rename.olddn, + req->op.rename.newdn, + req->controls, + ac, update_kt_op_callback, + req); + LDB_REQ_SET_LOCATION(down_req); + if (ret != LDB_SUCCESS) { + return ret; + } + + return ldb_next_request(module, down_req); +} + +/* prepare for a commit */ +static int update_kt_prepare_commit(struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct update_kt_private *data = talloc_get_type(ldb_module_get_private(module), struct update_kt_private); + struct dn_list *p; + struct smb_krb5_context *smb_krb5_context; + int krb5_ret = smb_krb5_init_context(data, + ldb_get_opaque(ldb, "loadparm"), + &smb_krb5_context); + TALLOC_CTX *tmp_ctx = NULL; + + if (krb5_ret != 0) { + ldb_asprintf_errstring(ldb, "Failed to setup krb5_context: %s", error_message(krb5_ret)); + goto fail; + } + + tmp_ctx = talloc_new(data); + if (!tmp_ctx) { + ldb_oom(ldb); + goto fail; + } + + for (p=data->changed_dns; p; p = p->next) { + const char *error_string; + const char *realm; + char *upper_realm; + struct ldb_message_element *spn_el = ldb_msg_find_element(p->msg, "servicePrincipalName"); + const char **SPNs = NULL; + int num_SPNs = 0; + int i; + + realm = ldb_msg_find_attr_as_string(p->msg, "realm", NULL); + + if (spn_el) { + upper_realm = strupper_talloc(tmp_ctx, realm); + if (!upper_realm) { + ldb_oom(ldb); + goto fail; + } + + num_SPNs = spn_el->num_values; + SPNs = talloc_array(tmp_ctx, const char *, num_SPNs); + if (!SPNs) { + ldb_oom(ldb); + goto fail; + } + for (i = 0; i < num_SPNs; i++) { + SPNs[i] = talloc_asprintf(SPNs, "%*.*s@%s", + (int)spn_el->values[i].length, + (int)spn_el->values[i].length, + (const char *)spn_el->values[i].data, + upper_realm); + if (!SPNs[i]) { + ldb_oom(ldb); + goto fail; + } + } + } + + krb5_ret = smb_krb5_update_keytab(tmp_ctx, smb_krb5_context->krb5_context, + keytab_name_from_msg(tmp_ctx, ldb, p->msg), + ldb_msg_find_attr_as_string(p->msg, "samAccountName", NULL), + realm, SPNs, num_SPNs, + ldb_msg_find_attr_as_string(p->msg, "saltPrincipal", NULL), + ldb_msg_find_attr_as_string(p->msg, "secret", NULL), + ldb_msg_find_attr_as_string(p->msg, "priorSecret", NULL), + ldb_msg_find_attr_as_int(p->msg, "msDS-KeyVersionNumber", 0), + (uint32_t)ldb_msg_find_attr_as_int(p->msg, "msDS-SupportedEncryptionTypes", ENC_ALL_TYPES), + p->do_delete, NULL, &error_string); + if (krb5_ret != 0) { + ldb_asprintf_errstring(ldb, "Failed to update keytab from entry %s in %s: %s", + ldb_dn_get_linearized(p->msg->dn), + (const char *)ldb_get_opaque(ldb, "ldb_url"), + error_string); + goto fail; + } + } + + talloc_free(data->changed_dns); + data->changed_dns = NULL; + talloc_free(tmp_ctx); + + return ldb_next_prepare_commit(module); + +fail: + talloc_free(data->changed_dns); + data->changed_dns = NULL; + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; +} + +/* end a transaction */ +static int update_kt_del_trans(struct ldb_module *module) +{ + struct update_kt_private *data = talloc_get_type(ldb_module_get_private(module), struct update_kt_private); + + talloc_free(data->changed_dns); + data->changed_dns = NULL; + + return ldb_next_del_trans(module); +} + +static int update_kt_init(struct ldb_module *module) +{ + struct ldb_context *ldb; + struct update_kt_private *data; + + ldb = ldb_module_get_ctx(module); + + data = talloc(module, struct update_kt_private); + if (data == NULL) { + return ldb_oom(ldb); + } + + data->changed_dns = NULL; + + ldb_module_set_private(module, data); + + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_update_keytab_module_ops = { + .name = "update_keytab", + .init_context = update_kt_init, + .add = update_kt_add, + .modify = update_kt_modify, + .rename = update_kt_rename, + .del = update_kt_delete, + .prepare_commit = update_kt_prepare_commit, + .del_transaction = update_kt_del_trans, +}; + +int ldb_update_keytab_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_update_keytab_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/util.c b/source4/dsdb/samdb/ldb_modules/util.c new file mode 100644 index 0000000..c2949f0 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/util.c @@ -0,0 +1,1890 @@ +/* + Unix SMB/CIFS implementation. + Samba utility functions + + Copyright (C) Andrew Tridgell 2009 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 2009 + Copyright (C) Matthieu Patou <mat@matws.net> 2011 + + 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/>. +*/ + +#include "includes.h" +#include "ldb.h" +#include "ldb_module.h" +#include "librpc/ndr/libndr.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/common/util.h" +#include "libcli/security/security.h" + +#undef strcasecmp + +/* + search for attrs on one DN, in the modules below + */ +int dsdb_module_search_dn(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_result **_res, + struct ldb_dn *basedn, + const char * const *attrs, + uint32_t dsdb_flags, + struct ldb_request *parent) +{ + int ret; + struct ldb_request *req; + TALLOC_CTX *tmp_ctx; + struct ldb_result *res; + + tmp_ctx = talloc_new(mem_ctx); + + res = talloc_zero(tmp_ctx, struct ldb_result); + if (!res) { + talloc_free(tmp_ctx); + return ldb_oom(ldb_module_get_ctx(module)); + } + + ret = ldb_build_search_req(&req, ldb_module_get_ctx(module), tmp_ctx, + basedn, + LDB_SCOPE_BASE, + NULL, + attrs, + NULL, + res, + ldb_search_default_callback, + parent); + LDB_REQ_SET_LOCATION(req); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_request_add_controls(req, dsdb_flags); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + if (dsdb_flags & DSDB_FLAG_TRUSTED) { + ldb_req_mark_trusted(req); + } + + /* Run the new request */ + if (dsdb_flags & DSDB_FLAG_NEXT_MODULE) { + ret = ldb_next_request(module, req); + } else if (dsdb_flags & DSDB_FLAG_TOP_MODULE) { + ret = ldb_request(ldb_module_get_ctx(module), req); + } else { + const struct ldb_module_ops *ops = ldb_module_get_ops(module); + SMB_ASSERT(dsdb_flags & DSDB_FLAG_OWN_MODULE); + ret = ops->search(module, req); + } + if (ret == LDB_SUCCESS) { + ret = ldb_wait(req->handle, LDB_WAIT_ALL); + } + + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + if (res->count != 1) { + /* we may be reading a DB that does not have the 'check base on search' option... */ + ret = LDB_ERR_NO_SUCH_OBJECT; + ldb_asprintf_errstring(ldb_module_get_ctx(module), + "dsdb_module_search_dn: did not find base dn %s (%d results)", + ldb_dn_get_linearized(basedn), res->count); + } else { + *_res = talloc_steal(mem_ctx, res); + } + talloc_free(tmp_ctx); + return ret; +} + +int dsdb_module_search_tree(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_result **_res, + struct ldb_dn *basedn, + enum ldb_scope scope, + struct ldb_parse_tree *tree, + const char * const *attrs, + int dsdb_flags, + struct ldb_request *parent) +{ + int ret; + struct ldb_request *req; + TALLOC_CTX *tmp_ctx; + struct ldb_result *res; + + tmp_ctx = talloc_new(mem_ctx); + + /* cross-partitions searches with a basedn break multi-domain support */ + SMB_ASSERT(basedn == NULL || (dsdb_flags & DSDB_SEARCH_SEARCH_ALL_PARTITIONS) == 0); + + res = talloc_zero(tmp_ctx, struct ldb_result); + if (!res) { + talloc_free(tmp_ctx); + return ldb_oom(ldb_module_get_ctx(module)); + } + + ret = ldb_build_search_req_ex(&req, ldb_module_get_ctx(module), tmp_ctx, + basedn, + scope, + tree, + attrs, + NULL, + res, + ldb_search_default_callback, + parent); + LDB_REQ_SET_LOCATION(req); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_request_add_controls(req, dsdb_flags); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + if (dsdb_flags & DSDB_FLAG_TRUSTED) { + ldb_req_mark_trusted(req); + } + + if (dsdb_flags & DSDB_FLAG_NEXT_MODULE) { + ret = ldb_next_request(module, req); + } else if (dsdb_flags & DSDB_FLAG_TOP_MODULE) { + ret = ldb_request(ldb_module_get_ctx(module), req); + } else { + const struct ldb_module_ops *ops = ldb_module_get_ops(module); + SMB_ASSERT(dsdb_flags & DSDB_FLAG_OWN_MODULE); + ret = ops->search(module, req); + } + if (ret == LDB_SUCCESS) { + ret = ldb_wait(req->handle, LDB_WAIT_ALL); + } + + if (dsdb_flags & DSDB_SEARCH_ONE_ONLY) { + if (res->count == 0) { + talloc_free(tmp_ctx); + return ldb_error(ldb_module_get_ctx(module), LDB_ERR_NO_SUCH_OBJECT, __func__); + } + if (res->count != 1) { + talloc_free(tmp_ctx); + ldb_reset_err_string(ldb_module_get_ctx(module)); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + } + + talloc_free(req); + if (ret == LDB_SUCCESS) { + *_res = talloc_steal(mem_ctx, res); + } + talloc_free(tmp_ctx); + return ret; +} + +/* + search for attrs in the modules below + */ +int dsdb_module_search(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_result **_res, + struct ldb_dn *basedn, enum ldb_scope scope, + const char * const *attrs, + int dsdb_flags, + struct ldb_request *parent, + const char *format, ...) _PRINTF_ATTRIBUTE(9, 10) +{ + int ret; + TALLOC_CTX *tmp_ctx; + va_list ap; + char *expression; + struct ldb_parse_tree *tree; + + /* cross-partitions searches with a basedn break multi-domain support */ + SMB_ASSERT(basedn == NULL || (dsdb_flags & DSDB_SEARCH_SEARCH_ALL_PARTITIONS) == 0); + + tmp_ctx = talloc_new(mem_ctx); + + if (format) { + va_start(ap, format); + expression = talloc_vasprintf(tmp_ctx, format, ap); + va_end(ap); + + if (!expression) { + talloc_free(tmp_ctx); + return ldb_oom(ldb_module_get_ctx(module)); + } + } else { + expression = NULL; + } + + tree = ldb_parse_tree(tmp_ctx, expression); + if (tree == NULL) { + talloc_free(tmp_ctx); + ldb_set_errstring(ldb_module_get_ctx(module), + "Unable to parse search expression"); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = dsdb_module_search_tree(module, + mem_ctx, + _res, + basedn, + scope, + tree, + attrs, + dsdb_flags, + parent); + + talloc_free(tmp_ctx); + return ret; +} + +/* + find a DN given a GUID. This searches across all partitions + */ +int dsdb_module_dn_by_guid(struct ldb_module *module, TALLOC_CTX *mem_ctx, + const struct GUID *guid, struct ldb_dn **dn, + struct ldb_request *parent) +{ + struct ldb_result *res; + const char *attrs[] = { NULL }; + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + int ret; + + ret = dsdb_module_search(module, tmp_ctx, &res, NULL, LDB_SCOPE_SUBTREE, + attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_SEARCH_ALL_PARTITIONS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + parent, + "objectGUID=%s", GUID_string(tmp_ctx, guid)); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + if (res->count == 0) { + talloc_free(tmp_ctx); + return ldb_error(ldb_module_get_ctx(module), LDB_ERR_NO_SUCH_OBJECT, __func__); + } + if (res->count != 1) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), "More than one object found matching objectGUID %s\n", + GUID_string(tmp_ctx, guid)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + *dn = talloc_steal(mem_ctx, res->msgs[0]->dn); + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + +/* + find a GUID given a DN. + */ +int dsdb_module_guid_by_dn(struct ldb_module *module, struct ldb_dn *dn, struct GUID *guid, + struct ldb_request *parent) +{ + const char *attrs[] = { NULL }; + struct ldb_result *res; + TALLOC_CTX *tmp_ctx = talloc_new(module); + int ret; + NTSTATUS status; + + ret = dsdb_module_search_dn(module, tmp_ctx, &res, dn, attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_SHOW_RECYCLED | + DSDB_SEARCH_SHOW_EXTENDED_DN, + parent); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb_module_get_ctx(module), "Failed to find GUID for %s", + ldb_dn_get_linearized(dn)); + talloc_free(tmp_ctx); + return ret; + } + + status = dsdb_get_extended_dn_guid(res->msgs[0]->dn, guid, "GUID"); + if (!NT_STATUS_IS_OK(status)) { + talloc_free(tmp_ctx); + return ldb_operr(ldb_module_get_ctx(module)); + } + + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + + +/* + a ldb_extended request operating on modules below the + current module + + Note that this does not automatically start a transaction. If you + need a transaction the caller needs to start it as needed. + */ +int dsdb_module_extended(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_result **_res, + const char* oid, void* data, + uint32_t dsdb_flags, + struct ldb_request *parent) +{ + struct ldb_request *req; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + TALLOC_CTX *tmp_ctx = talloc_new(module); + struct ldb_result *res; + + if (_res != NULL) { + (*_res) = NULL; + } + + res = talloc_zero(tmp_ctx, struct ldb_result); + if (!res) { + talloc_free(tmp_ctx); + return ldb_oom(ldb_module_get_ctx(module)); + } + + ret = ldb_build_extended_req(&req, ldb, + tmp_ctx, + oid, + data, + NULL, + res, ldb_extended_default_callback, + parent); + + LDB_REQ_SET_LOCATION(req); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_request_add_controls(req, dsdb_flags); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + if (dsdb_flags & DSDB_FLAG_TRUSTED) { + ldb_req_mark_trusted(req); + } + + /* Run the new request */ + if (dsdb_flags & DSDB_FLAG_NEXT_MODULE) { + ret = ldb_next_request(module, req); + } else if (dsdb_flags & DSDB_FLAG_TOP_MODULE) { + ret = ldb_request(ldb_module_get_ctx(module), req); + } else { + const struct ldb_module_ops *ops = ldb_module_get_ops(module); + SMB_ASSERT(dsdb_flags & DSDB_FLAG_OWN_MODULE); + ret = ops->extended(module, req); + } + if (ret == LDB_SUCCESS) { + ret = ldb_wait(req->handle, LDB_WAIT_ALL); + } + + if (_res != NULL && ret == LDB_SUCCESS) { + (*_res) = talloc_steal(mem_ctx, res); + } + + talloc_free(tmp_ctx); + return ret; +} + + +/* + a ldb_modify request operating on modules below the + current module + */ +int dsdb_module_modify(struct ldb_module *module, + const struct ldb_message *message, + uint32_t dsdb_flags, + struct ldb_request *parent) +{ + struct ldb_request *mod_req; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + TALLOC_CTX *tmp_ctx = talloc_new(module); + struct ldb_result *res; + + res = talloc_zero(tmp_ctx, struct ldb_result); + if (!res) { + talloc_free(tmp_ctx); + return ldb_oom(ldb_module_get_ctx(module)); + } + + ret = ldb_build_mod_req(&mod_req, ldb, tmp_ctx, + message, + NULL, + res, + ldb_modify_default_callback, + parent); + LDB_REQ_SET_LOCATION(mod_req); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_request_add_controls(mod_req, dsdb_flags); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + if (dsdb_flags & DSDB_FLAG_TRUSTED) { + ldb_req_mark_trusted(mod_req); + } + + /* Run the new request */ + if (dsdb_flags & DSDB_FLAG_NEXT_MODULE) { + ret = ldb_next_request(module, mod_req); + } else if (dsdb_flags & DSDB_FLAG_TOP_MODULE) { + ret = ldb_request(ldb_module_get_ctx(module), mod_req); + } else { + const struct ldb_module_ops *ops = ldb_module_get_ops(module); + SMB_ASSERT(dsdb_flags & DSDB_FLAG_OWN_MODULE); + ret = ops->modify(module, mod_req); + } + if (ret == LDB_SUCCESS) { + ret = ldb_wait(mod_req->handle, LDB_WAIT_ALL); + } + + talloc_free(tmp_ctx); + return ret; +} + + + +/* + a ldb_rename request operating on modules below the + current module + */ +int dsdb_module_rename(struct ldb_module *module, + struct ldb_dn *olddn, struct ldb_dn *newdn, + uint32_t dsdb_flags, + struct ldb_request *parent) +{ + struct ldb_request *req; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + TALLOC_CTX *tmp_ctx = talloc_new(module); + struct ldb_result *res; + + res = talloc_zero(tmp_ctx, struct ldb_result); + if (!res) { + talloc_free(tmp_ctx); + return ldb_oom(ldb_module_get_ctx(module)); + } + + ret = ldb_build_rename_req(&req, ldb, tmp_ctx, + olddn, + newdn, + NULL, + res, + ldb_modify_default_callback, + parent); + LDB_REQ_SET_LOCATION(req); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_request_add_controls(req, dsdb_flags); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + if (dsdb_flags & DSDB_FLAG_TRUSTED) { + ldb_req_mark_trusted(req); + } + + /* Run the new request */ + if (dsdb_flags & DSDB_FLAG_NEXT_MODULE) { + ret = ldb_next_request(module, req); + } else if (dsdb_flags & DSDB_FLAG_TOP_MODULE) { + ret = ldb_request(ldb_module_get_ctx(module), req); + } else { + const struct ldb_module_ops *ops = ldb_module_get_ops(module); + SMB_ASSERT(dsdb_flags & DSDB_FLAG_OWN_MODULE); + ret = ops->rename(module, req); + } + if (ret == LDB_SUCCESS) { + ret = ldb_wait(req->handle, LDB_WAIT_ALL); + } + + talloc_free(tmp_ctx); + return ret; +} + +/* + a ldb_add request operating on modules below the + current module + */ +int dsdb_module_add(struct ldb_module *module, + const struct ldb_message *message, + uint32_t dsdb_flags, + struct ldb_request *parent) +{ + struct ldb_request *req; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + TALLOC_CTX *tmp_ctx = talloc_new(module); + struct ldb_result *res; + + res = talloc_zero(tmp_ctx, struct ldb_result); + if (!res) { + talloc_free(tmp_ctx); + return ldb_oom(ldb_module_get_ctx(module)); + } + + ret = ldb_build_add_req(&req, ldb, tmp_ctx, + message, + NULL, + res, + ldb_modify_default_callback, + parent); + LDB_REQ_SET_LOCATION(req); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_request_add_controls(req, dsdb_flags); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + if (dsdb_flags & DSDB_FLAG_TRUSTED) { + ldb_req_mark_trusted(req); + } + + /* Run the new request */ + if (dsdb_flags & DSDB_FLAG_NEXT_MODULE) { + ret = ldb_next_request(module, req); + } else if (dsdb_flags & DSDB_FLAG_TOP_MODULE) { + ret = ldb_request(ldb_module_get_ctx(module), req); + } else { + const struct ldb_module_ops *ops = ldb_module_get_ops(module); + SMB_ASSERT(dsdb_flags & DSDB_FLAG_OWN_MODULE); + ret = ops->add(module, req); + } + if (ret == LDB_SUCCESS) { + ret = ldb_wait(req->handle, LDB_WAIT_ALL); + } + + talloc_free(tmp_ctx); + return ret; +} + +/* + a ldb_delete request operating on modules below the + current module + */ +int dsdb_module_del(struct ldb_module *module, + struct ldb_dn *dn, + uint32_t dsdb_flags, + struct ldb_request *parent) +{ + struct ldb_request *req; + int ret; + struct ldb_context *ldb = ldb_module_get_ctx(module); + TALLOC_CTX *tmp_ctx = talloc_new(module); + struct ldb_result *res; + + res = talloc_zero(tmp_ctx, struct ldb_result); + if (!res) { + talloc_free(tmp_ctx); + return ldb_oom(ldb); + } + + ret = ldb_build_del_req(&req, ldb, tmp_ctx, + dn, + NULL, + res, + ldb_modify_default_callback, + parent); + LDB_REQ_SET_LOCATION(req); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + ret = dsdb_request_add_controls(req, dsdb_flags); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + if (dsdb_flags & DSDB_FLAG_TRUSTED) { + ldb_req_mark_trusted(req); + } + + /* Run the new request */ + if (dsdb_flags & DSDB_FLAG_NEXT_MODULE) { + ret = ldb_next_request(module, req); + } else if (dsdb_flags & DSDB_FLAG_TOP_MODULE) { + ret = ldb_request(ldb_module_get_ctx(module), req); + } else { + const struct ldb_module_ops *ops = ldb_module_get_ops(module); + SMB_ASSERT(dsdb_flags & DSDB_FLAG_OWN_MODULE); + ret = ops->del(module, req); + } + if (ret == LDB_SUCCESS) { + ret = ldb_wait(req->handle, LDB_WAIT_ALL); + } + + talloc_free(tmp_ctx); + return ret; +} + +/* + check if a single valued link has multiple non-deleted values + + This is needed when we will be using the RELAX control to stop + ldb_tdb from checking single valued links + */ +int dsdb_check_single_valued_link(const struct dsdb_attribute *attr, + const struct ldb_message_element *el) +{ + bool found_active = false; + unsigned int i; + + if (!(attr->ldb_schema_attribute->flags & LDB_ATTR_FLAG_SINGLE_VALUE) || + el->num_values < 2) { + return LDB_SUCCESS; + } + + for (i=0; i<el->num_values; i++) { + if (!dsdb_dn_is_deleted_val(&el->values[i])) { + if (found_active) { + return LDB_ERR_ATTRIBUTE_OR_VALUE_EXISTS; + } + found_active = true; + } + } + + return LDB_SUCCESS; +} + + +int dsdb_check_samba_compatible_feature(struct ldb_module *module, + const char *feature, + bool *found) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_result *res; + static const char *samba_dsdb_attrs[] = { + SAMBA_COMPATIBLE_FEATURES_ATTR, + NULL + }; + int ret; + struct ldb_dn *samba_dsdb_dn = NULL; + TALLOC_CTX *tmp_ctx = talloc_new(ldb); + if (tmp_ctx == NULL) { + *found = false; + return ldb_oom(ldb); + } + *found = false; + + samba_dsdb_dn = ldb_dn_new(tmp_ctx, ldb, "@SAMBA_DSDB"); + if (samba_dsdb_dn == NULL) { + TALLOC_FREE(tmp_ctx); + return ldb_oom(ldb); + } + + ret = dsdb_module_search_dn(module, + tmp_ctx, + &res, + samba_dsdb_dn, + samba_dsdb_attrs, + DSDB_FLAG_NEXT_MODULE, + NULL); + if (ret == LDB_SUCCESS) { + *found = ldb_msg_check_string_attribute( + res->msgs[0], + SAMBA_COMPATIBLE_FEATURES_ATTR, + feature); + } else if (ret == LDB_ERR_NO_SUCH_OBJECT) { + /* it is not an error not to find it */ + ret = LDB_SUCCESS; + } + TALLOC_FREE(tmp_ctx); + return ret; +} + + +/* + check if an optional feature is enabled on our own NTDS DN + + Note that features can be marked as enabled in more than one + place. For example, the recyclebin feature is marked as enabled both + on the CN=Partitions,CN=Configurration object and on the NTDS DN of + each DC in the forest. It seems likely that it is the job of the KCC + to propagate between the two + */ +int dsdb_check_optional_feature(struct ldb_module *module, struct GUID op_feature_guid, bool *feature_enabled) +{ + TALLOC_CTX *tmp_ctx; + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_result *res; + struct ldb_dn *search_dn; + struct GUID search_guid; + const char *attrs[] = {"msDS-EnabledFeature", NULL}; + int ret; + unsigned int i; + struct ldb_message_element *el; + struct ldb_dn *feature_dn; + + tmp_ctx = talloc_new(ldb); + + feature_dn = samdb_ntds_settings_dn(ldb_module_get_ctx(module), tmp_ctx); + if (feature_dn == NULL) { + talloc_free(tmp_ctx); + return ldb_operr(ldb_module_get_ctx(module)); + } + + *feature_enabled = false; + + ret = dsdb_module_search_dn(module, tmp_ctx, &res, feature_dn, attrs, DSDB_FLAG_NEXT_MODULE, NULL); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "Could not find the feature object - dn: %s\n", + ldb_dn_get_linearized(feature_dn)); + talloc_free(tmp_ctx); + return LDB_ERR_NO_SUCH_OBJECT; + } + if (res->msgs[0]->num_elements > 0) { + const char *attrs2[] = {"msDS-OptionalFeatureGUID", NULL}; + + el = ldb_msg_find_element(res->msgs[0],"msDS-EnabledFeature"); + + for (i=0; i<el->num_values; i++) { + search_dn = ldb_dn_from_ldb_val(tmp_ctx, ldb, &el->values[i]); + + ret = dsdb_module_search_dn(module, tmp_ctx, &res, + search_dn, attrs2, DSDB_FLAG_NEXT_MODULE, NULL); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, + "Could no find object dn: %s\n", + ldb_dn_get_linearized(search_dn)); + talloc_free(tmp_ctx); + return LDB_ERR_OPERATIONS_ERROR; + } + + search_guid = samdb_result_guid(res->msgs[0], "msDS-OptionalFeatureGUID"); + + if (GUID_equal(&search_guid, &op_feature_guid)) { + *feature_enabled = true; + break; + } + } + } + talloc_free(tmp_ctx); + return LDB_SUCCESS; +} + +/* + find the NTDS GUID from a computers DN record + */ +int dsdb_module_find_ntdsguid_for_computer(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_dn *computer_dn, + struct GUID *ntds_guid, + struct ldb_request *parent) +{ + int ret; + struct ldb_dn *dn; + + *ntds_guid = GUID_zero(); + + ret = dsdb_module_reference_dn(module, mem_ctx, computer_dn, + "serverReferenceBL", &dn, parent); + if (ret != LDB_SUCCESS) { + return ret; + } + + if (!ldb_dn_add_child_fmt(dn, "CN=NTDS Settings")) { + talloc_free(dn); + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = dsdb_module_guid_by_dn(module, dn, ntds_guid, parent); + talloc_free(dn); + return ret; +} + +/* + find a 'reference' DN that points at another object + (eg. serverReference, rIDManagerReference etc) + */ +int dsdb_module_reference_dn(struct ldb_module *module, TALLOC_CTX *mem_ctx, struct ldb_dn *base, + const char *attribute, struct ldb_dn **dn, struct ldb_request *parent) +{ + const char *attrs[2]; + struct ldb_result *res; + int ret; + + attrs[0] = attribute; + attrs[1] = NULL; + + ret = dsdb_module_search_dn(module, mem_ctx, &res, base, attrs, + DSDB_FLAG_NEXT_MODULE | DSDB_SEARCH_SHOW_EXTENDED_DN, parent); + if (ret != LDB_SUCCESS) { + return ret; + } + + *dn = ldb_msg_find_attr_as_dn(ldb_module_get_ctx(module), + mem_ctx, res->msgs[0], attribute); + if (!*dn) { + ldb_reset_err_string(ldb_module_get_ctx(module)); + talloc_free(res); + return LDB_ERR_NO_SUCH_ATTRIBUTE; + } + + talloc_free(res); + return LDB_SUCCESS; +} + +/* + find the RID Manager$ DN via the rIDManagerReference attribute in the + base DN + */ +int dsdb_module_rid_manager_dn(struct ldb_module *module, TALLOC_CTX *mem_ctx, struct ldb_dn **dn, + struct ldb_request *parent) +{ + return dsdb_module_reference_dn(module, mem_ctx, + ldb_get_default_basedn(ldb_module_get_ctx(module)), + "rIDManagerReference", dn, parent); +} + +/* + used to chain to the callers callback + */ +int dsdb_next_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct ldb_request *up_req = talloc_get_type(req->context, struct ldb_request); + + if (!ares) { + return ldb_module_done(up_req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + if (ares->error != LDB_SUCCESS || ares->type == LDB_REPLY_DONE) { + return ldb_module_done(up_req, ares->controls, + ares->response, ares->error); + } + + /* Otherwise pass on the callback */ + switch (ares->type) { + case LDB_REPLY_ENTRY: + return ldb_module_send_entry(up_req, ares->message, + ares->controls); + + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral(up_req, + ares->referral); + default: + /* Can't happen */ + return LDB_ERR_OPERATIONS_ERROR; + } +} + +/* + load the uSNHighest and the uSNUrgent attributes from the @REPLCHANGED + object for a partition + */ +int dsdb_module_load_partition_usn(struct ldb_module *module, struct ldb_dn *dn, + uint64_t *uSN, uint64_t *urgent_uSN, struct ldb_request *parent) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_request *req; + int ret; + TALLOC_CTX *tmp_ctx = talloc_new(module); + struct dsdb_control_current_partition *p_ctrl; + struct ldb_result *res; + + res = talloc_zero(tmp_ctx, struct ldb_result); + if (!res) { + talloc_free(tmp_ctx); + return ldb_module_oom(module); + } + + ret = ldb_build_search_req(&req, ldb, tmp_ctx, + ldb_dn_new(tmp_ctx, ldb, "@REPLCHANGED"), + LDB_SCOPE_BASE, + NULL, NULL, + NULL, + res, ldb_search_default_callback, + parent); + LDB_REQ_SET_LOCATION(req); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + p_ctrl = talloc(req, struct dsdb_control_current_partition); + if (p_ctrl == NULL) { + talloc_free(tmp_ctx); + return ldb_module_oom(module); + } + p_ctrl->version = DSDB_CONTROL_CURRENT_PARTITION_VERSION; + p_ctrl->dn = dn; + + + ret = ldb_request_add_control(req, + DSDB_CONTROL_CURRENT_PARTITION_OID, + false, p_ctrl); + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + /* Run the new request */ + ret = ldb_next_request(module, req); + + if (ret == LDB_SUCCESS) { + ret = ldb_wait(req->handle, LDB_WAIT_ALL); + } + + if (ret == LDB_ERR_NO_SUCH_OBJECT || ret == LDB_ERR_INVALID_DN_SYNTAX) { + /* it hasn't been created yet, which means + an implicit value of zero */ + *uSN = 0; + talloc_free(tmp_ctx); + ldb_reset_err_string(ldb); + return LDB_SUCCESS; + } + + if (ret != LDB_SUCCESS) { + talloc_free(tmp_ctx); + return ret; + } + + if (res->count != 1) { + *uSN = 0; + if (urgent_uSN) { + *urgent_uSN = 0; + } + } else { + *uSN = ldb_msg_find_attr_as_uint64(res->msgs[0], "uSNHighest", 0); + if (urgent_uSN) { + *urgent_uSN = ldb_msg_find_attr_as_uint64(res->msgs[0], "uSNUrgent", 0); + } + } + + talloc_free(tmp_ctx); + + return LDB_SUCCESS; +} + +/* + save uSNHighest and uSNUrgent attributes in the @REPLCHANGED object for a + partition + */ +int dsdb_module_save_partition_usn(struct ldb_module *module, struct ldb_dn *dn, + uint64_t uSN, uint64_t urgent_uSN, + struct ldb_request *parent) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct ldb_request *req; + struct ldb_message *msg; + struct dsdb_control_current_partition *p_ctrl; + int ret; + struct ldb_result *res; + + msg = ldb_msg_new(module); + if (msg == NULL) { + return ldb_module_oom(module); + } + + msg->dn = ldb_dn_new(msg, ldb, "@REPLCHANGED"); + if (msg->dn == NULL) { + talloc_free(msg); + return ldb_operr(ldb_module_get_ctx(module)); + } + + res = talloc_zero(msg, struct ldb_result); + if (!res) { + talloc_free(msg); + return ldb_module_oom(module); + } + + ret = samdb_msg_add_uint64(ldb, msg, msg, "uSNHighest", uSN); + if (ret != LDB_SUCCESS) { + talloc_free(msg); + return ret; + } + msg->elements[0].flags = LDB_FLAG_MOD_REPLACE; + + /* urgent_uSN is optional so may not be stored */ + if (urgent_uSN) { + ret = samdb_msg_add_uint64(ldb, msg, msg, "uSNUrgent", + urgent_uSN); + if (ret != LDB_SUCCESS) { + talloc_free(msg); + return ret; + } + msg->elements[1].flags = LDB_FLAG_MOD_REPLACE; + } + + + p_ctrl = talloc(msg, struct dsdb_control_current_partition); + if (p_ctrl == NULL) { + talloc_free(msg); + return ldb_oom(ldb); + } + p_ctrl->version = DSDB_CONTROL_CURRENT_PARTITION_VERSION; + p_ctrl->dn = dn; + ret = ldb_build_mod_req(&req, ldb, msg, + msg, + NULL, + res, + ldb_modify_default_callback, + parent); + LDB_REQ_SET_LOCATION(req); +again: + if (ret != LDB_SUCCESS) { + talloc_free(msg); + return ret; + } + + ret = ldb_request_add_control(req, + DSDB_CONTROL_CURRENT_PARTITION_OID, + false, p_ctrl); + if (ret != LDB_SUCCESS) { + talloc_free(msg); + return ret; + } + + /* Run the new request */ + ret = ldb_next_request(module, req); + + if (ret == LDB_SUCCESS) { + ret = ldb_wait(req->handle, LDB_WAIT_ALL); + } + if (ret == LDB_ERR_NO_SUCH_OBJECT) { + ret = ldb_build_add_req(&req, ldb, msg, + msg, + NULL, + res, + ldb_modify_default_callback, + parent); + LDB_REQ_SET_LOCATION(req); + goto again; + } + + talloc_free(msg); + + return ret; +} + +bool dsdb_module_am_system(struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct auth_session_info *session_info + = talloc_get_type( + ldb_get_opaque(ldb, DSDB_SESSION_INFO), + struct auth_session_info); + return security_session_user_level(session_info, NULL) == SECURITY_SYSTEM; +} + +bool dsdb_module_am_administrator(struct ldb_module *module) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct auth_session_info *session_info + = talloc_get_type( + ldb_get_opaque(ldb, DSDB_SESSION_INFO), + struct auth_session_info); + return security_session_user_level(session_info, NULL) == SECURITY_ADMINISTRATOR; +} + +/* + check if the recyclebin is enabled + */ +int dsdb_recyclebin_enabled(struct ldb_module *module, bool *enabled) +{ + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct GUID recyclebin_guid; + int ret; + + GUID_from_string(DS_GUID_FEATURE_RECYCLE_BIN, &recyclebin_guid); + + ret = dsdb_check_optional_feature(module, recyclebin_guid, enabled); + if (ret != LDB_SUCCESS) { + ldb_asprintf_errstring(ldb, "Could not verify if Recycle Bin is enabled \n"); + return ret; + } + + return LDB_SUCCESS; +} + +int dsdb_msg_constrainted_update_int32(struct ldb_module *module, + struct ldb_message *msg, + const char *attr, + const int32_t *old_val, + const int32_t *new_val) +{ + struct ldb_message_element *el; + int ret; + char *vstring; + + if (old_val) { + ret = ldb_msg_add_empty(msg, attr, LDB_FLAG_MOD_DELETE, &el); + if (ret != LDB_SUCCESS) { + return ret; + } + el->num_values = 1; + el->values = talloc_array(msg, struct ldb_val, el->num_values); + if (!el->values) { + return ldb_module_oom(module); + } + vstring = talloc_asprintf(el->values, "%ld", (long)*old_val); + if (!vstring) { + return ldb_module_oom(module); + } + *el->values = data_blob_string_const(vstring); + } + + if (new_val) { + ret = ldb_msg_add_empty(msg, attr, LDB_FLAG_MOD_ADD, &el); + if (ret != LDB_SUCCESS) { + return ret; + } + el->num_values = 1; + el->values = talloc_array(msg, struct ldb_val, el->num_values); + if (!el->values) { + return ldb_module_oom(module); + } + vstring = talloc_asprintf(el->values, "%ld", (long)*new_val); + if (!vstring) { + return ldb_module_oom(module); + } + *el->values = data_blob_string_const(vstring); + } + + return LDB_SUCCESS; +} + +int dsdb_msg_constrainted_update_uint32(struct ldb_module *module, + struct ldb_message *msg, + const char *attr, + const uint32_t *old_val, + const uint32_t *new_val) +{ + return dsdb_msg_constrainted_update_int32(module, msg, attr, + (const int32_t *)old_val, + (const int32_t *)new_val); +} + +int dsdb_msg_constrainted_update_int64(struct ldb_module *module, + struct ldb_message *msg, + const char *attr, + const int64_t *old_val, + const int64_t *new_val) +{ + struct ldb_message_element *el; + int ret; + char *vstring; + + if (old_val) { + ret = ldb_msg_add_empty(msg, attr, LDB_FLAG_MOD_DELETE, &el); + if (ret != LDB_SUCCESS) { + return ret; + } + el->num_values = 1; + el->values = talloc_array(msg, struct ldb_val, el->num_values); + if (!el->values) { + return ldb_module_oom(module); + } + vstring = talloc_asprintf(el->values, "%lld", (long long)*old_val); + if (!vstring) { + return ldb_module_oom(module); + } + *el->values = data_blob_string_const(vstring); + } + + if (new_val) { + ret = ldb_msg_add_empty(msg, attr, LDB_FLAG_MOD_ADD, &el); + if (ret != LDB_SUCCESS) { + return ret; + } + el->num_values = 1; + el->values = talloc_array(msg, struct ldb_val, el->num_values); + if (!el->values) { + return ldb_module_oom(module); + } + vstring = talloc_asprintf(el->values, "%lld", (long long)*new_val); + if (!vstring) { + return ldb_module_oom(module); + } + *el->values = data_blob_string_const(vstring); + } + + return LDB_SUCCESS; +} + +int dsdb_msg_constrainted_update_uint64(struct ldb_module *module, + struct ldb_message *msg, + const char *attr, + const uint64_t *old_val, + const uint64_t *new_val) +{ + return dsdb_msg_constrainted_update_int64(module, msg, attr, + (const int64_t *)old_val, + (const int64_t *)new_val); +} + +/* + update an int32 attribute safely via a constrained delete/add + */ +int dsdb_module_constrainted_update_int32(struct ldb_module *module, + struct ldb_dn *dn, + const char *attr, + const int32_t *old_val, + const int32_t *new_val, + struct ldb_request *parent) +{ + struct ldb_message *msg; + int ret; + + msg = ldb_msg_new(module); + if (msg == NULL) { + return ldb_module_oom(module); + } + msg->dn = dn; + + ret = dsdb_msg_constrainted_update_int32(module, + msg, attr, + old_val, + new_val); + if (ret != LDB_SUCCESS) { + talloc_free(msg); + return ret; + } + + ret = dsdb_module_modify(module, msg, DSDB_FLAG_NEXT_MODULE, parent); + talloc_free(msg); + return ret; +} + +int dsdb_module_constrainted_update_uint32(struct ldb_module *module, + struct ldb_dn *dn, + const char *attr, + const uint32_t *old_val, + const uint32_t *new_val, + struct ldb_request *parent) +{ + return dsdb_module_constrainted_update_int32(module, dn, attr, + (const int32_t *)old_val, + (const int32_t *)new_val, parent); +} + +/* + update an int64 attribute safely via a constrained delete/add + */ +int dsdb_module_constrainted_update_int64(struct ldb_module *module, + struct ldb_dn *dn, + const char *attr, + const int64_t *old_val, + const int64_t *new_val, + struct ldb_request *parent) +{ + struct ldb_message *msg; + int ret; + + msg = ldb_msg_new(module); + if (msg == NULL) { + return ldb_module_oom(module); + } + msg->dn = dn; + + ret = dsdb_msg_constrainted_update_int64(module, + msg, attr, + old_val, + new_val); + if (ret != LDB_SUCCESS) { + talloc_free(msg); + return ret; + } + + ret = dsdb_module_modify(module, msg, DSDB_FLAG_NEXT_MODULE, parent); + talloc_free(msg); + return ret; +} + +int dsdb_module_constrainted_update_uint64(struct ldb_module *module, + struct ldb_dn *dn, + const char *attr, + const uint64_t *old_val, + const uint64_t *new_val, + struct ldb_request *parent) +{ + return dsdb_module_constrainted_update_int64(module, dn, attr, + (const int64_t *)old_val, + (const int64_t *)new_val, + parent); +} + + +const struct ldb_val *dsdb_module_find_dsheuristics(struct ldb_module *module, + TALLOC_CTX *mem_ctx, struct ldb_request *parent) +{ + int ret; + struct ldb_dn *new_dn; + struct ldb_context *ldb = ldb_module_get_ctx(module); + static const char *attrs[] = { "dSHeuristics", NULL }; + struct ldb_result *res; + + new_dn = ldb_dn_copy(mem_ctx, ldb_get_config_basedn(ldb)); + if (!ldb_dn_add_child_fmt(new_dn, + "CN=Directory Service,CN=Windows NT,CN=Services")) { + talloc_free(new_dn); + return NULL; + } + ret = dsdb_module_search_dn(module, mem_ctx, &res, + new_dn, + attrs, + DSDB_FLAG_NEXT_MODULE, + parent); + if (ret == LDB_SUCCESS && res->count == 1) { + talloc_free(new_dn); + return ldb_msg_find_ldb_val(res->msgs[0], + "dSHeuristics"); + } + talloc_free(new_dn); + return NULL; +} + +bool dsdb_block_anonymous_ops(struct ldb_module *module, struct ldb_request *parent) +{ + TALLOC_CTX *tmp_ctx = talloc_new(module); + bool result; + const struct ldb_val *hr_val = dsdb_module_find_dsheuristics(module, + tmp_ctx, parent); + if (hr_val == NULL || hr_val->length < DS_HR_BLOCK_ANONYMOUS_OPS) { + result = true; + } else if (hr_val->data[DS_HR_BLOCK_ANONYMOUS_OPS -1] == '2') { + result = false; + } else { + result = true; + } + + talloc_free(tmp_ctx); + return result; +} + +bool dsdb_user_password_support(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_request *parent) +{ + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + bool result; + const struct ldb_val *hr_val = dsdb_module_find_dsheuristics(module, + tmp_ctx, + parent); + if (hr_val == NULL || hr_val->length < DS_HR_USER_PASSWORD_SUPPORT) { + result = false; + } else if ((hr_val->data[DS_HR_USER_PASSWORD_SUPPORT -1] == '2') || + (hr_val->data[DS_HR_USER_PASSWORD_SUPPORT -1] == '0')) { + result = false; + } else { + result = true; + } + + talloc_free(tmp_ctx); + return result; +} + +bool dsdb_do_list_object(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_request *parent) +{ + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + bool result; + const struct ldb_val *hr_val = dsdb_module_find_dsheuristics(module, + tmp_ctx, + parent); + if (hr_val == NULL || hr_val->length < DS_HR_DOLISTOBJECT) { + result = false; + } else if (hr_val->data[DS_HR_DOLISTOBJECT -1] == '1') { + result = true; + } else { + result = false; + } + + talloc_free(tmp_ctx); + return result; +} + +bool dsdb_attribute_authz_on_ldap_add(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_request *parent) +{ + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + bool result = false; + const struct ldb_val *hr_val = dsdb_module_find_dsheuristics(module, + tmp_ctx, + parent); + if (hr_val != NULL && hr_val->length >= DS_HR_ATTR_AUTHZ_ON_LDAP_ADD) { + uint8_t val = hr_val->data[DS_HR_ATTR_AUTHZ_ON_LDAP_ADD - 1]; + if (val != '0' && val != '2') { + result = true; + } + } + + talloc_free(tmp_ctx); + return result; +} + +bool dsdb_block_owner_implicit_rights(struct ldb_module *module, + TALLOC_CTX *mem_ctx, + struct ldb_request *parent) +{ + TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx); + bool result = false; + const struct ldb_val *hr_val = dsdb_module_find_dsheuristics(module, + tmp_ctx, + parent); + if (hr_val != NULL && hr_val->length >= DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS) { + uint8_t val = hr_val->data[DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS - 1]; + if (val != '0' && val != '2') { + result = true; + } + } + + talloc_free(tmp_ctx); + return result; +} + +/* + show the chain of requests, useful for debugging async requests + */ +void dsdb_req_chain_debug(struct ldb_request *req, int level) +{ + char *s = ldb_module_call_chain(req, req); + DEBUG(level, ("%s\n", s)); + talloc_free(s); +} + +/* + * Get all the values that *might* be added by an ldb message, as a composite + * ldb element. + * + * This is useful when we need to check all the possible values against some + * criteria. + * + * In cases where a modify message mixes multiple ADDs, DELETEs, and REPLACES, + * the returned element might contain more values than would actually end up + * in the database if the message was run to its conclusion. + * + * If the operation is not LDB_ADD or LDB_MODIFY, an operations error is + * returned. + * + * The returned element might not be new, and should not be modified or freed + * before the message is finished. + */ + +int dsdb_get_expected_new_values(TALLOC_CTX *mem_ctx, + const struct ldb_message *msg, + const char *attr_name, + struct ldb_message_element **el, + enum ldb_request_type operation) +{ + unsigned int i; + unsigned int el_count = 0; + unsigned int val_count = 0; + struct ldb_val *v = NULL; + struct ldb_message_element *_el = NULL; + *el = NULL; + + if (operation != LDB_ADD && operation != LDB_MODIFY) { + DBG_ERR("inapplicable operation type: %d\n", operation); + return LDB_ERR_OPERATIONS_ERROR; + } + + /* count the adding or replacing elements */ + for (i = 0; i < msg->num_elements; i++) { + if (ldb_attr_cmp(msg->elements[i].name, attr_name) == 0) { + unsigned int tmp; + if ((operation == LDB_MODIFY) && + (LDB_FLAG_MOD_TYPE(msg->elements[i].flags) + == LDB_FLAG_MOD_DELETE)) { + continue; + } + el_count++; + tmp = val_count + msg->elements[i].num_values; + if (unlikely(tmp < val_count)) { + DBG_ERR("too many values for one element!"); + return LDB_ERR_OPERATIONS_ERROR; + } + val_count = tmp; + } + } + if (el_count == 0) { + /* nothing to see here */ + return LDB_SUCCESS; + } + + if (el_count == 1 || val_count == 0) { + /* + * There is one effective element, which we can return as-is, + * OR there are only elements with zero values -- any of which + * will do. + */ + for (i = 0; i < msg->num_elements; i++) { + if (ldb_attr_cmp(msg->elements[i].name, attr_name) == 0) { + if ((operation == LDB_MODIFY) && + (LDB_FLAG_MOD_TYPE(msg->elements[i].flags) + == LDB_FLAG_MOD_DELETE)) { + continue; + } + *el = &msg->elements[i]; + return LDB_SUCCESS; + } + } + } + + _el = talloc_zero(mem_ctx, struct ldb_message_element); + if (_el == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + _el->name = attr_name; + + if (val_count == 0) { + /* + * Seems unlikely, but sometimes we might be adding zero + * values in multiple separate elements. The talloc zero has + * already set the expected values = NULL, num_values = 0. + */ + *el = _el; + return LDB_SUCCESS; + } + + _el->values = talloc_array(_el, struct ldb_val, val_count); + if (_el->values == NULL) { + talloc_free(_el); + return LDB_ERR_OPERATIONS_ERROR; + } + _el->num_values = val_count; + + v = _el->values; + + for (i = 0; i < msg->num_elements; i++) { + if (ldb_attr_cmp(msg->elements[i].name, attr_name) == 0) { + const struct ldb_message_element *tmp_el = &msg->elements[i]; + if ((operation == LDB_MODIFY) && + (LDB_FLAG_MOD_TYPE(tmp_el->flags) + == LDB_FLAG_MOD_DELETE)) { + continue; + } + if (tmp_el->values == NULL || tmp_el->num_values == 0) { + continue; + } + memcpy(v, + tmp_el->values, + tmp_el->num_values * sizeof(*v)); + v += tmp_el->num_values; + } + } + + *el = _el; + return LDB_SUCCESS; +} + + +/* + * Get the value of a single-valued attribute from an ADDed message. 'val' will only live as + * long as 'msg' and 'original_val' do, and must not be freed. + */ +int dsdb_msg_add_get_single_value(const struct ldb_message *msg, + const char *attr_name, + const struct ldb_val **val) +{ + const struct ldb_message_element *el = NULL; + + /* + * The ldb_msg_normalize() call in ldb_request() ensures that + * there is at most one message element for each + * attribute. Thus, we don't need a loop to deal with an + * LDB_ADD. + */ + el = ldb_msg_find_element(msg, attr_name); + if (el == NULL) { + *val = NULL; + return LDB_SUCCESS; + } + if (el->num_values != 1) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + *val = &el->values[0]; + return LDB_SUCCESS; +} + +/* + * Get the value of a single-valued attribute after processing a + * message. 'operation' is either LDB_ADD or LDB_MODIFY. 'val' will only live as + * long as 'msg' and 'original_val' do, and must not be freed. + */ +int dsdb_msg_get_single_value(const struct ldb_message *msg, + const char *attr_name, + const struct ldb_val *original_val, + const struct ldb_val **val, + enum ldb_request_type operation) +{ + unsigned idx; + + *val = NULL; + + if (operation == LDB_ADD) { + if (original_val != NULL) { + /* This is an error on the caller's part. */ + return LDB_ERR_CONSTRAINT_VIOLATION; + } + return dsdb_msg_add_get_single_value(msg, attr_name, val); + } + + SMB_ASSERT(operation == LDB_MODIFY); + + *val = original_val; + + for (idx = 0; idx < msg->num_elements; ++idx) { + const struct ldb_message_element *el = &msg->elements[idx]; + + if (ldb_attr_cmp(el->name, attr_name) != 0) { + continue; + } + + switch (el->flags & LDB_FLAG_MOD_MASK) { + case LDB_FLAG_MOD_ADD: + if (el->num_values != 1) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + if (*val != NULL) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + *val = &el->values[0]; + + break; + + case LDB_FLAG_MOD_REPLACE: + if (el->num_values > 1) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + *val = el->num_values ? &el->values[0] : NULL; + + break; + + case LDB_FLAG_MOD_DELETE: + if (el->num_values > 1) { + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + /* + * If a value was specified for the delete, we don't + * bother checking it matches the value we currently + * have. Any mismatch will be caught later (e.g. in + * ldb_kv_modify_internal). + */ + + *val = NULL; + + break; + } + } + + return LDB_SUCCESS; +} + +/* + * This function determines the (last) structural or 88 object class of a passed + * "objectClass" attribute - per MS-ADTS 3.1.1.1.4 this is the last value. + * Without schema this does not work and hence NULL is returned. + */ +const struct dsdb_class *dsdb_get_last_structural_class(const struct dsdb_schema *schema, + const struct ldb_message_element *element) +{ + const struct dsdb_class *last_class; + + if (schema == NULL) { + return NULL; + } + + if (element->num_values == 0) { + return NULL; + } + + last_class = dsdb_class_by_lDAPDisplayName_ldb_val(schema, + &element->values[element->num_values-1]); + if (last_class == NULL) { + return NULL; + } + if (last_class->objectClassCategory > 1) { + return NULL; + } + + return last_class; +} + +const struct dsdb_class *dsdb_get_structural_oc_from_msg(const struct dsdb_schema *schema, + const struct ldb_message *msg) +{ + struct ldb_message_element *oc_el; + + oc_el = ldb_msg_find_element(msg, "objectClass"); + if (!oc_el) { + return NULL; + } + + return dsdb_get_last_structural_class(schema, oc_el); +} + +/* + Get the parent class of an objectclass, or NULL if none exists. + */ +const struct dsdb_class *dsdb_get_parent_class(const struct dsdb_schema *schema, + const struct dsdb_class *objectclass) +{ + if (ldb_attr_cmp(objectclass->lDAPDisplayName, "top") == 0) { + return NULL; + } + + if (objectclass->subClassOf == NULL) { + return NULL; + } + + return dsdb_class_by_lDAPDisplayName(schema, objectclass->subClassOf); +} + +/* + Return true if 'struct_objectclass' is a subclass of 'other_objectclass'. The + two objectclasses must originate from the same schema, to allow for + pointer-based identity comparison. + */ +bool dsdb_is_subclass_of(const struct dsdb_schema *schema, + const struct dsdb_class *struct_objectclass, + const struct dsdb_class *other_objectclass) +{ + while (struct_objectclass != NULL) { + /* Pointer comparison can be used due to the same schema str. */ + if (struct_objectclass == other_objectclass) { + return true; + } + + struct_objectclass = dsdb_get_parent_class(schema, struct_objectclass); + } + + return false; +} + +/* Fix the DN so that the relative attribute names are in upper case so that the DN: + cn=Adminstrator,cn=users,dc=samba,dc=example,dc=com becomes + CN=Adminstrator,CN=users,DC=samba,DC=example,DC=com +*/ +int dsdb_fix_dn_rdncase(struct ldb_context *ldb, struct ldb_dn *dn) +{ + int i, ret; + char *upper_rdn_attr; + + for (i=0; i < ldb_dn_get_comp_num(dn); i++) { + /* We need the attribute name in upper case */ + upper_rdn_attr = strupper_talloc(dn, + ldb_dn_get_component_name(dn, i)); + if (!upper_rdn_attr) { + return ldb_oom(ldb); + } + ret = ldb_dn_set_component(dn, i, upper_rdn_attr, + *ldb_dn_get_component_val(dn, i)); + talloc_free(upper_rdn_attr); + if (ret != LDB_SUCCESS) { + return ret; + } + } + return LDB_SUCCESS; +} + +/** + * Make most specific objectCategory for the objectClass of passed object + * NOTE: In this implementation we count that it is called on already + * verified objectClass attribute value. See objectclass.c thorough + * implementation for all the magic that involves + * + * @param ldb ldb context + * @param schema cached schema for ldb. We may get it, but it is very time consuming. + * Hence leave the responsibility to the caller. + * @param obj AD object to determint objectCategory for + * @param mem_ctx Memory context - usually it is obj actually + * @param pobjectcategory location to store found objectCategory + * + * @return LDB_SUCCESS or error including out of memory error + */ +int dsdb_make_object_category(struct ldb_context *ldb, const struct dsdb_schema *schema, + const struct ldb_message *obj, + TALLOC_CTX *mem_ctx, const char **pobjectcategory) +{ + const struct dsdb_class *objectclass; + struct ldb_message_element *objectclass_element; + struct dsdb_extended_dn_store_format *dn_format; + + objectclass_element = ldb_msg_find_element(obj, "objectClass"); + if (!objectclass_element) { + ldb_asprintf_errstring(ldb, "dsdb: Cannot add %s, no objectclass specified!", + ldb_dn_get_linearized(obj->dn)); + return LDB_ERR_OBJECT_CLASS_VIOLATION; + } + if (objectclass_element->num_values == 0) { + ldb_asprintf_errstring(ldb, "dsdb: Cannot add %s, at least one (structural) objectclass has to be specified!", + ldb_dn_get_linearized(obj->dn)); + return LDB_ERR_CONSTRAINT_VIOLATION; + } + + /* + * Get the new top-most structural object class and check for + * unrelated structural classes + */ + objectclass = dsdb_get_last_structural_class(schema, + objectclass_element); + if (objectclass == NULL) { + ldb_asprintf_errstring(ldb, + "Failed to find a structural class for %s", + ldb_dn_get_linearized(obj->dn)); + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + dn_format = talloc_get_type(ldb_get_opaque(ldb, DSDB_EXTENDED_DN_STORE_FORMAT_OPAQUE_NAME), + struct dsdb_extended_dn_store_format); + if (dn_format && dn_format->store_extended_dn_in_ldb == false) { + /* Strip off extended components */ + struct ldb_dn *dn = ldb_dn_new(mem_ctx, ldb, + objectclass->defaultObjectCategory); + *pobjectcategory = ldb_dn_alloc_linearized(mem_ctx, dn); + talloc_free(dn); + } else { + *pobjectcategory = talloc_strdup(mem_ctx, objectclass->defaultObjectCategory); + } + + if (*pobjectcategory == NULL) { + return ldb_oom(ldb); + } + + return LDB_SUCCESS; +} diff --git a/source4/dsdb/samdb/ldb_modules/util.h b/source4/dsdb/samdb/ldb_modules/util.h new file mode 100644 index 0000000..937767a --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/util.h @@ -0,0 +1,42 @@ +/* + Unix SMB/CIFS implementation. + Samba utility functions + + Copyright (C) Andrew Tridgell 2009 + Copyright (C) Andrew Bartlett <abartlet@samba.org> 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/>. +*/ + +/* predeclare some structures used by utility functions */ +struct dsdb_schema; +struct dsdb_attribute; +struct dsdb_fsmo_extended_op; +struct security_descriptor; +struct dom_sid; +struct netlogon_samlogon_response; + +#include "librpc/gen_ndr/misc.h" +#include "dsdb/samdb/ldb_modules/util_proto.h" +#include "dsdb/common/util.h" +#include "../libcli/netlogon/netlogon.h" + +/* extend the dsdb_request_add_controls() flags for module + specific functions */ +#define DSDB_FLAG_NEXT_MODULE 0x00100000 +#define DSDB_FLAG_OWN_MODULE 0x00400000 +#define DSDB_FLAG_TOP_MODULE 0x00800000 +#define DSDB_FLAG_TRUSTED 0x01000000 +#define DSDB_FLAG_REPLICATED_UPDATE 0x02000000 +#define DSDB_FLAG_FORCE_ALLOW_VALIDATED_DNS_HOSTNAME_SPN_WRITE 0x04000000 diff --git a/source4/dsdb/samdb/ldb_modules/vlv_pagination.c b/source4/dsdb/samdb/ldb_modules/vlv_pagination.c new file mode 100644 index 0000000..b389d3f --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/vlv_pagination.c @@ -0,0 +1,946 @@ +/* + ldb database library + + Copyright (C) Simo Sorce 2005-2008 + Copyright (C) Catalyst IT 2016 + + ** NOTE! The following LGPL license applies to the ldb + ** library. This does NOT imply that all of Samba is released + ** under the LGPL + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 3 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ + +/* + * Name: vlv_pagination + * + * Component: ldb vlv pagination control module + * + * Description: this module caches a complete search and sends back + * results in chunks as asked by the client + * + * Originally based on paged_results.c by Simo Sorce + * Modified by Douglas Bagnall and Garming Sam for Catalyst. + */ + +#include "includes.h" +#include "auth/auth.h" +#include <ldb.h> +#include "dsdb/samdb/samdb.h" +#include "libcli/security/security.h" +#include "libcli/ldap/ldap_errors.h" +#include <ldb.h> +#include "replace.h" +#include "system/filesys.h" +#include "system/time.h" +#include "ldb_module.h" +#include "dsdb/samdb/samdb.h" + +#include "dsdb/common/util.h" +#include "lib/util/binsearch.h" + +/* This is the number of concurrent searches per connection to cache. */ +#define VLV_N_SEARCHES 5 + + +struct results_store { + uint32_t contextId; + time_t timestamp; + + struct GUID *results; + size_t num_entries; + size_t result_array_size; + + struct referral_store *first_ref; + struct referral_store *last_ref; + + struct ldb_control **controls; + struct ldb_control **down_controls; + struct ldb_vlv_req_control *vlv_details; + struct ldb_server_sort_control *sort_details; +}; + +struct private_data { + uint32_t next_free_id; + struct results_store **store; + int n_stores; +}; + + +struct vlv_context { + struct ldb_module *module; + struct ldb_request *req; + struct results_store *store; + struct ldb_control **controls; + struct private_data *priv; +}; + + +static struct results_store *new_store(struct private_data *priv) +{ + struct results_store *store; + int i; + int best = 0; + time_t oldest = TIME_T_MAX; + for (i = 0; i < priv->n_stores; i++) { + if (priv->store[i] == NULL) { + best = i; + break; + } else if (priv->store[i]->timestamp < oldest){ + best = i; + oldest = priv->store[i]->timestamp; + } + } + + store = talloc_zero(priv, struct results_store); + if (store == NULL) { + return NULL; + } + if (priv->store[best] != NULL) { + TALLOC_FREE(priv->store[best]); + } + priv->store[best] = store; + store->timestamp = time(NULL); + return store; +} + + +struct vlv_sort_context { + struct ldb_context *ldb; + ldb_attr_comparison_t comparison_fn; + const char *attr; + struct vlv_context *ac; + int status; + struct ldb_val value; +}; + + +/* Referrals are temporarily stored in a linked list */ +struct referral_store { + char *ref; + struct referral_store *next; +}; + +/* + search for attrs on one DN, by the GUID of the DN, with true + LDB controls + */ + +static int vlv_search_by_dn_guid(struct ldb_module *module, + struct vlv_context *ac, + struct ldb_result **result, + const struct GUID *guid, + const char * const *attrs) +{ + struct ldb_dn *dn; + struct ldb_request *req; + struct ldb_result *res; + int ret; + struct GUID_txt_buf guid_str; + struct ldb_control **controls = ac->store->down_controls; + struct ldb_context *ldb = ldb_module_get_ctx(module); + + dn = ldb_dn_new_fmt(ac, ldb, "<GUID=%s>", + GUID_buf_string(guid, &guid_str)); + if (dn == NULL) { + return ldb_oom(ldb); + } + + res = talloc_zero(ac, struct ldb_result); + if (res == NULL) { + return ldb_oom(ldb); + } + + ret = ldb_build_search_req(&req, ldb, ac, + dn, + LDB_SCOPE_BASE, + NULL, + attrs, + controls, + res, + ldb_search_default_callback, + ac->req); + if (ret != LDB_SUCCESS) { + talloc_free(res); + return ret; + } + + ret = ldb_request(ldb, req); + if (ret == LDB_SUCCESS) { + ret = ldb_wait(req->handle, LDB_WAIT_ALL); + } + + talloc_free(req); + if (ret != LDB_SUCCESS) { + talloc_free(res); + return ret; + } + + *result = res; + return ret; +} + + +static int save_referral(struct results_store *store, char *ref) +{ + struct referral_store *node = talloc(store, + struct referral_store); + if (node == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + node->next = NULL; + node->ref = talloc_steal(node, ref); + + if (store->first_ref == NULL) { + store->first_ref = node; + } else { + store->last_ref->next = node; + } + store->last_ref = node; + return LDB_SUCCESS; +} + +static int send_referrals(struct results_store *store, + struct ldb_request *req) +{ + int ret; + struct referral_store *node; + while (store->first_ref != NULL) { + node = store->first_ref; + ret = ldb_module_send_referral(req, node->ref); + if (ret != LDB_SUCCESS) { + return ret; + } + store->first_ref = node->next; + talloc_free(node); + } + return LDB_SUCCESS; +} + + +/* vlv_value_compare() is used in a binary search */ + +static int vlv_value_compare(struct vlv_sort_context *target, + struct GUID *guid) +{ + struct ldb_result *result = NULL; + struct ldb_message_element *el = NULL; + struct vlv_context *ac = target->ac; + int ret; + const char *attrs[2] = { + target->attr, + NULL + }; + + ret = vlv_search_by_dn_guid(ac->module, ac, &result, guid, attrs); + + if (ret != LDB_SUCCESS) { + target->status = ret; + /* returning 0 ends the search. */ + return 0; + } + + el = ldb_msg_find_element(result->msgs[0], target->attr); + return target->comparison_fn(target->ldb, ac, + &target->value, &el->values[0]); + +} + +/* The same as vlv_value_compare() but sorting in the opposite direction. */ +static int vlv_value_compare_rev(struct vlv_sort_context *target, + struct GUID *guid) +{ + return -vlv_value_compare(target, guid); +} + + + +/* Convert a "greater than or equal to" VLV query into an index. This is + zero-based, so one less than the equivalent VLV offset query. + + If the query value is greater than (or less than in the reverse case) all + the items, An index just beyond the last position is used. + + If an error occurs during the search for the index, we stash it in the + status argument. + */ + +static int vlv_gt_eq_to_index(struct vlv_context *ac, + struct GUID *guid_array, + struct ldb_vlv_req_control *vlv_details, + struct ldb_server_sort_control *sort_details, + int *status) +{ + /* this has a >= comparison string, which needs to be + * converted into indices. + */ + size_t len = ac->store->num_entries; + struct ldb_context *ldb; + const struct ldb_schema_attribute *a; + struct GUID *result = NULL; + struct vlv_sort_context context; + struct ldb_val value = { + .data = (uint8_t *)vlv_details->match.gtOrEq.value, + .length = vlv_details->match.gtOrEq.value_len + }; + ldb = ldb_module_get_ctx(ac->module); + a = ldb_schema_attribute_by_name(ldb, sort_details->attributeName); + + context = (struct vlv_sort_context){ + .ldb = ldb, + .comparison_fn = a->syntax->comparison_fn, + .attr = sort_details->attributeName, + .ac = ac, + .status = LDB_SUCCESS, + .value = value + }; + + if (sort_details->reverse) { + /* when the sort is reversed, "gtOrEq" means + "less than or equal" */ + BINARY_ARRAY_SEARCH_GTE(guid_array, len, &context, + vlv_value_compare_rev, + result, result); + } else { + BINARY_ARRAY_SEARCH_GTE(guid_array, len, &context, + vlv_value_compare, + result, result); + } + if (context.status != LDB_SUCCESS) { + *status = context.status; + return len; + } + *status = LDB_SUCCESS; + + if (result == NULL) { + /* the target is beyond the end of the array */ + return len; + } + return result - guid_array; + +} + +/* return the zero-based index into the sorted results, or -1 on error. + + The VLV index is one-base, so one greater than this. + */ + +static int vlv_calc_real_offset(int offset, int denominator, int n_entries) +{ + double fraction; + + /* An offset of 0 (or less) is an error, unless the denominator is + also zero. */ + if (offset <= 0 && denominator != 0) { + return -1; + } + + /* a denominator of zero means the server should use the estimated + number of entries. */ + if (denominator == 0) { + if (offset == 0) { + /* 0/0 means the last one */ + return n_entries - 1; + } + denominator = n_entries; + } + + if (denominator == 1) { + /* The 1/1 case means the LAST index. + Strangely, for n > 1, n/1 means the FIRST index. + */ + if (offset == 1) { + return n_entries - 1; + } + return 0; + } + + if (offset >= denominator) { + /* we want the last one */ + return n_entries - 1; + } + /* if the denominator is exactly the number of entries, the offset is + already correct. */ + + if (denominator == n_entries) { + return offset - 1; + } + + /* The following formula was discovered by probing Windows. */ + fraction = (offset - 1.0) / (denominator - 1.0); + return (int)(fraction * (n_entries - 1.0) + 0.5); +} + + +/* vlv_results() is called when there is a valid contextID -- meaning the search + has been prepared earlier and saved -- or by vlv_search_callback() when a + search has just been completed. */ + +static int vlv_results(struct vlv_context *ac, struct ldb_reply *ares) +{ + struct ldb_extended *response = (ares != NULL ? ares->response : NULL); + struct ldb_vlv_resp_control *vlv; + unsigned int num_ctrls; + int ret, i, first_i, last_i; + struct ldb_vlv_req_control *vlv_details; + struct ldb_server_sort_control *sort_details; + int target = 0; + + if (ac->store == NULL) { + ret = LDB_ERR_OPERATIONS_ERROR; + return ldb_module_done( + ac->req, ac->controls, response, ret); + } + + if (ac->store->first_ref) { + /* There is no right place to put references in the sorted + results, so we send them as soon as possible. + */ + ret = send_referrals(ac->store, ac->req); + if (ret != LDB_SUCCESS) { + /* + * send_referrals will have called ldb_module_done + * if there was an error. + */ + return ret; + } + } + + vlv_details = ac->store->vlv_details; + sort_details = ac->store->sort_details; + + if (ac->store->num_entries != 0) { + if (vlv_details->type == 1) { + target = vlv_gt_eq_to_index(ac, ac->store->results, + vlv_details, + sort_details, &ret); + if (ret != LDB_SUCCESS) { + return ldb_module_done( + ac->req, + ac->controls, + response, + ret); + } + } else { + target = vlv_calc_real_offset(vlv_details->match.byOffset.offset, + vlv_details->match.byOffset.contentCount, + ac->store->num_entries); + if (target == -1) { + ret = LDB_ERR_OPERATIONS_ERROR; + return ldb_module_done( + ac->req, + ac->controls, + response, + ret); + } + } + + /* send the results */ + first_i = MAX(target - vlv_details->beforeCount, 0); + last_i = MIN(target + vlv_details->afterCount, + ac->store->num_entries - 1); + + for (i = first_i; i <= last_i; i++) { + struct ldb_result *result = NULL; + struct GUID *guid = &ac->store->results[i]; + + ret = vlv_search_by_dn_guid(ac->module, ac, &result, guid, + ac->req->op.search.attrs); + + if (ret == LDAP_NO_SUCH_OBJECT + || result->count != 1) { + /* + * The thing isn't there, which we quietly + * ignore and go on to send an extra one + * instead. + * + * result->count == 0 or > 1 can only + * happen if ASQ (which breaks all the + * rules) is somehow invoked (as this + * is a BASE search). + * + * (We skip the ASQ cookie for the + * GUID searches) + */ + if (last_i < ac->store->num_entries - 1) { + last_i++; + } + continue; + } else if (ret != LDB_SUCCESS) { + return ldb_module_done( + ac->req, + ac->controls, + response, + ret); + } + + ret = ldb_module_send_entry(ac->req, result->msgs[0], + NULL); + if (ret != LDB_SUCCESS) { + /* + * ldb_module_send_entry will have called + * ldb_module_done if there was an error + */ + return ret; + } + } + } else { + target = -1; + } + + /* return result done */ + num_ctrls = 1; + i = 0; + + if (ac->store->controls != NULL) { + while (ac->store->controls[i]){ + i++; /* counting */ + } + num_ctrls += i; + } + + ac->controls = talloc_array(ac, struct ldb_control *, num_ctrls + 1); + if (ac->controls == NULL) { + ret = LDB_ERR_OPERATIONS_ERROR; + return ldb_module_done( + ac->req, ac->controls, response, ret); + } + ac->controls[num_ctrls] = NULL; + + for (i = 0; i < (num_ctrls -1); i++) { + ac->controls[i] = talloc_reference(ac->controls, ac->store->controls[i]); + } + + ac->controls[i] = talloc(ac->controls, struct ldb_control); + if (ac->controls[i] == NULL) { + ret = LDB_ERR_OPERATIONS_ERROR; + return ldb_module_done( + ac->req, ac->controls, response, ret); + } + + ac->controls[i]->oid = talloc_strdup(ac->controls[i], + LDB_CONTROL_VLV_RESP_OID); + if (ac->controls[i]->oid == NULL) { + ret = LDB_ERR_OPERATIONS_ERROR; + return ldb_module_done( + ac->req, ac->controls, response, ret); + } + + ac->controls[i]->critical = 0; + + vlv = talloc(ac->controls[i], struct ldb_vlv_resp_control); + if (vlv == NULL) { + ret = LDB_ERR_OPERATIONS_ERROR; + return ldb_module_done( + ac->req, ac->controls, response, ret); + } + ac->controls[i]->data = vlv; + + ac->store->timestamp = time(NULL); + + ac->store->contextId = ac->priv->next_free_id; + ac->priv->next_free_id++; + vlv->contextId = talloc_memdup(vlv, &ac->store->contextId, sizeof(uint32_t)); + vlv->ctxid_len = sizeof(uint32_t); + vlv->vlv_result = 0; + vlv->contentCount = ac->store->num_entries; + if (target >= 0) { + vlv->targetPosition = target + 1; + } else if (vlv_details->type == 1) { + vlv->targetPosition = ac->store->num_entries + 1; + } else { + vlv->targetPosition = 0; + } + return LDB_SUCCESS; +} + + +/* vlv_search_callback() collects GUIDs found by the original search */ + +static int vlv_search_callback(struct ldb_request *req, struct ldb_reply *ares) +{ + struct vlv_context *ac; + struct results_store *store; + int ret; + + ac = talloc_get_type(req->context, struct vlv_context); + store = ac->store; + + if (!ares) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + if (ares->error != LDB_SUCCESS) { + return ldb_module_done(ac->req, ares->controls, + ares->response, ares->error); + } + + switch (ares->type) { + case LDB_REPLY_ENTRY: + if (store->results == NULL) { + store->num_entries = 0; + store->result_array_size = 16; + store->results = talloc_array(store, struct GUID, + store->result_array_size); + if (store->results == NULL) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + } else if (store->num_entries == store->result_array_size) { + store->result_array_size *= 2; + store->results = talloc_realloc(store, store->results, + struct GUID, + store->result_array_size); + if (store->results == NULL) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + } + store->results[store->num_entries] = \ + samdb_result_guid(ares->message, "objectGUID"); + store->num_entries++; + break; + + case LDB_REPLY_REFERRAL: + ret = save_referral(store, ares->referral); + if (ret != LDB_SUCCESS) { + return ldb_module_done(ac->req, NULL, NULL, ret); + } + break; + + case LDB_REPLY_DONE: + if (store->num_entries != 0) { + store->results = talloc_realloc(store, store->results, + struct GUID, + store->num_entries); + if (store->results == NULL) { + return ldb_module_done(ac->req, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + } + store->result_array_size = store->num_entries; + + ac->store->controls = talloc_move(ac->store, &ares->controls); + ret = vlv_results(ac, ares); + if (ret != LDB_SUCCESS) { + /* vlv_results will have called ldb_module_done + * if there was an error. + */ + return ret; + } + return ldb_module_done(ac->req, ac->controls, + ares->response, ret); + } + + return LDB_SUCCESS; +} + +static int copy_search_details(struct results_store *store, + struct ldb_vlv_req_control *vlv_ctrl, + struct ldb_server_sort_control *sort_ctrl) +{ + /* free the old details which are no longer going to be reachable. */ + if (store->vlv_details != NULL){ + TALLOC_FREE(store->vlv_details); + } + + if (store->sort_details != NULL){ + TALLOC_FREE(store->sort_details); + } + + store->vlv_details = talloc(store, struct ldb_vlv_req_control); + if (store->vlv_details == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + *store->vlv_details = *vlv_ctrl; + store->vlv_details->contextId = talloc_memdup(store, vlv_ctrl->contextId, + vlv_ctrl->ctxid_len); + if (store->vlv_details->contextId == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + if (vlv_ctrl->type == 1) { + char *v = talloc_array(store, char, + vlv_ctrl->match.gtOrEq.value_len + 1); + + if (v == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + memcpy(v, vlv_ctrl->match.gtOrEq.value, vlv_ctrl->match.gtOrEq.value_len); + v[vlv_ctrl->match.gtOrEq.value_len] = '\0'; + + store->vlv_details->match.gtOrEq.value = v; + } + + store->sort_details = talloc(store, struct ldb_server_sort_control); + if (store->sort_details == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + store->sort_details->attributeName = talloc_strdup(store, + sort_ctrl->attributeName); + if (store->sort_details->attributeName == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + if (sort_ctrl->orderingRule == NULL) { + store->sort_details->orderingRule = NULL; + } else { + store->sort_details->orderingRule = talloc_strdup(store, + sort_ctrl->orderingRule); + if (store->sort_details->orderingRule == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + } + store->sort_details->reverse = sort_ctrl->reverse; + + return LDB_SUCCESS; +} + + +static struct ldb_control ** +vlv_copy_down_controls(TALLOC_CTX *mem_ctx, struct ldb_control **controls) +{ + + struct ldb_control **new_controls; + unsigned int i, j, num_ctrls; + if (controls == NULL) { + return NULL; + } + + for (num_ctrls = 0; controls[num_ctrls]; num_ctrls++); + + new_controls = talloc_array(mem_ctx, struct ldb_control *, num_ctrls); + if (new_controls == NULL) { + return NULL; + } + + for (j = 0, i = 0; i < (num_ctrls); i++) { + struct ldb_control *control = controls[i]; + if (control->oid == NULL) { + break; + } + /* + * Do not re-use VLV, nor the server-sort, both are + * already handled here. + */ + if (strcmp(control->oid, LDB_CONTROL_VLV_REQ_OID) == 0 || + strcmp(control->oid, LDB_CONTROL_SERVER_SORT_OID) == 0) { + continue; + } + /* + * ASQ changes everything, do not copy it down for the + * per-GUID search + */ + if (strcmp(control->oid, LDB_CONTROL_ASQ_OID) == 0) { + continue; + } + new_controls[j] = talloc_steal(new_controls, control); + /* + * Sadly the caller is not obliged to make this a + * proper talloc tree, so we do so here. + */ + if (control->data) { + talloc_steal(control, control->data); + } + j++; + } + new_controls[j] = NULL; + return new_controls; +} + +static int vlv_search(struct ldb_module *module, struct ldb_request *req) +{ + struct ldb_context *ldb; + struct ldb_control *control; + struct ldb_control *sort_control; + struct private_data *priv; + struct ldb_vlv_req_control *vlv_ctrl; + struct ldb_server_sort_control **sort_ctrl; + struct ldb_request *search_req; + struct vlv_context *ac; + int ret, i, critical; + + ldb = ldb_module_get_ctx(module); + + control = ldb_request_get_control(req, LDB_CONTROL_VLV_REQ_OID); + if (control == NULL) { + /* There is no VLV. go on */ + return ldb_next_request(module, req); + } + critical = control->critical; + control->critical = 0; + + sort_control = ldb_request_get_control(req, LDB_CONTROL_SERVER_SORT_OID); + if (sort_control == NULL) { + /* VLV needs sort */ + return LDB_ERR_OPERATIONS_ERROR; + } + + vlv_ctrl = talloc_get_type(control->data, struct ldb_vlv_req_control); + if (vlv_ctrl == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + sort_ctrl = talloc_get_type(sort_control->data, struct ldb_server_sort_control *); + if (sort_ctrl == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + priv = talloc_get_type(ldb_module_get_private(module), + struct private_data); + + ac = talloc_zero(req, struct vlv_context); + if (ac == NULL) { + ldb_set_errstring(ldb, "Out of Memory"); + return LDB_ERR_OPERATIONS_ERROR; + } + + ac->module = module; + ac->req = req; + ac->priv = priv; + /* If there is no cookie, this is a new request, and we need to do the + * search in the database. Otherwise we try to refer to a previously + * saved search. + */ + if (vlv_ctrl->ctxid_len == 0) { + static const char * const attrs[2] = { + "objectGUID", NULL + }; + + ac->store = new_store(priv); + if (ac->store == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + ret = copy_search_details(ac->store, vlv_ctrl, sort_ctrl[0]); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = ldb_build_search_req_ex(&search_req, ldb, ac, + req->op.search.base, + req->op.search.scope, + req->op.search.tree, + attrs, + req->controls, + ac, + vlv_search_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + /* save it locally and remove it from the list */ + /* we do not need to replace them later as we + * are keeping the original req intact */ + if (!ldb_save_controls(control, search_req, NULL)) { + return LDB_ERR_OPERATIONS_ERROR; + } + + ac->store->down_controls = vlv_copy_down_controls(ac->store, + req->controls); + + if (ac->store->down_controls == NULL) { + return LDB_ERR_OPERATIONS_ERROR; + } + + return ldb_next_request(module, search_req); + + } else { + struct results_store *current = NULL; + uint8_t *id = vlv_ctrl->contextId; + + if (vlv_ctrl->ctxid_len != sizeof(uint32_t)){ + return LDB_ERR_UNWILLING_TO_PERFORM; + } + + for (i = 0; i < priv->n_stores; i++) { + current = priv->store[i]; + if (current == NULL) { + continue; + } + if (memcmp(¤t->contextId, id, sizeof(uint32_t)) == 0) { + current->timestamp = time(NULL); + break; + } + } + if (i == priv->n_stores) { + /* We were given a context id that we don't know about. */ + if (critical) { + return LDAP_UNAVAILABLE_CRITICAL_EXTENSION; + } else { + return ldb_next_request(module, req); + } + } + + ac->store = current; + ret = copy_search_details(ac->store, vlv_ctrl, sort_ctrl[0]); + if (ret != LDB_SUCCESS) { + return ret; + } + + ret = vlv_results(ac, NULL); + if (ret != LDB_SUCCESS) { + /* + * vlv_results() will have called ldb_module_done + * if there was an error. + */ + return ret; + } + return ldb_module_done(req, ac->controls, NULL, + LDB_SUCCESS); + } +} + + +static int vlv_request_init(struct ldb_module *module) +{ + struct ldb_context *ldb; + struct private_data *data; + int ret; + + ldb = ldb_module_get_ctx(module); + + data = talloc(module, struct private_data); + if (data == NULL) { + return LDB_ERR_OTHER; + } + + data->next_free_id = 1; + data->n_stores = VLV_N_SEARCHES; + data->store = talloc_zero_array(data, struct results_store *, data->n_stores); + + ldb_module_set_private(module, data); + + ret = ldb_mod_register_control(module, LDB_CONTROL_VLV_REQ_OID); + if (ret != LDB_SUCCESS) { + ldb_debug(ldb, LDB_DEBUG_WARNING, + "vlv:" + "Unable to register control with rootdse!"); + } + + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_vlv_module_ops = { + .name = "vlv", + .search = vlv_search, + .init_context = vlv_request_init +}; + +int ldb_vlv_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_vlv_module_ops); +} diff --git a/source4/dsdb/samdb/ldb_modules/wscript b/source4/dsdb/samdb/ldb_modules/wscript new file mode 100644 index 0000000..ce7f48d --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/wscript @@ -0,0 +1,38 @@ + +import sys +from waflib import Logs, Options +import samba3 + +def options(opt): + help = "Build with gpgme support (default=auto). " + help += "This requires gpgme devel and python packages " + help += "(e.g. libgpgme11-dev, python-gpgme on debian/ubuntu)." + + opt.samba_add_onoff_option('gpgme', default=True, help=(help)) + + return + +def configure(conf): + conf.SET_TARGET_TYPE('gpgme', 'EMPTY') + + if not Options.options.without_ad_dc \ + and Options.options.with_gpgme != False: + conf.find_program('gpgme-config', var='GPGME_CONFIG') + + if conf.env.GPGME_CONFIG: + conf.CHECK_CFG(path=conf.env.GPGME_CONFIG, args="--cflags --libs", + package="", uselib_store="gpgme", + msg='Checking for gpgme support') + + if conf.CHECK_FUNCS_IN('gpgme_new', 'gpgme', headers='gpgme.h'): + conf.DEFINE('ENABLE_GPGME', '1') + + if not conf.CONFIG_SET('ENABLE_GPGME'): + conf.fatal("GPGME support not found. " + "Try installing libgpgme11-dev or gpgme-devel " + "and python-gpgme. " + "Otherwise, use --without-gpgme to build without " + "GPGME support or --without-ad-dc to build without " + "the Samba AD DC. " + "GPGME support is required for the GPG encrypted " + "password sync feature") diff --git a/source4/dsdb/samdb/ldb_modules/wscript_build b/source4/dsdb/samdb/ldb_modules/wscript_build new file mode 100644 index 0000000..c3e8b54 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/wscript_build @@ -0,0 +1,60 @@ +#!/usr/bin/env python + +bld.SAMBA_LIBRARY('dsdb-module', + source=[], + deps='DSDB_MODULE_HELPERS DSDB_MODULE_HELPER_RIDALLOC', + private_library=True, + grouping_library=True) + +bld.SAMBA_SUBSYSTEM('DSDB_MODULE_HELPERS', + source='util.c acl_util.c schema_util.c netlogon.c', + autoproto='util_proto.h', + deps='ldb ndr samdb-common samba-security' + ) + +bld.SAMBA_SUBSYSTEM('DSDB_MODULE_HELPER_RIDALLOC', + source='ridalloc.c', + autoproto='ridalloc.h', + deps='MESSAGING', + ) + +# Build the cmocka unit tests +bld.SAMBA_BINARY('test_unique_object_sids', + source='tests/test_unique_object_sids.c', + deps=''' + talloc + samdb + cmocka + DSDB_MODULE_HELPERS + ''', + for_selftest=True) +bld.SAMBA_BINARY('test_encrypted_secrets_tdb', + source='tests/test_encrypted_secrets.c', + cflags='-DTEST_BE=\"tdb\"', + deps=''' + talloc + samba-util + samdb-common + samdb + cmocka + gnutls + DSDB_MODULE_HELPERS + ''', + for_selftest=True) +if conf.env.HAVE_LMDB: + bld.SAMBA_BINARY('test_encrypted_secrets_mdb', + source='tests/test_encrypted_secrets.c', + cflags='-DTEST_BE=\"mdb\"', + deps=''' + talloc + samba-util + samdb-common + samdb + cmocka + gnutls + DSDB_MODULE_HELPERS + ''', + for_selftest=True) + +if bld.AD_DC_BUILD_IS_ENABLED(): + bld.PROCESS_SEPARATE_RULE("server") diff --git a/source4/dsdb/samdb/ldb_modules/wscript_build_server b/source4/dsdb/samdb/ldb_modules/wscript_build_server new file mode 100644 index 0000000..7a63f43 --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/wscript_build_server @@ -0,0 +1,539 @@ +#!/usr/bin/env python + +bld.SAMBA_SUBSYSTEM('DSDB_MODULE_HELPERS_AUDIT', + source='audit_util.c', + autoproto='audit_util_proto.h', + deps='DSDB_MODULE_HELPERS audit_logging') + +# +# These tests require JANSSON, so we only build them if we are doing a +# build on the AD DC (where Jansson is required). +# + +bld.SAMBA_BINARY('test_audit_util', + source='tests/test_audit_util.c', + deps=''' + talloc + samba-util + samdb-common + samdb + cmocka + audit_logging + DSDB_MODULE_HELPERS + ''', + for_selftest=True) + +bld.SAMBA_BINARY('test_audit_log', + source='tests/test_audit_log.c', + deps=''' + talloc + samba-util + samdb-common + samdb + cmocka + audit_logging + DSDB_MODULE_HELPERS + DSDB_MODULE_HELPERS_AUDIT + ''', + for_selftest=True) + +bld.SAMBA_BINARY('test_audit_log_errors', + source='tests/test_audit_log_errors.c', + deps=''' + talloc + samba-util + samdb-common + samdb + cmocka + audit_logging + DSDB_MODULE_HELPERS + DSDB_MODULE_HELPERS_AUDIT + ''', + ldflags=''' + -Wl,--wrap,json_new_object + -Wl,--wrap,json_add_version + -Wl,--wrap,json_add_timestamp + ''', + for_selftest=True) + +bld.SAMBA_BINARY('test_group_audit', + source='tests/test_group_audit.c', + deps=''' + talloc + samba-util + samdb-common + samdb + cmocka + audit_logging + DSDB_MODULE_HELPERS + DSDB_MODULE_HELPERS_AUDIT + ''', + for_selftest=True) + +bld.SAMBA_BINARY('test_group_audit_errors', + source='tests/test_group_audit_errors.c', + deps=''' + talloc + samba-util + samdb-common + samdb + cmocka + audit_logging + DSDB_MODULE_HELPERS + DSDB_MODULE_HELPERS_AUDIT + ''', + ldflags=''' + -Wl,--wrap,json_new_object + -Wl,--wrap,json_add_version + -Wl,--wrap,json_add_timestamp + ''', + for_selftest=True) + +bld.SAMBA_MODULE('ldb_samba_dsdb', + source='samba_dsdb.c', + subsystem='ldb', + init_function='ldb_samba_dsdb_module_init', + module_init_name='ldb_init_module', + deps='samdb talloc ndr DSDB_MODULE_HELPERS', + internal_module=False, + ) + + +bld.SAMBA_MODULE('ldb_samba_secrets', + source='samba_secrets.c', + subsystem='ldb', + init_function='ldb_samba_secrets_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='samdb talloc ndr' + ) + + +bld.SAMBA_MODULE('ldb_objectguid', + source='objectguid.c', + subsystem='ldb', + init_function='ldb_objectguid_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='samdb talloc ndr DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_repl_meta_data', + source='repl_meta_data.c', + subsystem='ldb', + init_function='ldb_repl_meta_data_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='samdb talloc ndr NDR_DRSUAPI NDR_DRSBLOBS ndr DSDB_MODULE_HELPERS samba-security' + ) + + +bld.SAMBA_MODULE('ldb_schema_load', + source='schema_load.c', + subsystem='ldb', + init_function='ldb_schema_load_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='samdb talloc DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_schema_data', + source='schema_data.c', + subsystem='ldb', + init_function='ldb_schema_data_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='samdb talloc DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_samldb', + source='samldb.c', + subsystem='ldb', + init_function='ldb_samldb_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samdb DSDB_MODULE_HELPERS DSDB_MODULE_HELPER_RIDALLOC' + ) + + +bld.SAMBA_MODULE('ldb_samba3sam', + source='samba3sam.c', + subsystem='ldb', + init_function='ldb_samba3sam_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc ldb smbpasswdparser samba-security NDR_SECURITY' + ) + + +bld.SAMBA_MODULE('ldb_samba3sid', + source='samba3sid.c', + subsystem='ldb', + init_function='ldb_samba3sid_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc ldb samba-security NDR_SECURITY ldbsamba DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_rootdse', + source='rootdse.c', + subsystem='ldb', + init_function='ldb_rootdse_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samdb MESSAGING samba-security DSDB_MODULE_HELPERS RPC_NDR_IRPC' + ) + + +bld.SAMBA_MODULE('ldb_password_hash', + source='password_hash.c', + subsystem='ldb', + init_function='ldb_password_hash_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samdb LIBCLI_AUTH NDR_DRSBLOBS authkrb5 krb5 gpgme DSDB_MODULE_HELPERS crypt db-glue' + ) + + +bld.SAMBA_MODULE('ldb_extended_dn_in', + source='extended_dn_in.c', + subsystem='ldb', + init_function='ldb_extended_dn_in_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='ldb talloc samba-util DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_extended_dn_out', + source='extended_dn_out.c', + init_function='ldb_extended_dn_out_module_init', + module_init_name='ldb_init_module', + subsystem='ldb', + deps='talloc ndr samba-util samdb DSDB_MODULE_HELPERS', + internal_module=False, + ) + + +bld.SAMBA_MODULE('ldb_extended_dn_store', + source='extended_dn_store.c', + subsystem='ldb', + init_function='ldb_extended_dn_store_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-util samdb DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_show_deleted', + source='show_deleted.c', + subsystem='ldb', + init_function='ldb_show_deleted_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-util DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_partition', + source='partition.c partition_init.c partition_metadata.c', + autoproto='partition_proto.h', + subsystem='ldb', + init_function='ldb_partition_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samdb DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_new_partition', + source='new_partition.c', + subsystem='ldb', + init_function='ldb_new_partition_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samdb DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_update_keytab', + source='update_keytab.c', + subsystem='ldb', + init_function='ldb_update_keytab_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-credentials ldb com_err KERBEROS_SRV_KEYTAB SECRETS DSDB_MODULE_HELPERS' + ) + +bld.SAMBA_MODULE('ldb_secrets_tdb_sync', + source='secrets_tdb_sync.c', + subsystem='ldb', + init_function='ldb_secrets_tdb_sync_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc secrets3 DSDB_MODULE_HELPERS dbwrap gssapi' + ) + + +bld.SAMBA_MODULE('ldb_objectclass', + source='objectclass.c', + subsystem='ldb', + init_function='ldb_objectclass_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samdb DSDB_MODULE_HELPERS samba-util' + ) + + +bld.SAMBA_MODULE('ldb_objectclass_attrs', + source='objectclass_attrs.c', + subsystem='ldb', + init_function='ldb_objectclass_attrs_module_init', + module_init_name='ldb_init_module', + deps='talloc samdb DSDB_MODULE_HELPERS samba-util', + internal_module=False, + ) + + +bld.SAMBA_MODULE('ldb_subtree_rename', + source='subtree_rename.c', + subsystem='ldb', + init_function='ldb_subtree_rename_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-util ldb samdb-common DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_subtree_delete', + source='subtree_delete.c', + subsystem='ldb', + init_function='ldb_subtree_delete_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-util DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_linked_attributes', + source='linked_attributes.c', + subsystem='ldb', + init_function='ldb_linked_attributes_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samdb DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_ranged_results', + source='ranged_results.c', + subsystem='ldb', + init_function='ldb_ranged_results_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-util ldb' + ) + + +bld.SAMBA_MODULE('ldb_anr', + source='anr.c', + subsystem='ldb', + init_function='ldb_anr_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-util samdb' + ) + + +bld.SAMBA_MODULE('ldb_instancetype', + source='instancetype.c', + subsystem='ldb', + init_function='ldb_instancetype_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-util samdb DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_operational', + source='operational.c', + subsystem='ldb', + init_function='ldb_operational_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-util samdb-common DSDB_MODULE_HELPERS samdb' + ) + + +bld.SAMBA_MODULE('ldb_descriptor', + source='descriptor.c', + subsystem='ldb', + init_function='ldb_descriptor_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-security NDR_SECURITY samdb DSDB_MODULE_HELPERS' + ) + + +bld.SAMBA_MODULE('ldb_resolve_oids', + source='resolve_oids.c', + subsystem='ldb', + init_function='ldb_resolve_oids_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='samdb talloc ndr' + ) + + +bld.SAMBA_MODULE('ldb_acl', + source='acl.c', + subsystem='ldb', + init_function='ldb_acl_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-util samba-security samdb DSDB_MODULE_HELPERS krb5samba' + ) + + +bld.SAMBA_MODULE('ldb_lazy_commit', + source='lazy_commit.c', + subsystem='ldb', + internal_module=False, + module_init_name='ldb_init_module', + init_function='ldb_lazy_commit_module_init', + deps='samdb DSDB_MODULE_HELPERS' + ) + +bld.SAMBA_MODULE('ldb_aclread', + source='acl_read.c', + subsystem='ldb', + init_function='ldb_aclread_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-security samdb DSDB_MODULE_HELPERS', + ) + +bld.SAMBA_MODULE('ldb_dirsync', + source='dirsync.c', + subsystem='ldb', + init_function='ldb_dirsync_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-security samdb DSDB_MODULE_HELPERS' + ) + +bld.SAMBA_MODULE('ldb_dsdb_notification', + source='dsdb_notification.c', + subsystem='ldb', + init_function='ldb_dsdb_notification_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-security samdb DSDB_MODULE_HELPERS' + ) + +bld.SAMBA_MODULE('ldb_dns_notify', + source='dns_notify.c', + subsystem='ldb', + init_function='ldb_dns_notify_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samdb DSDB_MODULE_HELPERS MESSAGING RPC_NDR_IRPC' + ) + +bld.SAMBA_MODULE('tombstone_reanimate', + source='tombstone_reanimate.c', + subsystem='ldb', + init_function='ldb_tombstone_reanimate_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='talloc samba-util DSDB_MODULE_HELPERS' + ) + +bld.SAMBA_MODULE('ldb_vlv', + 'vlv_pagination.c', + init_function='ldb_vlv_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='samdb-common', + subsystem='ldb' + ) + +bld.SAMBA_MODULE('ldb_paged_results', + 'paged_results.c', + init_function='ldb_dsdb_paged_results_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='samdb-common', + subsystem='ldb' + ) + +bld.SAMBA_MODULE('ldb_unique_object_sids', + 'unique_object_sids.c', + init_function='ldb_unique_object_sids_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='samdb-common DSDB_MODULE_HELPERS', + subsystem='ldb' + ) + +bld.SAMBA_MODULE('ldb_encrypted_secrets', + source='encrypted_secrets.c', + subsystem='ldb', + init_function='ldb_encrypted_secrets_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps=''' + talloc + samba-util + samdb-common + DSDB_MODULE_HELPERS + samdb + gnutls + ''' + ) + +bld.SAMBA_MODULE('ldb_audit_log', + source='audit_log.c', + subsystem='ldb', + init_function='ldb_audit_log_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps=''' + audit_logging + talloc + samba-util + samdb-common + DSDB_MODULE_HELPERS_AUDIT + samdb + ''' + ) + +bld.SAMBA_MODULE('ldb_group_audit_log', + source='group_audit.c', + subsystem='ldb', + init_function='ldb_group_audit_log_module_init', + module_init_name='ldb_init_module', + internal_module=False, + deps=''' + audit_logging + talloc + samba-util + samdb-common + DSDB_MODULE_HELPERS_AUDIT + samdb + ''' + ) + + +bld.SAMBA_MODULE('count_attrs', + 'count_attrs.c', + init_function='ldb_count_attrs_init', + module_init_name='ldb_init_module', + internal_module=False, + deps='samdb-common DSDB_MODULE_HELPERS', + subsystem='ldb' +) |