summaryrefslogtreecommitdiffstats
path: root/source4/dsdb
diff options
context:
space:
mode:
Diffstat (limited to 'source4/dsdb')
-rw-r--r--source4/dsdb/common/dsdb_access.c183
-rw-r--r--source4/dsdb/common/dsdb_dn.c571
-rw-r--r--source4/dsdb/common/dsdb_dn.h21
-rw-r--r--source4/dsdb/common/rodc_helper.c284
-rw-r--r--source4/dsdb/common/tests/dsdb.c93
-rw-r--r--source4/dsdb/common/tests/dsdb_dn.c374
-rw-r--r--source4/dsdb/common/util.c6796
-rw-r--r--source4/dsdb/common/util.h101
-rw-r--r--source4/dsdb/common/util_groups.c200
-rw-r--r--source4/dsdb/common/util_links.c229
-rw-r--r--source4/dsdb/common/util_links.h48
-rw-r--r--source4/dsdb/common/util_samr.c593
-rw-r--r--source4/dsdb/common/util_trusts.c3443
-rw-r--r--source4/dsdb/dns/dns_update.c464
-rw-r--r--source4/dsdb/kcc/garbage_collect_tombstones.c347
-rw-r--r--source4/dsdb/kcc/garbage_collect_tombstones.h34
-rw-r--r--source4/dsdb/kcc/kcc_connection.c252
-rw-r--r--source4/dsdb/kcc/kcc_connection.h39
-rw-r--r--source4/dsdb/kcc/kcc_drs_replica_info.c911
-rw-r--r--source4/dsdb/kcc/kcc_periodic.c825
-rw-r--r--source4/dsdb/kcc/kcc_service.c364
-rw-r--r--source4/dsdb/kcc/kcc_service.h101
-rw-r--r--source4/dsdb/kcc/scavenge_dns_records.c531
-rw-r--r--source4/dsdb/kcc/scavenge_dns_records.h36
-rw-r--r--source4/dsdb/pydsdb.c1837
-rw-r--r--source4/dsdb/repl/drepl_extended.c211
-rw-r--r--source4/dsdb/repl/drepl_fsmo.c147
-rw-r--r--source4/dsdb/repl/drepl_notify.c485
-rw-r--r--source4/dsdb/repl/drepl_out_helpers.c1356
-rw-r--r--source4/dsdb/repl/drepl_out_helpers.h26
-rw-r--r--source4/dsdb/repl/drepl_out_pull.c260
-rw-r--r--source4/dsdb/repl/drepl_partitions.c665
-rw-r--r--source4/dsdb/repl/drepl_periodic.c157
-rw-r--r--source4/dsdb/repl/drepl_replica.c62
-rw-r--r--source4/dsdb/repl/drepl_ridalloc.c265
-rw-r--r--source4/dsdb/repl/drepl_secret.c146
-rw-r--r--source4/dsdb/repl/drepl_service.c545
-rw-r--r--source4/dsdb/repl/drepl_service.h251
-rw-r--r--source4/dsdb/repl/replicated_objects.c1283
-rw-r--r--source4/dsdb/samdb.pc.in10
-rw-r--r--source4/dsdb/samdb/cracknames.c1804
-rw-r--r--source4/dsdb/samdb/ldb_modules/acl.c2892
-rw-r--r--source4/dsdb/samdb/ldb_modules/acl_read.c1302
-rw-r--r--source4/dsdb/samdb/ldb_modules/acl_util.c376
-rw-r--r--source4/dsdb/samdb/ldb_modules/anr.c440
-rw-r--r--source4/dsdb/samdb/ldb_modules/audit_log.c1913
-rw-r--r--source4/dsdb/samdb/ldb_modules/audit_util.c697
-rw-r--r--source4/dsdb/samdb/ldb_modules/count_attrs.c644
-rw-r--r--source4/dsdb/samdb/ldb_modules/descriptor.c2069
-rw-r--r--source4/dsdb/samdb/ldb_modules/dirsync.c1381
-rw-r--r--source4/dsdb/samdb/ldb_modules/dns_notify.c450
-rw-r--r--source4/dsdb/samdb/ldb_modules/dsdb_notification.c262
-rw-r--r--source4/dsdb/samdb/ldb_modules/encrypted_secrets.c1401
-rw-r--r--source4/dsdb/samdb/ldb_modules/extended_dn_in.c801
-rw-r--r--source4/dsdb/samdb/ldb_modules/extended_dn_out.c672
-rw-r--r--source4/dsdb/samdb/ldb_modules/extended_dn_store.c830
-rw-r--r--source4/dsdb/samdb/ldb_modules/group_audit.c1555
-rw-r--r--source4/dsdb/samdb/ldb_modules/instancetype.c173
-rw-r--r--source4/dsdb/samdb/ldb_modules/lazy_commit.c128
-rw-r--r--source4/dsdb/samdb/ldb_modules/linked_attributes.c1587
-rw-r--r--source4/dsdb/samdb/ldb_modules/netlogon.c516
-rw-r--r--source4/dsdb/samdb/ldb_modules/new_partition.c213
-rw-r--r--source4/dsdb/samdb/ldb_modules/objectclass.c1477
-rw-r--r--source4/dsdb/samdb/ldb_modules/objectclass_attrs.c752
-rw-r--r--source4/dsdb/samdb/ldb_modules/objectguid.c252
-rw-r--r--source4/dsdb/samdb/ldb_modules/operational.c1920
-rw-r--r--source4/dsdb/samdb/ldb_modules/paged_results.c888
-rw-r--r--source4/dsdb/samdb/ldb_modules/partition.c1721
-rw-r--r--source4/dsdb/samdb/ldb_modules/partition.h64
-rw-r--r--source4/dsdb/samdb/ldb_modules/partition_init.c885
-rw-r--r--source4/dsdb/samdb/ldb_modules/partition_metadata.c596
-rw-r--r--source4/dsdb/samdb/ldb_modules/password_hash.c5220
-rw-r--r--source4/dsdb/samdb/ldb_modules/password_modules.h3
-rw-r--r--source4/dsdb/samdb/ldb_modules/proxy.c415
-rw-r--r--source4/dsdb/samdb/ldb_modules/ranged_results.c295
-rw-r--r--source4/dsdb/samdb/ldb_modules/repl_meta_data.c8764
-rw-r--r--source4/dsdb/samdb/ldb_modules/resolve_oids.c710
-rw-r--r--source4/dsdb/samdb/ldb_modules/ridalloc.c829
-rw-r--r--source4/dsdb/samdb/ldb_modules/rootdse.c1802
-rw-r--r--source4/dsdb/samdb/ldb_modules/samba3sam.c977
-rw-r--r--source4/dsdb/samdb/ldb_modules/samba3sid.c207
-rw-r--r--source4/dsdb/samdb/ldb_modules/samba_dsdb.c611
-rw-r--r--source4/dsdb/samdb/ldb_modules/samba_secrets.c103
-rw-r--r--source4/dsdb/samdb/ldb_modules/samldb.c5736
-rw-r--r--source4/dsdb/samdb/ldb_modules/schema_data.c691
-rw-r--r--source4/dsdb/samdb/ldb_modules/schema_load.c657
-rw-r--r--source4/dsdb/samdb/ldb_modules/schema_util.c345
-rw-r--r--source4/dsdb/samdb/ldb_modules/secrets_tdb_sync.c532
-rw-r--r--source4/dsdb/samdb/ldb_modules/show_deleted.c220
-rw-r--r--source4/dsdb/samdb/ldb_modules/subtree_delete.c142
-rw-r--r--source4/dsdb/samdb/ldb_modules/subtree_rename.c202
-rwxr-xr-xsource4/dsdb/samdb/ldb_modules/tests/possibleinferiors.py265
-rw-r--r--source4/dsdb/samdb/ldb_modules/tests/test_audit_log.c2305
-rw-r--r--source4/dsdb/samdb/ldb_modules/tests/test_audit_log_errors.c639
-rw-r--r--source4/dsdb/samdb/ldb_modules/tests/test_audit_util.c1261
-rw-r--r--source4/dsdb/samdb/ldb_modules/tests/test_encrypted_secrets.c973
-rw-r--r--source4/dsdb/samdb/ldb_modules/tests/test_group_audit.c2022
-rw-r--r--source4/dsdb/samdb/ldb_modules/tests/test_group_audit.valgrind19
-rw-r--r--source4/dsdb/samdb/ldb_modules/tests/test_group_audit_errors.c266
-rw-r--r--source4/dsdb/samdb/ldb_modules/tests/test_unique_object_sids.c514
-rw-r--r--source4/dsdb/samdb/ldb_modules/tombstone_reanimate.c423
-rw-r--r--source4/dsdb/samdb/ldb_modules/unique_object_sids.c262
-rw-r--r--source4/dsdb/samdb/ldb_modules/update_keytab.c510
-rw-r--r--source4/dsdb/samdb/ldb_modules/util.c1921
-rw-r--r--source4/dsdb/samdb/ldb_modules/util.h43
-rw-r--r--source4/dsdb/samdb/ldb_modules/vlv_pagination.c946
-rw-r--r--source4/dsdb/samdb/ldb_modules/wscript38
-rw-r--r--source4/dsdb/samdb/ldb_modules/wscript_build60
-rw-r--r--source4/dsdb/samdb/ldb_modules/wscript_build_server539
-rw-r--r--source4/dsdb/samdb/samdb.c344
-rw-r--r--source4/dsdb/samdb/samdb.h410
-rw-r--r--source4/dsdb/samdb/samdb_privilege.c134
-rw-r--r--source4/dsdb/schema/dsdb_dn.c102
-rw-r--r--source4/dsdb/schema/prefixmap.h54
-rw-r--r--source4/dsdb/schema/schema.h351
-rw-r--r--source4/dsdb/schema/schema_convert_to_ol.c377
-rw-r--r--source4/dsdb/schema/schema_description.c397
-rw-r--r--source4/dsdb/schema/schema_filtered.c101
-rw-r--r--source4/dsdb/schema/schema_inferiors.c347
-rw-r--r--source4/dsdb/schema/schema_info_attr.c235
-rw-r--r--source4/dsdb/schema/schema_init.c1038
-rw-r--r--source4/dsdb/schema/schema_prefixmap.c710
-rw-r--r--source4/dsdb/schema/schema_query.c620
-rw-r--r--source4/dsdb/schema/schema_set.c1191
-rw-r--r--source4/dsdb/schema/schema_syntax.c2811
-rw-r--r--source4/dsdb/schema/tests/schema_syntax.c271
-rwxr-xr-xsource4/dsdb/tests/python/acl.py5795
-rwxr-xr-xsource4/dsdb/tests/python/acl_modify.py179
-rw-r--r--source4/dsdb/tests/python/ad_dc_medley_performance.py523
-rw-r--r--source4/dsdb/tests/python/ad_dc_multi_bind.py88
-rw-r--r--source4/dsdb/tests/python/ad_dc_performance.py340
-rw-r--r--source4/dsdb/tests/python/ad_dc_provision_performance.py131
-rw-r--r--source4/dsdb/tests/python/ad_dc_search_performance.py296
-rw-r--r--source4/dsdb/tests/python/asq.py225
-rw-r--r--source4/dsdb/tests/python/attr_from_server.py149
-rwxr-xr-xsource4/dsdb/tests/python/confidential_attr.py1137
-rwxr-xr-xsource4/dsdb/tests/python/deletetest.py565
-rwxr-xr-xsource4/dsdb/tests/python/dirsync.py1107
-rw-r--r--source4/dsdb/tests/python/dsdb_schema_info.py203
-rw-r--r--source4/dsdb/tests/python/large_ldap.py338
-rwxr-xr-xsource4/dsdb/tests/python/ldap.py3332
-rw-r--r--source4/dsdb/tests/python/ldap_modify_order.py371
-rwxr-xr-xsource4/dsdb/tests/python/ldap_schema.py1597
-rwxr-xr-xsource4/dsdb/tests/python/ldap_syntaxes.py388
-rw-r--r--source4/dsdb/tests/python/linked_attributes.py839
-rwxr-xr-xsource4/dsdb/tests/python/login_basics.py273
-rw-r--r--source4/dsdb/tests/python/ndr_pack_performance.py215
-rwxr-xr-xsource4/dsdb/tests/python/notification.py367
-rwxr-xr-xsource4/dsdb/tests/python/password_lockout.py1704
-rw-r--r--source4/dsdb/tests/python/password_lockout_base.py785
-rw-r--r--source4/dsdb/tests/python/password_settings.py876
-rwxr-xr-xsource4/dsdb/tests/python/passwords.py1451
-rw-r--r--source4/dsdb/tests/python/priv_attrs.py392
-rwxr-xr-xsource4/dsdb/tests/python/rodc.py254
-rw-r--r--source4/dsdb/tests/python/rodc_rwdc.py1324
-rwxr-xr-xsource4/dsdb/tests/python/sam.py3874
-rwxr-xr-xsource4/dsdb/tests/python/sec_descriptor.py2705
-rwxr-xr-xsource4/dsdb/tests/python/sites.py637
-rw-r--r--source4/dsdb/tests/python/sort.py379
-rw-r--r--source4/dsdb/tests/python/subtree_rename.py422
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_account_locality_device-non-admin.expected31
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_account_locality_device.expected34
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_container_flags-non-admin.expected129
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_container_flags.expected134
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue-non-admin.expected127
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue.expected138
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_inapplicable-non-admin.expected31
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_inapplicable.expected34
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_member-non-admin.expected127
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_member.expected190
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_mixed-non-admin.expected128
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_mixed.expected143
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_mixed2-non-admin.expected128
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_mixed2.expected143
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_objectclass-non-admin.expected31
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_objectclass.expected35
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_objectclass2-non-admin.expected726
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_objectclass2.expected735
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_singlevalue-non-admin.expected727
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_singlevalue.expected740
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable-non-admin.expected127
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable.expected127
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_telephone-non-admin.expected727
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_telephone.expected752
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete-non-admin.expected727
-rw-r--r--source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete.expected736
-rw-r--r--source4/dsdb/tests/python/testdata/simplesort.expected8
-rw-r--r--source4/dsdb/tests/python/testdata/unicodesort.expected16
-rwxr-xr-xsource4/dsdb/tests/python/token_group.py738
-rwxr-xr-xsource4/dsdb/tests/python/tombstone_reanimation.py958
-rw-r--r--source4/dsdb/tests/python/unicodepwd_encrypted.py151
-rwxr-xr-xsource4/dsdb/tests/python/urgent_replication.py339
-rwxr-xr-xsource4/dsdb/tests/python/user_account_control.py1298
-rw-r--r--source4/dsdb/tests/python/vlv.py1786
-rw-r--r--source4/dsdb/wscript_build83
195 files changed, 152512 insertions, 0 deletions
diff --git a/source4/dsdb/common/dsdb_access.c b/source4/dsdb/common/dsdb_access.c
new file mode 100644
index 0000000..6edae35
--- /dev/null
+++ b/source4/dsdb/common/dsdb_access.c
@@ -0,0 +1,183 @@
+/*
+ ldb database library
+
+ 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: dsdb_access
+ *
+ * Description: utility functions for access checking on objects
+ *
+ * Authors: Nadezhda Ivanova
+ */
+
+#include "includes.h"
+#include "ldb.h"
+#include "ldb_module.h"
+#include "ldb_errors.h"
+#include "libcli/security/security.h"
+#include "librpc/gen_ndr/ndr_security.h"
+#include "libcli/ldap/ldap_ndr.h"
+#include "param/param.h"
+#include "auth/auth.h"
+#include "dsdb/samdb/samdb.h"
+#include "dsdb/common/util.h"
+
+void dsdb_acl_debug(struct security_descriptor *sd,
+ struct security_token *token,
+ struct ldb_dn *dn,
+ bool denied,
+ int level)
+{
+ if (denied) {
+ DEBUG(level, ("Access on %s denied\n", ldb_dn_get_linearized(dn)));
+ } else {
+ DEBUG(level, ("Access on %s granted\n", ldb_dn_get_linearized(dn)));
+ }
+
+ DEBUG(level,("Security context: %s\n",
+ ndr_print_struct_string(0,(ndr_print_fn_t)ndr_print_security_token,"", token)));
+ DEBUG(level,("Security descriptor: %s\n",
+ ndr_print_struct_string(0,(ndr_print_fn_t)ndr_print_security_descriptor,"", sd)));
+}
+
+int dsdb_get_sd_from_ldb_message(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message *acl_res,
+ struct security_descriptor **sd)
+{
+ struct ldb_message_element *sd_element;
+ 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");
+ }
+ *sd = talloc(mem_ctx, 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);
+ }
+
+ return LDB_SUCCESS;
+}
+
+int dsdb_check_access_on_dn_internal(struct ldb_context *ldb,
+ struct ldb_result *acl_res,
+ TALLOC_CTX *mem_ctx,
+ struct security_token *token,
+ struct ldb_dn *dn,
+ uint32_t access_mask,
+ const struct GUID *guid)
+{
+ struct security_descriptor *sd = NULL;
+ struct dom_sid *sid = NULL;
+ struct object_tree *root = NULL;
+ NTSTATUS status;
+ uint32_t access_granted;
+ int ret;
+
+ ret = dsdb_get_sd_from_ldb_message(ldb, mem_ctx, acl_res->msgs[0], &sd);
+ if (ret != LDB_SUCCESS) {
+ return ldb_operr(ldb);
+ }
+
+ sid = samdb_result_dom_sid(mem_ctx, acl_res->msgs[0], "objectSid");
+ if (guid) {
+ if (!insert_in_object_tree(mem_ctx, guid, access_mask, NULL,
+ &root)) {
+ TALLOC_FREE(sd);
+ TALLOC_FREE(sid);
+ return ldb_operr(ldb);
+ }
+ }
+ status = sec_access_check_ds(sd, token,
+ access_mask,
+ &access_granted,
+ root,
+ sid);
+ if (!NT_STATUS_IS_OK(status)) {
+ dsdb_acl_debug(sd,
+ token,
+ dn,
+ true,
+ 10);
+ ldb_asprintf_errstring(ldb,
+ "dsdb_access: Access check failed on %s",
+ ldb_dn_get_linearized(dn));
+ TALLOC_FREE(sd);
+ TALLOC_FREE(sid);
+ return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS;
+ }
+ return LDB_SUCCESS;
+}
+
+/* performs an access check from outside 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_check_access_on_dn(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *dn,
+ struct security_token *token,
+ uint32_t access_mask,
+ const char *ext_right)
+{
+ int ret;
+ struct GUID guid;
+ struct ldb_result *acl_res;
+ static const char *acl_attrs[] = {
+ "nTSecurityDescriptor",
+ "objectSid",
+ NULL
+ };
+
+ if (ext_right != NULL) {
+ NTSTATUS status = GUID_from_string(ext_right, &guid);
+ if (!NT_STATUS_IS_OK(status)) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ }
+
+ /*
+ * We need AS_SYSTEM in order to get the nTSecurityDescriptor attribute.
+ * Also the result of this search not controlled by the client
+ * nor is the result exposed to the client.
+ */
+ ret = dsdb_search_dn(ldb, mem_ctx, &acl_res, dn, acl_attrs,
+ DSDB_FLAG_AS_SYSTEM | DSDB_SEARCH_SHOW_RECYCLED);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(10,("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,
+ token,
+ dn,
+ access_mask,
+ ext_right ? &guid : NULL);
+}
+
diff --git a/source4/dsdb/common/dsdb_dn.c b/source4/dsdb/common/dsdb_dn.c
new file mode 100644
index 0000000..63a8628
--- /dev/null
+++ b/source4/dsdb/common/dsdb_dn.c
@@ -0,0 +1,571 @@
+/*
+ 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/>.
+*/
+
+#include "includes.h"
+#include "dsdb/samdb/samdb.h"
+#include <ldb_module.h>
+#include "librpc/ndr/libndr.h"
+#include "libcli/security/dom_sid.h"
+#include "lib/util/smb_strtox.h"
+
+enum dsdb_dn_format dsdb_dn_oid_to_format(const char *oid)
+{
+ if (strcmp(oid, LDB_SYNTAX_DN) == 0) {
+ return DSDB_NORMAL_DN;
+ } else if (strcmp(oid, DSDB_SYNTAX_BINARY_DN) == 0) {
+ return DSDB_BINARY_DN;
+ } else if (strcmp(oid, DSDB_SYNTAX_STRING_DN) == 0) {
+ return DSDB_STRING_DN;
+ } else if (strcmp(oid, DSDB_SYNTAX_OR_NAME) == 0) {
+ return DSDB_NORMAL_DN;
+ } else {
+ return DSDB_INVALID_DN;
+ }
+}
+
+static struct dsdb_dn *dsdb_dn_construct_internal(TALLOC_CTX *mem_ctx,
+ struct ldb_dn *dn,
+ DATA_BLOB extra_part,
+ enum dsdb_dn_format dn_format,
+ const char *oid)
+{
+ struct dsdb_dn *dsdb_dn = NULL;
+
+ switch (dn_format) {
+ case DSDB_BINARY_DN:
+ case DSDB_STRING_DN:
+ break;
+ case DSDB_NORMAL_DN:
+ if (extra_part.length != 0) {
+ errno = EINVAL;
+ return NULL;
+ }
+ break;
+ case DSDB_INVALID_DN:
+ default:
+ errno = EINVAL;
+ return NULL;
+ }
+
+ dsdb_dn = talloc(mem_ctx, struct dsdb_dn);
+ if (!dsdb_dn) {
+ errno = ENOMEM;
+ return NULL;
+ }
+ dsdb_dn->dn = talloc_steal(dsdb_dn, dn);
+ dsdb_dn->extra_part = extra_part;
+ dsdb_dn->dn_format = dn_format;
+
+ dsdb_dn->oid = oid;
+ talloc_steal(dsdb_dn, extra_part.data);
+ return dsdb_dn;
+}
+
+struct dsdb_dn *dsdb_dn_construct(TALLOC_CTX *mem_ctx, struct ldb_dn *dn, DATA_BLOB extra_part,
+ const char *oid)
+{
+ enum dsdb_dn_format dn_format = dsdb_dn_oid_to_format(oid);
+ return dsdb_dn_construct_internal(mem_ctx, dn, extra_part, dn_format, oid);
+}
+
+struct dsdb_dn *dsdb_dn_parse_trusted(TALLOC_CTX *mem_ctx, struct ldb_context *ldb,
+ const struct ldb_val *dn_blob, const char *dn_oid)
+{
+ struct dsdb_dn *dsdb_dn;
+ struct ldb_dn *dn;
+ size_t len;
+ TALLOC_CTX *tmp_ctx;
+ char *p1;
+ char *p2;
+ uint32_t blen;
+ struct ldb_val bval;
+ struct ldb_val dval;
+ char *dn_str;
+ int error = 0;
+
+ enum dsdb_dn_format dn_format = dsdb_dn_oid_to_format(dn_oid);
+
+ if (dn_blob == NULL || dn_blob->data == NULL || dn_blob->length == 0) {
+ return NULL;
+ }
+
+ switch (dn_format) {
+ case DSDB_INVALID_DN:
+ return NULL;
+ case DSDB_NORMAL_DN:
+ {
+ dn = ldb_dn_from_ldb_val(mem_ctx, ldb, dn_blob);
+ if (!dn) {
+ talloc_free(dn);
+ return NULL;
+ }
+ return dsdb_dn_construct_internal(mem_ctx, dn, data_blob_null, dn_format, dn_oid);
+ }
+ case DSDB_BINARY_DN:
+ if (dn_blob->length < 2 || dn_blob->data[0] != 'B' || dn_blob->data[1] != ':') {
+ return NULL;
+ }
+ break;
+ case DSDB_STRING_DN:
+ if (dn_blob->length < 2 || dn_blob->data[0] != 'S' || dn_blob->data[1] != ':') {
+ return NULL;
+ }
+ break;
+ default:
+ return NULL;
+ }
+
+ if (strlen((const char*)dn_blob->data) != dn_blob->length) {
+ /* The RDN must not contain a character with value 0x0 */
+ return NULL;
+ }
+
+ tmp_ctx = talloc_new(mem_ctx);
+ if (tmp_ctx == NULL) {
+ return NULL;
+ }
+
+ len = dn_blob->length - 2;
+ p1 = talloc_strndup(tmp_ctx, (const char *)dn_blob->data + 2, len);
+ if (!p1) {
+ goto failed;
+ }
+
+ errno = 0;
+ blen = smb_strtoul(p1, &p2, 10, &error, SMB_STR_STANDARD);
+ if (error != 0) {
+ DEBUG(10, (__location__ ": failed\n"));
+ goto failed;
+ }
+ if (p2 == NULL) {
+ DEBUG(10, (__location__ ": failed\n"));
+ goto failed;
+ }
+ if (p2[0] != ':') {
+ DEBUG(10, (__location__ ": failed\n"));
+ goto failed;
+ }
+ len -= PTR_DIFF(p2,p1);//???
+ p1 = p2+1;
+ len--;
+
+ if (blen >= len) {
+ DEBUG(10, (__location__ ": blen=%u len=%u\n", (unsigned)blen, (unsigned)len));
+ goto failed;
+ }
+
+ p2 = p1 + blen;
+ if (p2[0] != ':') {
+ DEBUG(10, (__location__ ": %s", p2));
+ goto failed;
+ }
+ dn_str = p2+1;
+
+
+ switch (dn_format) {
+ case DSDB_BINARY_DN:
+ if ((blen % 2 != 0)) {
+ DEBUG(10, (__location__ ": blen=%u - not an even number\n", (unsigned)blen));
+ goto failed;
+ }
+
+ if (blen >= 2) {
+ bval.length = (blen/2)+1;
+ bval.data = talloc_size(tmp_ctx, bval.length);
+ if (bval.data == NULL) {
+ DEBUG(10, (__location__ ": err\n"));
+ goto failed;
+ }
+ bval.data[bval.length-1] = 0;
+
+ bval.length = strhex_to_str((char *)bval.data, bval.length,
+ p1, blen);
+ if (bval.length != (blen / 2)) {
+ DEBUG(10, (__location__ ": non hexadecimal characters found in binary prefix\n"));
+ goto failed;
+ }
+ } else {
+ bval = data_blob_null;
+ }
+
+ break;
+ case DSDB_STRING_DN:
+ bval = data_blob(p1, blen);
+ break;
+ default:
+ /* never reached */
+ return NULL;
+ }
+
+
+ dval.data = (uint8_t *)dn_str;
+ dval.length = strlen(dn_str);
+
+ dn = ldb_dn_from_ldb_val(tmp_ctx, ldb, &dval);
+ if (!dn) {
+ DEBUG(10, (__location__ ": err\n"));
+ goto failed;
+ }
+
+ dsdb_dn = dsdb_dn_construct(mem_ctx, dn, bval, dn_oid);
+
+ talloc_free(tmp_ctx);
+ return dsdb_dn;
+
+failed:
+ talloc_free(tmp_ctx);
+ return NULL;
+}
+
+struct dsdb_dn *dsdb_dn_parse(TALLOC_CTX *mem_ctx, struct ldb_context *ldb,
+ const struct ldb_val *dn_blob, const char *dn_oid)
+{
+ struct dsdb_dn *dsdb_dn = dsdb_dn_parse_trusted(mem_ctx, ldb,
+ dn_blob, dn_oid);
+ if (dsdb_dn == NULL) {
+ return NULL;
+ }
+ if (ldb_dn_validate(dsdb_dn->dn) == false) {
+ DEBUG(10, ("could not parse %.*s as a %s DN\n",
+ (int)dn_blob->length, dn_blob->data,
+ dn_oid));
+ return NULL;
+ }
+ return dsdb_dn;
+}
+
+static char *dsdb_dn_get_with_postfix(TALLOC_CTX *mem_ctx,
+ struct dsdb_dn *dsdb_dn,
+ const char *postfix)
+{
+ if (!postfix) {
+ return NULL;
+ }
+
+ switch (dsdb_dn->dn_format) {
+ case DSDB_NORMAL_DN:
+ {
+ return talloc_strdup(mem_ctx, postfix);
+ }
+ case DSDB_BINARY_DN:
+ {
+ char *hexstr = data_blob_hex_string_upper(mem_ctx, &dsdb_dn->extra_part);
+
+ char *p = talloc_asprintf(mem_ctx, "B:%u:%s:%s", (unsigned)(dsdb_dn->extra_part.length*2), hexstr,
+ postfix);
+ talloc_free(hexstr);
+ return p;
+ }
+ case DSDB_STRING_DN:
+ {
+ return talloc_asprintf(mem_ctx, "S:%u:%*.*s:%s",
+ (unsigned)(dsdb_dn->extra_part.length),
+ (int)(dsdb_dn->extra_part.length),
+ (int)(dsdb_dn->extra_part.length),
+ (const char *)dsdb_dn->extra_part.data,
+ postfix);
+ }
+ default:
+ return NULL;
+ }
+}
+
+char *dsdb_dn_get_linearized(TALLOC_CTX *mem_ctx,
+ struct dsdb_dn *dsdb_dn)
+{
+ const char *postfix = ldb_dn_get_linearized(dsdb_dn->dn);
+ return dsdb_dn_get_with_postfix(mem_ctx, dsdb_dn, postfix);
+}
+
+char *dsdb_dn_get_casefold(TALLOC_CTX *mem_ctx,
+ struct dsdb_dn *dsdb_dn)
+{
+ const char *postfix = ldb_dn_get_casefold(dsdb_dn->dn);
+ return dsdb_dn_get_with_postfix(mem_ctx, dsdb_dn, postfix);
+}
+
+char *dsdb_dn_get_extended_linearized(TALLOC_CTX *mem_ctx,
+ struct dsdb_dn *dsdb_dn,
+ int mode)
+{
+ char *postfix = ldb_dn_get_extended_linearized(mem_ctx, dsdb_dn->dn, mode);
+ char *ret = dsdb_dn_get_with_postfix(mem_ctx, dsdb_dn, postfix);
+ talloc_free(postfix);
+ return ret;
+}
+
+int dsdb_dn_binary_canonicalise(struct ldb_context *ldb, void *mem_ctx,
+ const struct ldb_val *in, struct ldb_val *out)
+{
+ struct dsdb_dn *dsdb_dn = dsdb_dn_parse(mem_ctx, ldb, in, DSDB_SYNTAX_BINARY_DN);
+
+ if (!dsdb_dn) {
+ return -1;
+ }
+ *out = data_blob_string_const(dsdb_dn_get_casefold(mem_ctx, dsdb_dn));
+ talloc_free(dsdb_dn);
+ if (!out->data) {
+ return -1;
+ }
+ return 0;
+}
+
+int dsdb_dn_binary_comparison(struct ldb_context *ldb, void *mem_ctx,
+ const struct ldb_val *v1,
+ const struct ldb_val *v2)
+{
+ return ldb_any_comparison(ldb, mem_ctx, dsdb_dn_binary_canonicalise, v1, v2);
+}
+
+int dsdb_dn_string_canonicalise(struct ldb_context *ldb, void *mem_ctx,
+ const struct ldb_val *in, struct ldb_val *out)
+{
+ struct dsdb_dn *dsdb_dn = dsdb_dn_parse(mem_ctx, ldb, in, DSDB_SYNTAX_STRING_DN);
+
+ if (!dsdb_dn) {
+ return -1;
+ }
+ *out = data_blob_string_const(dsdb_dn_get_casefold(mem_ctx, dsdb_dn));
+ talloc_free(dsdb_dn);
+ if (!out->data) {
+ return -1;
+ }
+ return 0;
+}
+
+int dsdb_dn_string_comparison(struct ldb_context *ldb, void *mem_ctx,
+ const struct ldb_val *v1,
+ const struct ldb_val *v2)
+{
+ return ldb_any_comparison(ldb, mem_ctx, dsdb_dn_string_canonicalise, v1, v2);
+}
+
+/*
+ * format a drsuapi_DsReplicaObjectIdentifier naming context as a string for debugging
+ *
+ * When forming a DN for DB access you must use drs_ObjectIdentifier_to_dn()
+ */
+char *drs_ObjectIdentifier_to_debug_string(TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaObjectIdentifier *nc)
+{
+ char *ret = NULL;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ if (!GUID_all_zero(&nc->guid)) {
+ char *guid = GUID_string(tmp_ctx, &nc->guid);
+ if (guid) {
+ ret = talloc_asprintf_append(ret, "<GUID=%s>;", guid);
+ }
+ }
+ if (nc->__ndr_size_sid != 0 && nc->sid.sid_rev_num != 0) {
+ const char *sid = dom_sid_string(tmp_ctx, &nc->sid);
+ if (sid) {
+ ret = talloc_asprintf_append(ret, "<SID=%s>;", sid);
+ }
+ }
+ if (nc->__ndr_size_dn != 0 && nc->dn) {
+ ret = talloc_asprintf_append(ret, "%s", nc->dn);
+ }
+ talloc_free(tmp_ctx);
+ talloc_steal(mem_ctx, ret);
+ return ret;
+}
+
+/*
+ * Safely convert a drsuapi_DsReplicaObjectIdentifier into an LDB DN
+ *
+ * We need to have GUID and SID priority and not allow extended
+ * components in the DN.
+ *
+ * We must also totally honour the priority even if the string DN is not valid or able to parse as a DN.
+ */
+static struct ldb_dn *drs_ObjectIdentifier_to_dn(TALLOC_CTX *mem_ctx,
+ struct ldb_context *ldb,
+ struct drsuapi_DsReplicaObjectIdentifier *nc)
+{
+ struct ldb_dn *new_dn = NULL;
+
+ if (!GUID_all_zero(&nc->guid)) {
+ struct GUID_txt_buf buf;
+ char *guid = GUID_buf_string(&nc->guid, &buf);
+
+ new_dn = ldb_dn_new_fmt(mem_ctx,
+ ldb,
+ "<GUID=%s>",
+ guid);
+ if (new_dn == NULL) {
+ DBG_ERR("Failed to prepare drs_ObjectIdentifier "
+ "GUID %s into a DN\n",
+ guid);
+ return NULL;
+ }
+
+ return new_dn;
+ }
+
+ if (nc->__ndr_size_sid != 0 && nc->sid.sid_rev_num != 0) {
+ struct dom_sid_buf buf;
+ char *sid = dom_sid_str_buf(&nc->sid, &buf);
+
+ new_dn = ldb_dn_new_fmt(mem_ctx,
+ ldb,
+ "<SID=%s>",
+ sid);
+ if (new_dn == NULL) {
+ DBG_ERR("Failed to prepare drs_ObjectIdentifier "
+ "SID %s into a DN\n",
+ sid);
+ return NULL;
+ }
+ return new_dn;
+ }
+
+ if (nc->__ndr_size_dn != 0 && nc->dn) {
+ int dn_comp_num = 0;
+ bool new_dn_valid = false;
+
+ new_dn = ldb_dn_new(mem_ctx, ldb, nc->dn);
+ if (new_dn == NULL) {
+ /* Set to WARNING as this is user-controlled, don't print the value into the logs */
+ DBG_WARNING("Failed to parse string DN in "
+ "drs_ObjectIdentifier into an LDB DN\n");
+ return NULL;
+ }
+
+ new_dn_valid = ldb_dn_validate(new_dn);
+ if (!new_dn_valid) {
+ /*
+ * Set to WARNING as this is user-controlled,
+ * but can print the value into the logs as it
+ * parsed a bit
+ */
+ DBG_WARNING("Failed to validate string DN [%s] in "
+ "drs_ObjectIdentifier as an LDB DN\n",
+ ldb_dn_get_linearized(new_dn));
+ return NULL;
+ }
+
+ dn_comp_num = ldb_dn_get_comp_num(new_dn);
+ if (dn_comp_num <= 0) {
+ /*
+ * Set to WARNING as this is user-controlled,
+ * but can print the value into the logs as it
+ * parsed a bit
+ */
+ DBG_WARNING("DN [%s] in drs_ObjectIdentifier "
+ "must have 1 or more components\n",
+ ldb_dn_get_linearized(new_dn));
+ return NULL;
+ }
+
+ if (ldb_dn_is_special(new_dn)) {
+ /*
+ * Set to WARNING as this is user-controlled,
+ * but can print the value into the logs as it
+ * parsed a bit
+ */
+ DBG_WARNING("New string DN [%s] in "
+ "drs_ObjectIdentifier is a "
+ "special LDB DN\n",
+ ldb_dn_get_linearized(new_dn));
+ return NULL;
+ }
+
+ /*
+ * We want this just to be a string DN, extended
+ * components are manually handled above
+ */
+ if (ldb_dn_has_extended(new_dn)) {
+ /*
+ * Set to WARNING as this is user-controlled,
+ * but can print the value into the logs as it
+ * parsed a bit
+ */
+ DBG_WARNING("Refusing to parse New string DN [%s] in "
+ "drs_ObjectIdentifier as an "
+ "extended LDB DN "
+ "(GUIDs and SIDs should be in the "
+ ".guid and .sid IDL elements, "
+ "not in the string\n",
+ ldb_dn_get_extended_linearized(mem_ctx,
+ new_dn,
+ 1));
+ return NULL;
+ }
+ return new_dn;
+ }
+
+ DBG_WARNING("Refusing to parse empty string DN "
+ "(and no GUID or SID) "
+ "drs_ObjectIdentifier into a empty "
+ "(eg RootDSE) LDB DN\n");
+ return NULL;
+}
+
+/*
+ * Safely convert a drsuapi_DsReplicaObjectIdentifier into a validated
+ * LDB DN of an existing DB entry, and/or find the NC root
+ *
+ * We need to have GUID and SID priority and not allow extended
+ * components in the DN.
+ *
+ * We must also totally honour the priority even if the string DN is
+ * not valid or able to parse as a DN.
+ *
+ * Finally, we must return the DN as found in the DB, as otherwise a
+ * subsequence ldb_dn_compare(dn, nc_root) will fail (as this is based
+ * on the string components).
+ */
+int drs_ObjectIdentifier_to_dn_and_nc_root(TALLOC_CTX *mem_ctx,
+ struct ldb_context *ldb,
+ struct drsuapi_DsReplicaObjectIdentifier *nc,
+ struct ldb_dn **normalised_dn,
+ struct ldb_dn **nc_root)
+{
+ int ret;
+ struct ldb_dn *new_dn = NULL;
+
+ new_dn = drs_ObjectIdentifier_to_dn(mem_ctx,
+ ldb,
+ nc);
+ if (new_dn == NULL) {
+ return LDB_ERR_INVALID_DN_SYNTAX;
+ }
+
+ ret = dsdb_normalise_dn_and_find_nc_root(ldb,
+ mem_ctx,
+ new_dn,
+ normalised_dn,
+ nc_root);
+ if (ret != LDB_SUCCESS) {
+ /*
+ * dsdb_normalise_dn_and_find_nc_root() sets LDB error
+ * strings, and the functions it calls do also
+ */
+ DBG_NOTICE("Failed to find DN \"%s\" -> \"%s\" for normalisation: %s (%s)\n",
+ drs_ObjectIdentifier_to_debug_string(mem_ctx, nc),
+ ldb_dn_get_extended_linearized(mem_ctx, new_dn, 1),
+ ldb_errstring(ldb),
+ ldb_strerror(ret));
+ }
+
+ TALLOC_FREE(new_dn);
+ return ret;
+}
diff --git a/source4/dsdb/common/dsdb_dn.h b/source4/dsdb/common/dsdb_dn.h
new file mode 100644
index 0000000..f98e3e7
--- /dev/null
+++ b/source4/dsdb/common/dsdb_dn.h
@@ -0,0 +1,21 @@
+struct dsdb_dn {
+ struct ldb_dn *dn;
+ DATA_BLOB extra_part;
+ enum dsdb_dn_format dn_format;
+ const char *oid;
+};
+
+#define DSDB_SYNTAX_BINARY_DN "1.2.840.113556.1.4.903"
+#define DSDB_SYNTAX_STRING_DN "1.2.840.113556.1.4.904"
+#define DSDB_SYNTAX_OR_NAME "1.2.840.113556.1.4.1221"
+#define DSDB_SYNTAX_ACCESS_POINT "1.3.6.1.4.1.1466.115.121.1.2"
+
+
+/* RMD_FLAGS component in a DN */
+#define DSDB_RMD_FLAG_DELETED 1
+/*
+ * This is used on a backlink attribute
+ * if the backlink is not allowed on
+ * the objectClass
+ */
+#define DSDB_RMD_FLAG_HIDDEN_BL 2
diff --git a/source4/dsdb/common/rodc_helper.c b/source4/dsdb/common/rodc_helper.c
new file mode 100644
index 0000000..b4982ae
--- /dev/null
+++ b/source4/dsdb/common/rodc_helper.c
@@ -0,0 +1,284 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ common sid helper functions
+
+ 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/>.
+*/
+
+#include "includes.h"
+#include "rpc_server/dcerpc_server.h"
+#include "librpc/gen_ndr/ndr_security.h"
+#include "source4/dsdb/samdb/samdb.h"
+#include "libcli/security/security.h"
+
+/*
+ see if any SIDs in list1 are in list2
+ */
+static bool sid_list_match(uint32_t num_sids1,
+ const struct dom_sid *list1,
+ uint32_t num_sids2,
+ const struct dom_sid *list2)
+{
+ unsigned int i, j;
+ /* do we ever have enough SIDs here to worry about O(n^2) ? */
+ for (i=0; i < num_sids1; i++) {
+ for (j=0; j < num_sids2; j++) {
+ if (dom_sid_equal(&list1[i], &list2[j])) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+/*
+ * Return an array of SIDs from a ldb_message given an attribute name assumes
+ * the SIDs are in NDR form (with primary_sid applied on the start).
+ */
+static WERROR samdb_result_sid_array_ndr(struct ldb_context *sam_ctx,
+ struct ldb_message *msg,
+ TALLOC_CTX *mem_ctx,
+ const char *attr,
+ uint32_t *num_sids,
+ struct dom_sid **sids,
+ const struct dom_sid *primary_sid)
+{
+ struct ldb_message_element *el;
+ unsigned int i;
+
+ el = ldb_msg_find_element(msg, attr);
+ if (!el) {
+ *sids = NULL;
+ return WERR_OK;
+ }
+
+ /* Make array long enough for NULL and additional SID */
+ (*sids) = talloc_array(mem_ctx, struct dom_sid,
+ el->num_values + 1);
+ W_ERROR_HAVE_NO_MEMORY(*sids);
+
+ (*sids)[PRIMARY_USER_SID_INDEX] = *primary_sid;
+
+ for (i = 0; i<el->num_values; i++) {
+ enum ndr_err_code ndr_err;
+ struct dom_sid sid = { 0, };
+
+ ndr_err = ndr_pull_struct_blob_all_noalloc(&el->values[i], &sid,
+ (ndr_pull_flags_fn_t)ndr_pull_dom_sid);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ return WERR_INTERNAL_DB_CORRUPTION;
+ }
+ /* Primary SID is already in position zero. */
+ (*sids)[i+1] = sid;
+ }
+
+ *num_sids = i+1;
+
+ return WERR_OK;
+}
+
+/*
+ return an array of SIDs from a ldb_message given an attribute name
+ assumes the SIDs are in extended DN format
+ */
+WERROR samdb_result_sid_array_dn(struct ldb_context *sam_ctx,
+ const struct ldb_message *msg,
+ TALLOC_CTX *mem_ctx,
+ const char *attr,
+ uint32_t *num_sids,
+ struct dom_sid **sids)
+{
+ struct ldb_message_element *el;
+ unsigned int i;
+
+ el = ldb_msg_find_element(msg, attr);
+ if (!el) {
+ *sids = NULL;
+ return WERR_OK;
+ }
+
+ (*sids) = talloc_array(mem_ctx, struct dom_sid, el->num_values + 1);
+ W_ERROR_HAVE_NO_MEMORY(*sids);
+
+ for (i=0; i<el->num_values; i++) {
+ struct ldb_dn *dn = ldb_dn_from_ldb_val(mem_ctx, sam_ctx, &el->values[i]);
+ NTSTATUS status;
+ struct dom_sid sid = { 0, };
+
+ status = dsdb_get_extended_dn_sid(dn, &sid, "SID");
+ if (!NT_STATUS_IS_OK(status)) {
+ return WERR_INTERNAL_DB_CORRUPTION;
+ }
+ (*sids)[i] = sid;
+ }
+ *num_sids = i;
+
+ return WERR_OK;
+}
+
+WERROR samdb_confirm_rodc_allowed_to_repl_to_sid_list(struct ldb_context *sam_ctx,
+ const struct dom_sid *rodc_machine_account_sid,
+ const struct ldb_message *rodc_msg,
+ const struct ldb_message *obj_msg,
+ uint32_t num_token_sids,
+ const struct dom_sid *token_sids)
+{
+ uint32_t num_never_reveal_sids, num_reveal_sids;
+ struct dom_sid *never_reveal_sids, *reveal_sids;
+ TALLOC_CTX *frame = talloc_stackframe();
+ WERROR werr;
+ uint32_t rodc_uac;
+
+ /*
+ * We are not allowed to get anyone elses krbtgt secrets (and
+ * in callers that don't shortcut before this, the RODC should
+ * not deal with any krbtgt)
+ */
+ if (samdb_result_dn(sam_ctx, frame,
+ obj_msg, "msDS-KrbTgtLinkBL", NULL)) {
+ TALLOC_FREE(frame);
+ DBG_INFO("Denied attempt to replicate to/act as a RODC krbtgt trust account %s using RODC: %s\n",
+ ldb_dn_get_linearized(obj_msg->dn),
+ ldb_dn_get_linearized(rodc_msg->dn));
+ return WERR_DS_DRA_SECRETS_DENIED;
+ }
+
+ if (ldb_msg_find_attr_as_uint(obj_msg,
+ "userAccountControl", 0) &
+ UF_INTERDOMAIN_TRUST_ACCOUNT) {
+ DBG_INFO("Denied attempt to replicate to/act as a inter-domain trust account %s using RODC: %s\n",
+ ldb_dn_get_linearized(obj_msg->dn),
+ ldb_dn_get_linearized(rodc_msg->dn));
+ TALLOC_FREE(frame);
+ return WERR_DS_DRA_SECRETS_DENIED;
+ }
+
+ /* Be very sure the RODC is really an RODC */
+ rodc_uac = ldb_msg_find_attr_as_uint(rodc_msg,
+ "userAccountControl",
+ 0);
+ if ((rodc_uac & UF_PARTIAL_SECRETS_ACCOUNT)
+ != UF_PARTIAL_SECRETS_ACCOUNT) {
+ DBG_ERR("Attempt to use an RODC account that is not an RODC: %s\n",
+ ldb_dn_get_linearized(rodc_msg->dn));
+ TALLOC_FREE(frame);
+ return WERR_DOMAIN_CONTROLLER_NOT_FOUND;
+ }
+
+ werr = samdb_result_sid_array_dn(sam_ctx, rodc_msg,
+ frame, "msDS-NeverRevealGroup",
+ &num_never_reveal_sids,
+ &never_reveal_sids);
+ if (!W_ERROR_IS_OK(werr)) {
+ DBG_ERR("Failed to parse msDS-NeverRevealGroup on %s: %s\n",
+ ldb_dn_get_linearized(rodc_msg->dn),
+ win_errstr(werr));
+ TALLOC_FREE(frame);
+ return WERR_DS_DRA_SECRETS_DENIED;
+ }
+
+ werr = samdb_result_sid_array_dn(sam_ctx, rodc_msg,
+ frame, "msDS-RevealOnDemandGroup",
+ &num_reveal_sids,
+ &reveal_sids);
+ if (!W_ERROR_IS_OK(werr)) {
+ DBG_ERR("Failed to parse msDS-RevealOnDemandGroup on %s: %s\n",
+ ldb_dn_get_linearized(rodc_msg->dn),
+ win_errstr(werr));
+ TALLOC_FREE(frame);
+ return WERR_DS_DRA_SECRETS_DENIED;
+ }
+
+ /* The RODC can replicate and print tickets for itself. */
+ if (dom_sid_equal(&token_sids[PRIMARY_USER_SID_INDEX], rodc_machine_account_sid)) {
+ TALLOC_FREE(frame);
+ return WERR_OK;
+ }
+
+ if (never_reveal_sids &&
+ sid_list_match(num_token_sids,
+ token_sids,
+ num_never_reveal_sids,
+ never_reveal_sids)) {
+ TALLOC_FREE(frame);
+ return WERR_DS_DRA_SECRETS_DENIED;
+ }
+
+ if (reveal_sids &&
+ sid_list_match(num_token_sids,
+ token_sids,
+ num_reveal_sids,
+ reveal_sids)) {
+ TALLOC_FREE(frame);
+ return WERR_OK;
+ }
+
+ TALLOC_FREE(frame);
+ return WERR_DS_DRA_SECRETS_DENIED;
+
+}
+
+/*
+ * This is a wrapper for the above that pulls in the tokenGroups
+ * rather than relying on the caller providing those
+ */
+WERROR samdb_confirm_rodc_allowed_to_repl_to(struct ldb_context *sam_ctx,
+ struct dom_sid *rodc_machine_account_sid,
+ struct ldb_message *rodc_msg,
+ struct ldb_message *obj_msg)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ WERROR werr;
+ uint32_t num_token_sids;
+ struct dom_sid *token_sids;
+ const struct dom_sid *object_sid = NULL;
+
+ object_sid = samdb_result_dom_sid(frame,
+ obj_msg,
+ "objectSid");
+ if (object_sid == NULL) {
+ return WERR_DS_DRA_BAD_DN;
+ }
+
+ /*
+ * The SID list needs to include itself as well as the tokenGroups.
+ *
+ * TODO determine if sIDHistory is required for this check
+ */
+ werr = samdb_result_sid_array_ndr(sam_ctx,
+ obj_msg,
+ frame, "tokenGroups",
+ &num_token_sids,
+ &token_sids,
+ object_sid);
+ if (!W_ERROR_IS_OK(werr) || token_sids==NULL) {
+ DBG_ERR("Failed to get tokenGroups on %s to confirm access via RODC %s: %s\n",
+ ldb_dn_get_linearized(obj_msg->dn),
+ ldb_dn_get_linearized(rodc_msg->dn),
+ win_errstr(werr));
+ return WERR_DS_DRA_SECRETS_DENIED;
+ }
+
+ werr = samdb_confirm_rodc_allowed_to_repl_to_sid_list(sam_ctx,
+ rodc_machine_account_sid,
+ rodc_msg,
+ obj_msg,
+ num_token_sids,
+ token_sids);
+ TALLOC_FREE(frame);
+ return werr;
+}
diff --git a/source4/dsdb/common/tests/dsdb.c b/source4/dsdb/common/tests/dsdb.c
new file mode 100644
index 0000000..8b20b4d
--- /dev/null
+++ b/source4/dsdb/common/tests/dsdb.c
@@ -0,0 +1,93 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ Test DSDB search
+
+ Copyright (C) Andrew Bartlet <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/>.
+*/
+
+#include "includes.h"
+#include <ldb_module.h>
+#include "ldb_wrap.h"
+#include "param/param.h"
+#include "param/loadparm.h"
+#include "torture/smbtorture.h"
+#include "torture/dsdb_proto.h"
+#include "auth/auth.h"
+
+bool torture_ldb_no_attrs(struct torture_context *torture)
+{
+ struct ldb_context *ldb;
+ int ret;
+ struct ldb_request *req;
+ struct ldb_result *ctx;
+ struct ldb_dn *dn;
+ const char *attrs[] = { NULL };
+
+ struct auth_session_info *session;
+ struct dom_sid domain_sid;
+ const char *path;
+
+ path = lpcfg_private_path(NULL, torture->lp_ctx, "sam.ldb");
+ torture_assert(torture, path != NULL,
+ "Couldn't find sam.ldb. Run with -s $SERVERCONFFILE");
+
+ domain_sid = global_sid_Builtin;
+ session = admin_session(NULL, torture->lp_ctx, &domain_sid);
+ ldb = ldb_wrap_connect(torture, torture->ev, torture->lp_ctx,
+ path, session, NULL, 0);
+ torture_assert(torture, ldb, "Failed to connect to LDB target");
+
+ ctx = talloc_zero(ldb, struct ldb_result);
+
+ dn = ldb_get_default_basedn(ldb);
+ ldb_dn_add_child_fmt(dn, "cn=users");
+ ret = ldb_build_search_req(&req, ldb, ctx, dn, LDB_SCOPE_SUBTREE,
+ "(objectClass=*)", attrs, NULL,
+ ctx, ldb_search_default_callback, NULL);
+ torture_assert(torture, ret == LDB_SUCCESS,
+ "Failed to build search request");
+ ldb_req_mark_untrusted(req);
+
+ ret = ldb_request(ldb, req);
+ torture_assert(torture, ret == LDB_SUCCESS, ldb_errstring(ldb));
+
+ ret = ldb_wait(req->handle, LDB_WAIT_ALL);
+ torture_assert(torture, ret == LDB_SUCCESS, ldb_errstring(ldb));
+
+ torture_assert(torture, ctx->count > 0, "Users container empty");
+ torture_assert_int_equal(torture, ctx->msgs[0]->num_elements, 0,
+ "Attributes returned for request "
+ "with empty attribute list");
+
+ return true;
+}
+
+NTSTATUS torture_dsdb_init(TALLOC_CTX *mem_ctx)
+{
+ struct torture_suite *suite = torture_suite_create(mem_ctx, "dsdb");
+
+ if (suite == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ torture_suite_add_simple_test(suite, "no_attrs", torture_ldb_no_attrs);
+
+ suite->description = talloc_strdup(suite, "DSDB tests");
+
+ torture_register_suite(mem_ctx, suite);
+
+ return NT_STATUS_OK;
+}
diff --git a/source4/dsdb/common/tests/dsdb_dn.c b/source4/dsdb/common/tests/dsdb_dn.c
new file mode 100644
index 0000000..66c7e12
--- /dev/null
+++ b/source4/dsdb/common/tests/dsdb_dn.c
@@ -0,0 +1,374 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ Test LDB attribute functions
+
+ Copyright (C) Andrew Bartlet <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 "lib/events/events.h"
+#include <ldb.h>
+#include <ldb_errors.h>
+#include "lib/ldb-samba/ldif_handlers.h"
+#include "ldb_wrap.h"
+#include "dsdb/samdb/samdb.h"
+#include "param/param.h"
+#include "torture/smbtorture.h"
+#include "torture/local/proto.h"
+
+#define DSDB_DN_TEST_SID "S-1-5-21-4177067393-1453636373-93818737"
+
+static bool torture_dsdb_dn_attrs(struct torture_context *torture)
+{
+ TALLOC_CTX *mem_ctx = talloc_new(torture);
+ struct ldb_context *ldb;
+ const struct ldb_schema_syntax *syntax;
+ struct ldb_val dn1, dn2, dn3;
+
+ torture_assert(torture,
+ ldb = ldb_init(mem_ctx, torture->ev),
+ "Failed to init ldb");
+
+ torture_assert_int_equal(torture,
+ ldb_register_samba_handlers(ldb), LDB_SUCCESS,
+ "Failed to register Samba handlers");
+
+ ldb_set_utf8_fns(ldb, NULL, wrap_casefold);
+
+ /* Test DN+Binary behaviour */
+ torture_assert(torture, syntax = ldb_samba_syntax_by_name(ldb, DSDB_SYNTAX_BINARY_DN),
+ "Failed to get DN+Binary schema attribute");
+ /* Test compare with different case of HEX string */
+ dn1 = data_blob_string_const("B:6:abcdef:dc=samba,dc=org");
+ dn2 = data_blob_string_const("B:6:ABCDef:dc=samba,dc=org");
+ torture_assert_int_equal(torture,
+ syntax->comparison_fn(ldb, mem_ctx, &dn1, &dn2), 0,
+ "Failed to compare different case of binary in DN+Binary");
+ torture_assert_int_equal(torture,
+ syntax->canonicalise_fn(ldb, mem_ctx, &dn1, &dn3), 0,
+ "Failed to canonicalise DN+Binary");
+ torture_assert_data_blob_equal(torture, dn3, data_blob_string_const("B:6:ABCDEF:DC=SAMBA,DC=ORG"),
+ "Failed to canonicalise DN+Binary");
+ /* Test compare with different case of DN */
+ dn1 = data_blob_string_const("B:6:abcdef:dc=samba,dc=org");
+ dn2 = data_blob_string_const("B:6:abcdef:dc=SAMBa,dc=ORg");
+ torture_assert_int_equal(torture,
+ syntax->comparison_fn(ldb, mem_ctx, &dn1, &dn2), 0,
+ "Failed to compare different case of DN in DN+Binary");
+
+ /* Test compare (false) with binary and non-binary prefix */
+ dn1 = data_blob_string_const("B:6:abcdef:dc=samba,dc=org");
+ dn2 = data_blob_string_const("dc=samba,dc=org");
+ torture_assert(torture,
+ syntax->comparison_fn(ldb, mem_ctx, &dn1, &dn2) != 0,
+ "compare of binary+dn an dn should have failed");
+
+ /* Test compare (false) with different binary prefix */
+ dn1 = data_blob_string_const("B:6:abcdef:dc=samba,dc=org");
+ dn2 = data_blob_string_const("B:4:abcd:dc=samba,dc=org");
+ torture_assert(torture,
+ syntax->comparison_fn(ldb, mem_ctx, &dn1, &dn2) != 0,
+ "compare of binary+dn an dn should have failed");
+
+ /* Test DN+String behaviour */
+ torture_assert(torture, syntax = ldb_samba_syntax_by_name(ldb, DSDB_SYNTAX_STRING_DN),
+ "Failed to get DN+String schema attribute");
+
+ /* Test compare with different case of string */
+ dn1 = data_blob_string_const("S:8:hihohiho:dc=samba,dc=org");
+ dn2 = data_blob_string_const("S:8:HIHOHIHO:dc=samba,dc=org");
+ torture_assert(torture,
+ syntax->comparison_fn(ldb, mem_ctx, &dn1, &dn2) != 0,
+ "compare of string+dn an different case of string+dn should have failed");
+
+ /* Test compare with different case of DN */
+ dn1 = data_blob_string_const("S:8:hihohiho:dc=samba,dc=org");
+ dn2 = data_blob_string_const("S:8:hihohiho:dc=SAMBA,dc=org");
+ torture_assert_int_equal(torture,
+ syntax->comparison_fn(ldb, mem_ctx, &dn1, &dn2), 0,
+ "Failed to compare different case of DN in DN+String");
+ torture_assert_int_equal(torture,
+ syntax->canonicalise_fn(ldb, mem_ctx, &dn1, &dn3), 0,
+ "Failed to canonicalise DN+String");
+ torture_assert_data_blob_equal(torture, dn3, data_blob_string_const("S:8:hihohiho:DC=SAMBA,DC=ORG"),
+ "Failed to canonicalise DN+String");
+
+ /* Test compare (false) with string and non-string prefix */
+ dn1 = data_blob_string_const("S:6:abcdef:dc=samba,dc=org");
+ dn2 = data_blob_string_const("dc=samba,dc=org");
+ torture_assert(torture,
+ syntax->comparison_fn(ldb, mem_ctx, &dn1, &dn2) != 0,
+ "compare of string+dn an dn should have failed");
+
+ /* Test compare (false) with different string prefix */
+ dn1 = data_blob_string_const("S:6:abcdef:dc=samba,dc=org");
+ dn2 = data_blob_string_const("S:6:abcXYZ:dc=samba,dc=org");
+ torture_assert(torture,
+ syntax->comparison_fn(ldb, mem_ctx, &dn1, &dn2) != 0,
+ "compare of string+dn an dn should have failed");
+
+ talloc_free(mem_ctx);
+ return true;
+}
+
+static bool torture_dsdb_dn_valid(struct torture_context *torture)
+{
+ TALLOC_CTX *mem_ctx = talloc_new(torture);
+ struct ldb_context *ldb;
+ struct ldb_dn *dn;
+ struct dsdb_dn *dsdb_dn;
+
+ struct ldb_val val;
+
+ DATA_BLOB abcd_blob = data_blob_talloc(mem_ctx, "\xa\xb\xc\xd", 4);
+
+ torture_assert(torture,
+ ldb = ldb_init(mem_ctx, torture->ev),
+ "Failed to init ldb");
+
+ torture_assert_int_equal(torture,
+ ldb_register_samba_handlers(ldb), LDB_SUCCESS,
+ "Failed to register Samba handlers");
+
+ ldb_set_utf8_fns(ldb, NULL, wrap_casefold);
+
+ /* Check behaviour of a normal DN */
+ torture_assert(torture,
+ dn = ldb_dn_new(mem_ctx, ldb, NULL),
+ "Failed to create a NULL DN");
+ torture_assert(torture,
+ ldb_dn_validate(dn),
+ "Failed to validate NULL DN");
+ torture_assert(torture,
+ ldb_dn_add_base_fmt(dn, "dc=org"),
+ "Failed to add base DN");
+ torture_assert(torture,
+ ldb_dn_add_child_fmt(dn, "dc=samba"),
+ "Failed to add base DN");
+ torture_assert_str_equal(torture, ldb_dn_get_linearized(dn), "dc=samba,dc=org",
+ "linearized DN incorrect");
+ torture_assert(torture, dsdb_dn = dsdb_dn_construct(mem_ctx, dn, data_blob_null, LDB_SYNTAX_DN),
+ "Failed to build dsdb dn");
+ torture_assert_str_equal(torture, dsdb_dn_get_extended_linearized(mem_ctx, dsdb_dn, 0), "dc=samba,dc=org",
+ "extended linearized DN incorrect");
+ torture_assert_str_equal(torture, dsdb_dn_get_linearized(mem_ctx, dsdb_dn), "dc=samba,dc=org",
+ "linearized DN incorrect");
+ torture_assert_str_equal(torture, dsdb_dn_get_casefold(mem_ctx, dsdb_dn), "DC=SAMBA,DC=ORG",
+ "casefold DN incorrect");
+
+
+ /* Test constructing a binary DN */
+ torture_assert(torture, dsdb_dn = dsdb_dn_construct(mem_ctx, dn, abcd_blob, DSDB_SYNTAX_BINARY_DN),
+ "Failed to build binary dsdb dn");
+ torture_assert_str_equal(torture, dsdb_dn_get_extended_linearized(mem_ctx, dsdb_dn, 0), "B:8:0A0B0C0D:dc=samba,dc=org",
+ "extended linearized DN incorrect");
+ torture_assert_str_equal(torture, dsdb_dn_get_linearized(mem_ctx, dsdb_dn), "B:8:0A0B0C0D:dc=samba,dc=org",
+ "linearized DN incorrect");
+ torture_assert_str_equal(torture, dsdb_dn_get_casefold(mem_ctx, dsdb_dn), "B:8:0A0B0C0D:DC=SAMBA,DC=ORG",
+ "casefold DN incorrect");
+ torture_assert_int_equal(torture, dsdb_dn->extra_part.length, 4, "length of extra-part should be 2");
+
+
+ /* Test constructing a string DN */
+ torture_assert(torture, dsdb_dn = dsdb_dn_construct(mem_ctx, dn, data_blob_talloc(mem_ctx, "hello", 5), DSDB_SYNTAX_STRING_DN),
+ "Failed to build string dsdb dn");
+ torture_assert_str_equal(torture, dsdb_dn_get_extended_linearized(mem_ctx, dsdb_dn, 0), "S:5:hello:dc=samba,dc=org",
+ "extended linearized DN incorrect");
+ torture_assert_str_equal(torture, dsdb_dn_get_linearized(mem_ctx, dsdb_dn), "S:5:hello:dc=samba,dc=org",
+ "linearized DN incorrect");
+ torture_assert_str_equal(torture, dsdb_dn_get_casefold(mem_ctx, dsdb_dn), "S:5:hello:DC=SAMBA,DC=ORG",
+ "casefold DN incorrect");
+ torture_assert_int_equal(torture, dsdb_dn->extra_part.length, 5, "length of extra-part should be 5");
+
+
+ /* Test compose of binary+DN */
+ val = data_blob_string_const("B:0::CN=Zer0,DC=SAMBA,DC=org");
+ torture_assert(torture,
+ dsdb_dn = dsdb_dn_parse(mem_ctx, ldb, &val,
+ DSDB_SYNTAX_BINARY_DN),
+ "Failed to create a DN with a zero binary part in it");
+ torture_assert_int_equal(torture, dsdb_dn->extra_part.length, 0, "length of extra-part should be 0");
+ torture_assert_str_equal(torture, dsdb_dn_get_extended_linearized(mem_ctx, dsdb_dn, 0), "B:0::CN=Zer0,DC=SAMBA,DC=org",
+ "extended linearized DN incorrect");
+ torture_assert_str_equal(torture, dsdb_dn_get_linearized(mem_ctx, dsdb_dn), "B:0::CN=Zer0,DC=SAMBA,DC=org",
+ "linearized DN incorrect");
+ torture_assert_str_equal(torture, dsdb_dn_get_casefold(mem_ctx, dsdb_dn), "B:0::CN=ZER0,DC=SAMBA,DC=ORG",
+ "casefold DN incorrect");
+
+ /* Test parse of binary DN */
+ val = data_blob_string_const("B:8:abcdabcd:CN=4,DC=Samba,DC=org");
+ torture_assert(torture,
+ dsdb_dn = dsdb_dn_parse(mem_ctx, ldb, &val,
+ DSDB_SYNTAX_BINARY_DN),
+ "Failed to create a DN with a binary part in it");
+ torture_assert_int_equal(torture, dsdb_dn->extra_part.length, 4, "length of extra-part should be 4");
+
+ torture_assert_str_equal(torture, dsdb_dn_get_extended_linearized(mem_ctx, dsdb_dn, 0), "B:8:ABCDABCD:CN=4,DC=Samba,DC=org",
+ "extended linearized DN incorrect");
+ torture_assert_str_equal(torture, dsdb_dn_get_linearized(mem_ctx, dsdb_dn), "B:8:ABCDABCD:CN=4,DC=Samba,DC=org",
+ "linearized DN incorrect");
+ torture_assert_str_equal(torture, dsdb_dn_get_casefold(mem_ctx, dsdb_dn), "B:8:ABCDABCD:CN=4,DC=SAMBA,DC=ORG",
+ "casefold DN incorrect");
+
+ /* Test parse of string+DN */
+ val = data_blob_string_const("S:8:Goodbye!:CN=S,DC=Samba,DC=org");
+ torture_assert(torture,
+ dsdb_dn = dsdb_dn_parse(mem_ctx, ldb, &val,
+ DSDB_SYNTAX_STRING_DN),
+ "Failed to create a DN with a string part in it");
+ torture_assert_int_equal(torture, dsdb_dn->extra_part.length, 8, "length of extra-part should be 8");
+ torture_assert_str_equal(torture, dsdb_dn_get_extended_linearized(mem_ctx, dsdb_dn, 0), "S:8:Goodbye!:CN=S,DC=Samba,DC=org",
+ "extended linearized DN incorrect");
+
+ /* Test that the linearised DN is the postfix of the lineairsed dsdb_dn */
+ torture_assert_str_equal(torture, ldb_dn_get_extended_linearized(mem_ctx, dsdb_dn->dn, 0), "CN=S,DC=Samba,DC=org",
+ "extended linearized DN incorrect");
+ torture_assert_str_equal(torture, dsdb_dn_get_linearized(mem_ctx, dsdb_dn), "S:8:Goodbye!:CN=S,DC=Samba,DC=org",
+ "linearized DN incorrect");
+ torture_assert_str_equal(torture, ldb_dn_get_linearized(dsdb_dn->dn), "CN=S,DC=Samba,DC=org",
+ "linearized DN incorrect");
+ torture_assert_str_equal(torture, dsdb_dn_get_casefold(mem_ctx, dsdb_dn), "S:8:Goodbye!:CN=S,DC=SAMBA,DC=ORG",
+ "casefold DN incorrect");
+
+ /* Test that the casefold DN is the postfix of the casefolded dsdb_dn */
+ torture_assert_str_equal(torture, ldb_dn_get_casefold(dsdb_dn->dn), "CN=S,DC=SAMBA,DC=ORG",
+ "casefold DN incorrect");
+
+ talloc_free(mem_ctx);
+ return true;
+}
+
+static bool torture_dsdb_dn_invalid(struct torture_context *torture)
+{
+ TALLOC_CTX *mem_ctx = talloc_new(torture);
+ struct ldb_context *ldb;
+ struct ldb_val val;
+
+ torture_assert(torture,
+ ldb = ldb_init(mem_ctx, torture->ev),
+ "Failed to init ldb");
+
+ torture_assert_int_equal(torture,
+ ldb_register_samba_handlers(ldb), LDB_SUCCESS,
+ "Failed to register Samba handlers");
+
+ ldb_set_utf8_fns(ldb, NULL, wrap_casefold);
+
+ /* Check behaviour of a normal DN */
+ val = data_blob_string_const("samba,dc=org");
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val, LDB_SYNTAX_DN) == NULL,
+ "Should have failed to create a 'normal' invalid DN");
+
+ /* Test invalid binary DNs */
+ val = data_blob_string_const("B:5:AB:dc=samba,dc=org");
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val,
+ DSDB_SYNTAX_BINARY_DN) == NULL,
+ "Should have Failed to create an invalid 'binary' DN");
+ val = data_blob_string_const("B:5:ABCDEFG:dc=samba,dc=org");
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val,
+ DSDB_SYNTAX_BINARY_DN) == NULL,
+ "Should have Failed to create an invalid 'binary' DN");
+ val = data_blob_string_const("B:10:AB:dc=samba,dc=org");
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val,
+ DSDB_SYNTAX_BINARY_DN) == NULL,
+ "Should have Failed to create an invalid 'binary' DN");
+ val = data_blob_string_const("B:4:0xAB:dc=samba,dc=org");
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val,
+ DSDB_SYNTAX_BINARY_DN) == NULL,
+ "Should have Failed to create an invalid 0x preifx 'binary' DN");
+ val = data_blob_string_const("B:2:0xAB:dc=samba,dc=org");
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val,
+ DSDB_SYNTAX_BINARY_DN) == NULL,
+ "Should have Failed to create an invalid 0x preifx 'binary' DN");
+ val = data_blob_string_const("B:10:XXXXXXXXXX:dc=samba,dc=org");
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val,
+ DSDB_SYNTAX_BINARY_DN) == NULL,
+ "Should have Failed to create an invalid 'binary' DN");
+
+ val = data_blob_string_const("B:60::dc=samba,dc=org");
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val,
+ DSDB_SYNTAX_BINARY_DN) == NULL,
+ "Should have Failed to create an invalid 'binary' DN");
+
+ /* Test invalid string DNs */
+ val = data_blob_string_const("S:5:hi:dc=samba,dc=org");
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val,
+ DSDB_SYNTAX_STRING_DN) == NULL,
+ "Should have Failed to create an invalid 'string' DN");
+ val = data_blob_string_const("S:5:hihohiho:dc=samba,dc=org");
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val,
+ DSDB_SYNTAX_STRING_DN) == NULL,
+ "Should have Failed to create an invalid 'string' DN");
+
+ val = data_blob_string_const("<SID=" DSDB_DN_TEST_SID">;dc=samba,dc=org");
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val, DSDB_SYNTAX_BINARY_DN) == NULL,
+ "Should have failed to create an 'extended' DN marked as a binary DN");
+
+ /* Check DN based on MS-ADTS:3.1.1.5.1.2 Naming Constraints*/
+ val = data_blob_string_const("CN=New\nLine,DC=SAMBA,DC=org");
+
+ /* changed to a warning until we understand the DEL: DNs */
+ if (dsdb_dn_parse(mem_ctx, ldb, &val, LDB_SYNTAX_DN) != NULL) {
+ torture_warning(torture,
+ "Should have Failed to create a DN with 0xA in it");
+ }
+
+ val = data_blob_string_const("B:4:ABAB:CN=New\nLine,DC=SAMBA,DC=org");
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val, LDB_SYNTAX_DN) == NULL,
+ "Should have Failed to create a DN with 0xA in it");
+
+ val = data_blob_const("CN=Zer\0,DC=SAMBA,DC=org", 23);
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val, LDB_SYNTAX_DN) == NULL,
+ "Should have Failed to create a DN with 0x0 in it");
+
+ val = data_blob_const("B:4:ABAB:CN=Zer\0,DC=SAMBA,DC=org", 23+9);
+ torture_assert(torture,
+ dsdb_dn_parse(mem_ctx, ldb, &val, DSDB_SYNTAX_BINARY_DN) == NULL,
+ "Should have Failed to create a DN with 0x0 in it");
+
+ return true;
+}
+
+struct torture_suite *torture_dsdb_dn(TALLOC_CTX *mem_ctx)
+{
+ struct torture_suite *suite = torture_suite_create(mem_ctx, "dsdb.dn");
+
+ if (suite == NULL) {
+ return NULL;
+ }
+
+ torture_suite_add_simple_test(suite, "valid", torture_dsdb_dn_valid);
+ torture_suite_add_simple_test(suite, "invalid", torture_dsdb_dn_invalid);
+ torture_suite_add_simple_test(suite, "attrs", torture_dsdb_dn_attrs);
+
+ suite->description = talloc_strdup(suite, "DSDB DN tests");
+
+ return suite;
+}
diff --git a/source4/dsdb/common/util.c b/source4/dsdb/common/util.c
new file mode 100644
index 0000000..7030d9c
--- /dev/null
+++ b/source4/dsdb/common/util.c
@@ -0,0 +1,6796 @@
+/*
+ Unix SMB/CIFS implementation.
+ Samba utility functions
+
+ Copyright (C) Andrew Tridgell 2004
+ Copyright (C) Volker Lendecke 2004
+ Copyright (C) Andrew Bartlett <abartlet@samba.org> 2006
+ Copyright (C) Jelmer Vernooij <jelmer@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 "events/events.h"
+#include "ldb.h"
+#include "ldb_module.h"
+#include "ldb_errors.h"
+#include "../lib/util/util_ldb.h"
+#include "../lib/crypto/crypto.h"
+#include "dsdb/samdb/samdb.h"
+#include "libcli/security/security.h"
+#include "librpc/gen_ndr/ndr_security.h"
+#include "librpc/gen_ndr/ndr_misc.h"
+#include "../libds/common/flags.h"
+#include "dsdb/common/proto.h"
+#include "libcli/ldap/ldap_ndr.h"
+#include "param/param.h"
+#include "libcli/auth/libcli_auth.h"
+#include "librpc/gen_ndr/ndr_drsblobs.h"
+#include "system/locale.h"
+#include "system/filesys.h"
+#include "lib/util/tsort.h"
+#include "dsdb/common/util.h"
+#include "lib/socket/socket.h"
+#include "librpc/gen_ndr/irpc.h"
+#include "libds/common/flag_mapping.h"
+#include "lib/util/access.h"
+#include "lib/util/sys_rw_data.h"
+#include "libcli/util/ntstatus.h"
+#include "lib/util/smb_strtox.h"
+#include "auth/auth.h"
+
+#undef strncasecmp
+#undef strcasecmp
+
+/*
+ * This included to allow us to handle DSDB_FLAG_REPLICATED_UPDATE in
+ * dsdb_request_add_controls()
+ */
+#include "dsdb/samdb/ldb_modules/util.h"
+
+/* default is 30 minutes: -1e7 * 30 * 60 */
+#define DEFAULT_OBSERVATION_WINDOW (-18000000000)
+
+/*
+ search the sam for the specified attributes in a specific domain, filter on
+ objectSid being in domain_sid.
+*/
+int samdb_search_domain(struct ldb_context *sam_ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *basedn,
+ struct ldb_message ***res,
+ const char * const *attrs,
+ const struct dom_sid *domain_sid,
+ const char *format, ...) _PRINTF_ATTRIBUTE(7,8)
+{
+ va_list ap;
+ int i, count;
+
+ va_start(ap, format);
+ count = gendb_search_v(sam_ldb, mem_ctx, basedn,
+ res, attrs, format, ap);
+ va_end(ap);
+
+ i=0;
+
+ while (i<count) {
+ struct dom_sid *entry_sid;
+
+ entry_sid = samdb_result_dom_sid(mem_ctx, (*res)[i], "objectSid");
+
+ if ((entry_sid == NULL) ||
+ (!dom_sid_in_domain(domain_sid, entry_sid))) {
+ /* Delete that entry from the result set */
+ (*res)[i] = (*res)[count-1];
+ count -= 1;
+ talloc_free(entry_sid);
+ continue;
+ }
+ talloc_free(entry_sid);
+ i += 1;
+ }
+
+ return count;
+}
+
+/*
+ search the sam for a single string attribute in exactly 1 record
+*/
+const char *samdb_search_string_v(struct ldb_context *sam_ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *basedn,
+ const char *attr_name,
+ const char *format, va_list ap) _PRINTF_ATTRIBUTE(5,0)
+{
+ int count;
+ const char *attrs[2] = { NULL, NULL };
+ struct ldb_message **res = NULL;
+
+ attrs[0] = attr_name;
+
+ count = gendb_search_v(sam_ldb, mem_ctx, basedn, &res, attrs, format, ap);
+ if (count > 1) {
+ DEBUG(1,("samdb: search for %s %s not single valued (count=%d)\n",
+ attr_name, format, count));
+ }
+ if (count != 1) {
+ talloc_free(res);
+ return NULL;
+ }
+
+ return ldb_msg_find_attr_as_string(res[0], attr_name, NULL);
+}
+
+/*
+ search the sam for a single string attribute in exactly 1 record
+*/
+const char *samdb_search_string(struct ldb_context *sam_ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *basedn,
+ const char *attr_name,
+ const char *format, ...) _PRINTF_ATTRIBUTE(5,6)
+{
+ va_list ap;
+ const char *str;
+
+ va_start(ap, format);
+ str = samdb_search_string_v(sam_ldb, mem_ctx, basedn, attr_name, format, ap);
+ va_end(ap);
+
+ return str;
+}
+
+struct ldb_dn *samdb_search_dn(struct ldb_context *sam_ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *basedn,
+ const char *format, ...) _PRINTF_ATTRIBUTE(4,5)
+{
+ va_list ap;
+ struct ldb_dn *ret;
+ struct ldb_message **res = NULL;
+ int count;
+
+ va_start(ap, format);
+ count = gendb_search_v(sam_ldb, mem_ctx, basedn, &res, NULL, format, ap);
+ va_end(ap);
+
+ if (count != 1) return NULL;
+
+ ret = talloc_steal(mem_ctx, res[0]->dn);
+ talloc_free(res);
+
+ return ret;
+}
+
+/*
+ search the sam for a dom_sid attribute in exactly 1 record
+*/
+struct dom_sid *samdb_search_dom_sid(struct ldb_context *sam_ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *basedn,
+ const char *attr_name,
+ const char *format, ...) _PRINTF_ATTRIBUTE(5,6)
+{
+ va_list ap;
+ int count;
+ struct ldb_message **res;
+ const char *attrs[2] = { NULL, NULL };
+ struct dom_sid *sid;
+
+ attrs[0] = attr_name;
+
+ va_start(ap, format);
+ count = gendb_search_v(sam_ldb, mem_ctx, basedn, &res, attrs, format, ap);
+ va_end(ap);
+ if (count > 1) {
+ DEBUG(1,("samdb: search for %s %s not single valued (count=%d)\n",
+ attr_name, format, count));
+ }
+ if (count != 1) {
+ talloc_free(res);
+ return NULL;
+ }
+ sid = samdb_result_dom_sid(mem_ctx, res[0], attr_name);
+ talloc_free(res);
+ return sid;
+}
+
+/*
+ search the sam for a single integer attribute in exactly 1 record
+*/
+unsigned int samdb_search_uint(struct ldb_context *sam_ldb,
+ TALLOC_CTX *mem_ctx,
+ unsigned int default_value,
+ struct ldb_dn *basedn,
+ const char *attr_name,
+ const char *format, ...) _PRINTF_ATTRIBUTE(6,7)
+{
+ va_list ap;
+ int count;
+ struct ldb_message **res;
+ const char *attrs[2] = { NULL, NULL };
+
+ attrs[0] = attr_name;
+
+ va_start(ap, format);
+ count = gendb_search_v(sam_ldb, mem_ctx, basedn, &res, attrs, format, ap);
+ va_end(ap);
+
+ if (count != 1) {
+ return default_value;
+ }
+
+ return ldb_msg_find_attr_as_uint(res[0], attr_name, default_value);
+}
+
+/*
+ search the sam for a single signed 64 bit integer attribute in exactly 1 record
+*/
+int64_t samdb_search_int64(struct ldb_context *sam_ldb,
+ TALLOC_CTX *mem_ctx,
+ int64_t default_value,
+ struct ldb_dn *basedn,
+ const char *attr_name,
+ const char *format, ...) _PRINTF_ATTRIBUTE(6,7)
+{
+ va_list ap;
+ int count;
+ struct ldb_message **res;
+ const char *attrs[2] = { NULL, NULL };
+
+ attrs[0] = attr_name;
+
+ va_start(ap, format);
+ count = gendb_search_v(sam_ldb, mem_ctx, basedn, &res, attrs, format, ap);
+ va_end(ap);
+
+ if (count != 1) {
+ return default_value;
+ }
+
+ return ldb_msg_find_attr_as_int64(res[0], attr_name, default_value);
+}
+
+/*
+ search the sam for multiple records each giving a single string attribute
+ return the number of matches, or -1 on error
+*/
+int samdb_search_string_multiple(struct ldb_context *sam_ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *basedn,
+ const char ***strs,
+ const char *attr_name,
+ const char *format, ...) _PRINTF_ATTRIBUTE(6,7)
+{
+ va_list ap;
+ int count, i;
+ const char *attrs[2] = { NULL, NULL };
+ struct ldb_message **res = NULL;
+
+ attrs[0] = attr_name;
+
+ va_start(ap, format);
+ count = gendb_search_v(sam_ldb, mem_ctx, basedn, &res, attrs, format, ap);
+ va_end(ap);
+
+ if (count <= 0) {
+ return count;
+ }
+
+ /* make sure its single valued */
+ for (i=0;i<count;i++) {
+ if (res[i]->num_elements != 1) {
+ DEBUG(1,("samdb: search for %s %s not single valued\n",
+ attr_name, format));
+ talloc_free(res);
+ return -1;
+ }
+ }
+
+ *strs = talloc_array(mem_ctx, const char *, count+1);
+ if (! *strs) {
+ talloc_free(res);
+ return -1;
+ }
+
+ for (i=0;i<count;i++) {
+ (*strs)[i] = ldb_msg_find_attr_as_string(res[i], attr_name, NULL);
+ }
+ (*strs)[count] = NULL;
+
+ return count;
+}
+
+struct ldb_dn *samdb_result_dn(struct ldb_context *ldb, TALLOC_CTX *mem_ctx, const struct ldb_message *msg,
+ const char *attr, struct ldb_dn *default_value)
+{
+ struct ldb_dn *ret_dn = ldb_msg_find_attr_as_dn(ldb, mem_ctx, msg, attr);
+ if (!ret_dn) {
+ return default_value;
+ }
+ return ret_dn;
+}
+
+/*
+ pull a rid from a objectSid in a result set.
+*/
+uint32_t samdb_result_rid_from_sid(TALLOC_CTX *mem_ctx, const struct ldb_message *msg,
+ const char *attr, uint32_t default_value)
+{
+ struct dom_sid *sid;
+ uint32_t rid;
+
+ sid = samdb_result_dom_sid(mem_ctx, msg, attr);
+ if (sid == NULL) {
+ return default_value;
+ }
+ rid = sid->sub_auths[sid->num_auths-1];
+ talloc_free(sid);
+ return rid;
+}
+
+/*
+ pull a dom_sid structure from a objectSid in a result set.
+*/
+struct dom_sid *samdb_result_dom_sid(TALLOC_CTX *mem_ctx, const struct ldb_message *msg,
+ const char *attr)
+{
+ ssize_t ret;
+ const struct ldb_val *v;
+ struct dom_sid *sid;
+ v = ldb_msg_find_ldb_val(msg, attr);
+ if (v == NULL) {
+ return NULL;
+ }
+ sid = talloc(mem_ctx, struct dom_sid);
+ if (sid == NULL) {
+ return NULL;
+ }
+ ret = sid_parse(v->data, v->length, sid);
+ if (ret == -1) {
+ talloc_free(sid);
+ return NULL;
+ }
+ return sid;
+}
+
+
+/**
+ * Makes an auth_SidAttr structure from a objectSid in a result set and a
+ * supplied attribute value.
+ *
+ * @param [in] mem_ctx Talloc memory context on which to allocate the auth_SidAttr.
+ * @param [in] msg The message from which to take the objectSid.
+ * @param [in] attr The attribute name, usually "objectSid".
+ * @param [in] attrs SE_GROUP_* flags to go with the SID.
+ * @returns A pointer to the auth_SidAttr structure, or NULL on failure.
+ */
+struct auth_SidAttr *samdb_result_dom_sid_attrs(TALLOC_CTX *mem_ctx, const struct ldb_message *msg,
+ const char *attr, uint32_t attrs)
+{
+ ssize_t ret;
+ const struct ldb_val *v;
+ struct auth_SidAttr *sid;
+ v = ldb_msg_find_ldb_val(msg, attr);
+ if (v == NULL) {
+ return NULL;
+ }
+ sid = talloc(mem_ctx, struct auth_SidAttr);
+ if (sid == NULL) {
+ return NULL;
+ }
+ ret = sid_parse(v->data, v->length, &sid->sid);
+ if (ret == -1) {
+ talloc_free(sid);
+ return NULL;
+ }
+ sid->attrs = attrs;
+ return sid;
+}
+
+/*
+ pull a dom_sid structure from a objectSid in a result set.
+*/
+int samdb_result_dom_sid_buf(const struct ldb_message *msg,
+ const char *attr,
+ struct dom_sid *sid)
+{
+ ssize_t ret;
+ const struct ldb_val *v = NULL;
+ v = ldb_msg_find_ldb_val(msg, attr);
+ if (v == NULL) {
+ return LDB_ERR_NO_SUCH_ATTRIBUTE;
+ }
+ ret = sid_parse(v->data, v->length, sid);
+ if (ret == -1) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ return LDB_SUCCESS;
+}
+
+/*
+ pull a guid structure from a objectGUID in a result set.
+*/
+struct GUID samdb_result_guid(const struct ldb_message *msg, const char *attr)
+{
+ const struct ldb_val *v;
+ struct GUID guid;
+ NTSTATUS status;
+
+ v = ldb_msg_find_ldb_val(msg, attr);
+ if (!v) return GUID_zero();
+
+ status = GUID_from_ndr_blob(v, &guid);
+ if (!NT_STATUS_IS_OK(status)) {
+ return GUID_zero();
+ }
+
+ return guid;
+}
+
+/*
+ pull a sid prefix from a objectSid in a result set.
+ this is used to find the domain sid for a user
+*/
+struct dom_sid *samdb_result_sid_prefix(TALLOC_CTX *mem_ctx, const struct ldb_message *msg,
+ const char *attr)
+{
+ struct dom_sid *sid = samdb_result_dom_sid(mem_ctx, msg, attr);
+ if (!sid || sid->num_auths < 1) return NULL;
+ sid->num_auths--;
+ return sid;
+}
+
+/*
+ pull a NTTIME in a result set.
+*/
+NTTIME samdb_result_nttime(const struct ldb_message *msg, const char *attr,
+ NTTIME default_value)
+{
+ return ldb_msg_find_attr_as_uint64(msg, attr, default_value);
+}
+
+/*
+ * Windows stores 0 for lastLogoff.
+ * But when a MS DC return the lastLogoff (as Logoff Time)
+ * it returns INT64_MAX, not returning this value in this case
+ * cause windows 2008 and newer version to fail for SMB requests
+ */
+NTTIME samdb_result_last_logoff(const struct ldb_message *msg)
+{
+ NTTIME ret = ldb_msg_find_attr_as_uint64(msg, "lastLogoff",0);
+
+ if (ret == 0)
+ ret = INT64_MAX;
+
+ return ret;
+}
+
+/*
+ * Windows uses both 0 and 9223372036854775807 (INT64_MAX) to
+ * indicate an account doesn't expire.
+ *
+ * When Windows initially creates an account, it sets
+ * accountExpires = 9223372036854775807 (INT64_MAX). However,
+ * when changing from an account having a specific expiration date to
+ * that account never expiring, it sets accountExpires = 0.
+ *
+ * Consolidate that logic here to allow clearer logic for account expiry in
+ * the rest of the code.
+ */
+NTTIME samdb_result_account_expires(const struct ldb_message *msg)
+{
+ NTTIME ret = ldb_msg_find_attr_as_uint64(msg, "accountExpires",
+ 0);
+
+ if (ret == 0)
+ ret = INT64_MAX;
+
+ return ret;
+}
+
+/*
+ construct the allow_password_change field from the PwdLastSet attribute and the
+ domain password settings
+*/
+NTTIME samdb_result_allow_password_change(struct ldb_context *sam_ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *domain_dn,
+ const struct ldb_message *msg,
+ const char *attr)
+{
+ uint64_t attr_time = ldb_msg_find_attr_as_uint64(msg, attr, 0);
+ int64_t minPwdAge;
+
+ if (attr_time == 0) {
+ return 0;
+ }
+
+ minPwdAge = samdb_search_int64(sam_ldb, mem_ctx, 0, domain_dn, "minPwdAge", NULL);
+
+ /* yes, this is a -= not a += as minPwdAge is stored as the negative
+ of the number of 100-nano-seconds */
+ attr_time -= minPwdAge;
+
+ return attr_time;
+}
+
+/*
+ pull a samr_Password structutre from a result set.
+*/
+struct samr_Password *samdb_result_hash(TALLOC_CTX *mem_ctx, const struct ldb_message *msg, const char *attr)
+{
+ struct samr_Password *hash = NULL;
+ const struct ldb_val *val = ldb_msg_find_ldb_val(msg, attr);
+ if (val && (val->length >= sizeof(hash->hash))) {
+ hash = talloc(mem_ctx, struct samr_Password);
+ if (hash == NULL) {
+ return NULL;
+ }
+ memcpy(hash->hash, val->data, MIN(val->length, sizeof(hash->hash)));
+ }
+ return hash;
+}
+
+/*
+ pull an array of samr_Password structures from a result set.
+*/
+unsigned int samdb_result_hashes(TALLOC_CTX *mem_ctx, const struct ldb_message *msg,
+ const char *attr, struct samr_Password **hashes)
+{
+ unsigned int count, i;
+ const struct ldb_val *val = ldb_msg_find_ldb_val(msg, attr);
+
+ *hashes = NULL;
+ if (!val) {
+ return 0;
+ }
+ count = val->length / 16;
+ if (count == 0) {
+ return 0;
+ }
+
+ *hashes = talloc_array(mem_ctx, struct samr_Password, count);
+ if (! *hashes) {
+ return 0;
+ }
+ talloc_keep_secret(*hashes);
+
+ for (i=0;i<count;i++) {
+ memcpy((*hashes)[i].hash, (i*16)+(char *)val->data, 16);
+ }
+
+ return count;
+}
+
+NTSTATUS samdb_result_passwords_from_history(TALLOC_CTX *mem_ctx,
+ struct loadparm_context *lp_ctx,
+ const struct ldb_message *msg,
+ unsigned int idx,
+ const struct samr_Password **lm_pwd,
+ const struct samr_Password **nt_pwd)
+{
+ struct samr_Password *lmPwdHash, *ntPwdHash;
+
+ if (nt_pwd) {
+ unsigned int num_nt;
+ num_nt = samdb_result_hashes(mem_ctx, msg, "ntPwdHistory", &ntPwdHash);
+ if (num_nt <= idx) {
+ *nt_pwd = NULL;
+ } else {
+ *nt_pwd = &ntPwdHash[idx];
+ }
+ }
+ if (lm_pwd) {
+ /* Ensure that if we have turned off LM
+ * authentication, that we never use the LM hash, even
+ * if we store it */
+ if (lpcfg_lanman_auth(lp_ctx)) {
+ unsigned int num_lm;
+ num_lm = samdb_result_hashes(mem_ctx, msg, "lmPwdHistory", &lmPwdHash);
+ if (num_lm <= idx) {
+ *lm_pwd = NULL;
+ } else {
+ *lm_pwd = &lmPwdHash[idx];
+ }
+ } else {
+ *lm_pwd = NULL;
+ }
+ }
+ return NT_STATUS_OK;
+}
+
+NTSTATUS samdb_result_passwords_no_lockout(TALLOC_CTX *mem_ctx,
+ struct loadparm_context *lp_ctx,
+ const struct ldb_message *msg,
+ struct samr_Password **nt_pwd)
+{
+ struct samr_Password *ntPwdHash;
+
+ if (nt_pwd) {
+ unsigned int num_nt;
+ num_nt = samdb_result_hashes(mem_ctx, msg, "unicodePwd", &ntPwdHash);
+ if (num_nt == 0) {
+ *nt_pwd = NULL;
+ } else if (num_nt > 1) {
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ } else {
+ *nt_pwd = &ntPwdHash[0];
+ }
+ }
+ return NT_STATUS_OK;
+}
+
+NTSTATUS samdb_result_passwords(TALLOC_CTX *mem_ctx,
+ struct loadparm_context *lp_ctx,
+ const struct ldb_message *msg,
+ struct samr_Password **nt_pwd)
+{
+ uint16_t acct_flags;
+
+ acct_flags = samdb_result_acct_flags(msg,
+ "msDS-User-Account-Control-Computed");
+ /* Quit if the account was locked out. */
+ if (acct_flags & ACB_AUTOLOCK) {
+ DEBUG(3,("samdb_result_passwords: Account for user %s was locked out.\n",
+ ldb_dn_get_linearized(msg->dn)));
+ return NT_STATUS_ACCOUNT_LOCKED_OUT;
+ }
+
+ return samdb_result_passwords_no_lockout(mem_ctx, lp_ctx, msg,
+ nt_pwd);
+}
+
+/*
+ pull a samr_LogonHours structutre from a result set.
+*/
+struct samr_LogonHours samdb_result_logon_hours(TALLOC_CTX *mem_ctx, struct ldb_message *msg, const char *attr)
+{
+ struct samr_LogonHours hours = {};
+ size_t units_per_week = 168;
+ const struct ldb_val *val = ldb_msg_find_ldb_val(msg, attr);
+
+ if (val) {
+ units_per_week = val->length * 8;
+ }
+
+ hours.bits = talloc_array(mem_ctx, uint8_t, units_per_week/8);
+ if (!hours.bits) {
+ return hours;
+ }
+ hours.units_per_week = units_per_week;
+ memset(hours.bits, 0xFF, units_per_week/8);
+ if (val) {
+ memcpy(hours.bits, val->data, val->length);
+ }
+
+ return hours;
+}
+
+/*
+ pull a set of account_flags from a result set.
+
+ Naturally, this requires that userAccountControl and
+ (if not null) the attributes 'attr' be already
+ included in msg
+*/
+uint32_t samdb_result_acct_flags(const struct ldb_message *msg, const char *attr)
+{
+ uint32_t userAccountControl = ldb_msg_find_attr_as_uint(msg, "userAccountControl", 0);
+ uint32_t attr_flags = 0;
+ uint32_t acct_flags = ds_uf2acb(userAccountControl);
+ if (attr) {
+ attr_flags = ldb_msg_find_attr_as_uint(msg, attr, UF_ACCOUNTDISABLE);
+ if (attr_flags == UF_ACCOUNTDISABLE) {
+ DEBUG(0, ("Attribute %s not found, disabling account %s!\n", attr,
+ ldb_dn_get_linearized(msg->dn)));
+ }
+ acct_flags |= ds_uf2acb(attr_flags);
+ }
+
+ return acct_flags;
+}
+
+NTSTATUS samdb_result_parameters(TALLOC_CTX *mem_ctx,
+ struct ldb_message *msg,
+ const char *attr,
+ struct lsa_BinaryString *s)
+{
+ int i;
+ const struct ldb_val *val = ldb_msg_find_ldb_val(msg, attr);
+
+ ZERO_STRUCTP(s);
+
+ if (!val) {
+ return NT_STATUS_OK;
+ }
+
+ if ((val->length % 2) != 0) {
+ /*
+ * If the on-disk data is not even in length, we know
+ * it is corrupt, and can not be safely pushed. We
+ * would either truncate, send an uninitialised
+ * byte or send a forced zero byte
+ */
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ s->array = talloc_array(mem_ctx, uint16_t, val->length/2);
+ if (!s->array) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ s->length = s->size = val->length;
+
+ /* The on-disk format is the 'network' format, being UTF16LE (sort of) */
+ for (i = 0; i < s->length / 2; i++) {
+ s->array[i] = SVAL(val->data, i * 2);
+ }
+
+ return NT_STATUS_OK;
+}
+
+/* Find an attribute, with a particular value */
+
+/* The current callers of this function expect a very specific
+ * behaviour: In particular, objectClass subclass equivalence is not
+ * wanted. This means that we should not lookup the schema for the
+ * comparison function */
+struct ldb_message_element *samdb_find_attribute(struct ldb_context *ldb,
+ const struct ldb_message *msg,
+ const char *name, const char *value)
+{
+ unsigned int i;
+ struct ldb_message_element *el = ldb_msg_find_element(msg, name);
+
+ if (!el) {
+ return NULL;
+ }
+
+ for (i=0;i<el->num_values;i++) {
+ if (ldb_attr_cmp(value, (char *)el->values[i].data) == 0) {
+ return el;
+ }
+ }
+
+ return NULL;
+}
+
+static int samdb_find_or_add_attribute_ex(struct ldb_context *ldb,
+ struct ldb_message *msg,
+ const char *name,
+ const char *set_value,
+ unsigned attr_flags,
+ bool *added)
+{
+ int ret;
+ struct ldb_message_element *el;
+
+ SMB_ASSERT(attr_flags != 0);
+
+ el = ldb_msg_find_element(msg, name);
+ if (el) {
+ if (added != NULL) {
+ *added = false;
+ }
+
+ return LDB_SUCCESS;
+ }
+
+ ret = ldb_msg_add_empty(msg, name,
+ attr_flags,
+ &el);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ if (set_value != NULL) {
+ ret = ldb_msg_add_string(msg, name, set_value);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (added != NULL) {
+ *added = true;
+ }
+ return LDB_SUCCESS;
+}
+
+int samdb_find_or_add_attribute(struct ldb_context *ldb, struct ldb_message *msg, const char *name, const char *set_value)
+{
+ return samdb_find_or_add_attribute_ex(ldb, msg, name, set_value, LDB_FLAG_MOD_ADD, NULL);
+}
+
+/*
+ add a dom_sid element to a message
+*/
+int samdb_msg_add_dom_sid(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, const struct dom_sid *sid)
+{
+ struct ldb_val v;
+ enum ndr_err_code ndr_err;
+
+ ndr_err = ndr_push_struct_blob(&v, mem_ctx,
+ sid,
+ (ndr_push_flags_fn_t)ndr_push_dom_sid);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ return ldb_operr(sam_ldb);
+ }
+ return ldb_msg_add_value(msg, attr_name, &v, NULL);
+}
+
+
+/*
+ add a delete element operation to a message
+*/
+int samdb_msg_add_delete(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name)
+{
+ /* we use an empty replace rather than a delete, as it allows for
+ dsdb_replace() to be used everywhere */
+ return ldb_msg_add_empty(msg, attr_name, LDB_FLAG_MOD_REPLACE, NULL);
+}
+
+/*
+ add an add attribute value to a message or enhance an existing attribute
+ which has the same name and the add flag set.
+*/
+int samdb_msg_add_addval(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx,
+ struct ldb_message *msg, const char *attr_name,
+ const char *value)
+{
+ struct ldb_message_element *el;
+ struct ldb_val val;
+ char *v;
+ unsigned int i;
+ bool found = false;
+ int ret;
+
+ v = talloc_strdup(mem_ctx, value);
+ if (v == NULL) {
+ return ldb_oom(sam_ldb);
+ }
+
+ val.data = (uint8_t *) v;
+ val.length = strlen(v);
+
+ if (val.length == 0) {
+ /* allow empty strings as non-existent attributes */
+ return LDB_SUCCESS;
+ }
+
+ for (i = 0; i < msg->num_elements; i++) {
+ el = &msg->elements[i];
+ if ((ldb_attr_cmp(el->name, attr_name) == 0) &&
+ (LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_ADD)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ ret = ldb_msg_add_empty(msg, attr_name, LDB_FLAG_MOD_ADD,
+ &el);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ ret = ldb_msg_element_add_value(msg->elements, el, &val);
+ if (ret != LDB_SUCCESS) {
+ return ldb_oom(sam_ldb);
+ }
+
+ return LDB_SUCCESS;
+}
+
+/*
+ add a delete attribute value to a message or enhance an existing attribute
+ which has the same name and the delete flag set.
+*/
+int samdb_msg_add_delval(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx,
+ struct ldb_message *msg, const char *attr_name,
+ const char *value)
+{
+ struct ldb_message_element *el;
+ struct ldb_val val;
+ char *v;
+ unsigned int i;
+ bool found = false;
+ int ret;
+
+ v = talloc_strdup(mem_ctx, value);
+ if (v == NULL) {
+ return ldb_oom(sam_ldb);
+ }
+
+ val.data = (uint8_t *) v;
+ val.length = strlen(v);
+
+ if (val.length == 0) {
+ /* allow empty strings as non-existent attributes */
+ return LDB_SUCCESS;
+ }
+
+ for (i = 0; i < msg->num_elements; i++) {
+ el = &msg->elements[i];
+ if ((ldb_attr_cmp(el->name, attr_name) == 0) &&
+ (LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_DELETE)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ ret = ldb_msg_add_empty(msg, attr_name, LDB_FLAG_MOD_DELETE,
+ &el);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ ret = ldb_msg_element_add_value(msg->elements, el, &val);
+ if (ret != LDB_SUCCESS) {
+ return ldb_oom(sam_ldb);
+ }
+
+ return LDB_SUCCESS;
+}
+
+/*
+ add a int element to a message
+*/
+int samdb_msg_add_int(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, int v)
+{
+ const char *s = talloc_asprintf(mem_ctx, "%d", v);
+ if (s == NULL) {
+ return ldb_oom(sam_ldb);
+ }
+ return ldb_msg_add_string(msg, attr_name, s);
+}
+
+int samdb_msg_add_int_flags(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, int v, int flags)
+{
+ const char *s = talloc_asprintf(mem_ctx, "%d", v);
+ if (s == NULL) {
+ return ldb_oom(sam_ldb);
+ }
+ return ldb_msg_add_string_flags(msg, attr_name, s, flags);
+}
+
+/*
+ * Add an unsigned int element to a message
+ *
+ * The issue here is that we have not yet first cast to int32_t explicitly,
+ * before we cast to an signed int to printf() into the %d or cast to a
+ * int64_t before we then cast to a long long to printf into a %lld.
+ *
+ * There are *no* unsigned integers in Active Directory LDAP, even the RID
+ * allocations and ms-DS-Secondary-KrbTgt-Number are *signed* quantities.
+ * (See the schema, and the syntax definitions in schema_syntax.c).
+ *
+ */
+int samdb_msg_add_uint(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, unsigned int v)
+{
+ return samdb_msg_add_int(sam_ldb, mem_ctx, msg, attr_name, (int)v);
+}
+
+int samdb_msg_add_uint_flags(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, unsigned int v, int flags)
+{
+ return samdb_msg_add_int_flags(sam_ldb, mem_ctx, msg, attr_name, (int)v, flags);
+}
+
+/*
+ add a (signed) int64_t element to a message
+*/
+int samdb_msg_add_int64(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, int64_t v)
+{
+ const char *s = talloc_asprintf(mem_ctx, "%lld", (long long)v);
+ if (s == NULL) {
+ return ldb_oom(sam_ldb);
+ }
+ return ldb_msg_add_string(msg, attr_name, s);
+}
+
+/*
+ * Add an unsigned int64_t (uint64_t) element to a message
+ *
+ * The issue here is that we have not yet first cast to int32_t explicitly,
+ * before we cast to an signed int to printf() into the %d or cast to a
+ * int64_t before we then cast to a long long to printf into a %lld.
+ *
+ * There are *no* unsigned integers in Active Directory LDAP, even the RID
+ * allocations and ms-DS-Secondary-KrbTgt-Number are *signed* quantities.
+ * (See the schema, and the syntax definitions in schema_syntax.c).
+ *
+ */
+int samdb_msg_add_uint64(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, uint64_t v)
+{
+ return samdb_msg_add_int64(sam_ldb, mem_ctx, msg, attr_name, (int64_t)v);
+}
+
+/*
+ append a int element to a message
+*/
+int samdb_msg_append_int(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, int v, int flags)
+{
+ const char *s = talloc_asprintf(mem_ctx, "%d", v);
+ if (s == NULL) {
+ return ldb_oom(sam_ldb);
+ }
+ return ldb_msg_append_string(msg, attr_name, s, flags);
+}
+
+/*
+ * Append an unsigned int element to a message
+ *
+ * The issue here is that we have not yet first cast to int32_t explicitly,
+ * before we cast to an signed int to printf() into the %d or cast to a
+ * int64_t before we then cast to a long long to printf into a %lld.
+ *
+ * There are *no* unsigned integers in Active Directory LDAP, even the RID
+ * allocations and ms-DS-Secondary-KrbTgt-Number are *signed* quantities.
+ * (See the schema, and the syntax definitions in schema_syntax.c).
+ *
+ */
+int samdb_msg_append_uint(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, unsigned int v, int flags)
+{
+ return samdb_msg_append_int(sam_ldb, mem_ctx, msg, attr_name, (int)v, flags);
+}
+
+/*
+ append a (signed) int64_t element to a message
+*/
+int samdb_msg_append_int64(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, int64_t v, int flags)
+{
+ const char *s = talloc_asprintf(mem_ctx, "%lld", (long long)v);
+ if (s == NULL) {
+ return ldb_oom(sam_ldb);
+ }
+ return ldb_msg_append_string(msg, attr_name, s, flags);
+}
+
+/*
+ * Append an unsigned int64_t (uint64_t) element to a message
+ *
+ * The issue here is that we have not yet first cast to int32_t explicitly,
+ * before we cast to an signed int to printf() into the %d or cast to a
+ * int64_t before we then cast to a long long to printf into a %lld.
+ *
+ * There are *no* unsigned integers in Active Directory LDAP, even the RID
+ * allocations and ms-DS-Secondary-KrbTgt-Number are *signed* quantities.
+ * (See the schema, and the syntax definitions in schema_syntax.c).
+ *
+ */
+int samdb_msg_append_uint64(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, uint64_t v, int flags)
+{
+ return samdb_msg_append_int64(sam_ldb, mem_ctx, msg, attr_name, (int64_t)v, flags);
+}
+
+/*
+ add a samr_Password element to a message
+*/
+int samdb_msg_add_hash(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, const struct samr_Password *hash)
+{
+ struct ldb_val val;
+ val.data = talloc_memdup(mem_ctx, hash->hash, 16);
+ if (!val.data) {
+ return ldb_oom(sam_ldb);
+ }
+ val.length = 16;
+ return ldb_msg_add_value(msg, attr_name, &val, NULL);
+}
+
+/*
+ add a samr_Password array to a message
+*/
+int samdb_msg_add_hashes(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, struct samr_Password *hashes,
+ unsigned int count)
+{
+ struct ldb_val val;
+ unsigned int i;
+ val.data = talloc_array_size(mem_ctx, 16, count);
+ val.length = count*16;
+ if (!val.data) {
+ return ldb_oom(ldb);
+ }
+ for (i=0;i<count;i++) {
+ memcpy(i*16 + (char *)val.data, hashes[i].hash, 16);
+ }
+ return ldb_msg_add_value(msg, attr_name, &val, NULL);
+}
+
+/*
+ add a acct_flags element to a message
+*/
+int samdb_msg_add_acct_flags(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, uint32_t v)
+{
+ return samdb_msg_add_uint(sam_ldb, mem_ctx, msg, attr_name, ds_acb2uf(v));
+}
+
+/*
+ add a logon_hours element to a message
+*/
+int samdb_msg_add_logon_hours(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, struct samr_LogonHours *hours)
+{
+ struct ldb_val val;
+ val.length = hours->units_per_week / 8;
+ val.data = hours->bits;
+ return ldb_msg_add_value(msg, attr_name, &val, NULL);
+}
+
+/*
+ add a parameters element to a message
+*/
+int samdb_msg_add_parameters(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx, struct ldb_message *msg,
+ const char *attr_name, struct lsa_BinaryString *parameters)
+{
+ int i;
+ struct ldb_val val;
+ if ((parameters->length % 2) != 0) {
+ return LDB_ERR_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ val.data = talloc_array(mem_ctx, uint8_t, parameters->length);
+ if (val.data == NULL) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ val.length = parameters->length;
+ for (i = 0; i < parameters->length / 2; i++) {
+ /*
+ * The on-disk format needs to be in the 'network'
+ * format, parameters->array is a uint16_t array of
+ * length parameters->length / 2
+ */
+ SSVAL(val.data, i * 2, parameters->array[i]);
+ }
+ return ldb_msg_add_steal_value(msg, attr_name, &val);
+}
+
+/*
+ * Sets an unsigned int element in a message
+ *
+ * The issue here is that we have not yet first cast to int32_t explicitly,
+ * before we cast to an signed int to printf() into the %d or cast to a
+ * int64_t before we then cast to a long long to printf into a %lld.
+ *
+ * There are *no* unsigned integers in Active Directory LDAP, even the RID
+ * allocations and ms-DS-Secondary-KrbTgt-Number are *signed* quantities.
+ * (See the schema, and the syntax definitions in schema_syntax.c).
+ *
+ */
+int samdb_msg_set_uint(struct ldb_context *sam_ldb, TALLOC_CTX *mem_ctx,
+ struct ldb_message *msg, const char *attr_name,
+ unsigned int v)
+{
+ struct ldb_message_element *el;
+
+ el = ldb_msg_find_element(msg, attr_name);
+ if (el) {
+ el->num_values = 0;
+ }
+ return samdb_msg_add_uint(sam_ldb, mem_ctx, msg, attr_name, v);
+}
+
+/*
+ * Handle ldb_request in transaction
+ */
+int dsdb_autotransaction_request(struct ldb_context *sam_ldb,
+ struct ldb_request *req)
+{
+ int ret;
+
+ ret = ldb_transaction_start(sam_ldb);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ ret = ldb_request(sam_ldb, req);
+ if (ret == LDB_SUCCESS) {
+ ret = ldb_wait(req->handle, LDB_WAIT_ALL);
+ }
+
+ if (ret == LDB_SUCCESS) {
+ return ldb_transaction_commit(sam_ldb);
+ }
+ ldb_transaction_cancel(sam_ldb);
+
+ return ret;
+}
+
+/*
+ return a default security descriptor
+*/
+struct security_descriptor *samdb_default_security_descriptor(TALLOC_CTX *mem_ctx)
+{
+ struct security_descriptor *sd;
+
+ sd = security_descriptor_initialise(mem_ctx);
+
+ return sd;
+}
+
+struct ldb_dn *samdb_aggregate_schema_dn(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx)
+{
+ struct ldb_dn *schema_dn = ldb_get_schema_basedn(sam_ctx);
+ struct ldb_dn *aggregate_dn;
+ if (!schema_dn) {
+ return NULL;
+ }
+
+ aggregate_dn = ldb_dn_copy(mem_ctx, schema_dn);
+ if (!aggregate_dn) {
+ return NULL;
+ }
+ if (!ldb_dn_add_child_fmt(aggregate_dn, "CN=Aggregate")) {
+ return NULL;
+ }
+ return aggregate_dn;
+}
+
+struct ldb_dn *samdb_partitions_dn(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx)
+{
+ struct ldb_dn *new_dn;
+
+ new_dn = ldb_dn_copy(mem_ctx, ldb_get_config_basedn(sam_ctx));
+ if ( ! ldb_dn_add_child_fmt(new_dn, "CN=Partitions")) {
+ talloc_free(new_dn);
+ return NULL;
+ }
+ return new_dn;
+}
+
+struct ldb_dn *samdb_infrastructure_dn(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx)
+{
+ struct ldb_dn *new_dn;
+
+ new_dn = ldb_dn_copy(mem_ctx, ldb_get_default_basedn(sam_ctx));
+ if ( ! ldb_dn_add_child_fmt(new_dn, "CN=Infrastructure")) {
+ talloc_free(new_dn);
+ return NULL;
+ }
+ return new_dn;
+}
+
+struct ldb_dn *samdb_system_container_dn(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx)
+{
+ struct ldb_dn *new_dn = NULL;
+ bool ok;
+
+ new_dn = ldb_dn_copy(mem_ctx, ldb_get_default_basedn(sam_ctx));
+ if (new_dn == NULL) {
+ return NULL;
+ }
+
+ ok = ldb_dn_add_child_fmt(new_dn, "CN=System");
+ if (!ok) {
+ TALLOC_FREE(new_dn);
+ return NULL;
+ }
+
+ return new_dn;
+}
+
+struct ldb_dn *samdb_sites_dn(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx)
+{
+ struct ldb_dn *new_dn;
+
+ new_dn = ldb_dn_copy(mem_ctx, ldb_get_config_basedn(sam_ctx));
+ if ( ! ldb_dn_add_child_fmt(new_dn, "CN=Sites")) {
+ talloc_free(new_dn);
+ return NULL;
+ }
+ return new_dn;
+}
+
+struct ldb_dn *samdb_extended_rights_dn(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx)
+{
+ struct ldb_dn *new_dn;
+
+ new_dn = ldb_dn_copy(mem_ctx, ldb_get_config_basedn(sam_ctx));
+ if ( ! ldb_dn_add_child_fmt(new_dn, "CN=Extended-Rights")) {
+ talloc_free(new_dn);
+ return NULL;
+ }
+ return new_dn;
+}
+/*
+ work out the domain sid for the current open ldb
+*/
+const struct dom_sid *samdb_domain_sid(struct ldb_context *ldb)
+{
+ TALLOC_CTX *tmp_ctx;
+ const struct dom_sid *domain_sid;
+ const char *attrs[] = {
+ "objectSid",
+ NULL
+ };
+ struct ldb_result *res;
+ int ret;
+
+ /* see if we have a cached copy */
+ domain_sid = (struct dom_sid *)ldb_get_opaque(ldb, "cache.domain_sid");
+ if (domain_sid) {
+ return domain_sid;
+ }
+
+ tmp_ctx = talloc_new(ldb);
+ if (tmp_ctx == NULL) {
+ goto failed;
+ }
+
+ ret = ldb_search(ldb, tmp_ctx, &res, ldb_get_default_basedn(ldb), LDB_SCOPE_BASE, attrs, "objectSid=*");
+
+ if (ret != LDB_SUCCESS) {
+ goto failed;
+ }
+
+ if (res->count != 1) {
+ goto failed;
+ }
+
+ domain_sid = samdb_result_dom_sid(tmp_ctx, res->msgs[0], "objectSid");
+ if (domain_sid == NULL) {
+ goto failed;
+ }
+
+ /* cache the domain_sid in the ldb */
+ if (ldb_set_opaque(ldb, "cache.domain_sid", discard_const_p(struct dom_sid, domain_sid)) != LDB_SUCCESS) {
+ goto failed;
+ }
+
+ talloc_steal(ldb, domain_sid);
+ talloc_free(tmp_ctx);
+
+ return domain_sid;
+
+failed:
+ talloc_free(tmp_ctx);
+ return NULL;
+}
+
+/*
+ get domain sid from cache
+*/
+const struct dom_sid *samdb_domain_sid_cache_only(struct ldb_context *ldb)
+{
+ return (struct dom_sid *)ldb_get_opaque(ldb, "cache.domain_sid");
+}
+
+bool samdb_set_domain_sid(struct ldb_context *ldb, const struct dom_sid *dom_sid_in)
+{
+ TALLOC_CTX *tmp_ctx;
+ struct dom_sid *dom_sid_new;
+ struct dom_sid *dom_sid_old;
+
+ /* see if we have a cached copy */
+ dom_sid_old = talloc_get_type(ldb_get_opaque(ldb,
+ "cache.domain_sid"), struct dom_sid);
+
+ tmp_ctx = talloc_new(ldb);
+ if (tmp_ctx == NULL) {
+ goto failed;
+ }
+
+ dom_sid_new = dom_sid_dup(tmp_ctx, dom_sid_in);
+ if (!dom_sid_new) {
+ goto failed;
+ }
+
+ /* cache the domain_sid in the ldb */
+ if (ldb_set_opaque(ldb, "cache.domain_sid", dom_sid_new) != LDB_SUCCESS) {
+ goto failed;
+ }
+
+ talloc_steal(ldb, dom_sid_new);
+ talloc_free(tmp_ctx);
+ talloc_free(dom_sid_old);
+
+ return true;
+
+failed:
+ DEBUG(1,("Failed to set our own cached domain SID in the ldb!\n"));
+ talloc_free(tmp_ctx);
+ return false;
+}
+
+/*
+ work out the domain guid for the current open ldb
+*/
+const struct GUID *samdb_domain_guid(struct ldb_context *ldb)
+{
+ TALLOC_CTX *tmp_ctx = NULL;
+ struct GUID *domain_guid = NULL;
+ const char *attrs[] = {
+ "objectGUID",
+ NULL
+ };
+ struct ldb_result *res = NULL;
+ int ret;
+
+ /* see if we have a cached copy */
+ domain_guid = (struct GUID *)ldb_get_opaque(ldb, "cache.domain_guid");
+ if (domain_guid) {
+ return domain_guid;
+ }
+
+ tmp_ctx = talloc_new(ldb);
+ if (tmp_ctx == NULL) {
+ goto failed;
+ }
+
+ ret = ldb_search(ldb, tmp_ctx, &res, ldb_get_default_basedn(ldb), LDB_SCOPE_BASE, attrs, "objectGUID=*");
+ if (ret != LDB_SUCCESS) {
+ goto failed;
+ }
+
+ if (res->count != 1) {
+ goto failed;
+ }
+
+ domain_guid = talloc(tmp_ctx, struct GUID);
+ if (domain_guid == NULL) {
+ goto failed;
+ }
+ *domain_guid = samdb_result_guid(res->msgs[0], "objectGUID");
+
+ /* cache the domain_sid in the ldb */
+ if (ldb_set_opaque(ldb, "cache.domain_guid", domain_guid) != LDB_SUCCESS) {
+ goto failed;
+ }
+
+ talloc_steal(ldb, domain_guid);
+ talloc_free(tmp_ctx);
+
+ return domain_guid;
+
+failed:
+ talloc_free(tmp_ctx);
+ return NULL;
+}
+
+bool samdb_set_ntds_settings_dn(struct ldb_context *ldb, struct ldb_dn *ntds_settings_dn_in)
+{
+ TALLOC_CTX *tmp_ctx;
+ struct ldb_dn *ntds_settings_dn_new;
+ struct ldb_dn *ntds_settings_dn_old;
+
+ /* see if we have a forced copy from provision */
+ ntds_settings_dn_old = talloc_get_type(ldb_get_opaque(ldb,
+ "forced.ntds_settings_dn"), struct ldb_dn);
+
+ tmp_ctx = talloc_new(ldb);
+ if (tmp_ctx == NULL) {
+ goto failed;
+ }
+
+ ntds_settings_dn_new = ldb_dn_copy(tmp_ctx, ntds_settings_dn_in);
+ if (!ntds_settings_dn_new) {
+ goto failed;
+ }
+
+ /* set the DN in the ldb to avoid lookups during provision */
+ if (ldb_set_opaque(ldb, "forced.ntds_settings_dn", ntds_settings_dn_new) != LDB_SUCCESS) {
+ goto failed;
+ }
+
+ talloc_steal(ldb, ntds_settings_dn_new);
+ talloc_free(tmp_ctx);
+ talloc_free(ntds_settings_dn_old);
+
+ return true;
+
+failed:
+ DEBUG(1,("Failed to set our NTDS Settings DN in the ldb!\n"));
+ talloc_free(tmp_ctx);
+ return false;
+}
+
+/*
+ work out the ntds settings dn for the current open ldb
+*/
+struct ldb_dn *samdb_ntds_settings_dn(struct ldb_context *ldb, TALLOC_CTX *mem_ctx)
+{
+ TALLOC_CTX *tmp_ctx;
+ const char *root_attrs[] = { "dsServiceName", NULL };
+ int ret;
+ struct ldb_result *root_res;
+ struct ldb_dn *settings_dn;
+
+ /* see if we have a cached copy */
+ settings_dn = (struct ldb_dn *)ldb_get_opaque(ldb, "forced.ntds_settings_dn");
+ if (settings_dn) {
+ return ldb_dn_copy(mem_ctx, settings_dn);
+ }
+
+ tmp_ctx = talloc_new(mem_ctx);
+ if (tmp_ctx == NULL) {
+ goto failed;
+ }
+
+ ret = ldb_search(ldb, tmp_ctx, &root_res, ldb_dn_new(tmp_ctx, ldb, ""), LDB_SCOPE_BASE, root_attrs, NULL);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1,("Searching for dsServiceName in rootDSE failed: %s\n",
+ ldb_errstring(ldb)));
+ goto failed;
+ }
+
+ if (root_res->count != 1) {
+ goto failed;
+ }
+
+ settings_dn = ldb_msg_find_attr_as_dn(ldb, tmp_ctx, root_res->msgs[0], "dsServiceName");
+
+ /* note that we do not cache the DN here, as that would mean
+ * we could not handle server renames at runtime. Only
+ * provision sets up forced.ntds_settings_dn */
+
+ talloc_steal(mem_ctx, settings_dn);
+ talloc_free(tmp_ctx);
+
+ return settings_dn;
+
+failed:
+ DEBUG(1,("Failed to find our own NTDS Settings DN in the ldb!\n"));
+ talloc_free(tmp_ctx);
+ return NULL;
+}
+
+/*
+ work out the ntds settings invocationID/objectGUID for the current open ldb
+*/
+static const struct GUID *samdb_ntds_GUID(struct ldb_context *ldb,
+ const char *attribute,
+ const char *cache_name)
+{
+ TALLOC_CTX *tmp_ctx;
+ const char *attrs[] = { attribute, NULL };
+ int ret;
+ struct ldb_result *res;
+ struct GUID *ntds_guid;
+ struct ldb_dn *ntds_settings_dn = NULL;
+ const char *errstr = NULL;
+
+ /* see if we have a cached copy */
+ ntds_guid = (struct GUID *)ldb_get_opaque(ldb, cache_name);
+ if (ntds_guid != NULL) {
+ return ntds_guid;
+ }
+
+ tmp_ctx = talloc_new(ldb);
+ if (tmp_ctx == NULL) {
+ goto failed;
+ }
+
+ ntds_settings_dn = samdb_ntds_settings_dn(ldb, tmp_ctx);
+ if (ntds_settings_dn == NULL) {
+ errstr = "samdb_ntds_settings_dn() returned NULL";
+ goto failed;
+ }
+
+ ret = ldb_search(ldb, tmp_ctx, &res, ntds_settings_dn,
+ LDB_SCOPE_BASE, attrs, NULL);
+ if (ret) {
+ errstr = ldb_errstring(ldb);
+ goto failed;
+ }
+
+ if (res->count != 1) {
+ errstr = "incorrect number of results from base search";
+ goto failed;
+ }
+
+ ntds_guid = talloc(tmp_ctx, struct GUID);
+ if (ntds_guid == NULL) {
+ goto failed;
+ }
+
+ *ntds_guid = samdb_result_guid(res->msgs[0], attribute);
+
+ if (GUID_all_zero(ntds_guid)) {
+ if (ldb_msg_find_ldb_val(res->msgs[0], attribute)) {
+ errstr = "failed to find the GUID attribute";
+ } else {
+ errstr = "failed to parse the GUID";
+ }
+ goto failed;
+ }
+
+ /* cache the domain_sid in the ldb */
+ if (ldb_set_opaque(ldb, cache_name, ntds_guid) != LDB_SUCCESS) {
+ errstr = "ldb_set_opaque() failed";
+ goto failed;
+ }
+
+ talloc_steal(ldb, ntds_guid);
+ talloc_free(tmp_ctx);
+
+ return ntds_guid;
+
+failed:
+ DBG_WARNING("Failed to find our own NTDS Settings %s in the ldb: %s!\n",
+ attribute, errstr);
+ talloc_free(tmp_ctx);
+ return NULL;
+}
+
+/*
+ work out the ntds settings objectGUID for the current open ldb
+*/
+const struct GUID *samdb_ntds_objectGUID(struct ldb_context *ldb)
+{
+ return samdb_ntds_GUID(ldb, "objectGUID", "cache.ntds_guid");
+}
+
+/*
+ work out the ntds settings invocationId for the current open ldb
+*/
+const struct GUID *samdb_ntds_invocation_id(struct ldb_context *ldb)
+{
+ return samdb_ntds_GUID(ldb, "invocationId", "cache.invocation_id");
+}
+
+static bool samdb_set_ntds_GUID(struct ldb_context *ldb,
+ const struct GUID *ntds_guid_in,
+ const char *attribute,
+ const char *cache_name)
+{
+ TALLOC_CTX *tmp_ctx;
+ struct GUID *ntds_guid_new;
+ struct GUID *ntds_guid_old;
+
+ /* see if we have a cached copy */
+ ntds_guid_old = (struct GUID *)ldb_get_opaque(ldb, cache_name);
+
+ tmp_ctx = talloc_new(ldb);
+ if (tmp_ctx == NULL) {
+ goto failed;
+ }
+
+ ntds_guid_new = talloc(tmp_ctx, struct GUID);
+ if (!ntds_guid_new) {
+ goto failed;
+ }
+
+ *ntds_guid_new = *ntds_guid_in;
+
+ /* cache the domain_sid in the ldb */
+ if (ldb_set_opaque(ldb, cache_name, ntds_guid_new) != LDB_SUCCESS) {
+ goto failed;
+ }
+
+ talloc_steal(ldb, ntds_guid_new);
+ talloc_free(tmp_ctx);
+ talloc_free(ntds_guid_old);
+
+ return true;
+
+failed:
+ DBG_WARNING("Failed to set our own cached %s in the ldb!\n",
+ attribute);
+ talloc_free(tmp_ctx);
+ return false;
+}
+
+bool samdb_set_ntds_objectGUID(struct ldb_context *ldb, const struct GUID *ntds_guid_in)
+{
+ return samdb_set_ntds_GUID(ldb,
+ ntds_guid_in,
+ "objectGUID",
+ "cache.ntds_guid");
+}
+
+bool samdb_set_ntds_invocation_id(struct ldb_context *ldb, const struct GUID *invocation_id_in)
+{
+ return samdb_set_ntds_GUID(ldb,
+ invocation_id_in,
+ "invocationId",
+ "cache.invocation_id");
+}
+
+/*
+ work out the server dn for the current open ldb
+*/
+struct ldb_dn *samdb_server_dn(struct ldb_context *ldb, TALLOC_CTX *mem_ctx)
+{
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ struct ldb_dn *dn;
+ if (!tmp_ctx) {
+ return NULL;
+ }
+ dn = ldb_dn_get_parent(mem_ctx, samdb_ntds_settings_dn(ldb, tmp_ctx));
+ talloc_free(tmp_ctx);
+ return dn;
+
+}
+
+/*
+ work out the server dn for the current open ldb
+*/
+struct ldb_dn *samdb_server_site_dn(struct ldb_context *ldb, TALLOC_CTX *mem_ctx)
+{
+ struct ldb_dn *server_dn;
+ struct ldb_dn *servers_dn;
+ struct ldb_dn *server_site_dn;
+
+ /* TODO: there must be a saner way to do this!! */
+ server_dn = samdb_server_dn(ldb, mem_ctx);
+ if (!server_dn) return NULL;
+
+ servers_dn = ldb_dn_get_parent(mem_ctx, server_dn);
+ talloc_free(server_dn);
+ if (!servers_dn) return NULL;
+
+ server_site_dn = ldb_dn_get_parent(mem_ctx, servers_dn);
+ talloc_free(servers_dn);
+
+ return server_site_dn;
+}
+
+/*
+ find the site name from a computers DN record
+ */
+int samdb_find_site_for_computer(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx, struct ldb_dn *computer_dn,
+ const char **site_name)
+{
+ int ret;
+ struct ldb_dn *dn;
+ const struct ldb_val *rdn_val;
+
+ *site_name = NULL;
+
+ ret = samdb_reference_dn(ldb, mem_ctx, computer_dn, "serverReferenceBL", &dn);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ if (!ldb_dn_remove_child_components(dn, 2)) {
+ talloc_free(dn);
+ return LDB_ERR_INVALID_DN_SYNTAX;
+ }
+
+ rdn_val = ldb_dn_get_rdn_val(dn);
+ if (rdn_val == NULL) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ (*site_name) = talloc_strndup(mem_ctx, (const char *)rdn_val->data, rdn_val->length);
+ talloc_free(dn);
+ if (!*site_name) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ return LDB_SUCCESS;
+}
+
+/*
+ find the NTDS GUID from a computers DN record
+ */
+int samdb_find_ntdsguid_for_computer(struct ldb_context *ldb, struct ldb_dn *computer_dn,
+ struct GUID *ntds_guid)
+{
+ int ret;
+ struct ldb_dn *dn;
+
+ *ntds_guid = GUID_zero();
+
+ ret = samdb_reference_dn(ldb, ldb, computer_dn, "serverReferenceBL", &dn);
+ 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_find_guid_by_dn(ldb, dn, ntds_guid);
+ talloc_free(dn);
+ return ret;
+}
+
+/*
+ find a 'reference' DN that points at another object
+ (eg. serverReference, rIDManagerReference etc)
+ */
+int samdb_reference_dn(struct ldb_context *ldb, TALLOC_CTX *mem_ctx, struct ldb_dn *base,
+ const char *attribute, struct ldb_dn **dn)
+{
+ const char *attrs[2];
+ struct ldb_result *res;
+ int ret;
+
+ attrs[0] = attribute;
+ attrs[1] = NULL;
+
+ ret = dsdb_search(ldb, mem_ctx, &res, base, LDB_SCOPE_BASE, attrs, DSDB_SEARCH_ONE_ONLY|DSDB_SEARCH_SHOW_EXTENDED_DN, NULL);
+ if (ret != LDB_SUCCESS) {
+ ldb_asprintf_errstring(ldb, "Cannot find DN %s to get attribute %s for reference dn: %s",
+ ldb_dn_get_linearized(base), attribute, ldb_errstring(ldb));
+ return ret;
+ }
+
+ *dn = ldb_msg_find_attr_as_dn(ldb, mem_ctx, res->msgs[0], attribute);
+ if (!*dn) {
+ if (!ldb_msg_find_element(res->msgs[0], attribute)) {
+ ldb_asprintf_errstring(ldb, "Cannot find attribute %s of %s to calculate reference dn", attribute,
+ ldb_dn_get_linearized(base));
+ } else {
+ ldb_asprintf_errstring(ldb, "Cannot interpret attribute %s of %s as a dn", attribute,
+ ldb_dn_get_linearized(base));
+ }
+ talloc_free(res);
+ return LDB_ERR_NO_SUCH_ATTRIBUTE;
+ }
+
+ talloc_free(res);
+ return LDB_SUCCESS;
+}
+
+/*
+ find if a DN (must have GUID component!) is our ntdsDsa
+ */
+int samdb_dn_is_our_ntdsa(struct ldb_context *ldb, struct ldb_dn *dn, bool *is_ntdsa)
+{
+ NTSTATUS status;
+ struct GUID dn_guid;
+ const struct GUID *our_ntds_guid;
+ status = dsdb_get_extended_dn_guid(dn, &dn_guid, "GUID");
+ if (!NT_STATUS_IS_OK(status)) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ our_ntds_guid = samdb_ntds_objectGUID(ldb);
+ if (!our_ntds_guid) {
+ DEBUG(0, ("Failed to find our NTDS Settings GUID for comparison with %s - %s\n", ldb_dn_get_linearized(dn), ldb_errstring(ldb)));
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ *is_ntdsa = GUID_equal(&dn_guid, our_ntds_guid);
+ return LDB_SUCCESS;
+}
+
+/*
+ find a 'reference' DN that points at another object and indicate if it is our ntdsDsa
+ */
+int samdb_reference_dn_is_our_ntdsa(struct ldb_context *ldb, struct ldb_dn *base,
+ const char *attribute, bool *is_ntdsa)
+{
+ int ret;
+ struct ldb_dn *referenced_dn;
+ TALLOC_CTX *tmp_ctx = talloc_new(ldb);
+ if (tmp_ctx == NULL) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ ret = samdb_reference_dn(ldb, tmp_ctx, base, attribute, &referenced_dn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0, ("Failed to find object %s for attribute %s - %s\n", ldb_dn_get_linearized(base), attribute, ldb_errstring(ldb)));
+ return ret;
+ }
+
+ ret = samdb_dn_is_our_ntdsa(ldb, referenced_dn, is_ntdsa);
+
+ talloc_free(tmp_ctx);
+ return ret;
+}
+
+/*
+ find our machine account via the serverReference attribute in the
+ server DN
+ */
+int samdb_server_reference_dn(struct ldb_context *ldb, TALLOC_CTX *mem_ctx, struct ldb_dn **dn)
+{
+ struct ldb_dn *server_dn;
+ int ret;
+
+ server_dn = samdb_server_dn(ldb, mem_ctx);
+ if (server_dn == NULL) {
+ return ldb_error(ldb, LDB_ERR_NO_SUCH_OBJECT, __func__);
+ }
+
+ ret = samdb_reference_dn(ldb, mem_ctx, server_dn, "serverReference", dn);
+ talloc_free(server_dn);
+
+ return ret;
+}
+
+/*
+ find the RID Manager$ DN via the rIDManagerReference attribute in the
+ base DN
+ */
+int samdb_rid_manager_dn(struct ldb_context *ldb, TALLOC_CTX *mem_ctx, struct ldb_dn **dn)
+{
+ return samdb_reference_dn(ldb, mem_ctx, ldb_get_default_basedn(ldb),
+ "rIDManagerReference", dn);
+}
+
+/*
+ find the RID Set DN via the rIDSetReferences attribute in our
+ machine account DN
+ */
+int samdb_rid_set_dn(struct ldb_context *ldb, TALLOC_CTX *mem_ctx, struct ldb_dn **dn)
+{
+ struct ldb_dn *server_ref_dn = NULL;
+ int ret;
+
+ ret = samdb_server_reference_dn(ldb, mem_ctx, &server_ref_dn);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ ret = samdb_reference_dn(ldb, mem_ctx, server_ref_dn, "rIDSetReferences", dn);
+ talloc_free(server_ref_dn);
+ return ret;
+}
+
+const char *samdb_server_site_name(struct ldb_context *ldb, TALLOC_CTX *mem_ctx)
+{
+ const struct ldb_val *val = ldb_dn_get_rdn_val(samdb_server_site_dn(ldb,
+ mem_ctx));
+
+ if (val == NULL) {
+ return NULL;
+ }
+
+ return (const char *) val->data;
+}
+
+/*
+ * Finds the client site by using the client's IP address.
+ * The "subnet_name" returns the name of the subnet if parameter != NULL
+ *
+ * Has a Windows-based fallback to provide the only site available, or an empty
+ * string if there are multiple sites.
+ */
+const char *samdb_client_site_name(struct ldb_context *ldb, TALLOC_CTX *mem_ctx,
+ const char *ip_address, char **subnet_name,
+ bool fallback)
+{
+ const char *attrs[] = { "cn", "siteObject", NULL };
+ struct ldb_dn *sites_container_dn = NULL;
+ struct ldb_dn *subnets_dn = NULL;
+ struct ldb_dn *sites_dn = NULL;
+ struct ldb_result *res = NULL;
+ const struct ldb_val *val = NULL;
+ const char *site_name = NULL;
+ const char *l_subnet_name = NULL;
+ const char *allow_list[2] = { NULL, NULL };
+ unsigned int i, count;
+ int ret;
+
+ /*
+ * if we don't have a client ip e.g. ncalrpc
+ * the server site is the client site
+ */
+ if (ip_address == NULL) {
+ return samdb_server_site_name(ldb, mem_ctx);
+ }
+
+ sites_container_dn = samdb_sites_dn(ldb, mem_ctx);
+ if (sites_container_dn == NULL) {
+ goto exit;
+ }
+
+ subnets_dn = ldb_dn_copy(mem_ctx, sites_container_dn);
+ if ( ! ldb_dn_add_child_fmt(subnets_dn, "CN=Subnets")) {
+ goto exit;
+ }
+
+ ret = ldb_search(ldb, mem_ctx, &res, subnets_dn, LDB_SCOPE_ONELEVEL,
+ attrs, NULL);
+ if (ret == LDB_ERR_NO_SUCH_OBJECT) {
+ count = 0;
+ } else if (ret != LDB_SUCCESS) {
+ goto exit;
+ } else {
+ count = res->count;
+ }
+
+ for (i = 0; i < count; i++) {
+ l_subnet_name = ldb_msg_find_attr_as_string(res->msgs[i], "cn",
+ NULL);
+
+ allow_list[0] = l_subnet_name;
+
+ if (allow_access_nolog(NULL, allow_list, "", ip_address)) {
+ sites_dn = ldb_msg_find_attr_as_dn(ldb, mem_ctx,
+ res->msgs[i],
+ "siteObject");
+ if (sites_dn == NULL) {
+ /* No reference, maybe another subnet matches */
+ continue;
+ }
+
+ /* "val" cannot be NULL here since "sites_dn" != NULL */
+ val = ldb_dn_get_rdn_val(sites_dn);
+ site_name = talloc_strdup(mem_ctx,
+ (const char *) val->data);
+
+ TALLOC_FREE(sites_dn);
+
+ break;
+ }
+ }
+
+ if (site_name == NULL && fallback) {
+ /* This is the Windows Server fallback rule: when no subnet
+ * exists and we have only one site available then use it (it
+ * is for sure the same as our server site). If more sites do
+ * exist then we don't know which one to use and set the site
+ * name to "". */
+ size_t cnt = 0;
+ ret = dsdb_domain_count(
+ ldb,
+ &cnt,
+ sites_container_dn,
+ NULL,
+ LDB_SCOPE_SUBTREE,
+ "(objectClass=site)");
+ if (ret != LDB_SUCCESS) {
+ goto exit;
+ }
+ if (cnt == 1) {
+ site_name = samdb_server_site_name(ldb, mem_ctx);
+ } else {
+ site_name = talloc_strdup(mem_ctx, "");
+ }
+ l_subnet_name = NULL;
+ }
+
+ if (subnet_name != NULL) {
+ *subnet_name = talloc_strdup(mem_ctx, l_subnet_name);
+ }
+
+exit:
+ TALLOC_FREE(sites_container_dn);
+ TALLOC_FREE(subnets_dn);
+ TALLOC_FREE(res);
+
+ return site_name;
+}
+
+/*
+ work out if we are the PDC for the domain of the current open ldb
+*/
+bool samdb_is_pdc(struct ldb_context *ldb)
+{
+ int ret;
+ bool is_pdc;
+
+ ret = samdb_reference_dn_is_our_ntdsa(ldb, ldb_get_default_basedn(ldb), "fsmoRoleOwner",
+ &is_pdc);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1,("Failed to find if we are the PDC for this ldb: Searching for fSMORoleOwner in %s failed: %s\n",
+ ldb_dn_get_linearized(ldb_get_default_basedn(ldb)),
+ ldb_errstring(ldb)));
+ return false;
+ }
+
+ return is_pdc;
+}
+
+/*
+ work out if we are a Global Catalog server for the domain of the current open ldb
+*/
+bool samdb_is_gc(struct ldb_context *ldb)
+{
+ uint32_t options = 0;
+ if (samdb_ntds_options(ldb, &options) != LDB_SUCCESS) {
+ return false;
+ }
+ return (options & DS_NTDSDSA_OPT_IS_GC) != 0;
+}
+
+/* Find a domain object in the parents of a particular DN. */
+int samdb_search_for_parent_domain(struct ldb_context *ldb, TALLOC_CTX *mem_ctx, struct ldb_dn *dn,
+ struct ldb_dn **parent_dn, const char **errstring)
+{
+ TALLOC_CTX *local_ctx;
+ struct ldb_dn *sdn = dn;
+ struct ldb_result *res = NULL;
+ int ret = LDB_SUCCESS;
+ const char *attrs[] = { NULL };
+
+ local_ctx = talloc_new(mem_ctx);
+ if (local_ctx == NULL) return ldb_oom(ldb);
+
+ while ((sdn = ldb_dn_get_parent(local_ctx, sdn))) {
+ ret = ldb_search(ldb, local_ctx, &res, sdn, LDB_SCOPE_BASE, attrs,
+ "(|(objectClass=domain)(objectClass=builtinDomain))");
+ if (ret == LDB_SUCCESS) {
+ if (res->count == 1) {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+
+ if (ret != LDB_SUCCESS) {
+ *errstring = talloc_asprintf(mem_ctx, "Error searching for parent domain of %s, failed searching for %s: %s",
+ ldb_dn_get_linearized(dn),
+ ldb_dn_get_linearized(sdn),
+ ldb_errstring(ldb));
+ talloc_free(local_ctx);
+ return ret;
+ }
+ /* should never be true with 'ret=LDB_SUCCESS', here to satisfy clang */
+ if (res == NULL) {
+ talloc_free(local_ctx);
+ return LDB_ERR_OTHER;
+ }
+ if (res->count != 1) {
+ *errstring = talloc_asprintf(mem_ctx, "Invalid dn (%s), not child of a domain object",
+ ldb_dn_get_linearized(dn));
+ DEBUG(0,(__location__ ": %s\n", *errstring));
+ talloc_free(local_ctx);
+ return LDB_ERR_CONSTRAINT_VIOLATION;
+ }
+
+ *parent_dn = talloc_steal(mem_ctx, res->msgs[0]->dn);
+ talloc_free(local_ctx);
+ return ret;
+}
+
+static void pwd_timeout_debug(struct tevent_context *unused1,
+ struct tevent_timer *unused2,
+ struct timeval unused3,
+ void *unused4)
+{
+ DEBUG(0, ("WARNING: check_password_complexity: password script "
+ "took more than 1 second to run\n"));
+}
+
+
+/*
+ * Performs checks on a user password (plaintext UNIX format - attribute
+ * "password"). The remaining parameters have to be extracted from the domain
+ * object in the AD.
+ *
+ * Result codes from "enum samr_ValidationStatus" (consider "samr.idl")
+ */
+enum samr_ValidationStatus samdb_check_password(TALLOC_CTX *mem_ctx,
+ struct loadparm_context *lp_ctx,
+ const char *account_name,
+ const char *user_principal_name,
+ const char *full_name,
+ const DATA_BLOB *utf8_blob,
+ const uint32_t pwdProperties,
+ const uint32_t minPwdLength)
+{
+ const struct loadparm_substitution *lp_sub =
+ lpcfg_noop_substitution();
+ char *password_script = NULL;
+ const char *utf8_pw = (const char *)utf8_blob->data;
+
+ /*
+ * This looks strange because it is.
+ *
+ * The check for the number of characters in the password
+ * should clearly not be against the byte length, or else a
+ * single UTF8 character would count for more than one.
+ *
+ * We have chosen to use the number of 16-bit units that the
+ * password encodes to as the measure of length. This is not
+ * the same as the number of codepoints, if a password
+ * contains a character beyond the Basic Multilingual Plane
+ * (above 65535) it will count for more than one "character".
+ */
+
+ size_t password_characters_roughly = strlen_m(utf8_pw);
+
+ /* checks if the "minPwdLength" property is satisfied */
+ if (minPwdLength > password_characters_roughly) {
+ return SAMR_VALIDATION_STATUS_PWD_TOO_SHORT;
+ }
+
+ /* We might not be asked to check the password complexity */
+ if (!(pwdProperties & DOMAIN_PASSWORD_COMPLEX)) {
+ return SAMR_VALIDATION_STATUS_SUCCESS;
+ }
+
+ if (password_characters_roughly == 0) {
+ return SAMR_VALIDATION_STATUS_NOT_COMPLEX_ENOUGH;
+ }
+
+ password_script = lpcfg_check_password_script(lp_ctx, lp_sub, mem_ctx);
+ if (password_script != NULL && *password_script != '\0') {
+ int check_ret = 0;
+ int error = 0;
+ ssize_t nwritten = 0;
+ struct tevent_context *event_ctx = NULL;
+ struct tevent_req *req = NULL;
+ int cps_stdin = -1;
+ const char * const cmd[4] = {
+ "/bin/sh", "-c",
+ password_script,
+ NULL
+ };
+
+ event_ctx = tevent_context_init(mem_ctx);
+ if (event_ctx == NULL) {
+ TALLOC_FREE(password_script);
+ return SAMR_VALIDATION_STATUS_PASSWORD_FILTER_ERROR;
+ }
+
+ /* Gives a warning after 1 second, terminates after 10 */
+ tevent_add_timer(event_ctx, event_ctx,
+ tevent_timeval_current_ofs(1, 0),
+ pwd_timeout_debug, NULL);
+
+ check_ret = setenv("SAMBA_CPS_ACCOUNT_NAME", account_name, 1);
+ if (check_ret != 0) {
+ TALLOC_FREE(password_script);
+ TALLOC_FREE(event_ctx);
+ return SAMR_VALIDATION_STATUS_PASSWORD_FILTER_ERROR;
+ }
+ if (user_principal_name != NULL) {
+ check_ret = setenv("SAMBA_CPS_USER_PRINCIPAL_NAME",
+ user_principal_name, 1);
+ } else {
+ unsetenv("SAMBA_CPS_USER_PRINCIPAL_NAME");
+ }
+ if (check_ret != 0) {
+ TALLOC_FREE(password_script);
+ TALLOC_FREE(event_ctx);
+ return SAMR_VALIDATION_STATUS_PASSWORD_FILTER_ERROR;
+ }
+ if (full_name != NULL) {
+ check_ret = setenv("SAMBA_CPS_FULL_NAME", full_name, 1);
+ } else {
+ unsetenv("SAMBA_CPS_FULL_NAME");
+ }
+ if (check_ret != 0) {
+ TALLOC_FREE(password_script);
+ TALLOC_FREE(event_ctx);
+ return SAMR_VALIDATION_STATUS_PASSWORD_FILTER_ERROR;
+ }
+
+ req = samba_runcmd_send(event_ctx, event_ctx,
+ tevent_timeval_current_ofs(10, 0),
+ 100, 100, cmd, NULL);
+ unsetenv("SAMBA_CPS_ACCOUNT_NAME");
+ unsetenv("SAMBA_CPS_USER_PRINCIPAL_NAME");
+ unsetenv("SAMBA_CPS_FULL_NAME");
+ if (req == NULL) {
+ TALLOC_FREE(password_script);
+ TALLOC_FREE(event_ctx);
+ return SAMR_VALIDATION_STATUS_PASSWORD_FILTER_ERROR;
+ }
+
+ cps_stdin = samba_runcmd_export_stdin(req);
+
+ nwritten = write_data(
+ cps_stdin, utf8_blob->data, utf8_blob->length);
+ if (nwritten == -1) {
+ close(cps_stdin);
+ TALLOC_FREE(password_script);
+ TALLOC_FREE(event_ctx);
+ return SAMR_VALIDATION_STATUS_PASSWORD_FILTER_ERROR;
+ }
+
+ close(cps_stdin);
+
+ if (!tevent_req_poll(req, event_ctx)) {
+ TALLOC_FREE(password_script);
+ TALLOC_FREE(event_ctx);
+ return SAMR_VALIDATION_STATUS_PASSWORD_FILTER_ERROR;
+ }
+
+ check_ret = samba_runcmd_recv(req, &error);
+ TALLOC_FREE(event_ctx);
+
+ if (error == ETIMEDOUT) {
+ DEBUG(0, ("check_password_complexity: check password script took too long!\n"));
+ TALLOC_FREE(password_script);
+ return SAMR_VALIDATION_STATUS_PASSWORD_FILTER_ERROR;
+ }
+ DEBUG(5,("check_password_complexity: check password script (%s) "
+ "returned [%d]\n", password_script, check_ret));
+
+ if (check_ret != 0) {
+ DEBUG(1,("check_password_complexity: "
+ "check password script said new password is not good "
+ "enough!\n"));
+ TALLOC_FREE(password_script);
+ return SAMR_VALIDATION_STATUS_NOT_COMPLEX_ENOUGH;
+ }
+
+ TALLOC_FREE(password_script);
+ return SAMR_VALIDATION_STATUS_SUCCESS;
+ }
+
+ TALLOC_FREE(password_script);
+
+ /*
+ * Here are the standard AD password quality rules, which we
+ * run after the script.
+ */
+
+ if (!check_password_quality(utf8_pw)) {
+ return SAMR_VALIDATION_STATUS_NOT_COMPLEX_ENOUGH;
+ }
+
+ return SAMR_VALIDATION_STATUS_SUCCESS;
+}
+
+/*
+ * Callback for "samdb_set_password" password change
+ */
+int samdb_set_password_callback(struct ldb_request *req, struct ldb_reply *ares)
+{
+ int ret;
+
+ if (!ares) {
+ return ldb_request_done(req, LDB_ERR_OPERATIONS_ERROR);
+ }
+
+ if (ares->error != LDB_SUCCESS) {
+ ret = ares->error;
+ req->context = talloc_steal(req,
+ ldb_reply_get_control(ares, DSDB_CONTROL_PASSWORD_CHANGE_STATUS_OID));
+ talloc_free(ares);
+ return ldb_request_done(req, ret);
+ }
+
+ if (ares->type != LDB_REPLY_DONE) {
+ talloc_free(ares);
+ return ldb_request_done(req, LDB_ERR_OPERATIONS_ERROR);
+ }
+
+ req->context = talloc_steal(req,
+ ldb_reply_get_control(ares, DSDB_CONTROL_PASSWORD_CHANGE_STATUS_OID));
+ talloc_free(ares);
+ return ldb_request_done(req, LDB_SUCCESS);
+}
+
+/*
+ * Sets the user password using plaintext UTF16 (attribute "new_password") or
+ * LM (attribute "lmNewHash") or NT (attribute "ntNewHash") hash. Also pass
+ * the old LM and/or NT hash (attributes "lmOldHash"/"ntOldHash") if it is a
+ * user change or not. The "rejectReason" gives some more information if the
+ * change failed.
+ *
+ * Results: NT_STATUS_OK, NT_STATUS_INVALID_PARAMETER, NT_STATUS_UNSUCCESSFUL,
+ * NT_STATUS_WRONG_PASSWORD, NT_STATUS_PASSWORD_RESTRICTION,
+ * NT_STATUS_ACCESS_DENIED, NT_STATUS_ACCOUNT_LOCKED_OUT, NT_STATUS_NO_MEMORY
+ */
+static NTSTATUS samdb_set_password_internal(struct ldb_context *ldb, TALLOC_CTX *mem_ctx,
+ struct ldb_dn *user_dn, struct ldb_dn *domain_dn,
+ const DATA_BLOB *new_password,
+ const struct samr_Password *ntNewHash,
+ enum dsdb_password_checked old_password_checked,
+ enum samPwdChangeReason *reject_reason,
+ struct samr_DomInfo1 **_dominfo,
+ bool permit_interdomain_trust)
+{
+ struct ldb_message *msg;
+ struct ldb_message_element *el;
+ struct ldb_request *req;
+ struct dsdb_control_password_change_status *pwd_stat = NULL;
+ int ret;
+ bool hash_values = false;
+ NTSTATUS status = NT_STATUS_OK;
+
+#define CHECK_RET(x) \
+ if (x != LDB_SUCCESS) { \
+ talloc_free(msg); \
+ return NT_STATUS_NO_MEMORY; \
+ }
+
+ msg = ldb_msg_new(mem_ctx);
+ if (msg == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ msg->dn = user_dn;
+ if ((new_password != NULL)
+ && ((ntNewHash == NULL))) {
+ /* we have the password as plaintext UTF16 */
+ CHECK_RET(ldb_msg_add_value(msg, "clearTextPassword",
+ new_password, NULL));
+ el = ldb_msg_find_element(msg, "clearTextPassword");
+ el->flags = LDB_FLAG_MOD_REPLACE;
+ } else if ((new_password == NULL)
+ && ((ntNewHash != NULL))) {
+ /* we have a password as NT hash */
+ if (ntNewHash != NULL) {
+ CHECK_RET(samdb_msg_add_hash(ldb, mem_ctx, msg,
+ "unicodePwd", ntNewHash));
+ el = ldb_msg_find_element(msg, "unicodePwd");
+ el->flags = LDB_FLAG_MOD_REPLACE;
+ }
+ hash_values = true;
+ } else {
+ /* the password wasn't specified correctly */
+ talloc_free(msg);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ /* build modify request */
+ ret = ldb_build_mod_req(&req, ldb, mem_ctx, msg, NULL, NULL,
+ samdb_set_password_callback, NULL);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(msg);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ /* A password change operation */
+ if (old_password_checked == DSDB_PASSWORD_CHECKED_AND_CORRECT) {
+ struct dsdb_control_password_change *change;
+
+ change = talloc(req, struct dsdb_control_password_change);
+ if (change == NULL) {
+ talloc_free(req);
+ talloc_free(msg);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ change->old_password_checked = old_password_checked;
+
+ ret = ldb_request_add_control(req,
+ DSDB_CONTROL_PASSWORD_CHANGE_OLD_PW_CHECKED_OID,
+ true, change);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(req);
+ talloc_free(msg);
+ return NT_STATUS_NO_MEMORY;
+ }
+ }
+ if (hash_values) {
+ ret = ldb_request_add_control(req,
+ DSDB_CONTROL_PASSWORD_HASH_VALUES_OID,
+ true, NULL);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(req);
+ talloc_free(msg);
+ return NT_STATUS_NO_MEMORY;
+ }
+ }
+ if (permit_interdomain_trust) {
+ ret = ldb_request_add_control(req,
+ DSDB_CONTROL_PERMIT_INTERDOMAIN_TRUST_UAC_OID,
+ false, NULL);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(req);
+ talloc_free(msg);
+ return NT_STATUS_NO_MEMORY;
+ }
+ }
+ ret = ldb_request_add_control(req,
+ DSDB_CONTROL_PASSWORD_CHANGE_STATUS_OID,
+ true, NULL);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(req);
+ talloc_free(msg);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ ret = ldb_request(ldb, req);
+ if (ret == LDB_SUCCESS) {
+ ret = ldb_wait(req->handle, LDB_WAIT_ALL);
+ }
+
+ if (req->context != NULL) {
+ struct ldb_control *control = talloc_get_type_abort(req->context,
+ struct ldb_control);
+ pwd_stat = talloc_get_type_abort(control->data,
+ struct dsdb_control_password_change_status);
+ talloc_steal(mem_ctx, pwd_stat);
+ }
+
+ talloc_free(req);
+ talloc_free(msg);
+
+ /* Sets the domain info (if requested) */
+ if (_dominfo != NULL) {
+ struct samr_DomInfo1 *dominfo;
+
+ dominfo = talloc_zero(mem_ctx, struct samr_DomInfo1);
+ if (dominfo == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ if (pwd_stat != NULL) {
+ dominfo->min_password_length = pwd_stat->domain_data.minPwdLength;
+ dominfo->password_properties = pwd_stat->domain_data.pwdProperties;
+ dominfo->password_history_length = pwd_stat->domain_data.pwdHistoryLength;
+ dominfo->max_password_age = pwd_stat->domain_data.maxPwdAge;
+ dominfo->min_password_age = pwd_stat->domain_data.minPwdAge;
+ }
+
+ *_dominfo = dominfo;
+ }
+
+ if (reject_reason != NULL) {
+ if (pwd_stat != NULL) {
+ *reject_reason = pwd_stat->reject_reason;
+ } else {
+ *reject_reason = SAM_PWD_CHANGE_NO_ERROR;
+ }
+ }
+
+ if (pwd_stat != NULL) {
+ talloc_free(pwd_stat);
+ }
+
+ if (ret == LDB_ERR_CONSTRAINT_VIOLATION) {
+ const char *errmsg = ldb_errstring(ldb);
+ char *endptr = NULL;
+ WERROR werr = WERR_GEN_FAILURE;
+ status = NT_STATUS_UNSUCCESSFUL;
+ if (errmsg != NULL) {
+ werr = W_ERROR(strtol(errmsg, &endptr, 16));
+ DBG_WARNING("%s\n", errmsg);
+ }
+ if (endptr != errmsg) {
+ if (W_ERROR_EQUAL(werr, WERR_INVALID_PASSWORD)) {
+ status = NT_STATUS_WRONG_PASSWORD;
+ }
+ if (W_ERROR_EQUAL(werr, WERR_PASSWORD_RESTRICTION)) {
+ status = NT_STATUS_PASSWORD_RESTRICTION;
+ }
+ if (W_ERROR_EQUAL(werr, WERR_ACCOUNT_LOCKED_OUT)) {
+ status = NT_STATUS_ACCOUNT_LOCKED_OUT;
+ }
+ }
+ } else if (ret == LDB_ERR_NO_SUCH_OBJECT) {
+ /* don't let the caller know if an account doesn't exist */
+ status = NT_STATUS_WRONG_PASSWORD;
+ } else if (ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS) {
+ status = NT_STATUS_ACCESS_DENIED;
+ } else if (ret != LDB_SUCCESS) {
+ DEBUG(1, ("Failed to set password on %s: %s\n",
+ ldb_dn_get_linearized(user_dn),
+ ldb_errstring(ldb)));
+ status = NT_STATUS_UNSUCCESSFUL;
+ }
+
+ return status;
+}
+
+NTSTATUS samdb_set_password(struct ldb_context *ldb, TALLOC_CTX *mem_ctx,
+ struct ldb_dn *user_dn, struct ldb_dn *domain_dn,
+ const DATA_BLOB *new_password,
+ const struct samr_Password *ntNewHash,
+ enum dsdb_password_checked old_password_checked,
+ enum samPwdChangeReason *reject_reason,
+ struct samr_DomInfo1 **_dominfo)
+{
+ return samdb_set_password_internal(ldb, mem_ctx,
+ user_dn, domain_dn,
+ new_password,
+ ntNewHash,
+ old_password_checked,
+ reject_reason, _dominfo,
+ false); /* reject trusts */
+}
+
+/*
+ * Sets the user password using plaintext UTF16 (attribute "new_password") or
+ * LM (attribute "lmNewHash") or NT (attribute "ntNewHash") hash. Also pass
+ * the old LM and/or NT hash (attributes "lmOldHash"/"ntOldHash") if it is a
+ * user change or not. The "rejectReason" gives some more information if the
+ * change failed.
+ *
+ * This wrapper function for "samdb_set_password" takes a SID as input rather
+ * than a user DN.
+ *
+ * This call encapsulates a new LDB transaction for changing the password;
+ * therefore the user hasn't to start a new one.
+ *
+ * Results: NT_STATUS_OK, NT_STATUS_INTERNAL_DB_CORRUPTION,
+ * NT_STATUS_INVALID_PARAMETER, NT_STATUS_UNSUCCESSFUL,
+ * NT_STATUS_WRONG_PASSWORD, NT_STATUS_PASSWORD_RESTRICTION,
+ * NT_STATUS_ACCESS_DENIED, NT_STATUS_ACCOUNT_LOCKED_OUT, NT_STATUS_NO_MEMORY
+ * NT_STATUS_TRANSACTION_ABORTED, NT_STATUS_NO_SUCH_USER
+ */
+NTSTATUS samdb_set_password_sid(struct ldb_context *ldb, TALLOC_CTX *mem_ctx,
+ const struct dom_sid *user_sid,
+ const uint32_t *new_version, /* optional for trusts */
+ const DATA_BLOB *new_password,
+ const struct samr_Password *ntNewHash,
+ enum dsdb_password_checked old_password_checked,
+ enum samPwdChangeReason *reject_reason,
+ struct samr_DomInfo1 **_dominfo)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ NTSTATUS nt_status;
+ static const char * const attrs[] = {
+ "userAccountControl",
+ "sAMAccountName",
+ NULL
+ };
+ struct ldb_message *user_msg = NULL;
+ int ret;
+ uint32_t uac = 0;
+
+ ret = ldb_transaction_start(ldb);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1, ("Failed to start transaction: %s\n", ldb_errstring(ldb)));
+ TALLOC_FREE(frame);
+ return NT_STATUS_TRANSACTION_ABORTED;
+ }
+
+ ret = dsdb_search_one(ldb, frame, &user_msg, ldb_get_default_basedn(ldb),
+ LDB_SCOPE_SUBTREE, attrs, 0,
+ "(&(objectSid=%s)(objectClass=user))",
+ ldap_encode_ndr_dom_sid(frame, user_sid));
+ if (ret != LDB_SUCCESS) {
+ ldb_transaction_cancel(ldb);
+ DEBUG(3, ("samdb_set_password_sid: SID[%s] not found in samdb %s - %s, "
+ "returning NO_SUCH_USER\n",
+ dom_sid_string(frame, user_sid),
+ ldb_strerror(ret), ldb_errstring(ldb)));
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_SUCH_USER;
+ }
+
+ uac = ldb_msg_find_attr_as_uint(user_msg, "userAccountControl", 0);
+ if (!(uac & UF_ACCOUNT_TYPE_MASK)) {
+ ldb_transaction_cancel(ldb);
+ DEBUG(1, ("samdb_set_password_sid: invalid "
+ "userAccountControl[0x%08X] for SID[%s] DN[%s], "
+ "returning NO_SUCH_USER\n",
+ (unsigned)uac, dom_sid_string(frame, user_sid),
+ ldb_dn_get_linearized(user_msg->dn)));
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_SUCH_USER;
+ }
+
+ if (uac & UF_INTERDOMAIN_TRUST_ACCOUNT) {
+ static const char * const tdo_attrs[] = {
+ "trustAuthIncoming",
+ "trustDirection",
+ NULL
+ };
+ struct ldb_message *tdo_msg = NULL;
+ const char *account_name = NULL;
+ uint32_t trust_direction;
+ uint32_t i;
+ const struct ldb_val *old_val = NULL;
+ struct trustAuthInOutBlob old_blob = {
+ .count = 0,
+ };
+ uint32_t old_version = 0;
+ struct AuthenticationInformation *old_version_a = NULL;
+ uint32_t _new_version = 0;
+ struct trustAuthInOutBlob new_blob = {
+ .count = 0,
+ };
+ struct ldb_val new_val = {
+ .length = 0,
+ };
+ struct timeval tv = timeval_current();
+ NTTIME now = timeval_to_nttime(&tv);
+ enum ndr_err_code ndr_err;
+
+ if (new_password == NULL && ntNewHash == NULL) {
+ ldb_transaction_cancel(ldb);
+ DEBUG(1, ("samdb_set_password_sid: "
+ "no new password provided "
+ "sAMAccountName for SID[%s] DN[%s], "
+ "returning INVALID_PARAMETER\n",
+ dom_sid_string(frame, user_sid),
+ ldb_dn_get_linearized(user_msg->dn)));
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ if (new_password != NULL && ntNewHash != NULL) {
+ ldb_transaction_cancel(ldb);
+ DEBUG(1, ("samdb_set_password_sid: "
+ "two new passwords provided "
+ "sAMAccountName for SID[%s] DN[%s], "
+ "returning INVALID_PARAMETER\n",
+ dom_sid_string(frame, user_sid),
+ ldb_dn_get_linearized(user_msg->dn)));
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ if (new_password != NULL && (new_password->length % 2)) {
+ ldb_transaction_cancel(ldb);
+ DEBUG(2, ("samdb_set_password_sid: "
+ "invalid utf16 length (%zu) "
+ "sAMAccountName for SID[%s] DN[%s], "
+ "returning WRONG_PASSWORD\n",
+ new_password->length,
+ dom_sid_string(frame, user_sid),
+ ldb_dn_get_linearized(user_msg->dn)));
+ TALLOC_FREE(frame);
+ return NT_STATUS_WRONG_PASSWORD;
+ }
+
+ if (new_password != NULL && new_password->length >= 500) {
+ ldb_transaction_cancel(ldb);
+ DEBUG(2, ("samdb_set_password_sid: "
+ "utf16 password too long (%zu) "
+ "sAMAccountName for SID[%s] DN[%s], "
+ "returning WRONG_PASSWORD\n",
+ new_password->length,
+ dom_sid_string(frame, user_sid),
+ ldb_dn_get_linearized(user_msg->dn)));
+ TALLOC_FREE(frame);
+ return NT_STATUS_WRONG_PASSWORD;
+ }
+
+ account_name = ldb_msg_find_attr_as_string(user_msg,
+ "sAMAccountName", NULL);
+ if (account_name == NULL) {
+ ldb_transaction_cancel(ldb);
+ DEBUG(1, ("samdb_set_password_sid: missing "
+ "sAMAccountName for SID[%s] DN[%s], "
+ "returning NO_SUCH_USER\n",
+ dom_sid_string(frame, user_sid),
+ ldb_dn_get_linearized(user_msg->dn)));
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_SUCH_USER;
+ }
+
+ nt_status = dsdb_trust_search_tdo_by_type(ldb,
+ SEC_CHAN_DOMAIN,
+ account_name,
+ tdo_attrs,
+ frame, &tdo_msg);
+ if (!NT_STATUS_IS_OK(nt_status)) {
+ ldb_transaction_cancel(ldb);
+ DEBUG(1, ("samdb_set_password_sid: dsdb_trust_search_tdo "
+ "failed(%s) for sAMAccountName[%s] SID[%s] DN[%s], "
+ "returning INTERNAL_DB_CORRUPTION\n",
+ nt_errstr(nt_status), account_name,
+ dom_sid_string(frame, user_sid),
+ ldb_dn_get_linearized(user_msg->dn)));
+ TALLOC_FREE(frame);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ trust_direction = ldb_msg_find_attr_as_int(tdo_msg,
+ "trustDirection", 0);
+ if (!(trust_direction & LSA_TRUST_DIRECTION_INBOUND)) {
+ ldb_transaction_cancel(ldb);
+ DEBUG(1, ("samdb_set_password_sid: direction[0x%08X] is "
+ "not inbound for sAMAccountName[%s] "
+ "DN[%s] TDO[%s], "
+ "returning INTERNAL_DB_CORRUPTION\n",
+ (unsigned)trust_direction,
+ account_name,
+ ldb_dn_get_linearized(user_msg->dn),
+ ldb_dn_get_linearized(tdo_msg->dn)));
+ TALLOC_FREE(frame);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ old_val = ldb_msg_find_ldb_val(tdo_msg, "trustAuthIncoming");
+ if (old_val != NULL) {
+ ndr_err = ndr_pull_struct_blob(old_val, frame, &old_blob,
+ (ndr_pull_flags_fn_t)ndr_pull_trustAuthInOutBlob);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ ldb_transaction_cancel(ldb);
+ DEBUG(1, ("samdb_set_password_sid: "
+ "failed(%s) to parse "
+ "trustAuthOutgoing sAMAccountName[%s] "
+ "DN[%s] TDO[%s], "
+ "returning INTERNAL_DB_CORRUPTION\n",
+ ndr_map_error2string(ndr_err),
+ account_name,
+ ldb_dn_get_linearized(user_msg->dn),
+ ldb_dn_get_linearized(tdo_msg->dn)));
+
+ TALLOC_FREE(frame);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+ }
+
+ for (i = old_blob.current.count; i > 0; i--) {
+ struct AuthenticationInformation *a =
+ &old_blob.current.array[i - 1];
+
+ switch (a->AuthType) {
+ case TRUST_AUTH_TYPE_NONE:
+ if (i == old_blob.current.count) {
+ /*
+ * remove TRUST_AUTH_TYPE_NONE at the
+ * end
+ */
+ old_blob.current.count--;
+ }
+ break;
+
+ case TRUST_AUTH_TYPE_VERSION:
+ old_version_a = a;
+ old_version = a->AuthInfo.version.version;
+ break;
+
+ case TRUST_AUTH_TYPE_CLEAR:
+ break;
+
+ case TRUST_AUTH_TYPE_NT4OWF:
+ break;
+ }
+ }
+
+ if (new_version == NULL) {
+ _new_version = 0;
+ new_version = &_new_version;
+ }
+
+ if (old_version_a != NULL && *new_version != (old_version + 1)) {
+ old_version_a->LastUpdateTime = now;
+ old_version_a->AuthType = TRUST_AUTH_TYPE_NONE;
+ }
+
+ new_blob.count = MAX(old_blob.current.count, 2);
+ new_blob.current.array = talloc_zero_array(frame,
+ struct AuthenticationInformation,
+ new_blob.count);
+ if (new_blob.current.array == NULL) {
+ ldb_transaction_cancel(ldb);
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ new_blob.previous.array = talloc_zero_array(frame,
+ struct AuthenticationInformation,
+ new_blob.count);
+ if (new_blob.current.array == NULL) {
+ ldb_transaction_cancel(ldb);
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ for (i = 0; i < old_blob.current.count; i++) {
+ struct AuthenticationInformation *o =
+ &old_blob.current.array[i];
+ struct AuthenticationInformation *p =
+ &new_blob.previous.array[i];
+
+ *p = *o;
+ new_blob.previous.count++;
+ }
+ for (; i < new_blob.count; i++) {
+ struct AuthenticationInformation *pi =
+ &new_blob.previous.array[i];
+
+ if (i == 0) {
+ /*
+ * new_blob.previous is still empty so
+ * we'll do new_blob.previous = new_blob.current
+ * below.
+ */
+ break;
+ }
+
+ pi->LastUpdateTime = now;
+ pi->AuthType = TRUST_AUTH_TYPE_NONE;
+ new_blob.previous.count++;
+ }
+
+ for (i = 0; i < new_blob.count; i++) {
+ struct AuthenticationInformation *ci =
+ &new_blob.current.array[i];
+
+ ci->LastUpdateTime = now;
+ switch (i) {
+ case 0:
+ if (ntNewHash != NULL) {
+ ci->AuthType = TRUST_AUTH_TYPE_NT4OWF;
+ ci->AuthInfo.nt4owf.password = *ntNewHash;
+ break;
+ }
+
+ ci->AuthType = TRUST_AUTH_TYPE_CLEAR;
+ ci->AuthInfo.clear.size = new_password->length;
+ ci->AuthInfo.clear.password = new_password->data;
+ break;
+ case 1:
+ ci->AuthType = TRUST_AUTH_TYPE_VERSION;
+ ci->AuthInfo.version.version = *new_version;
+ break;
+ default:
+ ci->AuthType = TRUST_AUTH_TYPE_NONE;
+ break;
+ }
+
+ new_blob.current.count++;
+ }
+
+ if (new_blob.previous.count == 0) {
+ TALLOC_FREE(new_blob.previous.array);
+ new_blob.previous = new_blob.current;
+ }
+
+ ndr_err = ndr_push_struct_blob(&new_val, frame, &new_blob,
+ (ndr_push_flags_fn_t)ndr_push_trustAuthInOutBlob);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ ldb_transaction_cancel(ldb);
+ DEBUG(1, ("samdb_set_password_sid: "
+ "failed(%s) to generate "
+ "trustAuthOutgoing sAMAccountName[%s] "
+ "DN[%s] TDO[%s], "
+ "returning UNSUCCESSFUL\n",
+ ndr_map_error2string(ndr_err),
+ account_name,
+ ldb_dn_get_linearized(user_msg->dn),
+ ldb_dn_get_linearized(tdo_msg->dn)));
+ TALLOC_FREE(frame);
+ return NT_STATUS_UNSUCCESSFUL;
+ }
+
+ tdo_msg->num_elements = 0;
+ TALLOC_FREE(tdo_msg->elements);
+
+ ret = ldb_msg_append_value(tdo_msg, "trustAuthIncoming",
+ &new_val, LDB_FLAG_MOD_REPLACE);
+ if (ret != LDB_SUCCESS) {
+ ldb_transaction_cancel(ldb);
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ ret = ldb_modify(ldb, tdo_msg);
+ if (ret != LDB_SUCCESS) {
+ nt_status = dsdb_ldb_err_to_ntstatus(ret);
+ ldb_transaction_cancel(ldb);
+ DEBUG(1, ("samdb_set_password_sid: "
+ "failed to replace "
+ "trustAuthOutgoing sAMAccountName[%s] "
+ "DN[%s] TDO[%s], "
+ "%s - %s\n",
+ account_name,
+ ldb_dn_get_linearized(user_msg->dn),
+ ldb_dn_get_linearized(tdo_msg->dn),
+ nt_errstr(nt_status), ldb_errstring(ldb)));
+ TALLOC_FREE(frame);
+ return nt_status;
+ }
+ }
+
+ nt_status = samdb_set_password_internal(ldb, mem_ctx,
+ user_msg->dn, NULL,
+ new_password,
+ ntNewHash,
+ old_password_checked,
+ reject_reason, _dominfo,
+ true); /* permit trusts */
+ if (!NT_STATUS_IS_OK(nt_status)) {
+ ldb_transaction_cancel(ldb);
+ TALLOC_FREE(frame);
+ return nt_status;
+ }
+
+ ret = ldb_transaction_commit(ldb);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,("Failed to commit transaction to change password on %s: %s\n",
+ ldb_dn_get_linearized(user_msg->dn),
+ ldb_errstring(ldb)));
+ TALLOC_FREE(frame);
+ return NT_STATUS_TRANSACTION_ABORTED;
+ }
+
+ TALLOC_FREE(frame);
+ return NT_STATUS_OK;
+}
+
+
+NTSTATUS samdb_create_foreign_security_principal(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx,
+ struct dom_sid *sid, struct ldb_dn **ret_dn)
+{
+ struct ldb_message *msg;
+ struct ldb_dn *basedn = NULL;
+ char *sidstr;
+ int ret;
+
+ sidstr = dom_sid_string(mem_ctx, sid);
+ NT_STATUS_HAVE_NO_MEMORY(sidstr);
+
+ /* We might have to create a ForeignSecurityPrincipal, even if this user
+ * is in our own domain */
+
+ msg = ldb_msg_new(sidstr);
+ if (msg == NULL) {
+ talloc_free(sidstr);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ ret = dsdb_wellknown_dn(sam_ctx, sidstr,
+ ldb_get_default_basedn(sam_ctx),
+ DS_GUID_FOREIGNSECURITYPRINCIPALS_CONTAINER,
+ &basedn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0, ("Failed to find DN for "
+ "ForeignSecurityPrincipal container - %s\n", ldb_errstring(sam_ctx)));
+ talloc_free(sidstr);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ /* add core elements to the ldb_message for the alias */
+ msg->dn = basedn;
+ if ( ! ldb_dn_add_child_fmt(msg->dn, "CN=%s", sidstr)) {
+ talloc_free(sidstr);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ ret = ldb_msg_add_string(msg, "objectClass",
+ "foreignSecurityPrincipal");
+ if (ret != LDB_SUCCESS) {
+ talloc_free(sidstr);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ /* create the alias */
+ ret = ldb_add(sam_ctx, msg);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,("Failed to create foreignSecurityPrincipal "
+ "record %s: %s\n",
+ ldb_dn_get_linearized(msg->dn),
+ ldb_errstring(sam_ctx)));
+ talloc_free(sidstr);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ *ret_dn = talloc_steal(mem_ctx, msg->dn);
+ talloc_free(sidstr);
+
+ return NT_STATUS_OK;
+}
+
+
+/*
+ Find the DN of a domain, assuming it to be a dotted.dns name
+*/
+
+struct ldb_dn *samdb_dns_domain_to_dn(struct ldb_context *ldb, TALLOC_CTX *mem_ctx, const char *dns_domain)
+{
+ unsigned int i;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ const char *binary_encoded;
+ const char * const *split_realm;
+ struct ldb_dn *dn;
+
+ if (!tmp_ctx) {
+ return NULL;
+ }
+
+ split_realm = (const char * const *)str_list_make(tmp_ctx, dns_domain, ".");
+ if (!split_realm) {
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+ dn = ldb_dn_new(mem_ctx, ldb, NULL);
+ for (i=0; split_realm[i]; i++) {
+ binary_encoded = ldb_binary_encode_string(tmp_ctx, split_realm[i]);
+ if (binary_encoded == NULL) {
+ DEBUG(2, ("Failed to add dc= element to DN %s\n",
+ ldb_dn_get_linearized(dn)));
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+ if (!ldb_dn_add_base_fmt(dn, "dc=%s", binary_encoded)) {
+ DEBUG(2, ("Failed to add dc=%s element to DN %s\n",
+ binary_encoded, ldb_dn_get_linearized(dn)));
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+ }
+ if (!ldb_dn_validate(dn)) {
+ DEBUG(2, ("Failed to validated DN %s\n",
+ ldb_dn_get_linearized(dn)));
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+ talloc_free(tmp_ctx);
+ return dn;
+}
+
+
+/*
+ Find the DNS equivalent of a DN, in dotted DNS form
+*/
+char *samdb_dn_to_dns_domain(TALLOC_CTX *mem_ctx, struct ldb_dn *dn)
+{
+ int i, num_components = ldb_dn_get_comp_num(dn);
+ char *dns_name = talloc_strdup(mem_ctx, "");
+ if (dns_name == NULL) {
+ return NULL;
+ }
+
+ for (i=0; i<num_components; i++) {
+ const struct ldb_val *v = ldb_dn_get_component_val(dn, i);
+ char *s;
+ if (v == NULL) {
+ talloc_free(dns_name);
+ return NULL;
+ }
+ s = talloc_asprintf_append_buffer(dns_name, "%*.*s.",
+ (int)v->length, (int)v->length, (char *)v->data);
+ if (s == NULL) {
+ talloc_free(dns_name);
+ return NULL;
+ }
+ dns_name = s;
+ }
+
+ /* remove the last '.' */
+ if (dns_name[0] != 0) {
+ dns_name[strlen(dns_name)-1] = 0;
+ }
+
+ return dns_name;
+}
+
+/*
+ Find the DNS _msdcs name for a given NTDS GUID. The resulting DNS
+ name is based on the forest DNS name
+*/
+char *samdb_ntds_msdcs_dns_name(struct ldb_context *samdb,
+ TALLOC_CTX *mem_ctx,
+ const struct GUID *ntds_guid)
+{
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ const char *guid_str;
+ struct ldb_dn *forest_dn;
+ const char *dnsforest;
+ char *ret;
+
+ guid_str = GUID_string(tmp_ctx, ntds_guid);
+ if (guid_str == NULL) {
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+ forest_dn = ldb_get_root_basedn(samdb);
+ if (forest_dn == NULL) {
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+ dnsforest = samdb_dn_to_dns_domain(tmp_ctx, forest_dn);
+ if (dnsforest == NULL) {
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+ ret = talloc_asprintf(mem_ctx, "%s._msdcs.%s", guid_str, dnsforest);
+ talloc_free(tmp_ctx);
+ return ret;
+}
+
+
+/*
+ Find the DN of a domain, be it the netbios or DNS name
+*/
+struct ldb_dn *samdb_domain_to_dn(struct ldb_context *ldb, TALLOC_CTX *mem_ctx,
+ const char *domain_name)
+{
+ const char * const domain_ref_attrs[] = {
+ "ncName", NULL
+ };
+ const char * const domain_ref2_attrs[] = {
+ NULL
+ };
+ struct ldb_result *res_domain_ref;
+ char *escaped_domain = ldb_binary_encode_string(mem_ctx, domain_name);
+ int ret_domain;
+
+ if (escaped_domain == NULL) {
+ return NULL;
+ }
+
+ /* find the domain's DN */
+ ret_domain = ldb_search(ldb, mem_ctx,
+ &res_domain_ref,
+ samdb_partitions_dn(ldb, mem_ctx),
+ LDB_SCOPE_ONELEVEL,
+ domain_ref_attrs,
+ "(&(nETBIOSName=%s)(objectclass=crossRef))",
+ escaped_domain);
+ if (ret_domain != LDB_SUCCESS) {
+ return NULL;
+ }
+
+ if (res_domain_ref->count == 0) {
+ ret_domain = ldb_search(ldb, mem_ctx,
+ &res_domain_ref,
+ samdb_dns_domain_to_dn(ldb, mem_ctx, domain_name),
+ LDB_SCOPE_BASE,
+ domain_ref2_attrs,
+ "(objectclass=domain)");
+ if (ret_domain != LDB_SUCCESS) {
+ return NULL;
+ }
+
+ if (res_domain_ref->count == 1) {
+ return res_domain_ref->msgs[0]->dn;
+ }
+ return NULL;
+ }
+
+ if (res_domain_ref->count > 1) {
+ DEBUG(0,("Found %d records matching domain [%s]\n",
+ ret_domain, domain_name));
+ return NULL;
+ }
+
+ return samdb_result_dn(ldb, mem_ctx, res_domain_ref->msgs[0], "nCName", NULL);
+
+}
+
+
+/*
+ use a GUID to find a DN
+ */
+int dsdb_find_dn_by_guid(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ const struct GUID *guid,
+ uint32_t dsdb_flags,
+ struct ldb_dn **dn)
+{
+ int ret;
+ struct ldb_result *res;
+ const char *attrs[] = { NULL };
+ struct GUID_txt_buf buf;
+ char *guid_str = GUID_buf_string(guid, &buf);
+
+ ret = dsdb_search(ldb, mem_ctx, &res, NULL, LDB_SCOPE_SUBTREE, attrs,
+ DSDB_SEARCH_SEARCH_ALL_PARTITIONS |
+ DSDB_SEARCH_SHOW_EXTENDED_DN |
+ DSDB_SEARCH_ONE_ONLY | dsdb_flags,
+ "objectGUID=%s", guid_str);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ *dn = talloc_steal(mem_ctx, res->msgs[0]->dn);
+ talloc_free(res);
+
+ return LDB_SUCCESS;
+}
+
+/*
+ use a DN to find a GUID with a given attribute name
+ */
+int dsdb_find_guid_attr_by_dn(struct ldb_context *ldb,
+ struct ldb_dn *dn, const char *attribute,
+ struct GUID *guid)
+{
+ int ret;
+ struct ldb_result *res = NULL;
+ const char *attrs[2];
+ TALLOC_CTX *tmp_ctx = talloc_new(ldb);
+
+ attrs[0] = attribute;
+ attrs[1] = NULL;
+
+ ret = dsdb_search_dn(ldb, tmp_ctx, &res, dn, attrs,
+ DSDB_SEARCH_SHOW_DELETED |
+ DSDB_SEARCH_SHOW_RECYCLED);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+ /* satisfy clang */
+ if (res == NULL) {
+ talloc_free(tmp_ctx);
+ return LDB_ERR_OTHER;
+ }
+ if (res->count < 1) {
+ talloc_free(tmp_ctx);
+ return ldb_error(ldb, LDB_ERR_NO_SUCH_OBJECT, __func__);
+ }
+ *guid = samdb_result_guid(res->msgs[0], attribute);
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+}
+
+/*
+ use a DN to find a GUID
+ */
+int dsdb_find_guid_by_dn(struct ldb_context *ldb,
+ struct ldb_dn *dn, struct GUID *guid)
+{
+ return dsdb_find_guid_attr_by_dn(ldb, dn, "objectGUID", guid);
+}
+
+
+
+/*
+ adds the given GUID to the given ldb_message. This value is added
+ for the given attr_name (may be either "objectGUID" or "parentGUID").
+ This function is used in processing 'add' requests.
+ */
+int dsdb_msg_add_guid(struct ldb_message *msg,
+ struct GUID *guid,
+ const char *attr_name)
+{
+ int ret;
+ struct ldb_val v;
+ NTSTATUS status;
+ TALLOC_CTX *tmp_ctx = talloc_init("dsdb_msg_add_guid");
+
+ status = GUID_to_ndr_blob(guid, tmp_ctx, &v);
+ if (!NT_STATUS_IS_OK(status)) {
+ ret = LDB_ERR_OPERATIONS_ERROR;
+ goto done;
+ }
+
+ ret = ldb_msg_add_steal_value(msg, attr_name, &v);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(4,(__location__ ": Failed to add %s to the message\n",
+ attr_name));
+ goto done;
+ }
+
+ ret = LDB_SUCCESS;
+
+done:
+ talloc_free(tmp_ctx);
+ return ret;
+
+}
+
+
+/*
+ use a DN to find a SID
+ */
+int dsdb_find_sid_by_dn(struct ldb_context *ldb,
+ struct ldb_dn *dn, struct dom_sid *sid)
+{
+ int ret;
+ struct ldb_result *res = NULL;
+ const char *attrs[] = { "objectSid", NULL };
+ TALLOC_CTX *tmp_ctx = talloc_new(ldb);
+ struct dom_sid *s;
+
+ ZERO_STRUCTP(sid);
+
+ ret = dsdb_search_dn(ldb, tmp_ctx, &res, dn, attrs,
+ DSDB_SEARCH_SHOW_DELETED |
+ DSDB_SEARCH_SHOW_RECYCLED);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+ if (res == NULL) {
+ talloc_free(tmp_ctx);
+ return LDB_ERR_OTHER;
+ }
+ if (res->count < 1) {
+ talloc_free(tmp_ctx);
+ return ldb_error(ldb, LDB_ERR_NO_SUCH_OBJECT, __func__);
+ }
+ s = samdb_result_dom_sid(tmp_ctx, res->msgs[0], "objectSid");
+ if (s == NULL) {
+ talloc_free(tmp_ctx);
+ return ldb_error(ldb, LDB_ERR_NO_SUCH_OBJECT, __func__);
+ }
+ *sid = *s;
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+}
+
+/*
+ use a SID to find a DN
+ */
+int dsdb_find_dn_by_sid(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ struct dom_sid *sid, struct ldb_dn **dn)
+{
+ int ret;
+ struct ldb_result *res;
+ const char *attrs[] = { NULL };
+ char *sid_str = ldap_encode_ndr_dom_sid(mem_ctx, sid);
+
+ if (!sid_str) {
+ return ldb_operr(ldb);
+ }
+
+ ret = dsdb_search(ldb, mem_ctx, &res, NULL, LDB_SCOPE_SUBTREE, attrs,
+ DSDB_SEARCH_SEARCH_ALL_PARTITIONS |
+ DSDB_SEARCH_SHOW_EXTENDED_DN |
+ DSDB_SEARCH_ONE_ONLY,
+ "objectSid=%s", sid_str);
+ talloc_free(sid_str);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ *dn = talloc_steal(mem_ctx, res->msgs[0]->dn);
+ talloc_free(res);
+
+ return LDB_SUCCESS;
+}
+
+/*
+ load a repsFromTo blob list for a given partition GUID
+ attr must be "repsFrom" or "repsTo"
+ */
+WERROR dsdb_loadreps(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx, struct ldb_dn *dn,
+ const char *attr, struct repsFromToBlob **r, uint32_t *count)
+{
+ const char *attrs[] = { attr, NULL };
+ struct ldb_result *res = NULL;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ unsigned int i;
+ struct ldb_message_element *el;
+ int ret;
+
+ *r = NULL;
+ *count = 0;
+
+ if (tmp_ctx == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ ret = dsdb_search_dn(sam_ctx, tmp_ctx, &res, dn, attrs, 0);
+ if (ret == LDB_ERR_NO_SUCH_OBJECT) {
+ /* partition hasn't been replicated yet */
+ talloc_free(tmp_ctx);
+ return WERR_OK;
+ }
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,("dsdb_loadreps: failed to read partition object: %s\n", ldb_errstring(sam_ctx)));
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ /* satisfy clang */
+ if (res == NULL) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+ el = ldb_msg_find_element(res->msgs[0], attr);
+ if (el == NULL) {
+ /* it's OK to be empty */
+ talloc_free(tmp_ctx);
+ return WERR_OK;
+ }
+
+ *count = el->num_values;
+ *r = talloc_array(mem_ctx, struct repsFromToBlob, *count);
+ if (*r == NULL) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ for (i=0; i<(*count); i++) {
+ enum ndr_err_code ndr_err;
+ ndr_err = ndr_pull_struct_blob(&el->values[i],
+ mem_ctx,
+ &(*r)[i],
+ (ndr_pull_flags_fn_t)ndr_pull_repsFromToBlob);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+ }
+
+ talloc_free(tmp_ctx);
+
+ return WERR_OK;
+}
+
+/*
+ save the repsFromTo blob list for a given partition GUID
+ attr must be "repsFrom" or "repsTo"
+ */
+WERROR dsdb_savereps(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx, struct ldb_dn *dn,
+ const char *attr, struct repsFromToBlob *r, uint32_t count)
+{
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ struct ldb_message *msg;
+ struct ldb_message_element *el;
+ unsigned int i;
+
+ if (tmp_ctx == NULL) {
+ goto failed;
+ }
+
+ msg = ldb_msg_new(tmp_ctx);
+ if (msg == NULL) {
+ goto failed;
+ }
+ msg->dn = dn;
+ if (ldb_msg_add_empty(msg, attr, LDB_FLAG_MOD_REPLACE, &el) != LDB_SUCCESS) {
+ goto failed;
+ }
+
+ el->values = talloc_array(msg, struct ldb_val, count);
+ if (!el->values) {
+ goto failed;
+ }
+
+ for (i=0; i<count; i++) {
+ struct ldb_val v;
+ enum ndr_err_code ndr_err;
+
+ ndr_err = ndr_push_struct_blob(&v, tmp_ctx,
+ &r[i],
+ (ndr_push_flags_fn_t)ndr_push_repsFromToBlob);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ goto failed;
+ }
+
+ el->num_values++;
+ el->values[i] = v;
+ }
+
+ if (dsdb_modify(sam_ctx, msg, 0) != LDB_SUCCESS) {
+ DEBUG(0,("Failed to store %s - %s\n", attr, ldb_errstring(sam_ctx)));
+ goto failed;
+ }
+
+ talloc_free(tmp_ctx);
+
+ return WERR_OK;
+
+failed:
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+}
+
+
+/*
+ load the uSNHighest and the uSNUrgent attributes from the @REPLCHANGED
+ object for a partition
+ */
+int dsdb_load_partition_usn(struct ldb_context *ldb, struct ldb_dn *dn,
+ uint64_t *uSN, uint64_t *urgent_uSN)
+{
+ struct ldb_request *req;
+ int ret;
+ TALLOC_CTX *tmp_ctx = talloc_new(ldb);
+ struct dsdb_control_current_partition *p_ctrl;
+ struct ldb_result *res;
+
+ if (tmp_ctx == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ res = talloc_zero(tmp_ctx, struct ldb_result);
+ if (!res) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(ldb);
+ }
+
+ 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,
+ NULL);
+ 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_oom(ldb);
+ }
+ 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_request(ldb, 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);
+ 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;
+}
+
+int drsuapi_DsReplicaCursor2_compare(const struct drsuapi_DsReplicaCursor2 *c1,
+ const struct drsuapi_DsReplicaCursor2 *c2)
+{
+ return GUID_compare(&c1->source_dsa_invocation_id, &c2->source_dsa_invocation_id);
+}
+
+int drsuapi_DsReplicaCursor_compare(const struct drsuapi_DsReplicaCursor *c1,
+ const struct drsuapi_DsReplicaCursor *c2)
+{
+ return GUID_compare(&c1->source_dsa_invocation_id, &c2->source_dsa_invocation_id);
+}
+
+/*
+ * Return the NTDS object for a GUID, confirming it is in the
+ * configuration partition and a nTDSDSA object
+ */
+int samdb_get_ntds_obj_by_guid(TALLOC_CTX *mem_ctx,
+ struct ldb_context *sam_ctx,
+ const struct GUID *objectGUID,
+ const char **attrs,
+ struct ldb_message **msg)
+{
+ int ret;
+ struct ldb_result *res;
+ struct GUID_txt_buf guid_buf;
+ char *guid_str = GUID_buf_string(objectGUID, &guid_buf);
+ struct ldb_dn *config_dn = NULL;
+
+ config_dn = ldb_get_config_basedn(sam_ctx);
+ if (config_dn == NULL) {
+ return ldb_operr(sam_ctx);
+ }
+
+ ret = dsdb_search(sam_ctx,
+ mem_ctx,
+ &res,
+ config_dn,
+ LDB_SCOPE_SUBTREE,
+ attrs,
+ DSDB_SEARCH_ONE_ONLY,
+ "(&(objectGUID=%s)(objectClass=nTDSDSA))",
+ guid_str);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ if (msg) {
+ *msg = talloc_steal(mem_ctx, res->msgs[0]);
+ }
+ TALLOC_FREE(res);
+ return ret;
+}
+
+
+/*
+ see if a computer identified by its objectGUID is a RODC
+*/
+int samdb_is_rodc(struct ldb_context *sam_ctx, const struct GUID *objectGUID, bool *is_rodc)
+{
+ /* 1) find the DN for this servers NTDSDSA object
+ 2) search for the msDS-isRODC attribute
+ 3) if not present then not a RODC
+ 4) if present and TRUE then is a RODC
+ */
+ const char *attrs[] = { "msDS-isRODC", NULL };
+ int ret;
+ struct ldb_message *msg;
+ TALLOC_CTX *tmp_ctx = talloc_new(sam_ctx);
+
+ if (tmp_ctx == NULL) {
+ return ldb_oom(sam_ctx);
+ }
+
+ ret = samdb_get_ntds_obj_by_guid(tmp_ctx,
+ sam_ctx,
+ objectGUID,
+ attrs, &msg);
+
+ if (ret == LDB_ERR_NO_SUCH_OBJECT) {
+ *is_rodc = false;
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+ }
+
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1,("Failed to find our own NTDS Settings object by objectGUID=%s!\n",
+ GUID_string(tmp_ctx, objectGUID)));
+ *is_rodc = false;
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ ret = ldb_msg_find_attr_as_bool(msg, "msDS-isRODC", 0);
+ *is_rodc = (ret == 1);
+
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+}
+
+
+/*
+ see if we are a RODC
+*/
+int samdb_rodc(struct ldb_context *sam_ctx, bool *am_rodc)
+{
+ const struct GUID *objectGUID;
+ int ret;
+ bool *cached;
+
+ /* see if we have a cached copy */
+ cached = (bool *)ldb_get_opaque(sam_ctx, "cache.am_rodc");
+ if (cached) {
+ *am_rodc = *cached;
+ return LDB_SUCCESS;
+ }
+
+ objectGUID = samdb_ntds_objectGUID(sam_ctx);
+ if (!objectGUID) {
+ return ldb_operr(sam_ctx);
+ }
+
+ ret = samdb_is_rodc(sam_ctx, objectGUID, am_rodc);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ cached = talloc(sam_ctx, bool);
+ if (cached == NULL) {
+ return ldb_oom(sam_ctx);
+ }
+ *cached = *am_rodc;
+
+ ret = ldb_set_opaque(sam_ctx, "cache.am_rodc", cached);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(cached);
+ return ldb_operr(sam_ctx);
+ }
+
+ return LDB_SUCCESS;
+}
+
+int samdb_dns_host_name(struct ldb_context *sam_ctx, const char **host_name)
+{
+ const char *_host_name = NULL;
+ const char *attrs[] = { "dnsHostName", NULL };
+ TALLOC_CTX *tmp_ctx = NULL;
+ int ret;
+ struct ldb_result *res = NULL;
+
+ _host_name = (const char *)ldb_get_opaque(sam_ctx, "cache.dns_host_name");
+ if (_host_name != NULL) {
+ *host_name = _host_name;
+ return LDB_SUCCESS;
+ }
+
+ tmp_ctx = talloc_new(sam_ctx);
+ if (tmp_ctx == NULL) {
+ return ldb_oom(sam_ctx);
+ }
+
+ ret = dsdb_search_dn(sam_ctx, tmp_ctx, &res, NULL, attrs, 0);
+
+ if (res == NULL || res->count != 1 || ret != LDB_SUCCESS) {
+ DEBUG(0, ("Failed to get rootDSE for dnsHostName: %s\n",
+ ldb_errstring(sam_ctx)));
+ TALLOC_FREE(tmp_ctx);
+ return ret;
+ }
+
+ _host_name = ldb_msg_find_attr_as_string(res->msgs[0],
+ "dnsHostName",
+ NULL);
+ if (_host_name == NULL) {
+ DEBUG(0, ("Failed to get dnsHostName from rootDSE\n"));
+ TALLOC_FREE(tmp_ctx);
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ ret = ldb_set_opaque(sam_ctx, "cache.dns_host_name",
+ discard_const_p(char *, _host_name));
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(tmp_ctx);
+ return ldb_operr(sam_ctx);
+ }
+
+ *host_name = talloc_steal(sam_ctx, _host_name);
+
+ TALLOC_FREE(tmp_ctx);
+ return LDB_SUCCESS;
+}
+
+bool samdb_set_am_rodc(struct ldb_context *ldb, bool am_rodc)
+{
+ TALLOC_CTX *tmp_ctx;
+ bool *cached;
+
+ tmp_ctx = talloc_new(ldb);
+ if (tmp_ctx == NULL) {
+ goto failed;
+ }
+
+ cached = talloc(tmp_ctx, bool);
+ if (!cached) {
+ goto failed;
+ }
+
+ *cached = am_rodc;
+ if (ldb_set_opaque(ldb, "cache.am_rodc", cached) != LDB_SUCCESS) {
+ goto failed;
+ }
+
+ talloc_steal(ldb, cached);
+ talloc_free(tmp_ctx);
+ return true;
+
+failed:
+ DEBUG(1,("Failed to set our own cached am_rodc in the ldb!\n"));
+ talloc_free(tmp_ctx);
+ return false;
+}
+
+
+/*
+ * return NTDSSiteSettings options. See MS-ADTS 7.1.1.2.2.1.1
+ * flags are DS_NTDSSETTINGS_OPT_*
+ */
+int samdb_ntds_site_settings_options(struct ldb_context *ldb_ctx,
+ uint32_t *options)
+{
+ int rc;
+ TALLOC_CTX *tmp_ctx;
+ struct ldb_result *res;
+ struct ldb_dn *site_dn;
+ const char *attrs[] = { "options", NULL };
+
+ tmp_ctx = talloc_new(ldb_ctx);
+ if (tmp_ctx == NULL)
+ goto failed;
+
+ /* Retrieve the site dn for the ldb that we
+ * have open. This is our local site.
+ */
+ site_dn = samdb_server_site_dn(ldb_ctx, tmp_ctx);
+ if (site_dn == NULL)
+ goto failed;
+
+ /* Perform a one level (child) search from the local
+ * site distinguished name. We're looking for the
+ * "options" attribute within the nTDSSiteSettings
+ * object
+ */
+ rc = ldb_search(ldb_ctx, tmp_ctx, &res, site_dn,
+ LDB_SCOPE_ONELEVEL, attrs,
+ "objectClass=nTDSSiteSettings");
+
+ if (rc != LDB_SUCCESS || res->count != 1)
+ goto failed;
+
+ *options = ldb_msg_find_attr_as_uint(res->msgs[0], "options", 0);
+
+ talloc_free(tmp_ctx);
+
+ return LDB_SUCCESS;
+
+failed:
+ DEBUG(1,("Failed to find our NTDS Site Settings options in ldb!\n"));
+ talloc_free(tmp_ctx);
+ return ldb_error(ldb_ctx, LDB_ERR_NO_SUCH_OBJECT, __func__);
+}
+
+/*
+ return NTDS options flags. See MS-ADTS 7.1.1.2.2.1.2.1.1
+
+ flags are DS_NTDS_OPTION_*
+*/
+int samdb_ntds_options(struct ldb_context *ldb, uint32_t *options)
+{
+ TALLOC_CTX *tmp_ctx;
+ const char *attrs[] = { "options", NULL };
+ int ret;
+ struct ldb_result *res;
+
+ tmp_ctx = talloc_new(ldb);
+ if (tmp_ctx == NULL) {
+ goto failed;
+ }
+
+ ret = ldb_search(ldb, tmp_ctx, &res, samdb_ntds_settings_dn(ldb, tmp_ctx), LDB_SCOPE_BASE, attrs, NULL);
+ if (ret != LDB_SUCCESS) {
+ goto failed;
+ }
+
+ if (res->count != 1) {
+ goto failed;
+ }
+
+ *options = ldb_msg_find_attr_as_uint(res->msgs[0], "options", 0);
+
+ talloc_free(tmp_ctx);
+
+ return LDB_SUCCESS;
+
+failed:
+ DEBUG(1,("Failed to find our own NTDS Settings options in the ldb!\n"));
+ talloc_free(tmp_ctx);
+ return ldb_error(ldb, LDB_ERR_NO_SUCH_OBJECT, __func__);
+}
+
+const char* samdb_ntds_object_category(TALLOC_CTX *tmp_ctx, struct ldb_context *ldb)
+{
+ const char *attrs[] = { "objectCategory", NULL };
+ int ret;
+ struct ldb_result *res;
+
+ ret = ldb_search(ldb, tmp_ctx, &res, samdb_ntds_settings_dn(ldb, tmp_ctx), LDB_SCOPE_BASE, attrs, NULL);
+ if (ret != LDB_SUCCESS) {
+ goto failed;
+ }
+
+ if (res->count != 1) {
+ goto failed;
+ }
+
+ return ldb_msg_find_attr_as_string(res->msgs[0], "objectCategory", NULL);
+
+failed:
+ DEBUG(1,("Failed to find our own NTDS Settings objectCategory in the ldb!\n"));
+ return NULL;
+}
+
+/*
+ * Function which generates a "lDAPDisplayName" attribute from a "CN" one.
+ * Algorithm implemented according to MS-ADTS 3.1.1.2.3.4
+ */
+const char *samdb_cn_to_lDAPDisplayName(TALLOC_CTX *mem_ctx, const char *cn)
+{
+ char **tokens, *ret;
+ size_t i;
+
+ tokens = str_list_make(mem_ctx, cn, " -_");
+ if (tokens == NULL || tokens[0] == NULL) {
+ return NULL;
+ }
+
+ /* "tolower()" and "toupper()" should also work properly on 0x00 */
+ tokens[0][0] = tolower(tokens[0][0]);
+ for (i = 1; tokens[i] != NULL; i++)
+ tokens[i][0] = toupper(tokens[i][0]);
+
+ ret = talloc_strdup(mem_ctx, tokens[0]);
+ if (ret == NULL) {
+ talloc_free(tokens);
+ return NULL;
+ }
+ for (i = 1; tokens[i] != NULL; i++) {
+ ret = talloc_asprintf_append_buffer(ret, "%s", tokens[i]);
+ if (ret == NULL) {
+ talloc_free(tokens);
+ return NULL;
+ }
+ }
+
+ talloc_free(tokens);
+
+ return ret;
+}
+
+/*
+ * This detects and returns the domain functional level (DS_DOMAIN_FUNCTION_*)
+ */
+int dsdb_functional_level(struct ldb_context *ldb)
+{
+ int *domainFunctionality =
+ talloc_get_type(ldb_get_opaque(ldb, "domainFunctionality"), int);
+ if (!domainFunctionality) {
+ /* this is expected during initial provision */
+ DEBUG(4,(__location__ ": WARNING: domainFunctionality not setup\n"));
+ return DS_DOMAIN_FUNCTION_2000;
+ }
+ return *domainFunctionality;
+}
+
+/*
+ * This detects and returns the forest functional level (DS_DOMAIN_FUNCTION_*)
+ */
+int dsdb_forest_functional_level(struct ldb_context *ldb)
+{
+ int *forestFunctionality =
+ talloc_get_type(ldb_get_opaque(ldb, "forestFunctionality"), int);
+ if (!forestFunctionality) {
+ DEBUG(0,(__location__ ": WARNING: forestFunctionality not setup\n"));
+ return DS_DOMAIN_FUNCTION_2000;
+ }
+ return *forestFunctionality;
+}
+
+/*
+ * This detects and returns the DC functional level (DS_DOMAIN_FUNCTION_*)
+ */
+int dsdb_dc_functional_level(struct ldb_context *ldb)
+{
+ int *dcFunctionality =
+ talloc_get_type(ldb_get_opaque(ldb, "domainControllerFunctionality"), int);
+ if (!dcFunctionality) {
+ /* this is expected during initial provision */
+ DEBUG(4,(__location__ ": WARNING: domainControllerFunctionality not setup\n"));
+ return DS_DOMAIN_FUNCTION_2008_R2;
+ }
+ return *dcFunctionality;
+}
+
+const char *dsdb_dc_operatingSystemVersion(int dc_functional_level)
+{
+ const char *operatingSystemVersion = NULL;
+
+ /*
+ * While we are there also update
+ * operatingSystem and operatingSystemVersion
+ * as at least operatingSystemVersion is really
+ * important for some clients/applications (like exchange).
+ */
+
+ if (dc_functional_level >= DS_DOMAIN_FUNCTION_2016) {
+ /* Pretend Windows 2016 */
+ operatingSystemVersion = "10.0 (14393)";
+ } else if (dc_functional_level >= DS_DOMAIN_FUNCTION_2012_R2) {
+ /* Pretend Windows 2012 R2 */
+ operatingSystemVersion = "6.3 (9600)";
+ } else if (dc_functional_level >= DS_DOMAIN_FUNCTION_2012) {
+ /* Pretend Windows 2012 */
+ operatingSystemVersion = "6.2 (9200)";
+ } else {
+ /* Pretend Windows 2008 R2 */
+ operatingSystemVersion = "6.1 (7600)";
+ }
+
+ return operatingSystemVersion;
+}
+
+int dsdb_check_and_update_fl(struct ldb_context *ldb_ctx, struct loadparm_context *lp_ctx)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ int ret;
+
+ int db_dc_functional_level;
+ int db_domain_functional_level;
+ int db_forest_functional_level;
+ int lp_dc_functional_level = lpcfg_ad_dc_functional_level(lp_ctx);
+ bool am_rodc;
+ struct ldb_message *msg = NULL;
+ struct ldb_dn *dc_ntds_settings_dn = NULL;
+ struct ldb_dn *dc_computer_dn = NULL;
+ const char *operatingSystem = NULL;
+ const char *operatingSystemVersion = NULL;
+
+ db_dc_functional_level = dsdb_dc_functional_level(ldb_ctx);
+ db_domain_functional_level = dsdb_functional_level(ldb_ctx);
+ db_forest_functional_level = dsdb_forest_functional_level(ldb_ctx);
+
+ if (lp_dc_functional_level < db_domain_functional_level) {
+ DBG_ERR("Refusing to start as smb.conf 'ad dc functional level' maps to %d, "
+ "which is less than the domain functional level of %d\n",
+ lp_dc_functional_level, db_domain_functional_level);
+ TALLOC_FREE(frame);
+ return LDB_ERR_CONSTRAINT_VIOLATION;
+ }
+
+ if (lp_dc_functional_level < db_forest_functional_level) {
+ DBG_ERR("Refusing to start as smb.conf 'ad dc functional level' maps to %d, "
+ "which is less than the forest functional level of %d\n",
+ lp_dc_functional_level, db_forest_functional_level);
+ TALLOC_FREE(frame);
+ return LDB_ERR_CONSTRAINT_VIOLATION;
+ }
+
+ /* Check if we need to update the DB */
+ if (db_dc_functional_level == lp_dc_functional_level) {
+ /*
+ * Note that this early return means
+ * we're not updating operatingSystem and
+ * operatingSystemVersion.
+ *
+ * But at least for now that's
+ * exactly what we want.
+ */
+ TALLOC_FREE(frame);
+ return LDB_SUCCESS;
+ }
+
+ /* Confirm we are not an RODC before we try a modify */
+ ret = samdb_rodc(ldb_ctx, &am_rodc);
+ if (ret != LDB_SUCCESS) {
+ DBG_ERR("Failed to determine if this server is an RODC\n");
+ TALLOC_FREE(frame);
+ return ret;
+ }
+
+ if (am_rodc) {
+ DBG_WARNING("Unable to update DC's msDS-Behavior-Version "
+ "(from %d to %d) and operatingSystem[Version] "
+ "as we are an RODC\n",
+ db_dc_functional_level, lp_dc_functional_level);
+ TALLOC_FREE(frame);
+ return LDB_SUCCESS;
+ }
+
+ dc_ntds_settings_dn = samdb_ntds_settings_dn(ldb_ctx, frame);
+
+ if (dc_ntds_settings_dn == NULL) {
+ DBG_ERR("Failed to find own NTDS Settings DN\n");
+ TALLOC_FREE(frame);
+ return LDB_ERR_NO_SUCH_OBJECT;
+ }
+
+ /* Now update our msDS-Behavior-Version */
+
+ msg = ldb_msg_new(frame);
+ if (msg == NULL) {
+ DBG_ERR("Failed to allocate message to update msDS-Behavior-Version\n");
+ TALLOC_FREE(frame);
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ msg->dn = dc_ntds_settings_dn;
+
+ ret = samdb_msg_add_int(ldb_ctx, frame, msg, "msDS-Behavior-Version", lp_dc_functional_level);
+ if (ret != LDB_SUCCESS) {
+ DBG_ERR("Failed to set new msDS-Behavior-Version on message\n");
+ TALLOC_FREE(frame);
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ ret = dsdb_replace(ldb_ctx, msg, 0);
+ if (ret != LDB_SUCCESS) {
+ DBG_ERR("Failed to update DB with new msDS-Behavior-Version on %s: %s\n",
+ ldb_dn_get_linearized(dc_ntds_settings_dn),
+ ldb_errstring(ldb_ctx));
+ TALLOC_FREE(frame);
+ return ret;
+ }
+
+ /*
+ * We have to update the opaque because this particular ldb_context
+ * will not re-read the DB
+ */
+ {
+ int *val = talloc(ldb_ctx, int);
+ if (!val) {
+ TALLOC_FREE(frame);
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ *val = lp_dc_functional_level;
+ ret = ldb_set_opaque(ldb_ctx,
+ "domainControllerFunctionality", val);
+ if (ret != LDB_SUCCESS) {
+ DBG_ERR("Failed to re-set domainControllerFunctionality opaque\n");
+ TALLOC_FREE(val);
+ TALLOC_FREE(frame);
+ return ret;
+ }
+ }
+
+ /*
+ * While we are there also update
+ * operatingSystem and operatingSystemVersion
+ * as at least operatingSystemVersion is really
+ * important for some clients/applications (like exchange).
+ */
+
+ operatingSystem = talloc_asprintf(frame, "Samba-%s",
+ samba_version_string());
+ if (operatingSystem == NULL) {
+ TALLOC_FREE(frame);
+ return ldb_oom(ldb_ctx);
+ }
+
+ operatingSystemVersion = dsdb_dc_operatingSystemVersion(db_dc_functional_level);
+
+ ret = samdb_server_reference_dn(ldb_ctx, frame, &dc_computer_dn);
+ if (ret != LDB_SUCCESS) {
+ DBG_ERR("Failed to get the dc_computer_dn: %s\n",
+ ldb_errstring(ldb_ctx));
+ TALLOC_FREE(frame);
+ return ret;
+ }
+
+ msg = ldb_msg_new(frame);
+ if (msg == NULL) {
+ DBG_ERR("Failed to allocate message to update msDS-Behavior-Version\n");
+ TALLOC_FREE(frame);
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ msg->dn = dc_computer_dn;
+
+ ret = samdb_msg_add_addval(ldb_ctx, frame, msg,
+ "operatingSystem",
+ operatingSystem);
+ if (ret != LDB_SUCCESS) {
+ DBG_ERR("Failed to set new operatingSystem on message\n");
+ TALLOC_FREE(frame);
+ return ldb_operr(ldb_ctx);
+ }
+
+ ret = samdb_msg_add_addval(ldb_ctx, frame, msg,
+ "operatingSystemVersion",
+ operatingSystemVersion);
+ if (ret != LDB_SUCCESS) {
+ DBG_ERR("Failed to set new operatingSystemVersion on message\n");
+ TALLOC_FREE(frame);
+ return ldb_operr(ldb_ctx);
+ }
+
+ ret = dsdb_replace(ldb_ctx, msg, 0);
+ if (ret != LDB_SUCCESS) {
+ DBG_ERR("Failed to update DB with new operatingSystem[Version] on %s: %s\n",
+ ldb_dn_get_linearized(dc_computer_dn),
+ ldb_errstring(ldb_ctx));
+ TALLOC_FREE(frame);
+ return ret;
+ }
+
+ TALLOC_FREE(frame);
+ return LDB_SUCCESS;
+}
+
+
+/*
+ set a GUID in an extended DN structure
+ */
+int dsdb_set_extended_dn_guid(struct ldb_dn *dn, const struct GUID *guid, const char *component_name)
+{
+ struct ldb_val v;
+ NTSTATUS status;
+ int ret;
+
+ status = GUID_to_ndr_blob(guid, dn, &v);
+ if (!NT_STATUS_IS_OK(status)) {
+ return LDB_ERR_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ ret = ldb_dn_set_extended_component(dn, component_name, &v);
+ data_blob_free(&v);
+ return ret;
+}
+
+/*
+ return a GUID from a extended DN structure
+ */
+NTSTATUS dsdb_get_extended_dn_guid(struct ldb_dn *dn, struct GUID *guid, const char *component_name)
+{
+ const struct ldb_val *v;
+
+ v = ldb_dn_get_extended_component(dn, component_name);
+ if (v == NULL) {
+ return NT_STATUS_OBJECT_NAME_NOT_FOUND;
+ }
+
+ return GUID_from_ndr_blob(v, guid);
+}
+
+/*
+ return a uint64_t from a extended DN structure
+ */
+NTSTATUS dsdb_get_extended_dn_uint64(struct ldb_dn *dn, uint64_t *val, const char *component_name)
+{
+ const struct ldb_val *v;
+ int error = 0;
+
+ v = ldb_dn_get_extended_component(dn, component_name);
+ if (v == NULL) {
+ return NT_STATUS_OBJECT_NAME_NOT_FOUND;
+ }
+
+ /* Just check we don't allow the caller to fill our stack */
+ if (v->length >= 64) {
+ return NT_STATUS_INVALID_PARAMETER;
+ } else {
+ char s[v->length+1];
+ memcpy(s, v->data, v->length);
+ s[v->length] = 0;
+
+ *val = smb_strtoull(s, NULL, 0, &error, SMB_STR_STANDARD);
+ if (error != 0) {
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+ }
+ return NT_STATUS_OK;
+}
+
+/*
+ return a NTTIME from a extended DN structure
+ */
+NTSTATUS dsdb_get_extended_dn_nttime(struct ldb_dn *dn, NTTIME *nttime, const char *component_name)
+{
+ return dsdb_get_extended_dn_uint64(dn, nttime, component_name);
+}
+
+/*
+ return a uint32_t from a extended DN structure
+ */
+NTSTATUS dsdb_get_extended_dn_uint32(struct ldb_dn *dn, uint32_t *val, const char *component_name)
+{
+ const struct ldb_val *v;
+ int error = 0;
+
+ v = ldb_dn_get_extended_component(dn, component_name);
+ if (v == NULL) {
+ return NT_STATUS_OBJECT_NAME_NOT_FOUND;
+ }
+
+ /* Just check we don't allow the caller to fill our stack */
+ if (v->length >= 32) {
+ return NT_STATUS_INVALID_PARAMETER;
+ } else {
+ char s[v->length + 1];
+ memcpy(s, v->data, v->length);
+ s[v->length] = 0;
+ *val = smb_strtoul(s, NULL, 0, &error, SMB_STR_STANDARD);
+ if (error != 0) {
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+ }
+
+ return NT_STATUS_OK;
+}
+
+/*
+ return a dom_sid from a extended DN structure
+ */
+NTSTATUS dsdb_get_extended_dn_sid(struct ldb_dn *dn, struct dom_sid *sid, const char *component_name)
+{
+ const struct ldb_val *sid_blob;
+ enum ndr_err_code ndr_err;
+
+ sid_blob = ldb_dn_get_extended_component(dn, component_name);
+ if (!sid_blob) {
+ return NT_STATUS_OBJECT_NAME_NOT_FOUND;
+ }
+
+ ndr_err = ndr_pull_struct_blob_all_noalloc(sid_blob, sid,
+ (ndr_pull_flags_fn_t)ndr_pull_dom_sid);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ NTSTATUS status = ndr_map_error2ntstatus(ndr_err);
+ return status;
+ }
+
+ return NT_STATUS_OK;
+}
+
+
+/*
+ return RMD_FLAGS directly from a ldb_dn
+ returns 0 if not found
+ */
+uint32_t dsdb_dn_rmd_flags(struct ldb_dn *dn)
+{
+ uint32_t rmd_flags = 0;
+ NTSTATUS status = dsdb_get_extended_dn_uint32(dn, &rmd_flags,
+ "RMD_FLAGS");
+ if (NT_STATUS_IS_OK(status)) {
+ return rmd_flags;
+ }
+ return 0;
+}
+
+/*
+ return RMD_FLAGS directly from a ldb_val for a DN
+ returns 0 if RMD_FLAGS is not found
+ */
+uint32_t dsdb_dn_val_rmd_flags(const struct ldb_val *val)
+{
+ const char *p;
+ uint32_t flags;
+ char *end;
+ int error = 0;
+
+ if (val->length < 13) {
+ return 0;
+ }
+ p = memmem(val->data, val->length, "<RMD_FLAGS=", 11);
+ if (!p) {
+ return 0;
+ }
+ flags = smb_strtoul(p+11, &end, 10, &error, SMB_STR_STANDARD);
+ if (!end || *end != '>' || error != 0) {
+ /* it must end in a > */
+ return 0;
+ }
+ return flags;
+}
+
+/*
+ return true if a ldb_val containing a DN in storage form is deleted
+ */
+bool dsdb_dn_is_deleted_val(const struct ldb_val *val)
+{
+ return (dsdb_dn_val_rmd_flags(val) & DSDB_RMD_FLAG_DELETED) != 0;
+}
+
+/*
+ return true if a ldb_val containing a DN in storage form is
+ in the upgraded w2k3 linked attribute format
+ */
+bool dsdb_dn_is_upgraded_link_val(const struct ldb_val *val)
+{
+ return memmem(val->data, val->length, "<RMD_VERSION=", 13) != NULL;
+}
+
+/*
+ return a DN for a wellknown GUID
+ */
+int dsdb_wellknown_dn(struct ldb_context *samdb, TALLOC_CTX *mem_ctx,
+ struct ldb_dn *nc_root, const char *wk_guid,
+ struct ldb_dn **wkguid_dn)
+{
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ const char *attrs[] = { NULL };
+ int ret;
+ struct ldb_dn *dn;
+ struct ldb_result *res = NULL;
+
+ if (tmp_ctx == NULL) {
+ return ldb_oom(samdb);
+ }
+
+ /* construct the magic WKGUID DN */
+ dn = ldb_dn_new_fmt(tmp_ctx, samdb, "<WKGUID=%s,%s>",
+ wk_guid, ldb_dn_get_linearized(nc_root));
+ if (!wkguid_dn) {
+ talloc_free(tmp_ctx);
+ return ldb_operr(samdb);
+ }
+
+ ret = dsdb_search_dn(samdb, tmp_ctx, &res, dn, attrs,
+ DSDB_SEARCH_SHOW_DELETED |
+ DSDB_SEARCH_SHOW_RECYCLED);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+ /* fix clang warning */
+ if (res == NULL){
+ talloc_free(tmp_ctx);
+ return LDB_ERR_OTHER;
+ }
+
+ (*wkguid_dn) = talloc_steal(mem_ctx, res->msgs[0]->dn);
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+}
+
+
+static int dsdb_dn_compare_ptrs(struct ldb_dn **dn1, struct ldb_dn **dn2)
+{
+ return ldb_dn_compare(*dn1, *dn2);
+}
+
+/*
+ find a NC root given a DN within the NC by reading the rootDSE namingContexts
+ */
+static int dsdb_find_nc_root_string_based(struct ldb_context *samdb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *dn,
+ struct ldb_dn **nc_root)
+{
+ const char *root_attrs[] = { "namingContexts", NULL };
+ TALLOC_CTX *tmp_ctx;
+ int ret;
+ struct ldb_message_element *el;
+ struct ldb_result *root_res;
+ unsigned int i;
+ struct ldb_dn **nc_dns;
+
+ tmp_ctx = talloc_new(samdb);
+ if (tmp_ctx == NULL) {
+ return ldb_oom(samdb);
+ }
+
+ ret = ldb_search(samdb, tmp_ctx, &root_res,
+ ldb_dn_new(tmp_ctx, samdb, ""), LDB_SCOPE_BASE, root_attrs, NULL);
+ if (ret != LDB_SUCCESS || root_res->count == 0) {
+ DEBUG(1,("Searching for namingContexts in rootDSE failed: %s\n", ldb_errstring(samdb)));
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ el = ldb_msg_find_element(root_res->msgs[0], "namingContexts");
+ if ((el == NULL) || (el->num_values < 3)) {
+ struct ldb_message *tmp_msg;
+
+ DEBUG(5,("dsdb_find_nc_root: Finding a valid 'namingContexts' element in the RootDSE failed. Using a temporary list.\n"));
+
+ /* This generates a temporary list of NCs in order to let the
+ * provisioning work. */
+ tmp_msg = ldb_msg_new(tmp_ctx);
+ if (tmp_msg == NULL) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(samdb);
+ }
+ ret = ldb_msg_add_steal_string(tmp_msg, "namingContexts",
+ ldb_dn_alloc_linearized(tmp_msg, ldb_get_schema_basedn(samdb)));
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+ ret = ldb_msg_add_steal_string(tmp_msg, "namingContexts",
+ ldb_dn_alloc_linearized(tmp_msg, ldb_get_config_basedn(samdb)));
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+ ret = ldb_msg_add_steal_string(tmp_msg, "namingContexts",
+ ldb_dn_alloc_linearized(tmp_msg, ldb_get_default_basedn(samdb)));
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+ el = &tmp_msg->elements[0];
+ }
+
+ nc_dns = talloc_array(tmp_ctx, struct ldb_dn *, el->num_values);
+ if (!nc_dns) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(samdb);
+ }
+
+ for (i=0; i<el->num_values; i++) {
+ nc_dns[i] = ldb_dn_from_ldb_val(nc_dns, samdb, &el->values[i]);
+ if (nc_dns[i] == NULL) {
+ talloc_free(tmp_ctx);
+ return ldb_operr(samdb);
+ }
+ }
+
+ TYPESAFE_QSORT(nc_dns, el->num_values, dsdb_dn_compare_ptrs);
+
+ for (i=0; i<el->num_values; i++) {
+ if (ldb_dn_compare_base(nc_dns[i], dn) == 0) {
+ (*nc_root) = talloc_steal(mem_ctx, nc_dns[i]);
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+ }
+ }
+
+ talloc_free(tmp_ctx);
+ return ldb_error(samdb, LDB_ERR_NO_SUCH_OBJECT, __func__);
+}
+
+struct dsdb_get_partition_and_dn {
+ TALLOC_CTX *mem_ctx;
+ unsigned int count;
+ struct ldb_dn *dn;
+ struct ldb_dn *partition_dn;
+ bool want_partition_dn;
+};
+
+static int dsdb_get_partition_and_dn(struct ldb_request *req,
+ struct ldb_reply *ares)
+{
+ int ret;
+ struct dsdb_get_partition_and_dn *context = req->context;
+ struct ldb_control *partition_ctrl = NULL;
+ struct dsdb_control_current_partition *partition = NULL;
+
+ if (!ares) {
+ return ldb_request_done(req, LDB_ERR_OPERATIONS_ERROR);
+ }
+ if (ares->error != LDB_SUCCESS
+ && ares->error != LDB_ERR_NO_SUCH_OBJECT) {
+ return ldb_request_done(req, ares->error);
+ }
+
+ switch (ares->type) {
+ case LDB_REPLY_ENTRY:
+ if (context->count != 0) {
+ return ldb_request_done(req,
+ LDB_ERR_CONSTRAINT_VIOLATION);
+ }
+ context->count++;
+
+ context->dn = talloc_steal(context->mem_ctx,
+ ares->message->dn);
+ break;
+
+ case LDB_REPLY_REFERRAL:
+ talloc_free(ares);
+ return ldb_request_done(req, LDB_SUCCESS);
+
+ case LDB_REPLY_DONE:
+ partition_ctrl
+ = ldb_reply_get_control(ares,
+ DSDB_CONTROL_CURRENT_PARTITION_OID);
+ if (!context->want_partition_dn ||
+ partition_ctrl == NULL) {
+ ret = ares->error;
+ talloc_free(ares);
+
+ return ldb_request_done(req, ret);
+ }
+
+ partition
+ = talloc_get_type_abort(partition_ctrl->data,
+ struct dsdb_control_current_partition);
+ context->partition_dn
+ = ldb_dn_copy(context->mem_ctx, partition->dn);
+ if (context->partition_dn == NULL) {
+ return ldb_request_done(req,
+ LDB_ERR_OPERATIONS_ERROR);
+ }
+
+ ret = ares->error;
+ talloc_free(ares);
+
+ return ldb_request_done(req, ret);
+ }
+
+ talloc_free(ares);
+ return LDB_SUCCESS;
+}
+
+/*
+ find a NC root given a DN within the NC
+ */
+int dsdb_normalise_dn_and_find_nc_root(struct ldb_context *samdb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *dn,
+ struct ldb_dn **normalised_dn,
+ struct ldb_dn **nc_root)
+{
+ TALLOC_CTX *tmp_ctx;
+ int ret;
+ struct ldb_request *req;
+ struct ldb_result *res;
+ struct ldb_dn *search_dn = dn;
+ static const char * attrs[] = { NULL };
+ bool has_extended = ldb_dn_has_extended(dn);
+ bool has_normal_components = ldb_dn_get_comp_num(dn) >= 1;
+ struct dsdb_get_partition_and_dn context = {
+ .mem_ctx = mem_ctx,
+ .want_partition_dn = nc_root != NULL
+ };
+
+ if (!has_extended && !has_normal_components) {
+ return ldb_error(samdb, LDB_ERR_NO_SUCH_OBJECT,
+ "Request for NC root for rootDSE (\"\") denied.");
+ }
+
+ tmp_ctx = talloc_new(samdb);
+ if (tmp_ctx == NULL) {
+ return ldb_oom(samdb);
+ }
+
+ res = talloc_zero(tmp_ctx, struct ldb_result);
+ if (res == NULL) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(samdb);
+ }
+
+ if (has_extended && has_normal_components) {
+ bool minimise_ok;
+ search_dn = ldb_dn_copy(tmp_ctx, dn);
+ if (search_dn == NULL) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(samdb);
+ }
+ minimise_ok = ldb_dn_minimise(search_dn);
+ if (!minimise_ok) {
+ talloc_free(tmp_ctx);
+ return ldb_operr(samdb);
+ }
+ }
+
+ ret = ldb_build_search_req(&req, samdb, tmp_ctx,
+ search_dn,
+ LDB_SCOPE_BASE,
+ NULL,
+ attrs,
+ NULL,
+ &context,
+ dsdb_get_partition_and_dn,
+ NULL);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ ret = ldb_request_add_control(req,
+ DSDB_CONTROL_CURRENT_PARTITION_OID,
+ false, NULL);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ ret = dsdb_request_add_controls(req,
+ DSDB_SEARCH_SHOW_RECYCLED|
+ DSDB_SEARCH_SHOW_DELETED|
+ DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ ret = ldb_request(samdb, req);
+ if (ret == LDB_SUCCESS) {
+ ret = ldb_wait(req->handle, LDB_WAIT_ALL);
+ }
+
+ /*
+ * This could be a new DN, not in the DB, which is OK. If we
+ * don't need the normalised DN, we can continue.
+ *
+ * We may be told the partition it would be in in the search
+ * reply control, or if not we can do a string-based match.
+ */
+
+ if (ret == LDB_ERR_NO_SUCH_OBJECT) {
+ if (normalised_dn != NULL) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+ ret = LDB_SUCCESS;
+ ldb_reset_err_string(samdb);
+ } else if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ if (normalised_dn != NULL) {
+ if (context.count != 1) {
+ /* No results */
+ ldb_asprintf_errstring(samdb,
+ "Request for NC root for %s failed to return any results.",
+ ldb_dn_get_linearized(dn));
+ talloc_free(tmp_ctx);
+ return LDB_ERR_NO_SUCH_OBJECT;
+ }
+ *normalised_dn = context.dn;
+ }
+
+ /*
+ * If the user did not need to find the nc_root,
+ * we are done
+ */
+ if (nc_root == NULL) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ /*
+ * When we are working locally, both for the case were
+ * we find the DN, and the case where we fail, we get
+ * back via controls the partition it was in or should
+ * have been in, to return to the client
+ */
+ if (context.partition_dn != NULL) {
+ (*nc_root) = context.partition_dn;
+
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ /*
+ * This is a remote operation, which is a little harder as we
+ * have a work out the nc_root from the list of NCs. If we did
+ * at least resolve the DN to a string, get that now, it makes
+ * the string-based match below possible for a GUID-based
+ * input over remote LDAP.
+ */
+ if (context.dn) {
+ dn = context.dn;
+ } else if (has_extended && !has_normal_components) {
+ ldb_asprintf_errstring(samdb,
+ "Cannot determine NC root "
+ "for a not-found bare extended DN %s.",
+ ldb_dn_get_extended_linearized(tmp_ctx, dn, 1));
+ talloc_free(tmp_ctx);
+ return LDB_ERR_NO_SUCH_OBJECT;
+ }
+
+ /*
+ * Either we are working against a remote LDAP
+ * server or the object doesn't exist locally.
+ *
+ * This means any GUID that was present in the DN
+ * therefore could not be evaluated, so do a
+ * string-based match instead.
+ */
+ talloc_free(tmp_ctx);
+ return dsdb_find_nc_root_string_based(samdb,
+ mem_ctx,
+ dn,
+ nc_root);
+}
+
+/*
+ find a NC root given a DN within the NC
+ */
+int dsdb_find_nc_root(struct ldb_context *samdb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *dn,
+ struct ldb_dn **nc_root)
+{
+ return dsdb_normalise_dn_and_find_nc_root(samdb,
+ mem_ctx,
+ dn,
+ NULL,
+ nc_root);
+}
+
+/*
+ find the deleted objects DN for any object, by looking for the NC
+ root, then looking up the wellknown GUID
+ */
+int dsdb_get_deleted_objects_dn(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx, struct ldb_dn *obj_dn,
+ struct ldb_dn **do_dn)
+{
+ struct ldb_dn *nc_root;
+ int ret;
+
+ ret = dsdb_find_nc_root(ldb, mem_ctx, obj_dn, &nc_root);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ ret = dsdb_wellknown_dn(ldb, mem_ctx, nc_root, DS_GUID_DELETED_OBJECTS_CONTAINER, do_dn);
+ talloc_free(nc_root);
+ return ret;
+}
+
+/*
+ return the tombstoneLifetime, in days
+ */
+int dsdb_tombstone_lifetime(struct ldb_context *ldb, uint32_t *lifetime)
+{
+ struct ldb_dn *dn;
+ dn = ldb_get_config_basedn(ldb);
+ if (!dn) {
+ return ldb_error(ldb, LDB_ERR_NO_SUCH_OBJECT, __func__);
+ }
+ dn = ldb_dn_copy(ldb, dn);
+ if (!dn) {
+ return ldb_operr(ldb);
+ }
+ /* see MS-ADTS section 7.1.1.2.4.1.1. There doesn't appear to
+ be a wellknown GUID for this */
+ if (!ldb_dn_add_child_fmt(dn, "CN=Directory Service,CN=Windows NT,CN=Services")) {
+ talloc_free(dn);
+ return ldb_operr(ldb);
+ }
+
+ *lifetime = samdb_search_uint(ldb, dn, 180, dn, "tombstoneLifetime", "objectClass=nTDSService");
+ talloc_free(dn);
+ return LDB_SUCCESS;
+}
+
+/*
+ compare a ldb_val to a string case insensitively
+ */
+int samdb_ldb_val_case_cmp(const char *s, struct ldb_val *v)
+{
+ size_t len = strlen(s);
+ int ret;
+ if (len > v->length) return 1;
+ ret = strncasecmp(s, (const char *)v->data, v->length);
+ if (ret != 0) return ret;
+ if (v->length > len && v->data[len] != 0) {
+ return -1;
+ }
+ return 0;
+}
+
+
+/*
+ load the UDV for a partition in v2 format
+ The list is returned sorted, and with our local cursor added
+ */
+int dsdb_load_udv_v2(struct ldb_context *samdb, struct ldb_dn *dn, TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaCursor2 **cursors, uint32_t *count)
+{
+ static const char *attrs[] = { "replUpToDateVector", NULL };
+ struct ldb_result *r = NULL;
+ const struct ldb_val *ouv_value;
+ unsigned int i;
+ int ret;
+ uint64_t highest_usn = 0;
+ const struct GUID *our_invocation_id;
+ static const struct timeval tv1970;
+ NTTIME nt1970 = timeval_to_nttime(&tv1970);
+
+ ret = dsdb_search_dn(samdb, mem_ctx, &r, dn, attrs, DSDB_SEARCH_SHOW_RECYCLED|DSDB_SEARCH_SHOW_DELETED);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ /* fix clang warning */
+ if (r == NULL) {
+ return LDB_ERR_OTHER;
+ }
+ ouv_value = ldb_msg_find_ldb_val(r->msgs[0], "replUpToDateVector");
+ if (ouv_value) {
+ enum ndr_err_code ndr_err;
+ struct replUpToDateVectorBlob ouv;
+
+ ndr_err = ndr_pull_struct_blob(ouv_value, r, &ouv,
+ (ndr_pull_flags_fn_t)ndr_pull_replUpToDateVectorBlob);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ talloc_free(r);
+ return LDB_ERR_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ if (ouv.version != 2) {
+ /* we always store as version 2, and
+ * replUpToDateVector is not replicated
+ */
+ talloc_free(r);
+ return LDB_ERR_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ *count = ouv.ctr.ctr2.count;
+ *cursors = talloc_steal(mem_ctx, ouv.ctr.ctr2.cursors);
+ } else {
+ *count = 0;
+ *cursors = NULL;
+ }
+
+ talloc_free(r);
+
+ our_invocation_id = samdb_ntds_invocation_id(samdb);
+ if (!our_invocation_id) {
+ DEBUG(0,(__location__ ": No invocationID on samdb - %s\n", ldb_errstring(samdb)));
+ talloc_free(*cursors);
+ return ldb_operr(samdb);
+ }
+
+ ret = ldb_sequence_number(samdb, LDB_SEQ_HIGHEST_SEQ, &highest_usn);
+ if (ret != LDB_SUCCESS) {
+ /* nothing to add - this can happen after a vampire */
+ TYPESAFE_QSORT(*cursors, *count, drsuapi_DsReplicaCursor2_compare);
+ return LDB_SUCCESS;
+ }
+
+ for (i=0; i<*count; i++) {
+ if (GUID_equal(our_invocation_id, &(*cursors)[i].source_dsa_invocation_id)) {
+ (*cursors)[i].highest_usn = highest_usn;
+ (*cursors)[i].last_sync_success = nt1970;
+ TYPESAFE_QSORT(*cursors, *count, drsuapi_DsReplicaCursor2_compare);
+ return LDB_SUCCESS;
+ }
+ }
+
+ (*cursors) = talloc_realloc(mem_ctx, *cursors, struct drsuapi_DsReplicaCursor2, (*count)+1);
+ if (! *cursors) {
+ return ldb_oom(samdb);
+ }
+
+ (*cursors)[*count].source_dsa_invocation_id = *our_invocation_id;
+ (*cursors)[*count].highest_usn = highest_usn;
+ (*cursors)[*count].last_sync_success = nt1970;
+ (*count)++;
+
+ TYPESAFE_QSORT(*cursors, *count, drsuapi_DsReplicaCursor2_compare);
+
+ return LDB_SUCCESS;
+}
+
+/*
+ load the UDV for a partition in version 1 format
+ The list is returned sorted, and with our local cursor added
+ */
+int dsdb_load_udv_v1(struct ldb_context *samdb, struct ldb_dn *dn, TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaCursor **cursors, uint32_t *count)
+{
+ struct drsuapi_DsReplicaCursor2 *v2 = NULL;
+ uint32_t i;
+ int ret;
+
+ ret = dsdb_load_udv_v2(samdb, dn, mem_ctx, &v2, count);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ if (*count == 0) {
+ talloc_free(v2);
+ *cursors = NULL;
+ return LDB_SUCCESS;
+ }
+
+ *cursors = talloc_array(mem_ctx, struct drsuapi_DsReplicaCursor, *count);
+ if (*cursors == NULL) {
+ talloc_free(v2);
+ return ldb_oom(samdb);
+ }
+
+ for (i=0; i<*count; i++) {
+ (*cursors)[i].source_dsa_invocation_id = v2[i].source_dsa_invocation_id;
+ (*cursors)[i].highest_usn = v2[i].highest_usn;
+ }
+ talloc_free(v2);
+ return LDB_SUCCESS;
+}
+
+/*
+ add a set of controls to a ldb_request structure based on a set of
+ flags. See util.h for a list of available flags
+ */
+int dsdb_request_add_controls(struct ldb_request *req, uint32_t dsdb_flags)
+{
+ int ret;
+ if (dsdb_flags & DSDB_SEARCH_SEARCH_ALL_PARTITIONS) {
+ struct ldb_search_options_control *options;
+ /* Using the phantom root control allows us to search all partitions */
+ options = talloc(req, struct ldb_search_options_control);
+ if (options == NULL) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ options->search_options = LDB_SEARCH_OPTION_PHANTOM_ROOT;
+
+ ret = ldb_request_add_control(req,
+ LDB_CONTROL_SEARCH_OPTIONS_OID,
+ true, options);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_SEARCH_NO_GLOBAL_CATALOG) {
+ ret = ldb_request_add_control(req,
+ DSDB_CONTROL_NO_GLOBAL_CATALOG,
+ false, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_SEARCH_SHOW_DELETED) {
+ ret = ldb_request_add_control(req, LDB_CONTROL_SHOW_DELETED_OID, true, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_SEARCH_SHOW_RECYCLED) {
+ ret = ldb_request_add_control(req, LDB_CONTROL_SHOW_RECYCLED_OID, false, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT) {
+ ret = ldb_request_add_control(req, DSDB_CONTROL_DN_STORAGE_FORMAT_OID, false, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_SEARCH_SHOW_EXTENDED_DN) {
+ struct ldb_extended_dn_control *extended_ctrl = talloc(req, struct ldb_extended_dn_control);
+ if (!extended_ctrl) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ extended_ctrl->type = 1;
+
+ ret = ldb_request_add_control(req, LDB_CONTROL_EXTENDED_DN_OID, true, extended_ctrl);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_SEARCH_REVEAL_INTERNALS) {
+ ret = ldb_request_add_control(req, LDB_CONTROL_REVEAL_INTERNALS, false, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_MODIFY_RELAX) {
+ ret = ldb_request_add_control(req, LDB_CONTROL_RELAX_OID, false, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_MODIFY_PERMISSIVE) {
+ ret = ldb_request_add_control(req, LDB_CONTROL_PERMISSIVE_MODIFY_OID, false, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_FLAG_AS_SYSTEM) {
+ ret = ldb_request_add_control(req, LDB_CONTROL_AS_SYSTEM_OID, false, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_TREE_DELETE) {
+ ret = ldb_request_add_control(req, LDB_CONTROL_TREE_DELETE_OID, false, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_PROVISION) {
+ ret = ldb_request_add_control(req, LDB_CONTROL_PROVISION_OID, false, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ /* This is a special control to bypass the password_hash module for use in pdb_samba4 for Samba3 upgrades */
+ if (dsdb_flags & DSDB_BYPASS_PASSWORD_HASH) {
+ ret = ldb_request_add_control(req, DSDB_CONTROL_BYPASS_PASSWORD_HASH_OID, true, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_PASSWORD_BYPASS_LAST_SET) {
+ /*
+ * This must not be critical, as it will only be
+ * handled (and need to be handled) if the other
+ * attributes in the request bring password_hash into
+ * action
+ */
+ ret = ldb_request_add_control(req, DSDB_CONTROL_PASSWORD_BYPASS_LAST_SET_OID, false, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_REPLMD_VANISH_LINKS) {
+ ret = ldb_request_add_control(req, DSDB_CONTROL_REPLMD_VANISH_LINKS, true, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_MODIFY_PARTIAL_REPLICA) {
+ ret = ldb_request_add_control(req, DSDB_CONTROL_PARTIAL_REPLICA, false, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_FLAG_REPLICATED_UPDATE) {
+ ret = ldb_request_add_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID, false, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_FLAG_FORCE_ALLOW_VALIDATED_DNS_HOSTNAME_SPN_WRITE) {
+ ret = ldb_request_add_control(req, DSDB_CONTROL_FORCE_ALLOW_VALIDATED_DNS_HOSTNAME_SPN_WRITE_OID, true, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+
+ if (dsdb_flags & DSDB_MARK_REQ_UNTRUSTED) {
+ ldb_req_mark_untrusted(req);
+ }
+
+ return LDB_SUCCESS;
+}
+
+/*
+ returns true if a control with the specified "oid" exists
+*/
+bool dsdb_request_has_control(struct ldb_request *req, const char *oid)
+{
+ return (ldb_request_get_control(req, oid) != NULL);
+}
+
+/*
+ an add with a set of controls
+*/
+int dsdb_add(struct ldb_context *ldb, const struct ldb_message *message,
+ uint32_t dsdb_flags)
+{
+ struct ldb_request *req;
+ int ret;
+
+ ret = ldb_build_add_req(&req, ldb, ldb,
+ message,
+ NULL,
+ NULL,
+ ldb_op_default_callback,
+ NULL);
+
+ if (ret != LDB_SUCCESS) return ret;
+
+ ret = dsdb_request_add_controls(req, dsdb_flags);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(req);
+ return ret;
+ }
+
+ ret = dsdb_autotransaction_request(ldb, req);
+
+ talloc_free(req);
+ return ret;
+}
+
+/*
+ a modify with a set of controls
+*/
+int dsdb_modify(struct ldb_context *ldb, const struct ldb_message *message,
+ uint32_t dsdb_flags)
+{
+ struct ldb_request *req;
+ int ret;
+
+ ret = ldb_build_mod_req(&req, ldb, ldb,
+ message,
+ NULL,
+ NULL,
+ ldb_op_default_callback,
+ NULL);
+
+ if (ret != LDB_SUCCESS) return ret;
+
+ ret = dsdb_request_add_controls(req, dsdb_flags);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(req);
+ return ret;
+ }
+
+ ret = dsdb_autotransaction_request(ldb, req);
+
+ talloc_free(req);
+ return ret;
+}
+
+/*
+ a delete with a set of flags
+*/
+int dsdb_delete(struct ldb_context *ldb, struct ldb_dn *dn,
+ uint32_t dsdb_flags)
+{
+ struct ldb_request *req;
+ int ret;
+
+ ret = ldb_build_del_req(&req, ldb, ldb,
+ dn,
+ NULL,
+ NULL,
+ ldb_op_default_callback,
+ NULL);
+
+ if (ret != LDB_SUCCESS) return ret;
+
+ ret = dsdb_request_add_controls(req, dsdb_flags);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(req);
+ return ret;
+ }
+
+ ret = dsdb_autotransaction_request(ldb, req);
+
+ talloc_free(req);
+ return ret;
+}
+
+/*
+ like dsdb_modify() but set all the element flags to
+ LDB_FLAG_MOD_REPLACE
+ */
+int dsdb_replace(struct ldb_context *ldb, struct ldb_message *msg, uint32_t dsdb_flags)
+{
+ unsigned int i;
+
+ /* mark all the message elements as LDB_FLAG_MOD_REPLACE */
+ for (i=0;i<msg->num_elements;i++) {
+ msg->elements[i].flags = LDB_FLAG_MOD_REPLACE;
+ }
+
+ return dsdb_modify(ldb, msg, dsdb_flags);
+}
+
+const char *dsdb_search_scope_as_string(enum ldb_scope scope)
+{
+ const char *scope_str;
+
+ switch (scope) {
+ case LDB_SCOPE_BASE:
+ scope_str = "BASE";
+ break;
+ case LDB_SCOPE_ONELEVEL:
+ scope_str = "ONE";
+ break;
+ case LDB_SCOPE_SUBTREE:
+ scope_str = "SUB";
+ break;
+ default:
+ scope_str = "<Invalid scope>";
+ break;
+ }
+ return scope_str;
+}
+
+
+/*
+ search for attrs on one DN, allowing for dsdb_flags controls
+ */
+int dsdb_search_dn(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_result **_result,
+ struct ldb_dn *basedn,
+ const char * const *attrs,
+ uint32_t dsdb_flags)
+{
+ int ret;
+ struct ldb_request *req;
+ struct ldb_result *res;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+
+ if (tmp_ctx == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ res = talloc_zero(tmp_ctx, struct ldb_result);
+ if (!res) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(ldb);
+ }
+
+ ret = ldb_build_search_req(&req, ldb, res,
+ basedn,
+ LDB_SCOPE_BASE,
+ NULL,
+ attrs,
+ NULL,
+ res,
+ ldb_search_default_callback,
+ NULL);
+ 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;
+ }
+
+ ret = ldb_request(ldb, req);
+ if (ret == LDB_SUCCESS) {
+ ret = ldb_wait(req->handle, LDB_WAIT_ALL);
+ }
+
+ talloc_free(req);
+ if (ret != LDB_SUCCESS) {
+ DBG_INFO("flags=0x%08x %s -> %s (%s)\n",
+ dsdb_flags,
+ basedn?ldb_dn_get_extended_linearized(tmp_ctx,
+ basedn,
+ 1):"NULL",
+ ldb_errstring(ldb), ldb_strerror(ret));
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ DBG_DEBUG("flags=0x%08x %s -> %d\n",
+ dsdb_flags,
+ basedn?ldb_dn_get_extended_linearized(tmp_ctx,
+ basedn,
+ 1):"NULL",
+ res->count);
+
+ *_result = talloc_steal(mem_ctx, res);
+
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+}
+
+/*
+ search for attrs on one DN, by the GUID of the DN, allowing for
+ dsdb_flags controls
+ */
+int dsdb_search_by_dn_guid(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_result **_result,
+ const struct GUID *guid,
+ const char * const *attrs,
+ uint32_t dsdb_flags)
+{
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ struct ldb_dn *dn;
+ int ret;
+
+ if (tmp_ctx == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ dn = ldb_dn_new_fmt(tmp_ctx, ldb, "<GUID=%s>", GUID_string(tmp_ctx, guid));
+ if (dn == NULL) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(ldb);
+ }
+
+ ret = dsdb_search_dn(ldb, mem_ctx, _result, dn, attrs, dsdb_flags);
+ talloc_free(tmp_ctx);
+ return ret;
+}
+
+/*
+ general search with dsdb_flags for controls
+ */
+int dsdb_search(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_result **_result,
+ struct ldb_dn *basedn,
+ enum ldb_scope scope,
+ const char * const *attrs,
+ uint32_t dsdb_flags,
+ const char *exp_fmt, ...) _PRINTF_ATTRIBUTE(8, 9)
+{
+ int ret;
+ struct ldb_request *req;
+ struct ldb_result *res;
+ va_list ap;
+ char *expression = NULL;
+ TALLOC_CTX *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);
+
+ if (tmp_ctx == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ res = talloc_zero(tmp_ctx, struct ldb_result);
+ if (!res) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(ldb);
+ }
+
+ if (exp_fmt) {
+ va_start(ap, exp_fmt);
+ expression = talloc_vasprintf(tmp_ctx, exp_fmt, ap);
+ va_end(ap);
+
+ if (!expression) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(ldb);
+ }
+ }
+
+ ret = ldb_build_search_req(&req, ldb, tmp_ctx,
+ basedn,
+ scope,
+ expression,
+ attrs,
+ NULL,
+ res,
+ ldb_search_default_callback,
+ NULL);
+ 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);
+ ldb_reset_err_string(ldb);
+ return ret;
+ }
+
+ ret = ldb_request(ldb, req);
+ if (ret == LDB_SUCCESS) {
+ ret = ldb_wait(req->handle, LDB_WAIT_ALL);
+ }
+
+ if (ret != LDB_SUCCESS) {
+ DBG_INFO("%s flags=0x%08x %s %s -> %s (%s)\n",
+ dsdb_search_scope_as_string(scope),
+ dsdb_flags,
+ basedn?ldb_dn_get_extended_linearized(tmp_ctx,
+ basedn,
+ 1):"NULL",
+ expression?expression:"NULL",
+ ldb_errstring(ldb), ldb_strerror(ret));
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ if (dsdb_flags & DSDB_SEARCH_ONE_ONLY) {
+ if (res->count == 0) {
+ DBG_INFO("%s SEARCH_ONE_ONLY flags=0x%08x %s %s -> %u results\n",
+ dsdb_search_scope_as_string(scope),
+ dsdb_flags,
+ basedn?ldb_dn_get_extended_linearized(tmp_ctx,
+ basedn,
+ 1):"NULL",
+ expression?expression:"NULL", res->count);
+ talloc_free(tmp_ctx);
+ ldb_reset_err_string(ldb);
+ return ldb_error(ldb, LDB_ERR_NO_SUCH_OBJECT, __func__);
+ }
+ if (res->count != 1) {
+ DBG_INFO("%s SEARCH_ONE_ONLY flags=0x%08x %s %s -> %u (expected 1) results\n",
+ dsdb_search_scope_as_string(scope),
+ dsdb_flags,
+ basedn?ldb_dn_get_extended_linearized(tmp_ctx,
+ basedn,
+ 1):"NULL",
+ expression?expression:"NULL", res->count);
+ talloc_free(tmp_ctx);
+ ldb_reset_err_string(ldb);
+ return LDB_ERR_CONSTRAINT_VIOLATION;
+ }
+ }
+
+ *_result = talloc_steal(mem_ctx, res);
+
+ DBG_DEBUG("%s flags=0x%08x %s %s -> %d\n",
+ dsdb_search_scope_as_string(scope),
+ dsdb_flags,
+ basedn?ldb_dn_get_extended_linearized(tmp_ctx,
+ basedn,
+ 1):"NULL",
+ expression?expression:"NULL",
+ res->count);
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+}
+
+
+/*
+ general search with dsdb_flags for controls
+ returns exactly 1 record or an error
+ */
+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)
+{
+ int ret;
+ struct ldb_result *res;
+ va_list ap;
+ char *expression = NULL;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+
+ if (tmp_ctx == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ dsdb_flags |= DSDB_SEARCH_ONE_ONLY;
+
+ res = talloc_zero(tmp_ctx, struct ldb_result);
+ if (!res) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(ldb);
+ }
+
+ if (exp_fmt) {
+ va_start(ap, exp_fmt);
+ expression = talloc_vasprintf(tmp_ctx, exp_fmt, ap);
+ va_end(ap);
+
+ if (!expression) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(ldb);
+ }
+ ret = dsdb_search(ldb, tmp_ctx, &res, basedn, scope, attrs,
+ dsdb_flags, "%s", expression);
+ } else {
+ ret = dsdb_search(ldb, tmp_ctx, &res, basedn, scope, attrs,
+ dsdb_flags, NULL);
+ }
+
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ *msg = talloc_steal(mem_ctx, res->msgs[0]);
+ talloc_free(tmp_ctx);
+
+ return LDB_SUCCESS;
+}
+
+/* returns back the forest DNS name */
+const char *samdb_forest_name(struct ldb_context *ldb, TALLOC_CTX *mem_ctx)
+{
+ const char *forest_name = ldb_dn_canonical_string(mem_ctx,
+ ldb_get_root_basedn(ldb));
+ char *p;
+
+ if (forest_name == NULL) {
+ return NULL;
+ }
+
+ p = strchr(forest_name, '/');
+ if (p) {
+ *p = '\0';
+ }
+
+ return forest_name;
+}
+
+/* returns back the default domain DNS name */
+const char *samdb_default_domain_name(struct ldb_context *ldb, TALLOC_CTX *mem_ctx)
+{
+ const char *domain_name = ldb_dn_canonical_string(mem_ctx,
+ ldb_get_default_basedn(ldb));
+ char *p;
+
+ if (domain_name == NULL) {
+ return NULL;
+ }
+
+ p = strchr(domain_name, '/');
+ if (p) {
+ *p = '\0';
+ }
+
+ return domain_name;
+}
+
+/*
+ validate that an DSA GUID belongs to the specified user sid.
+ The user SID must be a domain controller account (either RODC or
+ RWDC)
+ */
+int dsdb_validate_dsa_guid(struct ldb_context *ldb,
+ const struct GUID *dsa_guid,
+ const struct dom_sid *sid)
+{
+ /* strategy:
+ - find DN of record with the DSA GUID in the
+ configuration partition (objectGUID)
+ - remove "NTDS Settings" component from DN
+ - do a base search on that DN for serverReference with
+ extended-dn enabled
+ - extract objectSid from resulting serverReference
+ attribute
+ - check this sid matches the sid argument
+ */
+ struct ldb_dn *config_dn;
+ TALLOC_CTX *tmp_ctx = talloc_new(ldb);
+ struct ldb_message *msg;
+ const char *attrs1[] = { NULL };
+ const char *attrs2[] = { "serverReference", NULL };
+ int ret;
+ struct ldb_dn *dn, *account_dn;
+ struct dom_sid sid2;
+ NTSTATUS status;
+
+ if (tmp_ctx == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ config_dn = ldb_get_config_basedn(ldb);
+
+ ret = dsdb_search_one(ldb, tmp_ctx, &msg, config_dn, LDB_SCOPE_SUBTREE,
+ attrs1, 0, "(&(objectGUID=%s)(objectClass=nTDSDSA))", GUID_string(tmp_ctx, dsa_guid));
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1,(__location__ ": Failed to find DSA objectGUID %s for sid %s\n",
+ GUID_string(tmp_ctx, dsa_guid), dom_sid_string(tmp_ctx, sid)));
+ talloc_free(tmp_ctx);
+ return ldb_operr(ldb);
+ }
+ dn = msg->dn;
+
+ if (!ldb_dn_remove_child_components(dn, 1)) {
+ talloc_free(tmp_ctx);
+ return ldb_operr(ldb);
+ }
+
+ ret = dsdb_search_one(ldb, tmp_ctx, &msg, dn, LDB_SCOPE_BASE,
+ attrs2, DSDB_SEARCH_SHOW_EXTENDED_DN,
+ "(objectClass=server)");
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1,(__location__ ": Failed to find server record for DSA with objectGUID %s, sid %s\n",
+ GUID_string(tmp_ctx, dsa_guid), dom_sid_string(tmp_ctx, sid)));
+ talloc_free(tmp_ctx);
+ return ldb_operr(ldb);
+ }
+
+ account_dn = ldb_msg_find_attr_as_dn(ldb, tmp_ctx, msg, "serverReference");
+ if (account_dn == NULL) {
+ DEBUG(1,(__location__ ": Failed to find account dn "
+ "(serverReference) for %s, parent of DSA with "
+ "objectGUID %s, sid %s\n",
+ ldb_dn_get_linearized(msg->dn),
+ GUID_string(tmp_ctx, dsa_guid),
+ dom_sid_string(tmp_ctx, sid)));
+ talloc_free(tmp_ctx);
+ return ldb_operr(ldb);
+ }
+
+ status = dsdb_get_extended_dn_sid(account_dn, &sid2, "SID");
+ if (!NT_STATUS_IS_OK(status)) {
+ DEBUG(1,(__location__ ": Failed to find SID for DSA with objectGUID %s, sid %s\n",
+ GUID_string(tmp_ctx, dsa_guid), dom_sid_string(tmp_ctx, sid)));
+ talloc_free(tmp_ctx);
+ return ldb_operr(ldb);
+ }
+
+ if (!dom_sid_equal(sid, &sid2)) {
+ /* someone is trying to spoof another account */
+ DEBUG(0,(__location__ ": Bad DSA objectGUID %s for sid %s - expected sid %s\n",
+ GUID_string(tmp_ctx, dsa_guid),
+ dom_sid_string(tmp_ctx, sid),
+ dom_sid_string(tmp_ctx, &sid2)));
+ talloc_free(tmp_ctx);
+ return ldb_operr(ldb);
+ }
+
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+}
+
+static const char * const secret_attributes[] = {
+ DSDB_SECRET_ATTRIBUTES,
+ NULL
+};
+
+/*
+ check if the attribute belongs to the RODC filtered attribute set
+ Note that attributes that are in the filtered attribute set are the
+ ones that _are_ always sent to a RODC
+*/
+bool dsdb_attr_in_rodc_fas(const struct dsdb_attribute *sa)
+{
+ /* they never get secret attributes */
+ if (ldb_attr_in_list(secret_attributes, sa->lDAPDisplayName)) {
+ return false;
+ }
+
+ /* they do get non-secret critical attributes */
+ if (sa->schemaFlagsEx & SCHEMA_FLAG_ATTR_IS_CRITICAL) {
+ return true;
+ }
+
+ /* they do get non-secret attributes marked as being in the FAS */
+ if (sa->searchFlags & SEARCH_FLAG_RODC_ATTRIBUTE) {
+ return true;
+ }
+
+ /* other attributes are denied */
+ return false;
+}
+
+/* return fsmo role dn and role owner dn for a particular role*/
+WERROR dsdb_get_fsmo_role_info(TALLOC_CTX *tmp_ctx,
+ struct ldb_context *ldb,
+ uint32_t role,
+ struct ldb_dn **fsmo_role_dn,
+ struct ldb_dn **role_owner_dn)
+{
+ int ret;
+ switch (role) {
+ case DREPL_NAMING_MASTER:
+ *fsmo_role_dn = samdb_partitions_dn(ldb, tmp_ctx);
+ ret = samdb_reference_dn(ldb, tmp_ctx, *fsmo_role_dn, "fSMORoleOwner", role_owner_dn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find fSMORoleOwner in Naming Master object - %s\n",
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+ break;
+ case DREPL_INFRASTRUCTURE_MASTER:
+ *fsmo_role_dn = samdb_infrastructure_dn(ldb, tmp_ctx);
+ ret = samdb_reference_dn(ldb, tmp_ctx, *fsmo_role_dn, "fSMORoleOwner", role_owner_dn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find fSMORoleOwner in Schema Master object - %s\n",
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+ break;
+ case DREPL_RID_MASTER:
+ ret = samdb_rid_manager_dn(ldb, tmp_ctx, fsmo_role_dn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0, (__location__ ": Failed to find RID Manager object - %s\n", ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ ret = samdb_reference_dn(ldb, tmp_ctx, *fsmo_role_dn, "fSMORoleOwner", role_owner_dn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find fSMORoleOwner in RID Manager object - %s\n",
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+ break;
+ case DREPL_SCHEMA_MASTER:
+ *fsmo_role_dn = ldb_get_schema_basedn(ldb);
+ ret = samdb_reference_dn(ldb, tmp_ctx, *fsmo_role_dn, "fSMORoleOwner", role_owner_dn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find fSMORoleOwner in Schema Master object - %s\n",
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+ break;
+ case DREPL_PDC_MASTER:
+ *fsmo_role_dn = ldb_get_default_basedn(ldb);
+ ret = samdb_reference_dn(ldb, tmp_ctx, *fsmo_role_dn, "fSMORoleOwner", role_owner_dn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find fSMORoleOwner in Pd Master object - %s\n",
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+ break;
+ default:
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+ return WERR_OK;
+}
+
+const char *samdb_dn_to_dnshostname(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *server_dn)
+{
+ int ldb_ret;
+ struct ldb_result *res = NULL;
+ const char * const attrs[] = { "dNSHostName", NULL};
+
+ ldb_ret = ldb_search(ldb, mem_ctx, &res,
+ server_dn,
+ LDB_SCOPE_BASE,
+ attrs, NULL);
+ if (ldb_ret != LDB_SUCCESS) {
+ DEBUG(4, ("Failed to find dNSHostName for dn %s, ldb error: %s\n",
+ ldb_dn_get_linearized(server_dn), ldb_errstring(ldb)));
+ return NULL;
+ }
+
+ return ldb_msg_find_attr_as_string(res->msgs[0], "dNSHostName", NULL);
+}
+
+/*
+ returns true if an attribute is in the filter,
+ false otherwise, provided that attribute value is provided with the expression
+*/
+bool dsdb_attr_in_parse_tree(struct ldb_parse_tree *tree,
+ const char *attr)
+{
+ unsigned int i;
+ switch (tree->operation) {
+ case LDB_OP_AND:
+ case LDB_OP_OR:
+ for (i=0;i<tree->u.list.num_elements;i++) {
+ if (dsdb_attr_in_parse_tree(tree->u.list.elements[i],
+ attr))
+ return true;
+ }
+ return false;
+ case LDB_OP_NOT:
+ return dsdb_attr_in_parse_tree(tree->u.isnot.child, attr);
+ case LDB_OP_EQUALITY:
+ if (ldb_attr_cmp(tree->u.equality.attr, attr) == 0) {
+ return true;
+ }
+ return false;
+ case LDB_OP_GREATER:
+ case LDB_OP_LESS:
+ case LDB_OP_APPROX:
+ if (ldb_attr_cmp(tree->u.comparison.attr, attr) == 0) {
+ return true;
+ }
+ return false;
+ case LDB_OP_SUBSTRING:
+ if (ldb_attr_cmp(tree->u.substring.attr, attr) == 0) {
+ return true;
+ }
+ return false;
+ case LDB_OP_PRESENT:
+ /* (attrname=*) is not filtered out */
+ return false;
+ case LDB_OP_EXTENDED:
+ if (tree->u.extended.attr &&
+ ldb_attr_cmp(tree->u.extended.attr, attr) == 0) {
+ return true;
+ }
+ return false;
+ }
+ return false;
+}
+
+int dsdb_werror_at(struct ldb_context *ldb, int ldb_ecode, WERROR werr,
+ const char *location, const char *func,
+ const char *reason)
+{
+ if (reason == NULL) {
+ reason = win_errstr(werr);
+ }
+ ldb_asprintf_errstring(ldb, "%08X: %s at %s:%s",
+ W_ERROR_V(werr), reason, location, func);
+ return ldb_ecode;
+}
+
+/*
+ map an ldb error code to an approximate NTSTATUS code
+ */
+NTSTATUS dsdb_ldb_err_to_ntstatus(int err)
+{
+ switch (err) {
+ case LDB_SUCCESS:
+ return NT_STATUS_OK;
+
+ case LDB_ERR_PROTOCOL_ERROR:
+ return NT_STATUS_DEVICE_PROTOCOL_ERROR;
+
+ case LDB_ERR_TIME_LIMIT_EXCEEDED:
+ return NT_STATUS_IO_TIMEOUT;
+
+ case LDB_ERR_SIZE_LIMIT_EXCEEDED:
+ return NT_STATUS_BUFFER_TOO_SMALL;
+
+ case LDB_ERR_COMPARE_FALSE:
+ case LDB_ERR_COMPARE_TRUE:
+ return NT_STATUS_REVISION_MISMATCH;
+
+ case LDB_ERR_AUTH_METHOD_NOT_SUPPORTED:
+ return NT_STATUS_NOT_SUPPORTED;
+
+ case LDB_ERR_STRONG_AUTH_REQUIRED:
+ case LDB_ERR_CONFIDENTIALITY_REQUIRED:
+ case LDB_ERR_SASL_BIND_IN_PROGRESS:
+ case LDB_ERR_INAPPROPRIATE_AUTHENTICATION:
+ case LDB_ERR_INVALID_CREDENTIALS:
+ case LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS:
+ case LDB_ERR_UNWILLING_TO_PERFORM:
+ return NT_STATUS_ACCESS_DENIED;
+
+ case LDB_ERR_NO_SUCH_OBJECT:
+ return NT_STATUS_OBJECT_NAME_NOT_FOUND;
+
+ case LDB_ERR_REFERRAL:
+ case LDB_ERR_NO_SUCH_ATTRIBUTE:
+ return NT_STATUS_NOT_FOUND;
+
+ case LDB_ERR_UNSUPPORTED_CRITICAL_EXTENSION:
+ return NT_STATUS_NOT_SUPPORTED;
+
+ case LDB_ERR_ADMIN_LIMIT_EXCEEDED:
+ return NT_STATUS_BUFFER_TOO_SMALL;
+
+ case LDB_ERR_UNDEFINED_ATTRIBUTE_TYPE:
+ case LDB_ERR_INAPPROPRIATE_MATCHING:
+ case LDB_ERR_CONSTRAINT_VIOLATION:
+ case LDB_ERR_INVALID_ATTRIBUTE_SYNTAX:
+ case LDB_ERR_INVALID_DN_SYNTAX:
+ case LDB_ERR_NAMING_VIOLATION:
+ case LDB_ERR_OBJECT_CLASS_VIOLATION:
+ case LDB_ERR_NOT_ALLOWED_ON_NON_LEAF:
+ case LDB_ERR_NOT_ALLOWED_ON_RDN:
+ return NT_STATUS_INVALID_PARAMETER;
+
+ case LDB_ERR_ATTRIBUTE_OR_VALUE_EXISTS:
+ case LDB_ERR_ENTRY_ALREADY_EXISTS:
+ return NT_STATUS_ERROR_DS_OBJ_STRING_NAME_EXISTS;
+
+ case LDB_ERR_BUSY:
+ return NT_STATUS_NETWORK_BUSY;
+
+ case LDB_ERR_ALIAS_PROBLEM:
+ case LDB_ERR_ALIAS_DEREFERENCING_PROBLEM:
+ case LDB_ERR_UNAVAILABLE:
+ case LDB_ERR_LOOP_DETECT:
+ case LDB_ERR_OBJECT_CLASS_MODS_PROHIBITED:
+ case LDB_ERR_AFFECTS_MULTIPLE_DSAS:
+ case LDB_ERR_OTHER:
+ case LDB_ERR_OPERATIONS_ERROR:
+ break;
+ }
+ return NT_STATUS_UNSUCCESSFUL;
+}
+
+
+/*
+ create a new naming context that will hold a partial replica
+ */
+int dsdb_create_partial_replica_NC(struct ldb_context *ldb, struct ldb_dn *dn)
+{
+ TALLOC_CTX *tmp_ctx = talloc_new(ldb);
+ struct ldb_message *msg;
+ int ret;
+
+ if (tmp_ctx == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ msg = ldb_msg_new(tmp_ctx);
+ if (msg == NULL) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(ldb);
+ }
+
+ msg->dn = dn;
+ ret = ldb_msg_add_string(msg, "objectClass", "top");
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(ldb);
+ }
+
+ /* [MS-DRSR] implies that we should only add the 'top'
+ * objectclass, but that would cause lots of problems with our
+ * objectclass code as top is not structural, so we add
+ * 'domainDNS' as well to keep things sane. We're expecting
+ * this new NC to be of objectclass domainDNS after
+ * replication anyway
+ */
+ ret = ldb_msg_add_string(msg, "objectClass", "domainDNS");
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(ldb);
+ }
+
+ ret = ldb_msg_add_fmt(msg, "instanceType", "%u",
+ INSTANCE_TYPE_IS_NC_HEAD|
+ INSTANCE_TYPE_NC_ABOVE|
+ INSTANCE_TYPE_UNINSTANT);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ldb_oom(ldb);
+ }
+
+ ret = dsdb_add(ldb, msg, DSDB_MODIFY_PARTIAL_REPLICA);
+ if (ret != LDB_SUCCESS && ret != LDB_ERR_ENTRY_ALREADY_EXISTS) {
+ DEBUG(0,("Failed to create new NC for %s - %s (%s)\n",
+ ldb_dn_get_linearized(dn),
+ ldb_errstring(ldb), ldb_strerror(ret)));
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ DEBUG(1,("Created new NC for %s\n", ldb_dn_get_linearized(dn)));
+
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+}
+
+/*
+ * Return the effective badPwdCount
+ *
+ * This requires that the user_msg have (if present):
+ * - badPasswordTime
+ * - badPwdCount
+ *
+ * This also requires that the domain_msg have (if present):
+ * - lockOutObservationWindow
+ */
+int dsdb_effective_badPwdCount(const struct ldb_message *user_msg,
+ int64_t lockOutObservationWindow,
+ NTTIME now)
+{
+ int64_t badPasswordTime;
+ badPasswordTime = ldb_msg_find_attr_as_int64(user_msg, "badPasswordTime", 0);
+
+ if (badPasswordTime - lockOutObservationWindow >= now) {
+ return ldb_msg_find_attr_as_int(user_msg, "badPwdCount", 0);
+ } else {
+ return 0;
+ }
+}
+
+/*
+ * Returns a user's PSO, or NULL if none was found
+ */
+static struct ldb_result *lookup_user_pso(struct ldb_context *sam_ldb,
+ TALLOC_CTX *mem_ctx,
+ const struct ldb_message *user_msg,
+ const char * const *attrs)
+{
+ struct ldb_result *res = NULL;
+ struct ldb_dn *pso_dn = NULL;
+ int ret;
+
+ /* if the user has a PSO that applies, then use the PSO's setting */
+ pso_dn = ldb_msg_find_attr_as_dn(sam_ldb, mem_ctx, user_msg,
+ "msDS-ResultantPSO");
+
+ if (pso_dn != NULL) {
+
+ ret = dsdb_search_dn(sam_ldb, mem_ctx, &res, pso_dn, attrs, 0);
+ if (ret != LDB_SUCCESS) {
+
+ /*
+ * log the error. The caller should fallback to using
+ * the default domain password settings
+ */
+ DBG_ERR("Error retrieving msDS-ResultantPSO %s for %s\n",
+ ldb_dn_get_linearized(pso_dn),
+ ldb_dn_get_linearized(user_msg->dn));
+ }
+ talloc_free(pso_dn);
+ }
+ return res;
+}
+
+/*
+ * Return the msDS-LockoutObservationWindow for a user message
+ *
+ * This requires that the user_msg have (if present):
+ * - msDS-ResultantPSO
+ */
+int64_t samdb_result_msds_LockoutObservationWindow(
+ struct ldb_context *sam_ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *domain_dn,
+ const struct ldb_message *user_msg)
+{
+ int64_t lockOutObservationWindow;
+ struct ldb_result *res = NULL;
+ const char *attrs[] = { "msDS-LockoutObservationWindow",
+ NULL };
+ if (domain_dn == NULL) {
+ smb_panic("domain dn is NULL");
+ }
+ res = lookup_user_pso(sam_ldb, mem_ctx, user_msg, attrs);
+
+ if (res != NULL) {
+ lockOutObservationWindow =
+ ldb_msg_find_attr_as_int64(res->msgs[0],
+ "msDS-LockoutObservationWindow",
+ DEFAULT_OBSERVATION_WINDOW);
+ talloc_free(res);
+ } else {
+
+ /* no PSO was found, lookup the default domain setting */
+ lockOutObservationWindow =
+ samdb_search_int64(sam_ldb, mem_ctx, 0, domain_dn,
+ "lockOutObservationWindow", NULL);
+ }
+ return lockOutObservationWindow;
+}
+
+/*
+ * Return the effective badPwdCount
+ *
+ * This requires that the user_msg have (if present):
+ * - badPasswordTime
+ * - badPwdCount
+ * - msDS-ResultantPSO
+ */
+int samdb_result_effective_badPwdCount(struct ldb_context *sam_ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *domain_dn,
+ const struct ldb_message *user_msg)
+{
+ struct timeval tv_now = timeval_current();
+ NTTIME now = timeval_to_nttime(&tv_now);
+ int64_t lockOutObservationWindow =
+ samdb_result_msds_LockoutObservationWindow(
+ sam_ldb, mem_ctx, domain_dn, user_msg);
+ return dsdb_effective_badPwdCount(user_msg, lockOutObservationWindow, now);
+}
+
+/*
+ * Returns the lockoutThreshold that applies. If a PSO is specified, then that
+ * setting is used over the domain defaults
+ */
+static int64_t get_lockout_threshold(struct ldb_message *domain_msg,
+ struct ldb_message *pso_msg)
+{
+ if (pso_msg != NULL) {
+ return ldb_msg_find_attr_as_int(pso_msg,
+ "msDS-LockoutThreshold", 0);
+ } else {
+ return ldb_msg_find_attr_as_int(domain_msg,
+ "lockoutThreshold", 0);
+ }
+}
+
+/*
+ * Returns the lockOutObservationWindow that applies. If a PSO is specified,
+ * then that setting is used over the domain defaults
+ */
+static int64_t get_lockout_observation_window(struct ldb_message *domain_msg,
+ struct ldb_message *pso_msg)
+{
+ if (pso_msg != NULL) {
+ return ldb_msg_find_attr_as_int64(pso_msg,
+ "msDS-LockoutObservationWindow",
+ DEFAULT_OBSERVATION_WINDOW);
+ } else {
+ return ldb_msg_find_attr_as_int64(domain_msg,
+ "lockOutObservationWindow",
+ DEFAULT_OBSERVATION_WINDOW);
+ }
+}
+
+/*
+ * Prepare an update to the badPwdCount and associated attributes.
+ *
+ * This requires that the user_msg have (if present):
+ * - objectSid
+ * - badPasswordTime
+ * - badPwdCount
+ *
+ * This also requires that the domain_msg have (if present):
+ * - pwdProperties
+ * - lockoutThreshold
+ * - lockOutObservationWindow
+ *
+ * This also requires that the pso_msg have (if present):
+ * - msDS-LockoutThreshold
+ * - msDS-LockoutObservationWindow
+ */
+NTSTATUS dsdb_update_bad_pwd_count(TALLOC_CTX *mem_ctx,
+ struct ldb_context *sam_ctx,
+ struct ldb_message *user_msg,
+ struct ldb_message *domain_msg,
+ struct ldb_message *pso_msg,
+ struct ldb_message **_mod_msg)
+{
+ int ret, badPwdCount;
+ unsigned int i;
+ int64_t lockoutThreshold, lockOutObservationWindow;
+ struct dom_sid *sid;
+ struct timeval tv_now = timeval_current();
+ NTTIME now = timeval_to_nttime(&tv_now);
+ NTSTATUS status;
+ uint32_t pwdProperties, rid = 0;
+ struct ldb_message *mod_msg;
+
+ sid = samdb_result_dom_sid(mem_ctx, user_msg, "objectSid");
+
+ pwdProperties = ldb_msg_find_attr_as_uint(domain_msg,
+ "pwdProperties", -1);
+ if (sid && !(pwdProperties & DOMAIN_PASSWORD_LOCKOUT_ADMINS)) {
+ status = dom_sid_split_rid(NULL, sid, NULL, &rid);
+ if (!NT_STATUS_IS_OK(status)) {
+ /*
+ * This can't happen anyway, but always try
+ * and update the badPwdCount on failure
+ */
+ rid = 0;
+ }
+ }
+ TALLOC_FREE(sid);
+
+ /*
+ * Work out if we are doing password lockout on the domain.
+ * Also, the built in administrator account is exempt:
+ * http://msdn.microsoft.com/en-us/library/windows/desktop/aa375371%28v=vs.85%29.aspx
+ */
+ lockoutThreshold = get_lockout_threshold(domain_msg, pso_msg);
+ if (lockoutThreshold == 0 || (rid == DOMAIN_RID_ADMINISTRATOR)) {
+ DEBUG(5, ("Not updating badPwdCount on %s after wrong password\n",
+ ldb_dn_get_linearized(user_msg->dn)));
+ return NT_STATUS_OK;
+ }
+
+ mod_msg = ldb_msg_new(mem_ctx);
+ if (mod_msg == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ mod_msg->dn = ldb_dn_copy(mod_msg, user_msg->dn);
+ if (mod_msg->dn == NULL) {
+ TALLOC_FREE(mod_msg);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ lockOutObservationWindow = get_lockout_observation_window(domain_msg,
+ pso_msg);
+
+ badPwdCount = dsdb_effective_badPwdCount(user_msg, lockOutObservationWindow, now);
+
+ badPwdCount++;
+
+ ret = samdb_msg_add_int(sam_ctx, mod_msg, mod_msg, "badPwdCount", badPwdCount);
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(mod_msg);
+ return NT_STATUS_NO_MEMORY;
+ }
+ ret = samdb_msg_add_int64(sam_ctx, mod_msg, mod_msg, "badPasswordTime", now);
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(mod_msg);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ if (badPwdCount >= lockoutThreshold) {
+ ret = samdb_msg_add_int64(sam_ctx, mod_msg, mod_msg, "lockoutTime", now);
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(mod_msg);
+ return NT_STATUS_NO_MEMORY;
+ }
+ DEBUGC( DBGC_AUTH, 1, ("Locked out user %s after %d wrong passwords\n",
+ ldb_dn_get_linearized(user_msg->dn), badPwdCount));
+ } else {
+ DEBUGC( DBGC_AUTH, 5, ("Updated badPwdCount on %s after %d wrong passwords\n",
+ ldb_dn_get_linearized(user_msg->dn), badPwdCount));
+ }
+
+ /* mark all the message elements as LDB_FLAG_MOD_REPLACE */
+ for (i=0; i< mod_msg->num_elements; i++) {
+ mod_msg->elements[i].flags = LDB_FLAG_MOD_REPLACE;
+ }
+
+ *_mod_msg = mod_msg;
+ return NT_STATUS_OK;
+}
+
+/**
+ * Sets defaults for a User object
+ * List of default attributes set:
+ * accountExpires, badPasswordTime, badPwdCount,
+ * codePage, countryCode, lastLogoff, lastLogon
+ * logonCount, pwdLastSet
+ */
+int dsdb_user_obj_set_defaults(struct ldb_context *ldb,
+ struct ldb_message *usr_obj,
+ struct ldb_request *req)
+{
+ size_t i;
+ int ret;
+ const struct attribute_values {
+ const char *name;
+ const char *value;
+ const char *add_value;
+ const char *mod_value;
+ const char *control;
+ unsigned add_flags;
+ unsigned mod_flags;
+ } map[] = {
+ {
+ .name = "accountExpires",
+ .add_value = "9223372036854775807",
+ .mod_value = "0",
+ },
+ {
+ .name = "badPasswordTime",
+ .value = "0"
+ },
+ {
+ .name = "badPwdCount",
+ .value = "0"
+ },
+ {
+ .name = "codePage",
+ .value = "0"
+ },
+ {
+ .name = "countryCode",
+ .value = "0"
+ },
+ {
+ .name = "lastLogoff",
+ .value = "0"
+ },
+ {
+ .name = "lastLogon",
+ .value = "0"
+ },
+ {
+ .name = "logonCount",
+ .value = "0"
+ },
+ {
+ .name = "logonHours",
+ .add_flags = DSDB_FLAG_INTERNAL_FORCE_META_DATA,
+ },
+ {
+ .name = "pwdLastSet",
+ .value = "0",
+ .control = DSDB_CONTROL_PASSWORD_DEFAULT_LAST_SET_OID,
+ },
+ {
+ .name = "adminCount",
+ .mod_value = "0",
+ },
+ {
+ .name = "operatorCount",
+ .mod_value = "0",
+ },
+ };
+
+ for (i = 0; i < ARRAY_SIZE(map); i++) {
+ bool added = false;
+ const char *value = NULL;
+ unsigned flags = 0;
+
+ if (req != NULL && req->operation == LDB_ADD) {
+ value = map[i].add_value;
+ flags = map[i].add_flags;
+ } else {
+ value = map[i].mod_value;
+ flags = map[i].mod_flags;
+ }
+
+ if (value == NULL) {
+ value = map[i].value;
+ }
+
+ if (value != NULL) {
+ flags |= LDB_FLAG_MOD_ADD;
+ }
+
+ if (flags == 0) {
+ continue;
+ }
+
+ ret = samdb_find_or_add_attribute_ex(ldb, usr_obj,
+ map[i].name,
+ value, flags,
+ &added);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ if (req != NULL && added && map[i].control != NULL) {
+ ret = ldb_request_add_control(req,
+ map[i].control,
+ false, NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+ }
+
+ return LDB_SUCCESS;
+}
+
+/**
+ * Sets 'sAMAccountType on user object based on userAccountControl.
+ * This function is used in processing both 'add' and 'modify' requests.
+ * @param ldb Current ldb_context
+ * @param usr_obj ldb_message representing User object
+ * @param user_account_control Value for userAccountControl flags
+ * @param account_type_p Optional pointer to account_type to return
+ * @return LDB_SUCCESS or LDB_ERR* code on failure
+ */
+int dsdb_user_obj_set_account_type(struct ldb_context *ldb, struct ldb_message *usr_obj,
+ uint32_t user_account_control, uint32_t *account_type_p)
+{
+ int ret;
+ uint32_t account_type;
+
+ account_type = ds_uf2atype(user_account_control);
+ if (account_type == 0) {
+ ldb_set_errstring(ldb, "dsdb: Unrecognized account type!");
+ return LDB_ERR_UNWILLING_TO_PERFORM;
+ }
+ ret = samdb_msg_add_uint_flags(ldb, usr_obj, usr_obj,
+ "sAMAccountType",
+ account_type,
+ LDB_FLAG_MOD_REPLACE);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ if (account_type_p) {
+ *account_type_p = account_type;
+ }
+
+ return LDB_SUCCESS;
+}
+
+/**
+ * Determine and set primaryGroupID based on userAccountControl value.
+ * This function is used in processing both 'add' and 'modify' requests.
+ * @param ldb Current ldb_context
+ * @param usr_obj ldb_message representing User object
+ * @param user_account_control Value for userAccountControl flags
+ * @param group_rid_p Optional pointer to group RID to return
+ * @return LDB_SUCCESS or LDB_ERR* code on failure
+ */
+int dsdb_user_obj_set_primary_group_id(struct ldb_context *ldb, struct ldb_message *usr_obj,
+ uint32_t user_account_control, uint32_t *group_rid_p)
+{
+ int ret;
+ uint32_t rid;
+
+ rid = ds_uf2prim_group_rid(user_account_control);
+
+ ret = samdb_msg_add_uint_flags(ldb, usr_obj, usr_obj,
+ "primaryGroupID", rid,
+ LDB_FLAG_MOD_REPLACE);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ if (group_rid_p) {
+ *group_rid_p = rid;
+ }
+
+ return LDB_SUCCESS;
+}
+
+/**
+ * Returns True if the source and target DNs both have the same naming context,
+ * i.e. they're both in the same partition.
+ */
+bool dsdb_objects_have_same_nc(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *source_dn,
+ struct ldb_dn *target_dn)
+{
+ TALLOC_CTX *tmp_ctx;
+ struct ldb_dn *source_nc = NULL;
+ struct ldb_dn *target_nc = NULL;
+ int ret;
+ bool same_nc = true;
+
+ tmp_ctx = talloc_new(mem_ctx);
+ if (tmp_ctx == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ ret = dsdb_find_nc_root(ldb, tmp_ctx, source_dn, &source_nc);
+ /* fix clang warning */
+ if (source_nc == NULL) {
+ ret = LDB_ERR_OTHER;
+ }
+ if (ret != LDB_SUCCESS) {
+ DBG_ERR("Failed to find base DN for source %s: %s\n",
+ ldb_dn_get_linearized(source_dn), ldb_errstring(ldb));
+ talloc_free(tmp_ctx);
+ return true;
+ }
+
+ ret = dsdb_find_nc_root(ldb, tmp_ctx, target_dn, &target_nc);
+ /* fix clang warning */
+ if (target_nc == NULL) {
+ ret = LDB_ERR_OTHER;
+ }
+ if (ret != LDB_SUCCESS) {
+ DBG_ERR("Failed to find base DN for target %s: %s\n",
+ ldb_dn_get_linearized(target_dn), ldb_errstring(ldb));
+ talloc_free(tmp_ctx);
+ return true;
+ }
+
+ same_nc = (ldb_dn_compare(source_nc, target_nc) == 0);
+
+ talloc_free(tmp_ctx);
+
+ return same_nc;
+}
+/*
+ * Context for dsdb_count_domain_callback
+ */
+struct dsdb_count_domain_context {
+ /*
+ * Number of matching records
+ */
+ size_t count;
+ /*
+ * sid of the domain that the records must belong to.
+ * if NULL records can belong to any domain.
+ */
+ struct dom_sid *dom_sid;
+};
+
+/*
+ * @brief ldb async callback for dsdb_domain_count.
+ *
+ * count the number of records in the database matching an LDAP query,
+ * optionally filtering for domain membership.
+ *
+ * @param [in,out] req the ldb request being processed
+ * req->context contains:
+ * count The number of matching records
+ * dom_sid The domain sid, if present records must belong
+ * to the domain to be counted.
+ *@param [in,out] ares The query result.
+ *
+ * @return an LDB error code
+ *
+ */
+static int dsdb_count_domain_callback(
+ struct ldb_request *req,
+ struct ldb_reply *ares)
+{
+
+ if (ares == NULL) {
+ return ldb_request_done(req, LDB_ERR_OPERATIONS_ERROR);
+ }
+ if (ares->error != LDB_SUCCESS) {
+ int error = ares->error;
+ TALLOC_FREE(ares);
+ return ldb_request_done(req, error);
+ }
+
+ switch (ares->type) {
+ case LDB_REPLY_ENTRY:
+ {
+ struct dsdb_count_domain_context *context = NULL;
+ ssize_t ret;
+ bool in_domain;
+ struct dom_sid sid;
+ const struct ldb_val *v;
+
+ context = req->context;
+ if (context->dom_sid == NULL) {
+ context->count++;
+ break;
+ }
+
+ v = ldb_msg_find_ldb_val(ares->message, "objectSid");
+ if (v == NULL) {
+ break;
+ }
+
+ ret = sid_parse(v->data, v->length, &sid);
+ if (ret == -1) {
+ break;
+ }
+
+ in_domain = dom_sid_in_domain(context->dom_sid, &sid);
+ if (!in_domain) {
+ break;
+ }
+
+ context->count++;
+ break;
+ }
+ case LDB_REPLY_REFERRAL:
+ break;
+
+ case LDB_REPLY_DONE:
+ TALLOC_FREE(ares);
+ return ldb_request_done(req, LDB_SUCCESS);
+ }
+
+ TALLOC_FREE(ares);
+
+ return LDB_SUCCESS;
+}
+
+/*
+ * @brief Count the number of records matching a query.
+ *
+ * Count the number of entries in the database matching the supplied query,
+ * optionally filtering only those entries belonging to the supplied domain.
+ *
+ * @param ldb [in] Current ldb context
+ * @param count [out] Pointer to the count
+ * @param base [in] The base dn for the query
+ * @param dom_sid [in] The domain sid, if non NULL records that are not a member
+ * of the domain are ignored.
+ * @param scope [in] Search scope.
+ * @param exp_fmt [in] format string for the query.
+ *
+ * @return LDB_STATUS code.
+ */
+int PRINTF_ATTRIBUTE(6, 7) dsdb_domain_count(
+ struct ldb_context *ldb,
+ size_t *count,
+ struct ldb_dn *base,
+ struct dom_sid *dom_sid,
+ enum ldb_scope scope,
+ const char *exp_fmt, ...)
+{
+ TALLOC_CTX *tmp_ctx = NULL;
+ struct ldb_request *req = NULL;
+ struct dsdb_count_domain_context *context = NULL;
+ char *expression = NULL;
+ const char *object_sid[] = {"objectSid", NULL};
+ const char *none[] = {NULL};
+ va_list ap;
+ int ret;
+
+ *count = 0;
+ tmp_ctx = talloc_new(ldb);
+ if (tmp_ctx == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ context = talloc_zero(tmp_ctx, struct dsdb_count_domain_context);
+ if (context == NULL) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ context->dom_sid = dom_sid;
+
+ if (exp_fmt) {
+ va_start(ap, exp_fmt);
+ expression = talloc_vasprintf(tmp_ctx, exp_fmt, ap);
+ va_end(ap);
+
+ if (expression == NULL) {
+ TALLOC_FREE(context);
+ TALLOC_FREE(tmp_ctx);
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ }
+
+ ret = ldb_build_search_req(
+ &req,
+ ldb,
+ tmp_ctx,
+ base,
+ scope,
+ expression,
+ (dom_sid == NULL) ? none : object_sid,
+ NULL,
+ context,
+ dsdb_count_domain_callback,
+ NULL);
+ ldb_req_set_location(req, "dsdb_domain_count");
+
+ if (ret != LDB_SUCCESS) goto done;
+
+ ret = ldb_request(ldb, req);
+
+ if (ret == LDB_SUCCESS) {
+ ret = ldb_wait(req->handle, LDB_WAIT_ALL);
+ if (ret == LDB_SUCCESS) {
+ *count = context->count;
+ }
+ }
+
+
+done:
+ TALLOC_FREE(expression);
+ TALLOC_FREE(req);
+ TALLOC_FREE(context);
+ TALLOC_FREE(tmp_ctx);
+
+ return ret;
+}
+
+/*
+ * Returns 1 if 'sids' contains the Protected Users group SID for the domain, 0
+ * if not. Returns a negative value on error.
+ */
+int dsdb_is_protected_user(struct ldb_context *ldb,
+ const struct auth_SidAttr *sids,
+ uint32_t num_sids)
+{
+ const struct dom_sid *domain_sid = NULL;
+ struct dom_sid protected_users_sid;
+ uint32_t i;
+
+ domain_sid = samdb_domain_sid(ldb);
+ if (domain_sid == NULL) {
+ return -1;
+ }
+
+ protected_users_sid = *domain_sid;
+ if (!sid_append_rid(&protected_users_sid, DOMAIN_RID_PROTECTED_USERS)) {
+ return -1;
+ }
+
+ for (i = 0; i < num_sids; ++i) {
+ if (dom_sid_equal(&protected_users_sid, &sids[i].sid)) {
+ return 1;
+ }
+ }
+
+ return 0;
+}
diff --git a/source4/dsdb/common/util.h b/source4/dsdb/common/util.h
new file mode 100644
index 0000000..63cfd79
--- /dev/null
+++ b/source4/dsdb/common/util.h
@@ -0,0 +1,101 @@
+/*
+ Unix SMB/CIFS implementation.
+ Samba utility functions
+
+ Copyright (C) Andrew Tridgell 2010
+ 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/>.
+*/
+
+#ifndef __DSDB_COMMON_UTIL_H__
+#define __DSDB_COMMON_UTIL_H__
+
+/*
+ flags for dsdb_request_add_controls(). For the module functions,
+ the upper 16 bits are in dsdb/samdb/ldb_modules/util.h
+*/
+#define DSDB_SEARCH_SEARCH_ALL_PARTITIONS 0x00001
+#define DSDB_SEARCH_SHOW_DELETED 0x00002
+#define DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT 0x00004
+#define DSDB_SEARCH_REVEAL_INTERNALS 0x00008
+#define DSDB_SEARCH_SHOW_EXTENDED_DN 0x00010
+#define DSDB_MODIFY_RELAX 0x00020
+#define DSDB_MODIFY_PERMISSIVE 0x00040
+#define DSDB_FLAG_AS_SYSTEM 0x00080
+#define DSDB_TREE_DELETE 0x00100
+#define DSDB_SEARCH_ONE_ONLY 0x00200 /* give an error unless 1 record */
+#define DSDB_SEARCH_SHOW_RECYCLED 0x00400
+#define DSDB_PROVISION 0x00800
+#define DSDB_BYPASS_PASSWORD_HASH 0x01000
+#define DSDB_SEARCH_NO_GLOBAL_CATALOG 0x02000
+#define DSDB_MODIFY_PARTIAL_REPLICA 0x04000
+#define DSDB_PASSWORD_BYPASS_LAST_SET 0x08000
+#define DSDB_REPLMD_VANISH_LINKS 0x10000
+#define DSDB_MARK_REQ_UNTRUSTED 0x20000
+
+#define DSDB_SECRET_ATTRIBUTES_EX(sep) \
+ "pekList" sep \
+ "msDS-ExecuteScriptPassword" sep \
+ "currentValue" sep \
+ "dBCSPwd" sep \
+ "initialAuthIncoming" sep \
+ "initialAuthOutgoing" sep \
+ "lmPwdHistory" sep \
+ "ntPwdHistory" sep \
+ "priorValue" sep \
+ "supplementalCredentials" sep \
+ "trustAuthIncoming" sep \
+ "trustAuthOutgoing" sep \
+ "unicodePwd" sep \
+ "clearTextPassword"
+
+#define DSDB_SECRET_ATTRIBUTES_COMMA ,
+#define DSDB_SECRET_ATTRIBUTES DSDB_SECRET_ATTRIBUTES_EX(DSDB_SECRET_ATTRIBUTES_COMMA)
+
+#define DSDB_PASSWORD_ATTRIBUTES \
+ "userPassword", \
+ "clearTextPassword", \
+ "unicodePwd", \
+ "dBCSPwd"
+
+/*
+ * ldb opaque values used to pass the user session information to ldb modules
+ */
+#define DSDB_SESSION_INFO "sessionInfo"
+#define DSDB_NETWORK_SESSION_INFO "networkSessionInfo"
+
+struct GUID;
+
+struct ldb_context;
+
+int dsdb_werror_at(struct ldb_context *ldb, int ldb_ecode, WERROR werr,
+ const char *location, const char *func,
+ const char *reason);
+
+#define dsdb_module_werror(module, ldb_ecode, werr, reason) \
+ dsdb_werror_at(ldb_module_get_ctx(module), ldb_ecode, werr, \
+ __location__, __func__, reason)
+
+
+struct dsdb_ldb_dn_list_node {
+ struct dsdb_ldb_dn_list_node *prev, *next;
+
+ /* the dn of the partition */
+ struct ldb_dn *dn;
+};
+
+
+
+#endif /* __DSDB_COMMON_UTIL_H__ */
diff --git a/source4/dsdb/common/util_groups.c b/source4/dsdb/common/util_groups.c
new file mode 100644
index 0000000..27a8735
--- /dev/null
+++ b/source4/dsdb/common/util_groups.c
@@ -0,0 +1,200 @@
+/*
+ Unix SMB/CIFS implementation.
+ Password and authentication handling
+ Copyright (C) Andrew Bartlett <abartlet@samba.org> 2001-2010
+ Copyright (C) Stefan Metzmacher 2005
+ 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/>.
+*/
+
+#include "includes.h"
+#include "auth/auth.h"
+#include <ldb.h>
+#include "dsdb/samdb/samdb.h"
+#include "libcli/security/security.h"
+#include "dsdb/common/util.h"
+
+/*
+ * This function generates the transitive closure of a given SAM object "dn_val"
+ * (it basically expands nested memberships).
+ * If the object isn't located in the "res_sids" structure yet and the
+ * "only_childs" flag is false, we add it to "res_sids".
+ * Then we've always to consider the "memberOf" attributes. We invoke the
+ * function recursively on each of it with the "only_childs" flag set to
+ * "false".
+ * The "only_childs" flag is particularly useful if you have a user object and
+ * want to include all it's groups (referenced with "memberOf") but not itself
+ * or considering if that object matches the filter.
+ *
+ * At the beginning "res_sids" should reference to a NULL pointer.
+ */
+NTSTATUS dsdb_expand_nested_groups(struct ldb_context *sam_ctx,
+ struct ldb_val *dn_val, const bool only_childs, const char *filter,
+ TALLOC_CTX *res_sids_ctx, struct auth_SidAttr **res_sids,
+ uint32_t *num_res_sids)
+{
+ static const char * const attrs[] = { "groupType", "memberOf", NULL };
+ unsigned int i;
+ int ret;
+ struct ldb_dn *dn;
+ struct dom_sid sid;
+ TALLOC_CTX *tmp_ctx;
+ struct ldb_result *res;
+ NTSTATUS status;
+ const struct ldb_message_element *el;
+
+ if (*res_sids == NULL) {
+ *num_res_sids = 0;
+ }
+
+ if (!sam_ctx) {
+ DEBUG(0, ("No SAM available, cannot determine local groups\n"));
+ return NT_STATUS_INVALID_SYSTEM_SERVICE;
+ }
+
+ tmp_ctx = talloc_new(res_sids_ctx);
+ if (tmp_ctx == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ dn = ldb_dn_from_ldb_val(tmp_ctx, sam_ctx, dn_val);
+ if (dn == NULL) {
+ talloc_free(tmp_ctx);
+ DEBUG(0, (__location__ ": we failed parsing DN %.*s, so we cannot calculate the group token\n",
+ (int)dn_val->length, dn_val->data));
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ status = dsdb_get_extended_dn_sid(dn, &sid, "SID");
+ if (NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) {
+ /* If we fail finding a SID then this is no error since it could
+ * be a non SAM object - e.g. a group with object class
+ * "groupOfNames" */
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+ } else if (!NT_STATUS_IS_OK(status)) {
+ DEBUG(0, (__location__ ": when parsing DN '%s' we failed to parse it's SID component, so we cannot calculate the group token: %s\n",
+ ldb_dn_get_extended_linearized(tmp_ctx, dn, 1),
+ nt_errstr(status)));
+ talloc_free(tmp_ctx);
+ return status;
+ }
+
+ if (!ldb_dn_minimise(dn)) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ if (only_childs) {
+ ret = dsdb_search_dn(sam_ctx, tmp_ctx, &res, dn, attrs,
+ DSDB_SEARCH_SHOW_EXTENDED_DN);
+ } else {
+ ret = dsdb_search(sam_ctx, tmp_ctx, &res, dn, LDB_SCOPE_BASE,
+ attrs, DSDB_SEARCH_SHOW_EXTENDED_DN, "%s",
+ filter);
+ }
+
+ /*
+ * We have the problem with the caller creating a <SID=S-....>
+ * DN for ForeignSecurityPrincipals as they also have
+ * duplicate objects with the SAME SID under CN=Configuration.
+ * This causes a SID= DN to fail with NO_SUCH_OBJECT on Samba
+ * and on Windows. So, we allow this to fail, and
+ * double-check if we can find it with a search in the main
+ * domain partition.
+ */
+ if (ret == LDB_ERR_NO_SUCH_OBJECT && only_childs) {
+ char *sid_string = dom_sid_string(tmp_ctx,
+ &sid);
+ if (!sid_string) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+ }
+
+ ret = dsdb_search(sam_ctx, tmp_ctx, &res,
+ ldb_get_default_basedn(sam_ctx),
+ LDB_SCOPE_SUBTREE,
+ attrs, DSDB_SEARCH_SHOW_EXTENDED_DN,
+ "(&(objectClass=foreignSecurityPrincipal)(objectSID=%s))",
+ sid_string);
+ }
+
+ if (ret == LDB_ERR_NO_SUCH_OBJECT) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+ }
+
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1, (__location__ ": dsdb_search for %s failed: %s\n",
+ ldb_dn_get_extended_linearized(tmp_ctx, dn, 1),
+ ldb_errstring(sam_ctx)));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ /* We may get back 0 results, if the SID didn't match the filter - such as it wasn't a domain group, for example */
+ if (res->count != 1) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+ }
+
+ /* We only apply this test once we know the SID matches the filter */
+ if (!only_childs) {
+ unsigned group_type;
+ uint32_t sid_attrs;
+ bool already_there;
+
+ sid_attrs = SE_GROUP_DEFAULT_FLAGS;
+ group_type = ldb_msg_find_attr_as_uint(res->msgs[0], "groupType", 0);
+ if (group_type & GROUP_TYPE_RESOURCE_GROUP) {
+ sid_attrs |= SE_GROUP_RESOURCE;
+ }
+
+ /* This is an O(n^2) linear search */
+ already_there = sids_contains_sid_attrs(*res_sids, *num_res_sids,
+ &sid, sid_attrs);
+ if (already_there) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+ }
+
+ *res_sids = talloc_realloc(res_sids_ctx, *res_sids,
+ struct auth_SidAttr, *num_res_sids + 1);
+ if (*res_sids == NULL) {
+ TALLOC_FREE(tmp_ctx);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ (*res_sids)[*num_res_sids].sid = sid;
+ (*res_sids)[*num_res_sids].attrs = sid_attrs;
+
+ ++(*num_res_sids);
+ }
+
+ el = ldb_msg_find_element(res->msgs[0], "memberOf");
+
+ for (i = 0; el && i < el->num_values; i++) {
+ status = dsdb_expand_nested_groups(sam_ctx, &el->values[i],
+ false, filter, res_sids_ctx, res_sids, num_res_sids);
+ if (!NT_STATUS_IS_OK(status)) {
+ talloc_free(tmp_ctx);
+ return status;
+ }
+ }
+
+ talloc_free(tmp_ctx);
+
+ return NT_STATUS_OK;
+}
diff --git a/source4/dsdb/common/util_links.c b/source4/dsdb/common/util_links.c
new file mode 100644
index 0000000..d41d1f2
--- /dev/null
+++ b/source4/dsdb/common/util_links.c
@@ -0,0 +1,229 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ Helpers to search for links in the DB
+
+ 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/>.
+*/
+
+#include "includes.h"
+#include "dsdb/samdb/samdb.h"
+#include "lib/util/binsearch.h"
+#include "librpc/gen_ndr/ndr_misc.h"
+
+/*
+ * We choose, as the sort order, the same order as is used in DRS replication,
+ * which is the memcmp() order of the NDR GUID, not that obtained from
+ * GUID_compare().
+ *
+ * This means that sorted links will be in the same order as a new DC would
+ * see them.
+ */
+int ndr_guid_compare(const struct GUID *guid1, const struct GUID *guid2)
+{
+ uint8_t v1_data[16] = { 0 };
+ struct ldb_val v1 = data_blob_const(v1_data, sizeof(v1_data));
+ uint8_t v2_data[16];
+ struct ldb_val v2 = data_blob_const(v2_data, sizeof(v2_data));
+
+ /* This can't fail */
+ ndr_push_struct_into_fixed_blob(&v1, guid1,
+ (ndr_push_flags_fn_t)ndr_push_GUID);
+ /* This can't fail */
+ ndr_push_struct_into_fixed_blob(&v2, guid2,
+ (ndr_push_flags_fn_t)ndr_push_GUID);
+ return data_blob_cmp(&v1, &v2);
+}
+
+
+static int la_guid_compare_with_trusted_dn(struct compare_ctx *ctx,
+ struct parsed_dn *p)
+{
+ int cmp = 0;
+ /*
+ * This works like a standard compare function in its return values,
+ * but has an extra trick to deal with errors: zero is returned and
+ * ctx->err is set to the ldb error code.
+ *
+ * That is, if (as is expected in most cases) you get a non-zero
+ * result, you don't need to check for errors.
+ *
+ * We assume the second argument refers to a DN is from the database
+ * and has a GUID -- but this GUID might not have been parsed out yet.
+ */
+ if (p->dsdb_dn == NULL) {
+ int ret = really_parse_trusted_dn(ctx->mem_ctx, ctx->ldb, p,
+ ctx->ldap_oid);
+ if (ret != LDB_SUCCESS) {
+ ctx->err = ret;
+ return 0;
+ }
+ }
+ cmp = ndr_guid_compare(ctx->guid, &p->guid);
+ if (cmp == 0 && ctx->compare_extra_part) {
+ if (ctx->partial_extra_part_length != 0) {
+ /* Allow a prefix match on the blob. */
+ return memcmp(ctx->extra_part.data,
+ p->dsdb_dn->extra_part.data,
+ MIN(ctx->partial_extra_part_length,
+ p->dsdb_dn->extra_part.length));
+ } else {
+ return data_blob_cmp(&ctx->extra_part,
+ &p->dsdb_dn->extra_part);
+ }
+ }
+
+ return cmp;
+}
+
+/* When a parsed_dn comes from the database, sometimes it is not really parsed. */
+
+int really_parse_trusted_dn(TALLOC_CTX *mem_ctx, struct ldb_context *ldb,
+ struct parsed_dn *pdn, const char *ldap_oid)
+{
+ NTSTATUS status;
+ struct dsdb_dn *dsdb_dn = dsdb_dn_parse_trusted(mem_ctx, ldb, pdn->v,
+ ldap_oid);
+ if (dsdb_dn == NULL) {
+ return LDB_ERR_INVALID_DN_SYNTAX;
+ }
+
+ status = dsdb_get_extended_dn_guid(dsdb_dn->dn, &pdn->guid, "GUID");
+ if (!NT_STATUS_IS_OK(status)) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ pdn->dsdb_dn = dsdb_dn;
+ return LDB_SUCCESS;
+}
+
+
+int get_parsed_dns_trusted(TALLOC_CTX *mem_ctx, struct ldb_message_element *el,
+ struct parsed_dn **pdn)
+{
+ /* Here we get a list of 'struct parsed_dns' without the parsing */
+ unsigned int i;
+ *pdn = talloc_zero_array(mem_ctx, struct parsed_dn,
+ el->num_values);
+ if (!*pdn) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ for (i = 0; i < el->num_values; i++) {
+ (*pdn)[i].v = &el->values[i];
+ }
+
+ return LDB_SUCCESS;
+}
+
+
+int parsed_dn_find(struct ldb_context *ldb, struct parsed_dn *pdn,
+ unsigned int count,
+ const struct GUID *guid,
+ struct ldb_dn *target_dn,
+ DATA_BLOB extra_part,
+ size_t partial_extra_part_length,
+ struct parsed_dn **exact,
+ struct parsed_dn **next,
+ const char *ldap_oid,
+ bool compare_extra_part)
+{
+ unsigned int i;
+ struct compare_ctx ctx;
+ if (pdn == NULL) {
+ *exact = NULL;
+ *next = NULL;
+ return LDB_SUCCESS;
+ }
+
+ if (unlikely(GUID_all_zero(guid))) {
+ /*
+ * When updating a link using DRS, we sometimes get a NULL
+ * GUID when a forward link has been deleted and its GUID has
+ * for some reason been forgotten. The best we can do is try
+ * and match by DN via a linear search. Note that this
+ * probably only happens in the ADD case, in which we only
+ * allow modification of link if it is already deleted, so
+ * this seems very close to an elaborate NO-OP, but we are not
+ * quite prepared to declare it so.
+ *
+ * If the DN is not in our list, we have to add it to the
+ * beginning of the list, where it would naturally sort.
+ */
+ struct parsed_dn *p;
+ if (target_dn == NULL) {
+ /* We don't know the target DN, so we can't search for DN */
+ DEBUG(1, ("parsed_dn_find has a NULL GUID for a linked "
+ "attribute but we don't have a DN to compare "
+ "it with\n"));
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ *exact = NULL;
+ *next = NULL;
+
+ DEBUG(3, ("parsed_dn_find has a NULL GUID for a link to DN "
+ "%s; searching through links for it\n",
+ ldb_dn_get_linearized(target_dn)));
+
+ for (i = 0; i < count; i++) {
+ int cmp;
+ p = &pdn[i];
+ if (p->dsdb_dn == NULL) {
+ int ret = really_parse_trusted_dn(pdn, ldb, p, ldap_oid);
+ if (ret != LDB_SUCCESS) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ }
+
+ cmp = ldb_dn_compare(p->dsdb_dn->dn, target_dn);
+ if (cmp == 0) {
+ *exact = p;
+ return LDB_SUCCESS;
+ }
+ }
+ /*
+ * Here we have a null guid which doesn't match any existing
+ * link. This is a bit unexpected because null guids occur
+ * when a forward link has been deleted and we are replicating
+ * that deletion.
+ *
+ * The best thing to do is weep into the logs and add the
+ * offending link to the beginning of the list which is
+ * at least the correct sort position.
+ */
+ DEBUG(1, ("parsed_dn_find has been given a NULL GUID for a "
+ "link to unknown DN %s\n",
+ ldb_dn_get_linearized(target_dn)));
+ *next = pdn;
+ return LDB_SUCCESS;
+ }
+
+ ctx.guid = guid;
+ ctx.ldb = ldb;
+ ctx.mem_ctx = pdn;
+ ctx.ldap_oid = ldap_oid;
+ ctx.extra_part = extra_part;
+ ctx.partial_extra_part_length = partial_extra_part_length;
+ ctx.compare_extra_part = compare_extra_part;
+ ctx.err = 0;
+
+ BINARY_ARRAY_SEARCH_GTE(pdn, count, &ctx, la_guid_compare_with_trusted_dn,
+ *exact, *next);
+
+ if (ctx.err != 0) {
+ return ctx.err;
+ }
+ return LDB_SUCCESS;
+}
diff --git a/source4/dsdb/common/util_links.h b/source4/dsdb/common/util_links.h
new file mode 100644
index 0000000..e6dc41b
--- /dev/null
+++ b/source4/dsdb/common/util_links.h
@@ -0,0 +1,48 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ Helpers to search for links in the DB
+
+ 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/>.
+*/
+
+#ifndef __DSDB_COMMON_UTIL_LINKS_H__
+#define __DSDB_COMMON_UTIL_LINKS_H__
+
+struct compare_ctx {
+ const struct GUID *guid;
+ struct ldb_context *ldb;
+ TALLOC_CTX *mem_ctx;
+ const char *ldap_oid;
+ int err;
+ const struct GUID *invocation_id;
+ DATA_BLOB extra_part;
+ size_t partial_extra_part_length;
+ bool compare_extra_part;
+};
+
+struct parsed_dn {
+ struct dsdb_dn *dsdb_dn;
+ struct GUID guid;
+ struct ldb_val *v;
+};
+
+
+int get_parsed_dns_trusted(TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *el,
+ struct parsed_dn **pdn);
+
+#endif /* __DSDB_COMMON_UTIL_LINKS_H__ */
diff --git a/source4/dsdb/common/util_samr.c b/source4/dsdb/common/util_samr.c
new file mode 100644
index 0000000..0a48fcf
--- /dev/null
+++ b/source4/dsdb/common/util_samr.c
@@ -0,0 +1,593 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ Helpers to add users and groups to the DB
+
+ Copyright (C) Andrew Tridgell 2004
+ Copyright (C) Volker Lendecke 2004
+ Copyright (C) Andrew Bartlett <abartlet@samba.org> 2004-2010
+ 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/>.
+*/
+
+#include "includes.h"
+#include "dsdb/samdb/samdb.h"
+#include "dsdb/common/util.h"
+#include "../libds/common/flags.h"
+#include "libcli/security/security.h"
+
+#include "libds/common/flag_mapping.h"
+
+/* Add a user, SAMR style, including the correct transaction
+ * semantics. Used by the SAMR server and by pdb_samba4 */
+NTSTATUS dsdb_add_user(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ const char *account_name,
+ uint32_t acct_flags,
+ const struct dom_sid *forced_sid,
+ struct dom_sid **sid,
+ struct ldb_dn **dn)
+{
+ const char *name;
+ struct ldb_message *msg;
+ int ret;
+ const char *container, *obj_class=NULL;
+ char *cn_name;
+ size_t cn_name_len;
+
+ const char *attrs[] = {
+ "objectSid",
+ "userAccountControl",
+ NULL
+ };
+
+ uint32_t user_account_control;
+ struct ldb_dn *account_dn;
+ struct dom_sid *account_sid;
+
+ const char *account_name_encoded = NULL;
+
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ NT_STATUS_HAVE_NO_MEMORY(tmp_ctx);
+
+ account_name_encoded = ldb_binary_encode_string(tmp_ctx, account_name);
+ if (account_name_encoded == NULL) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ /*
+ * Start a transaction, so we can query and do a subsequent atomic
+ * modify
+ */
+
+ ret = ldb_transaction_start(ldb);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,("Failed to start a transaction for user creation: %s\n",
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_LOCK_NOT_GRANTED;
+ }
+
+ /* check if the user already exists */
+ name = samdb_search_string(ldb, tmp_ctx, NULL,
+ "sAMAccountName",
+ "(&(sAMAccountName=%s)(objectclass=user))",
+ account_name_encoded);
+ if (name != NULL) {
+ ldb_transaction_cancel(ldb);
+ talloc_free(tmp_ctx);
+ return NT_STATUS_USER_EXISTS;
+ }
+
+ cn_name = talloc_strdup(tmp_ctx, account_name);
+ if (!cn_name) {
+ ldb_transaction_cancel(ldb);
+ talloc_free(tmp_ctx);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ cn_name_len = strlen(cn_name);
+ if (cn_name_len < 1) {
+ ldb_transaction_cancel(ldb);
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ msg = ldb_msg_new(tmp_ctx);
+ if (msg == NULL) {
+ ldb_transaction_cancel(ldb);
+ talloc_free(tmp_ctx);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ /* This must be one of these values *only* */
+ if (acct_flags == ACB_NORMAL) {
+ container = "CN=Users";
+ obj_class = "user";
+ user_account_control = UF_NORMAL_ACCOUNT;
+ } else if (acct_flags == ACB_WSTRUST) {
+ if (cn_name[cn_name_len - 1] != '$') {
+ ldb_transaction_cancel(ldb);
+ return NT_STATUS_FOOBAR;
+ }
+ cn_name[cn_name_len - 1] = '\0';
+ container = "CN=Computers";
+ obj_class = "computer";
+ user_account_control = UF_WORKSTATION_TRUST_ACCOUNT;
+
+ } else if (acct_flags == ACB_SVRTRUST) {
+ if (cn_name[cn_name_len - 1] != '$') {
+ ldb_transaction_cancel(ldb);
+ return NT_STATUS_FOOBAR;
+ }
+ cn_name[cn_name_len - 1] = '\0';
+ container = "OU=Domain Controllers";
+ obj_class = "computer";
+ user_account_control = UF_SERVER_TRUST_ACCOUNT;
+ } else if (acct_flags == ACB_DOMTRUST) {
+ DEBUG(3, ("Invalid account flags specified: cannot create domain trusts via this interface (must use LSA CreateTrustedDomain calls\n"));
+ ldb_transaction_cancel(ldb);
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INVALID_PARAMETER;
+ } else {
+ DEBUG(3, ("Invalid account flags specified 0x%08X, must be exactly one of \n"
+ "ACB_NORMAL (0x%08X) ACB_WSTRUST (0x%08X) or ACB_SVRTRUST (0x%08X)\n",
+ acct_flags,
+ ACB_NORMAL, ACB_WSTRUST, ACB_SVRTRUST));
+ ldb_transaction_cancel(ldb);
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ user_account_control |= UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD;
+
+ /* add core elements to the ldb_message for the user */
+ msg->dn = ldb_dn_copy(msg, ldb_get_default_basedn(ldb));
+ if ( ! ldb_dn_add_child_fmt(msg->dn, "CN=%s,%s", cn_name, container)) {
+ ldb_transaction_cancel(ldb);
+ talloc_free(tmp_ctx);
+ return NT_STATUS_FOOBAR;
+ }
+
+ ret = ldb_msg_add_string(msg, "sAMAccountName", account_name);
+ if (ret != LDB_SUCCESS) {
+ goto failed;
+ }
+ ret = ldb_msg_add_string(msg, "objectClass", obj_class);
+ if (ret != LDB_SUCCESS) {
+ goto failed;
+ }
+ ret = samdb_msg_add_uint(ldb, tmp_ctx, msg,
+ "userAccountControl",
+ user_account_control);
+ if (ret != LDB_SUCCESS) {
+ goto failed;
+ }
+
+ /* This is only here for migrations using pdb_samba4, the
+ * caller and the samldb are responsible for ensuring it makes
+ * sense */
+ if (forced_sid) {
+ ret = samdb_msg_add_dom_sid(ldb, msg, msg, "objectSID", forced_sid);
+ if (ret != LDB_SUCCESS) {
+ ldb_transaction_cancel(ldb);
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+ }
+
+ /* create the user */
+ ret = ldb_add(ldb, msg);
+ switch (ret) {
+ case LDB_SUCCESS:
+ break;
+ case LDB_ERR_ENTRY_ALREADY_EXISTS:
+ ldb_transaction_cancel(ldb);
+ DEBUG(0,("Failed to create user record %s: %s\n",
+ ldb_dn_get_linearized(msg->dn),
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_USER_EXISTS;
+ case LDB_ERR_UNWILLING_TO_PERFORM:
+ case LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS:
+ ldb_transaction_cancel(ldb);
+ DEBUG(0,("Failed to create user record %s: %s\n",
+ ldb_dn_get_linearized(msg->dn),
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_ACCESS_DENIED;
+ default:
+ ldb_transaction_cancel(ldb);
+ DEBUG(0,("Failed to create user record %s: %s\n",
+ ldb_dn_get_linearized(msg->dn),
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ account_dn = msg->dn;
+
+ /* retrieve the sid and account control bits for the user just created */
+ ret = dsdb_search_one(ldb, tmp_ctx, &msg,
+ account_dn, LDB_SCOPE_BASE, attrs, 0, NULL);
+
+ if (ret != LDB_SUCCESS) {
+ ldb_transaction_cancel(ldb);
+ DEBUG(0,("Can't locate the account we just created %s: %s\n",
+ ldb_dn_get_linearized(account_dn), ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+ account_sid = samdb_result_dom_sid(tmp_ctx, msg, "objectSid");
+ if (account_sid == NULL) {
+ ldb_transaction_cancel(ldb);
+ DEBUG(0,("Apparently we failed to get the objectSid of the just created account record %s\n",
+ ldb_dn_get_linearized(msg->dn)));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ ret = ldb_transaction_commit(ldb);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,("Failed to commit transaction to add and modify account record %s: %s\n",
+ ldb_dn_get_linearized(msg->dn),
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+ *dn = talloc_steal(mem_ctx, account_dn);
+ if (sid) {
+ *sid = talloc_steal(mem_ctx, account_sid);
+ }
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+
+ failed:
+ ldb_transaction_cancel(ldb);
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INTERNAL_ERROR;
+}
+
+/*
+ called by samr_CreateDomainGroup and pdb_samba4
+*/
+NTSTATUS dsdb_add_domain_group(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ const char *groupname,
+ struct dom_sid **sid,
+ struct ldb_dn **dn)
+{
+ const char *name;
+ struct ldb_message *msg;
+ struct dom_sid *group_sid;
+ const char *groupname_encoded = NULL;
+ int ret;
+
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ NT_STATUS_HAVE_NO_MEMORY(tmp_ctx);
+
+ groupname_encoded = ldb_binary_encode_string(tmp_ctx, groupname);
+ if (groupname_encoded == NULL) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ /* check if the group already exists */
+ name = samdb_search_string(ldb, tmp_ctx, NULL,
+ "sAMAccountName",
+ "(&(sAMAccountName=%s)(objectclass=group))",
+ groupname_encoded);
+ if (name != NULL) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_GROUP_EXISTS;
+ }
+
+ msg = ldb_msg_new(tmp_ctx);
+ if (msg == NULL) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ /* add core elements to the ldb_message for the user */
+ msg->dn = ldb_dn_copy(tmp_ctx, ldb_get_default_basedn(ldb));
+ ldb_dn_add_child_fmt(msg->dn, "CN=%s,CN=Users", groupname);
+ if (!msg->dn) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_NO_MEMORY;
+ }
+ ldb_msg_add_string(msg, "sAMAccountName", groupname);
+ ldb_msg_add_string(msg, "objectClass", "group");
+
+ /* create the group */
+ ret = ldb_add(ldb, msg);
+ switch (ret) {
+ case LDB_SUCCESS:
+ break;
+ case LDB_ERR_ENTRY_ALREADY_EXISTS:
+ DEBUG(0,("Failed to create group record %s: %s\n",
+ ldb_dn_get_linearized(msg->dn),
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_GROUP_EXISTS;
+ case LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS:
+ DEBUG(0,("Failed to create group record %s: %s\n",
+ ldb_dn_get_linearized(msg->dn),
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_ACCESS_DENIED;
+ default:
+ DEBUG(0,("Failed to create group record %s: %s\n",
+ ldb_dn_get_linearized(msg->dn),
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ /* retrieve the sid for the group just created */
+ group_sid = samdb_search_dom_sid(ldb, tmp_ctx,
+ msg->dn, "objectSid", NULL);
+ if (group_sid == NULL) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_UNSUCCESSFUL;
+ }
+
+ *dn = talloc_steal(mem_ctx, msg->dn);
+ *sid = talloc_steal(mem_ctx, group_sid);
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_add_domain_alias(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ const char *alias_name,
+ struct dom_sid **sid,
+ struct ldb_dn **dn)
+{
+ const char *name;
+ struct ldb_message *msg;
+ struct dom_sid *alias_sid;
+ const char *alias_name_encoded = NULL;
+ int ret;
+
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ NT_STATUS_HAVE_NO_MEMORY(tmp_ctx);
+
+ alias_name_encoded = ldb_binary_encode_string(tmp_ctx, alias_name);
+ if (alias_name_encoded == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ if (ldb_transaction_start(ldb) != LDB_SUCCESS) {
+ DEBUG(0, ("Failed to start transaction in dsdb_add_domain_alias(): %s\n", ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ /* Check if alias already exists */
+ name = samdb_search_string(ldb, tmp_ctx, NULL,
+ "sAMAccountName",
+ "(sAMAccountName=%s)(objectclass=group))",
+ alias_name_encoded);
+
+ if (name != NULL) {
+ talloc_free(tmp_ctx);
+ ldb_transaction_cancel(ldb);
+ return NT_STATUS_ALIAS_EXISTS;
+ }
+
+ msg = ldb_msg_new(tmp_ctx);
+ if (msg == NULL) {
+ talloc_free(tmp_ctx);
+ ldb_transaction_cancel(ldb);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ /* add core elements to the ldb_message for the alias */
+ msg->dn = ldb_dn_copy(mem_ctx, ldb_get_default_basedn(ldb));
+ ldb_dn_add_child_fmt(msg->dn, "CN=%s,CN=Users", alias_name);
+ if (!msg->dn) {
+ talloc_free(tmp_ctx);
+ ldb_transaction_cancel(ldb);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ ldb_msg_add_string(msg, "sAMAccountName", alias_name);
+ ldb_msg_add_string(msg, "objectClass", "group");
+ samdb_msg_add_int(ldb, mem_ctx, msg, "groupType", GTYPE_SECURITY_DOMAIN_LOCAL_GROUP);
+
+ /* create the alias */
+ ret = ldb_add(ldb, msg);
+ switch (ret) {
+ case LDB_SUCCESS:
+ break;
+ case LDB_ERR_ENTRY_ALREADY_EXISTS:
+ talloc_free(tmp_ctx);
+ ldb_transaction_cancel(ldb);
+ return NT_STATUS_ALIAS_EXISTS;
+ case LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS:
+ talloc_free(tmp_ctx);
+ ldb_transaction_cancel(ldb);
+ return NT_STATUS_ACCESS_DENIED;
+ default:
+ DEBUG(0,("Failed to create alias record %s: %s\n",
+ ldb_dn_get_linearized(msg->dn),
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ ldb_transaction_cancel(ldb);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ /* retrieve the sid for the alias just created */
+ alias_sid = samdb_search_dom_sid(ldb, tmp_ctx,
+ msg->dn, "objectSid", NULL);
+
+ if (ldb_transaction_commit(ldb) != LDB_SUCCESS) {
+ DEBUG(0, ("Failed to commit transaction in dsdb_add_domain_alias(): %s\n",
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ *dn = talloc_steal(mem_ctx, msg->dn);
+ *sid = talloc_steal(mem_ctx, alias_sid);
+ talloc_free(tmp_ctx);
+
+
+ return NT_STATUS_OK;
+}
+
+/* Return the members of this group (which may be a domain group or an alias) */
+NTSTATUS dsdb_enum_group_mem(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *dn,
+ struct dom_sid **members_out,
+ unsigned int *pnum_members)
+{
+ struct ldb_message *msg;
+ unsigned int i, j;
+ int ret;
+ struct dom_sid *members;
+ struct ldb_message_element *member_el;
+ const char *attrs[] = { "member", NULL };
+ NTSTATUS status;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ NT_STATUS_HAVE_NO_MEMORY(tmp_ctx);
+
+ ret = dsdb_search_one(ldb, tmp_ctx, &msg, dn, LDB_SCOPE_BASE, attrs,
+ DSDB_SEARCH_SHOW_EXTENDED_DN, NULL);
+ if (ret == LDB_ERR_NO_SUCH_OBJECT) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OBJECT_NAME_NOT_FOUND;
+ }
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1, ("dsdb_enum_group_mem: dsdb_search for %s failed: %s\n",
+ ldb_dn_get_linearized(dn), ldb_errstring(ldb)));
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ member_el = ldb_msg_find_element(msg, "member");
+ if (!member_el) {
+ *members_out = NULL;
+ *pnum_members = 0;
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+ }
+
+ members = talloc_array(mem_ctx, struct dom_sid, member_el->num_values);
+ if (members == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ j = 0;
+ for (i=0; i <member_el->num_values; i++) {
+ struct ldb_dn *member_dn = ldb_dn_from_ldb_val(tmp_ctx, ldb,
+ &member_el->values[i]);
+ if (!member_dn || !ldb_dn_validate(member_dn)) {
+ DEBUG(1, ("Could not parse %*.*s as a DN\n",
+ (int)member_el->values[i].length,
+ (int)member_el->values[i].length,
+ (const char *)member_el->values[i].data));
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ status = dsdb_get_extended_dn_sid(member_dn, &members[j],
+ "SID");
+ if (NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) {
+ /* If we fail finding a SID then this is no error since
+ * it could be a non SAM object - e.g. a contact */
+ continue;
+ } else if (!NT_STATUS_IS_OK(status)) {
+ DEBUG(1, ("When parsing DN '%s' we failed to parse it's SID component, so we cannot fetch the membership: %s\n",
+ ldb_dn_get_extended_linearized(tmp_ctx, member_dn, 1),
+ nt_errstr(status)));
+ talloc_free(tmp_ctx);
+ return status;
+ }
+
+ ++j;
+ }
+
+ *members_out = members;
+ *pnum_members = j;
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_lookup_rids(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ const struct dom_sid *domain_sid,
+ unsigned int num_rids,
+ uint32_t *rids,
+ const char **names,
+ enum lsa_SidType *lsa_attrs)
+{
+ const char *attrs[] = { "sAMAccountType", "sAMAccountName", NULL };
+ unsigned int i, num_mapped;
+
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ NT_STATUS_HAVE_NO_MEMORY(tmp_ctx);
+
+ num_mapped = 0;
+
+ for (i=0; i<num_rids; i++) {
+ struct ldb_message *msg;
+ struct ldb_dn *dn;
+ uint32_t attr;
+ int rc;
+
+ lsa_attrs[i] = SID_NAME_UNKNOWN;
+
+ dn = ldb_dn_new_fmt(tmp_ctx, ldb, "<SID=%s>",
+ dom_sid_string(tmp_ctx,
+ dom_sid_add_rid(tmp_ctx, domain_sid,
+ rids[i])));
+ if (dn == NULL) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_NO_MEMORY;
+ }
+ rc = dsdb_search_one(ldb, tmp_ctx, &msg, dn, LDB_SCOPE_BASE, attrs, 0, "samAccountName=*");
+ if (rc == LDB_ERR_NO_SUCH_OBJECT) {
+ continue;
+ } else if (rc != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ names[i] = ldb_msg_find_attr_as_string(msg, "samAccountName", NULL);
+ if (names[i] == NULL) {
+ DEBUG(10, ("no samAccountName\n"));
+ continue;
+ }
+ talloc_steal(names, names[i]);
+ attr = ldb_msg_find_attr_as_uint(msg, "samAccountType", 0);
+ lsa_attrs[i] = ds_atype_map(attr);
+ if (lsa_attrs[i] == SID_NAME_UNKNOWN) {
+ continue;
+ }
+ num_mapped += 1;
+ }
+ talloc_free(tmp_ctx);
+
+ if (num_mapped == 0) {
+ return NT_STATUS_NONE_MAPPED;
+ }
+ if (num_mapped < num_rids) {
+ return STATUS_SOME_UNMAPPED;
+ }
+ return NT_STATUS_OK;
+}
+
diff --git a/source4/dsdb/common/util_trusts.c b/source4/dsdb/common/util_trusts.c
new file mode 100644
index 0000000..5003e74
--- /dev/null
+++ b/source4/dsdb/common/util_trusts.c
@@ -0,0 +1,3443 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ 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.h"
+#include "../lib/util/util_ldb.h"
+#include "dsdb/samdb/samdb.h"
+#include "libcli/security/security.h"
+#include "librpc/gen_ndr/ndr_security.h"
+#include "librpc/gen_ndr/ndr_misc.h"
+#include "../libds/common/flags.h"
+#include "dsdb/common/proto.h"
+#include "param/param.h"
+#include "librpc/gen_ndr/ndr_drsblobs.h"
+#include "lib/util/tsort.h"
+#include "dsdb/common/util.h"
+#include "libds/common/flag_mapping.h"
+#include "../lib/util/dlinklist.h"
+#include "lib/crypto/md4.h"
+#include "libcli/ldap/ldap_ndr.h"
+
+#undef strcasecmp
+
+NTSTATUS dsdb_trust_forest_info_from_lsa(TALLOC_CTX *mem_ctx,
+ const struct lsa_ForestTrustInformation *lfti,
+ struct ForestTrustInfo **_fti)
+{
+ struct ForestTrustInfo *fti;
+ uint32_t i;
+
+ *_fti = NULL;
+
+ fti = talloc_zero(mem_ctx, struct ForestTrustInfo);
+ if (fti == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ fti->version = 1;
+ fti->count = lfti->count;
+ fti->records = talloc_zero_array(mem_ctx,
+ struct ForestTrustInfoRecordArmor,
+ fti->count);
+ if (fti->records == NULL) {
+ TALLOC_FREE(fti);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ for (i = 0; i < fti->count; i++) {
+ const struct lsa_ForestTrustRecord *lftr = lfti->entries[i];
+ struct ForestTrustInfoRecord *ftr = &fti->records[i].record;
+ struct ForestTrustString *str = NULL;
+ const struct lsa_StringLarge *lstr = NULL;
+ const struct lsa_ForestTrustDomainInfo *linfo = NULL;
+ struct ForestTrustDataDomainInfo *info = NULL;
+
+ if (lftr == NULL) {
+ TALLOC_FREE(fti);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ ftr->flags = lftr->flags;
+ ftr->timestamp = lftr->time;
+ ftr->type = (enum ForestTrustInfoRecordType)lftr->type;
+
+ switch (lftr->type) {
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ lstr = &lftr->forest_trust_data.top_level_name;
+ str = &ftr->data.name;
+
+ str->string = talloc_strdup(mem_ctx, lstr->string);
+ if (str->string == NULL) {
+ TALLOC_FREE(fti);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ break;
+
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX:
+ lstr = &lftr->forest_trust_data.top_level_name_ex;
+ str = &ftr->data.name;
+
+ str->string = talloc_strdup(mem_ctx, lstr->string);
+ if (str->string == NULL) {
+ TALLOC_FREE(fti);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ break;
+
+ case LSA_FOREST_TRUST_DOMAIN_INFO:
+ linfo = &lftr->forest_trust_data.domain_info;
+ info = &ftr->data.info;
+
+ if (linfo->domain_sid == NULL) {
+ TALLOC_FREE(fti);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+ info->sid = *linfo->domain_sid;
+
+ lstr = &linfo->dns_domain_name;
+ str = &info->dns_name;
+ str->string = talloc_strdup(mem_ctx, lstr->string);
+ if (str->string == NULL) {
+ TALLOC_FREE(fti);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ lstr = &linfo->netbios_domain_name;
+ str = &info->netbios_name;
+ str->string = talloc_strdup(mem_ctx, lstr->string);
+ if (str->string == NULL) {
+ TALLOC_FREE(fti);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ break;
+
+ default:
+ return NT_STATUS_NOT_SUPPORTED;
+ }
+ }
+
+ *_fti = fti;
+ return NT_STATUS_OK;
+}
+
+static NTSTATUS dsdb_trust_forest_record_to_lsa(TALLOC_CTX *mem_ctx,
+ const struct ForestTrustInfoRecord *ftr,
+ struct lsa_ForestTrustRecord **_lftr)
+{
+ struct lsa_ForestTrustRecord *lftr = NULL;
+ const struct ForestTrustString *str = NULL;
+ struct lsa_StringLarge *lstr = NULL;
+ const struct ForestTrustDataDomainInfo *info = NULL;
+ struct lsa_ForestTrustDomainInfo *linfo = NULL;
+
+ *_lftr = NULL;
+
+ lftr = talloc_zero(mem_ctx, struct lsa_ForestTrustRecord);
+ if (lftr == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ lftr->flags = ftr->flags;
+ lftr->time = ftr->timestamp;
+ lftr->type = (enum lsa_ForestTrustRecordType)ftr->type;
+
+ switch (lftr->type) {
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ lstr = &lftr->forest_trust_data.top_level_name;
+ str = &ftr->data.name;
+
+ lstr->string = talloc_strdup(mem_ctx, str->string);
+ if (lstr->string == NULL) {
+ TALLOC_FREE(lftr);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ break;
+
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX:
+ lstr = &lftr->forest_trust_data.top_level_name_ex;
+ str = &ftr->data.name;
+
+ lstr->string = talloc_strdup(mem_ctx, str->string);
+ if (lstr->string == NULL) {
+ TALLOC_FREE(lftr);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ break;
+
+ case LSA_FOREST_TRUST_DOMAIN_INFO:
+ linfo = &lftr->forest_trust_data.domain_info;
+ info = &ftr->data.info;
+
+ linfo->domain_sid = dom_sid_dup(lftr, &info->sid);
+ if (linfo->domain_sid == NULL) {
+ TALLOC_FREE(lftr);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ lstr = &linfo->dns_domain_name;
+ str = &info->dns_name;
+ lstr->string = talloc_strdup(mem_ctx, str->string);
+ if (lstr->string == NULL) {
+ TALLOC_FREE(lftr);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ lstr = &linfo->netbios_domain_name;
+ str = &info->netbios_name;
+ lstr->string = talloc_strdup(mem_ctx, str->string);
+ if (lstr->string == NULL) {
+ TALLOC_FREE(lftr);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ break;
+
+ default:
+ return NT_STATUS_NOT_SUPPORTED;
+ }
+
+ *_lftr = lftr;
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_trust_forest_info_to_lsa(TALLOC_CTX *mem_ctx,
+ const struct ForestTrustInfo *fti,
+ struct lsa_ForestTrustInformation **_lfti)
+{
+ struct lsa_ForestTrustInformation *lfti;
+ uint32_t i;
+
+ *_lfti = NULL;
+
+ if (fti->version != 1) {
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ lfti = talloc_zero(mem_ctx, struct lsa_ForestTrustInformation);
+ if (lfti == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ lfti->count = fti->count;
+ lfti->entries = talloc_zero_array(mem_ctx,
+ struct lsa_ForestTrustRecord *,
+ lfti->count);
+ if (lfti->entries == NULL) {
+ TALLOC_FREE(lfti);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ for (i = 0; i < fti->count; i++) {
+ struct ForestTrustInfoRecord *ftr = &fti->records[i].record;
+ struct lsa_ForestTrustRecord *lftr = NULL;
+ NTSTATUS status;
+
+ status = dsdb_trust_forest_record_to_lsa(lfti->entries, ftr,
+ &lftr);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(lfti);
+ return NT_STATUS_NO_MEMORY;
+ }
+ lfti->entries[i] = lftr;
+ }
+
+ *_lfti = lfti;
+ return NT_STATUS_OK;
+}
+
+static NTSTATUS dsdb_trust_forest_info_add_record(struct lsa_ForestTrustInformation *fti,
+ const struct lsa_ForestTrustRecord *ftr)
+{
+ struct lsa_ForestTrustRecord **es = NULL;
+ struct lsa_ForestTrustRecord *e = NULL;
+ const struct lsa_StringLarge *dns1 = NULL;
+ struct lsa_StringLarge *dns2 = NULL;
+ const struct lsa_ForestTrustDomainInfo *d1 = NULL;
+ struct lsa_ForestTrustDomainInfo *d2 = NULL;
+ size_t len = 0;
+
+ es = talloc_realloc(fti, fti->entries,
+ struct lsa_ForestTrustRecord *,
+ fti->count + 1);
+ if (!es) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ fti->entries = es;
+
+ e = talloc_zero(es, struct lsa_ForestTrustRecord);
+ if (e == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ e->type = ftr->type;
+ e->flags = ftr->flags;
+ e->time = ftr->time;
+
+ switch (ftr->type) {
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ dns1 = &ftr->forest_trust_data.top_level_name;
+ dns2 = &e->forest_trust_data.top_level_name;
+ break;
+
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX:
+ dns1 = &ftr->forest_trust_data.top_level_name_ex;
+ dns2 = &e->forest_trust_data.top_level_name_ex;
+ break;
+
+ case LSA_FOREST_TRUST_DOMAIN_INFO:
+ dns1 = &ftr->forest_trust_data.domain_info.dns_domain_name;
+ dns2 = &e->forest_trust_data.domain_info.dns_domain_name;
+ d1 = &ftr->forest_trust_data.domain_info;
+ d2 = &e->forest_trust_data.domain_info;
+ break;
+ default:
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ if (dns1->string == NULL) {
+ TALLOC_FREE(e);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ len = strlen(dns1->string);
+ if (len == 0) {
+ TALLOC_FREE(e);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ dns2->string = talloc_strdup(e, dns1->string);
+ if (dns2->string == NULL) {
+ TALLOC_FREE(e);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ if (d1 != NULL) {
+ const struct lsa_StringLarge *nb1 = &d1->netbios_domain_name;
+ struct lsa_StringLarge *nb2 = &d2->netbios_domain_name;
+
+ if (nb1->string == NULL) {
+ TALLOC_FREE(e);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ len = strlen(nb1->string);
+ if (len == 0) {
+ TALLOC_FREE(e);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+ if (len > 15) {
+ TALLOC_FREE(e);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ nb2->string = talloc_strdup(e, nb1->string);
+ if (nb2->string == NULL) {
+ TALLOC_FREE(e);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ if (d1->domain_sid == NULL) {
+ TALLOC_FREE(e);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ d2->domain_sid = dom_sid_dup(e, d1->domain_sid);
+ if (d2->domain_sid == NULL) {
+ TALLOC_FREE(e);
+ return NT_STATUS_NO_MEMORY;
+ }
+ }
+
+ fti->entries[fti->count++] = e;
+ return NT_STATUS_OK;
+}
+
+static NTSTATUS dsdb_trust_parse_crossref_info(TALLOC_CTX *mem_ctx,
+ struct ldb_context *sam_ctx,
+ const struct ldb_message *msg,
+ struct lsa_TrustDomainInfoInfoEx **_tdo)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ struct lsa_TrustDomainInfoInfoEx *tdo = NULL;
+ const char *dns = NULL;
+ const char *netbios = NULL;
+ struct ldb_dn *nc_dn = NULL;
+ struct dom_sid sid = {
+ .num_auths = 0,
+ };
+ NTSTATUS status;
+
+ *_tdo = NULL;
+ tdo = talloc_zero(mem_ctx, struct lsa_TrustDomainInfoInfoEx);
+ if (tdo == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ talloc_steal(frame, tdo);
+
+ dns = ldb_msg_find_attr_as_string(msg, "dnsRoot", NULL);
+ if (dns == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+ tdo->domain_name.string = talloc_strdup(tdo, dns);
+ if (tdo->domain_name.string == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ netbios = ldb_msg_find_attr_as_string(msg, "nETBIOSName", NULL);
+ if (netbios == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+ tdo->netbios_name.string = talloc_strdup(tdo, netbios);
+ if (tdo->netbios_name.string == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ nc_dn = samdb_result_dn(sam_ctx, frame, msg, "ncName", NULL);
+ if (nc_dn == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ status = dsdb_get_extended_dn_sid(nc_dn, &sid, "SID");
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ tdo->sid = dom_sid_dup(tdo, &sid);
+ if (tdo->sid == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ tdo->trust_type = LSA_TRUST_TYPE_UPLEVEL;
+ tdo->trust_direction = LSA_TRUST_DIRECTION_INBOUND |
+ LSA_TRUST_DIRECTION_OUTBOUND;
+ tdo->trust_attributes = LSA_TRUST_ATTRIBUTE_WITHIN_FOREST;
+
+ *_tdo = talloc_move(mem_ctx, &tdo);
+ TALLOC_FREE(frame);
+ return NT_STATUS_OK;
+}
+
+static NTSTATUS dsdb_trust_crossref_tdo_info(TALLOC_CTX *mem_ctx,
+ struct ldb_context *sam_ctx,
+ struct ldb_dn *domain_dn,
+ const char *extra_filter,
+ struct lsa_TrustDomainInfoInfoEx **_tdo,
+ struct lsa_TrustDomainInfoInfoEx **_root_trust_tdo,
+ struct lsa_TrustDomainInfoInfoEx **_trust_parent_tdo)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ struct lsa_TrustDomainInfoInfoEx *tdo = NULL;
+ struct lsa_TrustDomainInfoInfoEx *root_trust_tdo = NULL;
+ struct lsa_TrustDomainInfoInfoEx *trust_parent_tdo = NULL;
+ struct ldb_dn *partitions_dn = NULL;
+ const char * const cross_attrs[] = {
+ "dnsRoot",
+ "nETBIOSName",
+ "nCName",
+ "rootTrust",
+ "trustParent",
+ NULL,
+ };
+ struct ldb_result *cross_res = NULL;
+ struct ldb_message *msg = NULL;
+ struct ldb_dn *root_trust_dn = NULL;
+ struct ldb_dn *trust_parent_dn = NULL;
+ NTSTATUS status;
+ int ret;
+
+ if (extra_filter == NULL) {
+ extra_filter = "";
+ }
+
+ *_tdo = NULL;
+ if (_root_trust_tdo != NULL) {
+ *_root_trust_tdo = NULL;
+ }
+ if (_trust_parent_tdo != NULL) {
+ *_trust_parent_tdo = NULL;
+ }
+
+ partitions_dn = samdb_partitions_dn(sam_ctx, frame);
+ if (partitions_dn == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ ret = dsdb_search(sam_ctx, partitions_dn, &cross_res,
+ partitions_dn, LDB_SCOPE_ONELEVEL,
+ cross_attrs,
+ DSDB_SEARCH_ONE_ONLY |
+ DSDB_SEARCH_SHOW_EXTENDED_DN,
+ "(&"
+ "(ncName=%s)"
+ "(objectClass=crossRef)"
+ "(systemFlags:%s:=%u)"
+ "%s"
+ ")",
+ ldb_dn_get_linearized(domain_dn),
+ LDB_OID_COMPARATOR_AND,
+ SYSTEM_FLAG_CR_NTDS_DOMAIN,
+ extra_filter);
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(frame);
+ return dsdb_ldb_err_to_ntstatus(ret);
+ }
+ msg = cross_res->msgs[0];
+
+ status = dsdb_trust_parse_crossref_info(mem_ctx, sam_ctx, msg, &tdo);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ talloc_steal(frame, tdo);
+
+ if (_root_trust_tdo != NULL) {
+ root_trust_dn = samdb_result_dn(sam_ctx, frame, msg,
+ "rootTrust", NULL);
+ }
+ if (_trust_parent_tdo != NULL) {
+ trust_parent_dn = samdb_result_dn(sam_ctx, frame, msg,
+ "trustParent", NULL);
+ }
+
+ if (root_trust_dn != NULL) {
+ struct ldb_message *root_trust_msg = NULL;
+
+ ret = dsdb_search_one(sam_ctx, frame,
+ &root_trust_msg,
+ root_trust_dn,
+ LDB_SCOPE_BASE,
+ cross_attrs,
+ DSDB_SEARCH_NO_GLOBAL_CATALOG,
+ "(objectClass=crossRef)");
+ if (ret != LDB_SUCCESS) {
+ status = dsdb_ldb_err_to_ntstatus(ret);
+ DEBUG(3, ("Failed to search for %s: %s - %s\n",
+ ldb_dn_get_linearized(root_trust_dn),
+ nt_errstr(status), ldb_errstring(sam_ctx)));
+ TALLOC_FREE(frame);
+ return status;
+ }
+
+ status = dsdb_trust_parse_crossref_info(mem_ctx, sam_ctx,
+ root_trust_msg,
+ &root_trust_tdo);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ talloc_steal(frame, root_trust_tdo);
+ }
+
+ if (trust_parent_dn != NULL) {
+ struct ldb_message *trust_parent_msg = NULL;
+
+ ret = dsdb_search_one(sam_ctx, frame,
+ &trust_parent_msg,
+ trust_parent_dn,
+ LDB_SCOPE_BASE,
+ cross_attrs,
+ DSDB_SEARCH_NO_GLOBAL_CATALOG,
+ "(objectClass=crossRef)");
+ if (ret != LDB_SUCCESS) {
+ status = dsdb_ldb_err_to_ntstatus(ret);
+ DEBUG(3, ("Failed to search for %s: %s - %s\n",
+ ldb_dn_get_linearized(trust_parent_dn),
+ nt_errstr(status), ldb_errstring(sam_ctx)));
+ TALLOC_FREE(frame);
+ return status;
+ }
+
+ status = dsdb_trust_parse_crossref_info(mem_ctx, sam_ctx,
+ trust_parent_msg,
+ &trust_parent_tdo);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ talloc_steal(frame, trust_parent_tdo);
+ }
+
+ *_tdo = talloc_move(mem_ctx, &tdo);
+ if (_root_trust_tdo != NULL) {
+ *_root_trust_tdo = talloc_move(mem_ctx, &root_trust_tdo);
+ }
+ if (_trust_parent_tdo != NULL) {
+ *_trust_parent_tdo = talloc_move(mem_ctx, &trust_parent_tdo);
+ }
+ TALLOC_FREE(frame);
+ return NT_STATUS_OK;
+}
+
+#define DNS_CMP_FIRST_IS_CHILD -2
+#define DNS_CMP_FIRST_IS_LESS -1
+#define DNS_CMP_MATCH 0
+#define DNS_CMP_SECOND_IS_LESS 1
+#define DNS_CMP_SECOND_IS_CHILD 2
+
+#define DNS_CMP_IS_NO_MATCH(__cmp) \
+ ((__cmp == DNS_CMP_FIRST_IS_LESS) || (__cmp == DNS_CMP_SECOND_IS_LESS))
+
+/*
+ * this function assumes names are well formed DNS names.
+ * it doesn't validate them
+ *
+ * It allows strings up to a length of UINT16_MAX - 1
+ * with up to UINT8_MAX components. On overflow this
+ * just returns the result of strcasecmp_m().
+ *
+ * Trailing dots (only one) are ignored.
+ *
+ * The DNS names are compared per component, starting from
+ * the last one.
+ */
+static int dns_cmp(const char *s1, const char *s2)
+{
+ size_t l1 = 0;
+ const char *p1 = NULL;
+ size_t num_comp1 = 0;
+ uint16_t comp1[UINT8_MAX] = {0};
+ size_t l2 = 0;
+ const char *p2 = NULL;
+ size_t num_comp2 = 0;
+ uint16_t comp2[UINT8_MAX] = {0};
+ size_t i;
+
+ if (s1 != NULL) {
+ l1 = strlen(s1);
+ }
+
+ if (s2 != NULL) {
+ l2 = strlen(s2);
+ }
+
+ /*
+ * trailing '.' are ignored.
+ */
+ if (l1 > 1 && s1[l1 - 1] == '.') {
+ l1--;
+ }
+ if (l2 > 1 && s2[l2 - 1] == '.') {
+ l2--;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(comp1); i++) {
+ char *p;
+
+ if (i == 0) {
+ p1 = s1;
+
+ if (l1 == 0 || l1 >= UINT16_MAX) {
+ /* just use one single component on overflow */
+ break;
+ }
+ }
+
+ comp1[num_comp1++] = PTR_DIFF(p1, s1);
+
+ p = strchr_m(p1, '.');
+ if (p == NULL) {
+ p1 = NULL;
+ break;
+ }
+
+ p1 = p + 1;
+ }
+
+ if (p1 != NULL) {
+ /* just use one single component on overflow */
+ num_comp1 = 0;
+ comp1[num_comp1++] = 0;
+ p1 = NULL;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(comp2); i++) {
+ char *p;
+
+ if (i == 0) {
+ p2 = s2;
+
+ if (l2 == 0 || l2 >= UINT16_MAX) {
+ /* just use one single component on overflow */
+ break;
+ }
+ }
+
+ comp2[num_comp2++] = PTR_DIFF(p2, s2);
+
+ p = strchr_m(p2, '.');
+ if (p == NULL) {
+ p2 = NULL;
+ break;
+ }
+
+ p2 = p + 1;
+ }
+
+ if (p2 != NULL) {
+ /* just use one single component on overflow */
+ num_comp2 = 0;
+ comp2[num_comp2++] = 0;
+ p2 = NULL;
+ }
+
+ for (i = 0; i < UINT8_MAX; i++) {
+ int cmp;
+
+ if (i < num_comp1) {
+ size_t idx = num_comp1 - (i + 1);
+ p1 = s1 + comp1[idx];
+ } else {
+ p1 = NULL;
+ }
+
+ if (i < num_comp2) {
+ size_t idx = num_comp2 - (i + 1);
+ p2 = s2 + comp2[idx];
+ } else {
+ p2 = NULL;
+ }
+
+ if (p1 == NULL && p2 == NULL) {
+ return DNS_CMP_MATCH;
+ }
+ if (p1 != NULL && p2 == NULL) {
+ return DNS_CMP_FIRST_IS_CHILD;
+ }
+ if (p1 == NULL && p2 != NULL) {
+ return DNS_CMP_SECOND_IS_CHILD;
+ }
+
+ cmp = strcasecmp_m(p1, p2);
+ if (cmp < 0) {
+ return DNS_CMP_FIRST_IS_LESS;
+ }
+ if (cmp > 0) {
+ return DNS_CMP_SECOND_IS_LESS;
+ }
+ }
+
+ smb_panic(__location__);
+ return -1;
+}
+
+static int dsdb_trust_find_tln_match_internal(const struct lsa_ForestTrustInformation *info,
+ enum lsa_ForestTrustRecordType type,
+ uint32_t disable_mask,
+ const char *tln)
+{
+ uint32_t i;
+
+ for (i = 0; i < info->count; i++) {
+ struct lsa_ForestTrustRecord *e = info->entries[i];
+ struct lsa_StringLarge *t = NULL;
+ int cmp;
+
+ if (e == NULL) {
+ continue;
+ }
+
+ if (e->type != type) {
+ continue;
+ }
+
+ if (e->flags & disable_mask) {
+ continue;
+ }
+
+ switch (type) {
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ t = &e->forest_trust_data.top_level_name;
+ break;
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX:
+ t = &e->forest_trust_data.top_level_name_ex;
+ break;
+ default:
+ break;
+ }
+
+ if (t == NULL) {
+ continue;
+ }
+
+ cmp = dns_cmp(tln, t->string);
+ switch (cmp) {
+ case DNS_CMP_MATCH:
+ case DNS_CMP_FIRST_IS_CHILD:
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+static bool dsdb_trust_find_tln_match(const struct lsa_ForestTrustInformation *info,
+ const char *tln)
+{
+ int m;
+
+ m = dsdb_trust_find_tln_match_internal(info,
+ LSA_FOREST_TRUST_TOP_LEVEL_NAME,
+ LSA_TLN_DISABLED_MASK,
+ tln);
+ if (m != -1) {
+ return true;
+ }
+
+ return false;
+}
+
+static bool dsdb_trust_find_tln_ex_match(const struct lsa_ForestTrustInformation *info,
+ const char *tln)
+{
+ int m;
+
+ m = dsdb_trust_find_tln_match_internal(info,
+ LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX,
+ 0,
+ tln);
+ if (m != -1) {
+ return true;
+ }
+
+ return false;
+}
+
+NTSTATUS dsdb_trust_local_tdo_info(TALLOC_CTX *mem_ctx,
+ struct ldb_context *sam_ctx,
+ struct lsa_TrustDomainInfoInfoEx **_tdo)
+{
+ struct ldb_dn *domain_dn = NULL;
+
+ domain_dn = ldb_get_default_basedn(sam_ctx);
+ if (domain_dn == NULL) {
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ return dsdb_trust_crossref_tdo_info(mem_ctx, sam_ctx,
+ domain_dn, NULL,
+ _tdo, NULL, NULL);
+}
+
+NTSTATUS dsdb_trust_xref_tdo_info(TALLOC_CTX *mem_ctx,
+ struct ldb_context *sam_ctx,
+ struct lsa_TrustDomainInfoInfoEx **_tdo)
+{
+ /*
+ * The extra filter makes sure we only find the forest root domain
+ */
+ const char *extra_filter = "(!(|(rootTrust=*)(trustParent=*)))";
+ struct ldb_dn *domain_dn = NULL;
+
+ domain_dn = ldb_get_default_basedn(sam_ctx);
+ if (domain_dn == NULL) {
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ return dsdb_trust_crossref_tdo_info(mem_ctx, sam_ctx,
+ domain_dn, extra_filter,
+ _tdo, NULL, NULL);
+}
+
+static int dsdb_trust_xref_sort_msgs(struct ldb_message **_m1,
+ struct ldb_message **_m2)
+{
+ struct ldb_message *m1 = *_m1;
+ struct ldb_message *m2 = *_m2;
+ const char *dns1 = NULL;
+ const char *dns2 = NULL;
+ int cmp;
+ struct ldb_message_element *rootTrust1 = NULL;
+ struct ldb_message_element *trustParent1 = NULL;
+ struct ldb_message_element *rootTrust2 = NULL;
+ struct ldb_message_element *trustParent2 = NULL;
+
+ dns1 = ldb_msg_find_attr_as_string(m1, "dnsRoot", NULL);
+ dns2 = ldb_msg_find_attr_as_string(m2, "dnsRoot", NULL);
+
+ cmp = dns_cmp(dns1, dns2);
+ switch (cmp) {
+ case DNS_CMP_FIRST_IS_CHILD:
+ return -1;
+ case DNS_CMP_SECOND_IS_CHILD:
+ return 1;
+ }
+
+ rootTrust1 = ldb_msg_find_element(m1, "rootTrust");
+ trustParent1 = ldb_msg_find_element(m1, "trustParent");
+ rootTrust2 = ldb_msg_find_element(m2, "rootTrust");
+ trustParent2 = ldb_msg_find_element(m2, "trustParent");
+
+ if (rootTrust1 == NULL && trustParent1 == NULL) {
+ /* m1 is the forest root */
+ return -1;
+ }
+ if (rootTrust2 == NULL && trustParent2 == NULL) {
+ /* m2 is the forest root */
+ return 1;
+ }
+
+ return cmp;
+}
+
+static int dsdb_trust_xref_sort_vals(struct ldb_val *v1,
+ struct ldb_val *v2)
+{
+ const char *dns1 = (const char *)v1->data;
+ const char *dns2 = (const char *)v2->data;
+
+ return dns_cmp(dns1, dns2);
+}
+
+NTSTATUS dsdb_trust_xref_forest_info(TALLOC_CTX *mem_ctx,
+ struct ldb_context *sam_ctx,
+ struct lsa_ForestTrustInformation **_info)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ struct lsa_ForestTrustInformation *info = NULL;
+ struct ldb_dn *partitions_dn = NULL;
+ const char * const cross_attrs1[] = {
+ "uPNSuffixes",
+ "msDS-SPNSuffixes",
+ NULL,
+ };
+ struct ldb_result *cross_res1 = NULL;
+ struct ldb_message_element *upn_el = NULL;
+ struct ldb_message_element *spn_el = NULL;
+ struct ldb_message *tln_msg = NULL;
+ struct ldb_message_element *tln_el = NULL;
+ const char * const cross_attrs2[] = {
+ "dnsRoot",
+ "nETBIOSName",
+ "nCName",
+ "rootTrust",
+ "trustParent",
+ NULL,
+ };
+ struct ldb_result *cross_res2 = NULL;
+ int ret;
+ unsigned int i;
+ bool restart = false;
+
+ *_info = NULL;
+ info = talloc_zero(mem_ctx, struct lsa_ForestTrustInformation);
+ if (info == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ talloc_steal(frame, info);
+
+ partitions_dn = samdb_partitions_dn(sam_ctx, frame);
+ if (partitions_dn == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ ret = dsdb_search_dn(sam_ctx, partitions_dn, &cross_res1,
+ partitions_dn, cross_attrs1, 0);
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(frame);
+ return dsdb_ldb_err_to_ntstatus(ret);
+ }
+
+ ret = dsdb_search(sam_ctx, partitions_dn, &cross_res2,
+ partitions_dn, LDB_SCOPE_ONELEVEL,
+ cross_attrs2,
+ DSDB_SEARCH_SHOW_EXTENDED_DN,
+ "(&(objectClass=crossRef)"
+ "(systemFlags:%s:=%u))",
+ LDB_OID_COMPARATOR_AND,
+ SYSTEM_FLAG_CR_NTDS_DOMAIN);
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(frame);
+ return dsdb_ldb_err_to_ntstatus(ret);
+ }
+
+ /*
+ * Sort the domains as trees, starting with the forest root
+ */
+ TYPESAFE_QSORT(cross_res2->msgs, cross_res2->count,
+ dsdb_trust_xref_sort_msgs);
+
+ upn_el = ldb_msg_find_element(cross_res1->msgs[0], "uPNSuffixes");
+ if (upn_el != NULL) {
+ upn_el->name = "__tln__";
+ }
+ spn_el = ldb_msg_find_element(cross_res1->msgs[0], "msDS-SPNSuffixes");
+ if (spn_el != NULL) {
+ spn_el->name = "__tln__";
+ }
+ ret = ldb_msg_normalize(sam_ctx, frame, cross_res1->msgs[0], &tln_msg);
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(frame);
+ return dsdb_ldb_err_to_ntstatus(ret);
+ }
+ tln_el = ldb_msg_find_element(tln_msg, "__tln__");
+ if (tln_el != NULL) {
+ /*
+ * Sort the domains as trees
+ */
+ TYPESAFE_QSORT(tln_el->values, tln_el->num_values,
+ dsdb_trust_xref_sort_vals);
+ }
+
+ for (i=0; i < cross_res2->count; i++) {
+ struct ldb_message *m = cross_res2->msgs[i];
+ const char *dns = NULL;
+ const char *netbios = NULL;
+ struct ldb_dn *nc_dn = NULL;
+ struct dom_sid sid = {
+ .num_auths = 0,
+ };
+ struct lsa_ForestTrustRecord e = {
+ .flags = 0,
+ };
+ struct lsa_ForestTrustDomainInfo *d = NULL;
+ struct lsa_StringLarge *t = NULL;
+ bool match = false;
+ NTSTATUS status;
+
+ dns = ldb_msg_find_attr_as_string(m, "dnsRoot", NULL);
+ if (dns == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ netbios = ldb_msg_find_attr_as_string(m, "nETBIOSName", NULL);
+ if (netbios == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ nc_dn = samdb_result_dn(sam_ctx, m, m, "ncName", NULL);
+ if (nc_dn == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ status = dsdb_get_extended_dn_sid(nc_dn, &sid, "SID");
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+
+ match = dsdb_trust_find_tln_match(info, dns);
+ if (!match) {
+ /*
+ * First the TOP_LEVEL_NAME, if required
+ */
+ e = (struct lsa_ForestTrustRecord) {
+ .flags = 0,
+ .type = LSA_FOREST_TRUST_TOP_LEVEL_NAME,
+ .time = 0, /* so far always 0 in traces. */
+ };
+
+ t = &e.forest_trust_data.top_level_name;
+ t->string = dns;
+
+ status = dsdb_trust_forest_info_add_record(info, &e);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ }
+
+ /*
+ * Then the DOMAIN_INFO
+ */
+ e = (struct lsa_ForestTrustRecord) {
+ .flags = 0,
+ .type = LSA_FOREST_TRUST_DOMAIN_INFO,
+ .time = 0, /* so far always 0 in traces. */
+ };
+ d = &e.forest_trust_data.domain_info;
+ d->domain_sid = &sid;
+ d->dns_domain_name.string = dns;
+ d->netbios_domain_name.string = netbios;
+
+ status = dsdb_trust_forest_info_add_record(info, &e);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ }
+
+ for (i=0; (tln_el != NULL) && i < tln_el->num_values; i++) {
+ const struct ldb_val *v = &tln_el->values[i];
+ const char *dns = (const char *)v->data;
+ struct lsa_ForestTrustRecord e = {
+ .flags = 0,
+ };
+ struct lsa_StringLarge *t = NULL;
+ bool match = false;
+ NTSTATUS status;
+
+ if (dns == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ match = dsdb_trust_find_tln_match(info, dns);
+ if (match) {
+ continue;
+ }
+
+ /*
+ * an additional the TOP_LEVEL_NAME
+ */
+ e = (struct lsa_ForestTrustRecord) {
+ .flags = 0,
+ .type = LSA_FOREST_TRUST_TOP_LEVEL_NAME,
+ .time = 0, /* so far always 0 in traces. */
+ };
+ t = &e.forest_trust_data.top_level_name;
+ t->string = dns;
+
+ status = dsdb_trust_forest_info_add_record(info, &e);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ }
+
+ for (i=0; i < info->count; restart ? i=0 : i++) {
+ struct lsa_ForestTrustRecord *tr = info->entries[i];
+ const struct lsa_StringLarge *ts = NULL;
+ uint32_t c;
+
+ restart = false;
+
+ if (tr->type != LSA_FOREST_TRUST_TOP_LEVEL_NAME) {
+ continue;
+ }
+
+ ts = &tr->forest_trust_data.top_level_name;
+
+ for (c = i + 1; c < info->count; c++) {
+ struct lsa_ForestTrustRecord *cr = info->entries[c];
+ const struct lsa_StringLarge *cs = NULL;
+ uint32_t j;
+ int cmp;
+
+ if (cr->type != LSA_FOREST_TRUST_TOP_LEVEL_NAME) {
+ continue;
+ }
+
+ cs = &cr->forest_trust_data.top_level_name;
+
+ cmp = dns_cmp(ts->string, cs->string);
+ if (DNS_CMP_IS_NO_MATCH(cmp)) {
+ continue;
+ }
+ if (cmp != DNS_CMP_FIRST_IS_CHILD) {
+ /* can't happen ... */
+ continue;
+ }
+
+ ts = NULL;
+ tr = NULL;
+ TALLOC_FREE(info->entries[i]);
+ info->entries[i] = info->entries[c];
+
+ for (j = c + 1; j < info->count; j++) {
+ info->entries[j-1] = info->entries[j];
+ }
+ info->count -= 1;
+ restart = true;
+ break;
+ }
+ }
+
+ *_info = talloc_move(mem_ctx, &info);
+ TALLOC_FREE(frame);
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_trust_parse_tdo_info(TALLOC_CTX *mem_ctx,
+ struct ldb_message *m,
+ struct lsa_TrustDomainInfoInfoEx **_tdo)
+{
+ struct lsa_TrustDomainInfoInfoEx *tdo = NULL;
+ const char *dns = NULL;
+ const char *netbios = NULL;
+
+ *_tdo = NULL;
+
+ tdo = talloc_zero(mem_ctx, struct lsa_TrustDomainInfoInfoEx);
+ if (tdo == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ dns = ldb_msg_find_attr_as_string(m, "trustPartner", NULL);
+ if (dns == NULL) {
+ TALLOC_FREE(tdo);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+ tdo->domain_name.string = talloc_strdup(tdo, dns);
+ if (tdo->domain_name.string == NULL) {
+ TALLOC_FREE(tdo);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ netbios = ldb_msg_find_attr_as_string(m, "flatName", NULL);
+ if (netbios == NULL) {
+ TALLOC_FREE(tdo);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+ tdo->netbios_name.string = talloc_strdup(tdo, netbios);
+ if (tdo->netbios_name.string == NULL) {
+ TALLOC_FREE(tdo);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ tdo->sid = samdb_result_dom_sid(tdo, m, "securityIdentifier");
+ if (tdo->sid == NULL) {
+ TALLOC_FREE(tdo);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ tdo->trust_type = ldb_msg_find_attr_as_uint(m, "trustType", 0);
+ tdo->trust_direction = ldb_msg_find_attr_as_uint(m, "trustDirection", 0);
+ tdo->trust_attributes = ldb_msg_find_attr_as_uint(m, "trustAttributes", 0);
+
+ *_tdo = tdo;
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_trust_parse_forest_info(TALLOC_CTX *mem_ctx,
+ struct ldb_message *m,
+ struct ForestTrustInfo **_fti)
+{
+ const struct ldb_val *ft_blob = NULL;
+ struct ForestTrustInfo *fti = NULL;
+ enum ndr_err_code ndr_err;
+
+ *_fti = NULL;
+
+ ft_blob = ldb_msg_find_ldb_val(m, "msDS-TrustForestTrustInfo");
+ if (ft_blob == NULL) {
+ return NT_STATUS_NOT_FOUND;
+ }
+
+ fti = talloc_zero(mem_ctx, struct ForestTrustInfo);
+ if (fti == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ /* ldb_val is equivalent to DATA_BLOB */
+ ndr_err = ndr_pull_struct_blob_all(ft_blob, fti, fti,
+ (ndr_pull_flags_fn_t)ndr_pull_ForestTrustInfo);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ TALLOC_FREE(fti);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ *_fti = fti;
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_trust_normalize_forest_info_step1(TALLOC_CTX *mem_ctx,
+ const struct lsa_ForestTrustInformation *gfti,
+ struct lsa_ForestTrustInformation **_nfti)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ struct lsa_ForestTrustInformation *nfti;
+ uint32_t n;
+
+ *_nfti = NULL;
+
+ nfti = talloc_zero(mem_ctx, struct lsa_ForestTrustInformation);
+ if (nfti == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ talloc_steal(frame, nfti);
+
+ /*
+ * First we copy every record and remove possible trailing dots
+ * from dns names.
+ *
+ * We also NULL out duplicates. The first one wins and
+ * we keep 'count' as is. This is required in order to
+ * provide the correct index for collision records.
+ */
+ for (n = 0; n < gfti->count; n++) {
+ const struct lsa_ForestTrustRecord *gftr = gfti->entries[n];
+ struct lsa_ForestTrustRecord *nftr = NULL;
+ struct lsa_ForestTrustDomainInfo *ninfo = NULL;
+ struct lsa_StringLarge *ntln = NULL;
+ struct lsa_StringLarge *nnb = NULL;
+ struct dom_sid *nsid = NULL;
+ NTSTATUS status;
+ size_t len = 0;
+ char *p = NULL;
+ uint32_t c;
+
+ if (gftr == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ status = dsdb_trust_forest_info_add_record(nfti, gftr);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+
+ nftr = nfti->entries[n];
+
+ switch (nftr->type) {
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ ntln = &nftr->forest_trust_data.top_level_name;
+ break;
+
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX:
+ ntln = &nftr->forest_trust_data.top_level_name_ex;
+ break;
+
+ case LSA_FOREST_TRUST_DOMAIN_INFO:
+ ninfo = &nftr->forest_trust_data.domain_info;
+ ntln = &ninfo->dns_domain_name;
+ nnb = &ninfo->netbios_domain_name;
+ nsid = ninfo->domain_sid;
+ break;
+
+ default:
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ /*
+ * We remove one trailing '.' before checking
+ * for invalid dots.
+ *
+ * domain.com. becomes domain.com
+ * domain.com.. becomes domain.com.
+ *
+ * Then the following is invalid:
+ *
+ * domain..com
+ * .domain.com
+ * domain.com.
+ */
+ len = strlen(ntln->string);
+ if (len > 1 && ntln->string[len - 1] == '.') {
+ const char *cp = &ntln->string[len - 1];
+ p = discard_const_p(char, cp);
+ *p= '\0';
+ }
+ if (ntln->string[0] == '.') {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+ p = strstr_m(ntln->string, "..");
+ if (p != NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ for (c = 0; c < n; c++) {
+ const struct lsa_ForestTrustRecord *cftr = nfti->entries[c];
+ const struct lsa_ForestTrustDomainInfo *cinfo = NULL;
+ const struct lsa_StringLarge *ctln = NULL;
+ const struct lsa_StringLarge *cnb = NULL;
+ const struct dom_sid *csid = NULL;
+ int cmp;
+
+ if (cftr == NULL) {
+ continue;
+ }
+
+ if (cftr->type != nftr->type) {
+ continue;
+ }
+
+ switch (cftr->type) {
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ ctln = &cftr->forest_trust_data.top_level_name;
+ break;
+
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX:
+ ctln = &cftr->forest_trust_data.top_level_name_ex;
+ break;
+
+ case LSA_FOREST_TRUST_DOMAIN_INFO:
+ cinfo = &cftr->forest_trust_data.domain_info;
+ ctln = &cinfo->dns_domain_name;
+ cnb = &cinfo->netbios_domain_name;
+ csid = cinfo->domain_sid;
+ break;
+
+ default:
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ cmp = dns_cmp(ntln->string, ctln->string);
+ if (cmp == DNS_CMP_MATCH) {
+ nftr = NULL;
+ TALLOC_FREE(nfti->entries[n]);
+ break;
+ }
+
+ if (cinfo == NULL) {
+ continue;
+ }
+
+ cmp = strcasecmp_m(nnb->string, cnb->string);
+ if (cmp == 0) {
+ nftr = NULL;
+ TALLOC_FREE(nfti->entries[n]);
+ break;
+ }
+
+ cmp = dom_sid_compare(nsid, csid);
+ if (cmp == 0) {
+ nftr = NULL;
+ TALLOC_FREE(nfti->entries[n]);
+ break;
+ }
+ }
+ }
+
+ /*
+ * Now we check that only true top level names are provided
+ */
+ for (n = 0; n < nfti->count; n++) {
+ const struct lsa_ForestTrustRecord *nftr = nfti->entries[n];
+ const struct lsa_StringLarge *ntln = NULL;
+ uint32_t c;
+
+ if (nftr == NULL) {
+ continue;
+ }
+
+ if (nftr->type != LSA_FOREST_TRUST_TOP_LEVEL_NAME) {
+ continue;
+ }
+
+ ntln = &nftr->forest_trust_data.top_level_name;
+
+ for (c = 0; c < nfti->count; c++) {
+ const struct lsa_ForestTrustRecord *cftr = nfti->entries[c];
+ const struct lsa_StringLarge *ctln = NULL;
+ int cmp;
+
+ if (cftr == NULL) {
+ continue;
+ }
+
+ if (cftr == nftr) {
+ continue;
+ }
+
+ if (cftr->type != nftr->type) {
+ continue;
+ }
+
+ ctln = &cftr->forest_trust_data.top_level_name;
+
+ cmp = dns_cmp(ntln->string, ctln->string);
+ if (DNS_CMP_IS_NO_MATCH(cmp)) {
+ continue;
+ }
+
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+ }
+
+ /*
+ * Now we check that only true sub level excludes are provided
+ */
+ for (n = 0; n < nfti->count; n++) {
+ const struct lsa_ForestTrustRecord *nftr = nfti->entries[n];
+ const struct lsa_StringLarge *ntln = NULL;
+ uint32_t c;
+ bool found_tln = false;
+
+ if (nftr == NULL) {
+ continue;
+ }
+
+ if (nftr->type != LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX) {
+ continue;
+ }
+
+ ntln = &nftr->forest_trust_data.top_level_name;
+
+ for (c = 0; c < nfti->count; c++) {
+ const struct lsa_ForestTrustRecord *cftr = nfti->entries[c];
+ const struct lsa_StringLarge *ctln = NULL;
+ int cmp;
+
+ if (cftr == NULL) {
+ continue;
+ }
+
+ if (cftr == nftr) {
+ continue;
+ }
+
+ if (cftr->type != LSA_FOREST_TRUST_TOP_LEVEL_NAME) {
+ continue;
+ }
+
+ ctln = &cftr->forest_trust_data.top_level_name;
+
+ cmp = dns_cmp(ntln->string, ctln->string);
+ if (cmp == DNS_CMP_FIRST_IS_CHILD) {
+ found_tln = true;
+ break;
+ }
+ }
+
+ if (found_tln) {
+ continue;
+ }
+
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ /*
+ * Now we check that there's a top level name for each domain
+ */
+ for (n = 0; n < nfti->count; n++) {
+ const struct lsa_ForestTrustRecord *nftr = nfti->entries[n];
+ const struct lsa_ForestTrustDomainInfo *ninfo = NULL;
+ const struct lsa_StringLarge *ntln = NULL;
+ uint32_t c;
+ bool found_tln = false;
+
+ if (nftr == NULL) {
+ continue;
+ }
+
+ if (nftr->type != LSA_FOREST_TRUST_DOMAIN_INFO) {
+ continue;
+ }
+
+ ninfo = &nftr->forest_trust_data.domain_info;
+ ntln = &ninfo->dns_domain_name;
+
+ for (c = 0; c < nfti->count; c++) {
+ const struct lsa_ForestTrustRecord *cftr = nfti->entries[c];
+ const struct lsa_StringLarge *ctln = NULL;
+ int cmp;
+
+ if (cftr == NULL) {
+ continue;
+ }
+
+ if (cftr == nftr) {
+ continue;
+ }
+
+ if (cftr->type != LSA_FOREST_TRUST_TOP_LEVEL_NAME) {
+ continue;
+ }
+
+ ctln = &cftr->forest_trust_data.top_level_name;
+
+ cmp = dns_cmp(ntln->string, ctln->string);
+ if (cmp == DNS_CMP_MATCH) {
+ found_tln = true;
+ break;
+ }
+ if (cmp == DNS_CMP_FIRST_IS_CHILD) {
+ found_tln = true;
+ break;
+ }
+ }
+
+ if (found_tln) {
+ continue;
+ }
+
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ *_nfti = talloc_move(mem_ctx, &nfti);
+ TALLOC_FREE(frame);
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_trust_normalize_forest_info_step2(TALLOC_CTX *mem_ctx,
+ const struct lsa_ForestTrustInformation *gfti,
+ struct lsa_ForestTrustInformation **_nfti)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ struct timeval tv = timeval_current();
+ NTTIME now = timeval_to_nttime(&tv);
+ struct lsa_ForestTrustInformation *nfti;
+ uint32_t g;
+
+ *_nfti = NULL;
+
+ nfti = talloc_zero(mem_ctx, struct lsa_ForestTrustInformation);
+ if (nfti == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ talloc_steal(frame, nfti);
+
+ /*
+ * Now we add TOP_LEVEL_NAME[_EX] in reverse order
+ * followed by LSA_FOREST_TRUST_DOMAIN_INFO in reverse order.
+ *
+ * This also removes the possible NULL entries generated in step1.
+ */
+
+ for (g = 0; g < gfti->count; g++) {
+ const struct lsa_ForestTrustRecord *gftr = gfti->entries[gfti->count - (g+1)];
+ struct lsa_ForestTrustRecord tftr;
+ bool skip = false;
+ NTSTATUS status;
+
+ if (gftr == NULL) {
+ continue;
+ }
+
+ switch (gftr->type) {
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX:
+ break;
+
+ case LSA_FOREST_TRUST_DOMAIN_INFO:
+ skip = true;
+ break;
+
+ default:
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ if (skip) {
+ continue;
+ }
+
+ /* make a copy in order to update the time. */
+ tftr = *gftr;
+ if (tftr.time == 0) {
+ tftr.time = now;
+ }
+
+ status = dsdb_trust_forest_info_add_record(nfti, &tftr);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ }
+
+ for (g = 0; g < gfti->count; g++) {
+ const struct lsa_ForestTrustRecord *gftr = gfti->entries[gfti->count - (g+1)];
+ struct lsa_ForestTrustRecord tftr;
+ bool skip = false;
+ NTSTATUS status;
+
+ if (gftr == NULL) {
+ continue;
+ }
+
+ switch (gftr->type) {
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX:
+ skip = true;
+ break;
+
+ case LSA_FOREST_TRUST_DOMAIN_INFO:
+ break;
+
+ default:
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ if (skip) {
+ continue;
+ }
+
+ /* make a copy in order to update the time. */
+ tftr = *gftr;
+ if (tftr.time == 0) {
+ tftr.time = now;
+ }
+
+ status = dsdb_trust_forest_info_add_record(nfti, &tftr);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ }
+
+ *_nfti = talloc_move(mem_ctx, &nfti);
+ TALLOC_FREE(frame);
+ return NT_STATUS_OK;
+}
+
+static NTSTATUS dsdb_trust_add_collision(
+ struct lsa_ForestTrustCollisionInfo *c_info,
+ enum lsa_ForestTrustCollisionRecordType type,
+ uint32_t idx, uint32_t flags,
+ const char *tdo_name)
+{
+ struct lsa_ForestTrustCollisionRecord **es;
+ uint32_t i = c_info->count;
+
+ es = talloc_realloc(c_info, c_info->entries,
+ struct lsa_ForestTrustCollisionRecord *, i + 1);
+ if (es == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ c_info->entries = es;
+ c_info->count = i + 1;
+
+ es[i] = talloc_zero(es, struct lsa_ForestTrustCollisionRecord);
+ if (es[i] == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ es[i]->index = idx;
+ es[i]->type = type;
+ es[i]->flags = flags;
+ es[i]->name.string = talloc_strdup(es[i], tdo_name);
+ if (es[i]->name.string == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_trust_verify_forest_info(const struct lsa_TrustDomainInfoInfoEx *ref_tdo,
+ const struct lsa_ForestTrustInformation *ref_fti,
+ enum lsa_ForestTrustCollisionRecordType collision_type,
+ struct lsa_ForestTrustCollisionInfo *c_info,
+ struct lsa_ForestTrustInformation *new_fti)
+{
+ uint32_t n;
+
+ for (n = 0; n < new_fti->count; n++) {
+ struct lsa_ForestTrustRecord *nftr = new_fti->entries[n];
+ struct lsa_StringLarge *ntln = NULL;
+ bool ntln_excluded = false;
+ uint32_t flags = 0;
+ uint32_t r;
+ NTSTATUS status;
+
+ if (nftr == NULL) {
+ continue;
+ }
+
+ if (nftr->type != LSA_FOREST_TRUST_TOP_LEVEL_NAME) {
+ continue;
+ }
+
+ ntln = &nftr->forest_trust_data.top_level_name;
+ if (ntln->string == NULL) {
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ ntln_excluded = dsdb_trust_find_tln_ex_match(ref_fti,
+ ntln->string);
+
+ /* check if this is already taken and not excluded */
+ for (r = 0; r < ref_fti->count; r++) {
+ const struct lsa_ForestTrustRecord *rftr =
+ ref_fti->entries[r];
+ const struct lsa_StringLarge *rtln = NULL;
+ int cmp;
+
+ if (rftr == NULL) {
+ continue;
+ }
+
+ if (rftr->type != LSA_FOREST_TRUST_TOP_LEVEL_NAME) {
+ continue;
+ }
+
+ rtln = &rftr->forest_trust_data.top_level_name;
+ if (rtln->string == NULL) {
+ continue;
+ }
+
+ cmp = dns_cmp(ntln->string, rtln->string);
+ if (DNS_CMP_IS_NO_MATCH(cmp)) {
+ continue;
+ }
+ if (cmp == DNS_CMP_MATCH) {
+ /* We need to normalize the string */
+ ntln->string = talloc_strdup(nftr,
+ rtln->string);
+ if (ntln->string == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ }
+
+ if (ntln_excluded) {
+ continue;
+ }
+
+ if (rftr->flags & LSA_TLN_DISABLED_MASK) {
+ continue;
+ }
+
+ if (nftr->flags & LSA_TLN_DISABLED_MASK) {
+ continue;
+ }
+
+ if (cmp == DNS_CMP_SECOND_IS_CHILD) {
+ bool m;
+
+ /*
+ * If the conflicting tln is a child, check if
+ * we have an exclusion record for it.
+ */
+ m = dsdb_trust_find_tln_ex_match(new_fti,
+ rtln->string);
+ if (m) {
+ continue;
+ }
+ }
+
+ flags |= LSA_TLN_DISABLED_CONFLICT;
+ }
+
+ if (flags == 0) {
+ continue;
+ }
+
+ nftr->flags |= flags;
+
+ status = dsdb_trust_add_collision(c_info,
+ collision_type,
+ n, nftr->flags,
+ ref_tdo->domain_name.string);
+ if (!NT_STATUS_IS_OK(status)) {
+ return status;
+ }
+ }
+
+ for (n = 0; n < new_fti->count; n++) {
+ struct lsa_ForestTrustRecord *nftr = new_fti->entries[n];
+ struct lsa_ForestTrustDomainInfo *ninfo = NULL;
+ struct lsa_StringLarge *ntln = NULL;
+ struct lsa_StringLarge *nnb = NULL;
+ struct dom_sid *nsid = NULL;
+ bool ntln_found = false;
+ uint32_t flags = 0;
+ uint32_t r;
+ NTSTATUS status;
+
+ if (nftr == NULL) {
+ continue;
+ }
+
+ if (nftr->type != LSA_FOREST_TRUST_DOMAIN_INFO) {
+ continue;
+ }
+
+ ninfo = &nftr->forest_trust_data.domain_info;
+ ntln = &ninfo->dns_domain_name;
+ if (ntln->string == NULL) {
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+ nnb = &ninfo->netbios_domain_name;
+ if (nnb->string == NULL) {
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+ nsid = ninfo->domain_sid;
+ if (nsid == NULL) {
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ ntln_found = dsdb_trust_find_tln_match(ref_fti, ntln->string);
+
+ /* check if this is already taken and not excluded */
+ for (r = 0; r < ref_fti->count; r++) {
+ const struct lsa_ForestTrustRecord *rftr =
+ ref_fti->entries[r];
+ const struct lsa_ForestTrustDomainInfo *rinfo = NULL;
+ const struct lsa_StringLarge *rtln = NULL;
+ const struct lsa_StringLarge *rnb = NULL;
+ const struct dom_sid *rsid = NULL;
+ bool nb_possible = true;
+ bool sid_possible = true;
+ int cmp;
+
+ if (rftr == NULL) {
+ continue;
+ }
+
+ if (!ntln_found) {
+ /*
+ * If the dns name doesn't match any existing
+ * tln any conflict is ignored, but name
+ * normalization still happens.
+ *
+ * I guess that's a bug in Windows
+ * (tested with Windows 2012r2).
+ */
+ nb_possible = false;
+ sid_possible = false;
+ }
+
+ if (nftr->flags & LSA_SID_DISABLED_MASK) {
+ sid_possible = false;
+ }
+
+ if (nftr->flags & LSA_NB_DISABLED_MASK) {
+ nb_possible = false;
+ }
+
+ switch (rftr->type) {
+ case LSA_FOREST_TRUST_TOP_LEVEL_NAME:
+ rtln = &rftr->forest_trust_data.top_level_name;
+ nb_possible = false;
+ sid_possible = false;
+ break;
+
+ case LSA_FOREST_TRUST_DOMAIN_INFO:
+ rinfo = &rftr->forest_trust_data.domain_info;
+ rtln = &rinfo->dns_domain_name;
+ rnb = &rinfo->netbios_domain_name;
+ rsid = rinfo->domain_sid;
+
+ if (rftr->flags & LSA_SID_DISABLED_MASK) {
+ sid_possible = false;
+ }
+
+ if (rftr->flags & LSA_NB_DISABLED_MASK) {
+ nb_possible = false;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ if (rtln == NULL) {
+ continue;
+ }
+
+ if (rtln->string == NULL) {
+ continue;
+ }
+
+ cmp = dns_cmp(ntln->string, rtln->string);
+ if (DNS_CMP_IS_NO_MATCH(cmp)) {
+ nb_possible = false;
+ sid_possible = false;
+ }
+ if (cmp == DNS_CMP_MATCH) {
+ /* We need to normalize the string */
+ ntln->string = talloc_strdup(nftr,
+ rtln->string);
+ if (ntln->string == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ }
+
+ if (rinfo == NULL) {
+ continue;
+ }
+
+ if (rsid != NULL) {
+ cmp = dom_sid_compare(nsid, rsid);
+ } else {
+ cmp = -1;
+ }
+ if (cmp == 0) {
+ if (sid_possible) {
+ flags |= LSA_SID_DISABLED_CONFLICT;
+ }
+ }
+
+ if (rnb->string != NULL) {
+ cmp = strcasecmp_m(nnb->string, rnb->string);
+ } else {
+ cmp = -1;
+ }
+ if (cmp == 0) {
+ nnb->string = talloc_strdup(nftr, rnb->string);
+ if (nnb->string == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ if (nb_possible) {
+ flags |= LSA_NB_DISABLED_CONFLICT;
+ }
+ }
+ }
+
+ if (flags == 0) {
+ continue;
+ }
+
+ nftr->flags |= flags;
+
+ status = dsdb_trust_add_collision(c_info,
+ collision_type,
+ n, nftr->flags,
+ ref_tdo->domain_name.string);
+ if (!NT_STATUS_IS_OK(status)) {
+ return status;
+ }
+ }
+
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_trust_merge_forest_info(TALLOC_CTX *mem_ctx,
+ const struct lsa_TrustDomainInfoInfoEx *tdo,
+ const struct lsa_ForestTrustInformation *ofti,
+ const struct lsa_ForestTrustInformation *nfti,
+ struct lsa_ForestTrustInformation **_mfti)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ struct lsa_ForestTrustInformation *mfti = NULL;
+ uint32_t ni;
+ uint32_t oi;
+ NTSTATUS status;
+ int cmp;
+
+ *_mfti = NULL;
+ mfti = talloc_zero(mem_ctx, struct lsa_ForestTrustInformation);
+ if (mfti == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ talloc_steal(frame, mfti);
+
+ /*
+ * First we add all top unique level names.
+ *
+ * The one matching the tdo dns name, will be
+ * added without further checking. All others
+ * may keep the flags and time values.
+ */
+ for (ni = 0; ni < nfti->count; ni++) {
+ const struct lsa_ForestTrustRecord *nftr = nfti->entries[ni];
+ struct lsa_ForestTrustRecord tftr = {
+ .flags = 0,
+ };
+ const char *ndns = NULL;
+ bool ignore_new = false;
+ bool found_old = false;
+ uint32_t mi;
+
+ if (nftr == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ if (nftr->type != LSA_FOREST_TRUST_TOP_LEVEL_NAME) {
+ continue;
+ }
+
+ ndns = nftr->forest_trust_data.top_level_name.string;
+ if (ndns == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ cmp = dns_cmp(tdo->domain_name.string, ndns);
+ if (cmp == DNS_CMP_MATCH) {
+ status = dsdb_trust_forest_info_add_record(mfti, nftr);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ }
+
+ for (mi = 0; mi < mfti->count; mi++) {
+ const struct lsa_ForestTrustRecord *mftr =
+ mfti->entries[mi];
+ const char *mdns = NULL;
+
+ /*
+ * we just added this above, so we're sure to have a
+ * valid LSA_FOREST_TRUST_TOP_LEVEL_NAME record
+ */
+ mdns = mftr->forest_trust_data.top_level_name.string;
+
+ cmp = dns_cmp(mdns, ndns);
+ switch (cmp) {
+ case DNS_CMP_MATCH:
+ case DNS_CMP_SECOND_IS_CHILD:
+ ignore_new = true;
+ break;
+ }
+
+ if (ignore_new) {
+ break;
+ }
+ }
+
+ if (ignore_new) {
+ continue;
+ }
+
+ /*
+ * make a temporary copy where we can change time and flags
+ */
+ tftr = *nftr;
+
+ for (oi = 0; oi < ofti->count; oi++) {
+ const struct lsa_ForestTrustRecord *oftr =
+ ofti->entries[oi];
+ const char *odns = NULL;
+
+ if (oftr == NULL) {
+ /*
+ * broken record => ignore...
+ */
+ continue;
+ }
+
+ if (oftr->type != LSA_FOREST_TRUST_TOP_LEVEL_NAME) {
+ continue;
+ }
+
+ odns = oftr->forest_trust_data.top_level_name.string;
+ if (odns == NULL) {
+ /*
+ * broken record => ignore...
+ */
+ continue;
+ }
+
+ cmp = dns_cmp(odns, ndns);
+ if (cmp != DNS_CMP_MATCH) {
+ continue;
+ }
+
+ found_old = true;
+ tftr.flags = oftr->flags;
+ tftr.time = oftr->time;
+ }
+
+ if (!found_old) {
+ tftr.flags = LSA_TLN_DISABLED_NEW;
+ tftr.time = 0;
+ }
+
+ status = dsdb_trust_forest_info_add_record(mfti, &tftr);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ }
+
+ /*
+ * Now we add all unique (based on their SID) domains
+ * and may keep the flags and time values.
+ */
+ for (ni = 0; ni < nfti->count; ni++) {
+ const struct lsa_ForestTrustRecord *nftr = nfti->entries[ni];
+ struct lsa_ForestTrustRecord tftr = {
+ .flags = 0,
+ };
+ const struct lsa_ForestTrustDomainInfo *nd = NULL;
+ const char *ndns = NULL;
+ const char *nnbt = NULL;
+ bool ignore_new = false;
+ bool found_old = false;
+ uint32_t mi;
+
+ if (nftr == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ if (nftr->type != LSA_FOREST_TRUST_DOMAIN_INFO) {
+ continue;
+ }
+
+ nd = &nftr->forest_trust_data.domain_info;
+ if (nd->domain_sid == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+ ndns = nd->dns_domain_name.string;
+ if (ndns == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+ nnbt = nd->netbios_domain_name.string;
+ if (nnbt == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ for (mi = 0; mi < mfti->count; mi++) {
+ const struct lsa_ForestTrustRecord *mftr =
+ mfti->entries[mi];
+ const struct lsa_ForestTrustDomainInfo *md = NULL;
+
+ if (mftr->type != LSA_FOREST_TRUST_DOMAIN_INFO) {
+ continue;
+ }
+
+ /*
+ * we just added this above, so we're sure to have a
+ * valid LSA_FOREST_TRUST_DOMAIN_INFO record
+ */
+ md = &mftr->forest_trust_data.domain_info;
+
+ cmp = dom_sid_compare(nd->domain_sid, md->domain_sid);
+ if (cmp == 0) {
+ ignore_new = true;
+ break;
+ }
+ }
+
+ if (ignore_new) {
+ continue;
+ }
+
+ /*
+ * make a temporary copy where we can change time and flags
+ */
+ tftr = *nftr;
+
+ for (oi = 0; oi < ofti->count; oi++) {
+ const struct lsa_ForestTrustRecord *oftr =
+ ofti->entries[oi];
+ const struct lsa_ForestTrustDomainInfo *od = NULL;
+ const char *onbt = NULL;
+
+ if (oftr == NULL) {
+ /*
+ * broken record => ignore...
+ */
+ continue;
+ }
+
+ if (oftr->type != LSA_FOREST_TRUST_DOMAIN_INFO) {
+ continue;
+ }
+
+ od = &oftr->forest_trust_data.domain_info;
+ onbt = od->netbios_domain_name.string;
+ if (onbt == NULL) {
+ /*
+ * broken record => ignore...
+ */
+ continue;
+ }
+
+ cmp = strcasecmp(onbt, nnbt);
+ if (cmp != 0) {
+ continue;
+ }
+
+ found_old = true;
+ tftr.flags = oftr->flags;
+ tftr.time = oftr->time;
+ }
+
+ if (!found_old) {
+ tftr.flags = 0;
+ tftr.time = 0;
+ }
+
+ status = dsdb_trust_forest_info_add_record(mfti, &tftr);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ }
+
+ /*
+ * We keep old domain records disabled by the admin
+ * if not already in the list.
+ */
+ for (oi = 0; oi < ofti->count; oi++) {
+ const struct lsa_ForestTrustRecord *oftr =
+ ofti->entries[oi];
+ const struct lsa_ForestTrustDomainInfo *od = NULL;
+ const char *odns = NULL;
+ const char *onbt = NULL;
+ bool ignore_old = true;
+ uint32_t mi;
+
+ if (oftr == NULL) {
+ /*
+ * broken record => ignore...
+ */
+ continue;
+ }
+
+ if (oftr->type != LSA_FOREST_TRUST_DOMAIN_INFO) {
+ continue;
+ }
+
+ od = &oftr->forest_trust_data.domain_info;
+ odns = od->dns_domain_name.string;
+ if (odns == NULL) {
+ /*
+ * broken record => ignore...
+ */
+ continue;
+ }
+ onbt = od->netbios_domain_name.string;
+ if (onbt == NULL) {
+ /*
+ * broken record => ignore...
+ */
+ continue;
+ }
+ if (od->domain_sid == NULL) {
+ /*
+ * broken record => ignore...
+ */
+ continue;
+ }
+
+ if (oftr->flags & LSA_NB_DISABLED_ADMIN) {
+ ignore_old = false;
+ } else if (oftr->flags & LSA_SID_DISABLED_ADMIN) {
+ ignore_old = false;
+ }
+
+ for (mi = 0; mi < mfti->count; mi++) {
+ const struct lsa_ForestTrustRecord *mftr =
+ mfti->entries[mi];
+ const struct lsa_ForestTrustDomainInfo *md = NULL;
+
+ if (mftr->type != LSA_FOREST_TRUST_DOMAIN_INFO) {
+ continue;
+ }
+
+ /*
+ * we just added this above, so we're sure to have a
+ * valid LSA_FOREST_TRUST_DOMAIN_INFO record
+ */
+ md = &mftr->forest_trust_data.domain_info;
+
+ cmp = dom_sid_compare(od->domain_sid, md->domain_sid);
+ if (cmp == 0) {
+ ignore_old = true;
+ break;
+ }
+ }
+
+ if (ignore_old) {
+ continue;
+ }
+
+ status = dsdb_trust_forest_info_add_record(mfti, oftr);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ }
+
+ /*
+ * Finally we readd top level exclusions,
+ * if they still match a top level name.
+ */
+ for (oi = 0; oi < ofti->count; oi++) {
+ const struct lsa_ForestTrustRecord *oftr =
+ ofti->entries[oi];
+ const char *odns = NULL;
+ bool ignore_old = false;
+ uint32_t mi;
+
+ if (oftr == NULL) {
+ /*
+ * broken record => ignore...
+ */
+ continue;
+ }
+
+ if (oftr->type != LSA_FOREST_TRUST_TOP_LEVEL_NAME_EX) {
+ continue;
+ }
+
+ odns = oftr->forest_trust_data.top_level_name_ex.string;
+ if (odns == NULL) {
+ /*
+ * broken record => ignore...
+ */
+ continue;
+ }
+
+ for (mi = 0; mi < mfti->count; mi++) {
+ const struct lsa_ForestTrustRecord *mftr =
+ mfti->entries[mi];
+ const char *mdns = NULL;
+
+ if (mftr->type != LSA_FOREST_TRUST_TOP_LEVEL_NAME) {
+ continue;
+ }
+
+ /*
+ * we just added this above, so we're sure to have a
+ * valid LSA_FOREST_TRUST_TOP_LEVEL_NAME.
+ */
+ mdns = mftr->forest_trust_data.top_level_name.string;
+
+ cmp = dns_cmp(mdns, odns);
+ switch (cmp) {
+ case DNS_CMP_MATCH:
+ case DNS_CMP_SECOND_IS_CHILD:
+ break;
+ default:
+ ignore_old = true;
+ break;
+ }
+
+ if (ignore_old) {
+ break;
+ }
+ }
+
+ if (ignore_old) {
+ continue;
+ }
+
+ status = dsdb_trust_forest_info_add_record(mfti, oftr);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ }
+
+ *_mfti = talloc_move(mem_ctx, &mfti);
+ TALLOC_FREE(frame);
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_trust_search_tdo(struct ldb_context *sam_ctx,
+ const char *netbios, const char *dns,
+ const char * const *attrs,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message **msg)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ int ret;
+ struct ldb_dn *system_dn = NULL;
+ char *netbios_encoded = NULL;
+ char *dns_encoded = NULL;
+ char *filter = NULL;
+
+ *msg = NULL;
+
+ if (netbios == NULL && dns == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER_MIX;
+ }
+
+ system_dn = samdb_system_container_dn(sam_ctx, frame);
+ if (system_dn == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ if (netbios != NULL) {
+ netbios_encoded = ldb_binary_encode_string(frame, netbios);
+ if (netbios_encoded == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ }
+
+ if (dns != NULL) {
+ dns_encoded = ldb_binary_encode_string(frame, dns);
+ if (dns_encoded == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ }
+
+ if (netbios != NULL && dns != NULL) {
+ filter = talloc_asprintf(frame,
+ "(&(objectClass=trustedDomain)"
+ "(|(trustPartner=%s)(flatName=%s))"
+ ")",
+ dns_encoded, netbios_encoded);
+ if (filter == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ } else if (netbios != NULL) {
+ filter = talloc_asprintf(frame,
+ "(&(objectClass=trustedDomain)(flatName=%s))",
+ netbios_encoded);
+ if (filter == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ } else if (dns != NULL) {
+ filter = talloc_asprintf(frame,
+ "(&(objectClass=trustedDomain)(trustPartner=%s))",
+ dns_encoded);
+ if (filter == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ }
+
+ ret = dsdb_search_one(sam_ctx, mem_ctx, msg,
+ system_dn,
+ LDB_SCOPE_ONELEVEL, attrs,
+ DSDB_SEARCH_NO_GLOBAL_CATALOG,
+ "%s", filter);
+ if (ret != LDB_SUCCESS) {
+ NTSTATUS status = dsdb_ldb_err_to_ntstatus(ret);
+ DEBUG(3, ("Failed to search for %s: %s - %s\n",
+ filter, nt_errstr(status), ldb_errstring(sam_ctx)));
+ TALLOC_FREE(frame);
+ return status;
+ }
+
+ TALLOC_FREE(frame);
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_trust_search_tdo_by_type(struct ldb_context *sam_ctx,
+ enum netr_SchannelType type,
+ const char *name,
+ const char * const *attrs,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message **msg)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ NTSTATUS status;
+ size_t len;
+ char trailer = '$';
+ bool require_trailer = true;
+ char *encoded_name = NULL;
+ const char *netbios = NULL;
+ const char *dns = NULL;
+
+ if (type != SEC_CHAN_DOMAIN && type != SEC_CHAN_DNS_DOMAIN) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+
+ if (type == SEC_CHAN_DNS_DOMAIN) {
+ trailer = '.';
+ require_trailer = false;
+ }
+
+ encoded_name = ldb_binary_encode_string(frame, name);
+ if (encoded_name == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ len = strlen(encoded_name);
+ if (len < 2) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_OBJECT_NAME_NOT_FOUND;
+ }
+
+ if (require_trailer && encoded_name[len - 1] != trailer) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_OBJECT_NAME_NOT_FOUND;
+ }
+ encoded_name[len - 1] = '\0';
+
+ if (type == SEC_CHAN_DNS_DOMAIN) {
+ dns = encoded_name;
+ } else {
+ netbios = encoded_name;
+ }
+
+ status = dsdb_trust_search_tdo(sam_ctx, netbios, dns,
+ attrs, mem_ctx, msg);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+
+ TALLOC_FREE(frame);
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_trust_search_tdo_by_sid(struct ldb_context *sam_ctx,
+ const struct dom_sid *sid,
+ const char * const *attrs,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message **msg)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ int ret;
+ struct ldb_dn *system_dn = NULL;
+ char *encoded_sid = NULL;
+ char *filter = NULL;
+
+ *msg = NULL;
+
+ if (sid == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INVALID_PARAMETER_MIX;
+ }
+
+ encoded_sid = ldap_encode_ndr_dom_sid(frame, sid);
+ if (encoded_sid == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ system_dn = samdb_system_container_dn(sam_ctx, frame);
+ if (system_dn == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ filter = talloc_asprintf(frame,
+ "(&"
+ "(objectClass=trustedDomain)"
+ "(securityIdentifier=%s)"
+ ")",
+ encoded_sid);
+ if (filter == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ ret = dsdb_search_one(sam_ctx, mem_ctx, msg,
+ system_dn,
+ LDB_SCOPE_ONELEVEL, attrs,
+ DSDB_SEARCH_NO_GLOBAL_CATALOG,
+ "%s", filter);
+ if (ret != LDB_SUCCESS) {
+ NTSTATUS status = dsdb_ldb_err_to_ntstatus(ret);
+ DEBUG(3, ("Failed to search for %s: %s - %s\n",
+ filter, nt_errstr(status), ldb_errstring(sam_ctx)));
+ TALLOC_FREE(frame);
+ return status;
+ }
+
+ TALLOC_FREE(frame);
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_trust_get_incoming_passwords(struct ldb_message *msg,
+ TALLOC_CTX *mem_ctx,
+ struct samr_Password **_current,
+ struct samr_Password **_previous)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ struct samr_Password __current = {
+ .hash = {0},
+ };
+ struct samr_Password __previous = {
+ .hash = {0},
+ };
+ struct samr_Password *current = NULL;
+ struct samr_Password *previous = NULL;
+ const struct ldb_val *blob = NULL;
+ enum ndr_err_code ndr_err;
+ struct trustAuthInOutBlob incoming = {
+ .count = 0,
+ };
+ uint32_t i;
+
+ if (_current != NULL) {
+ *_current = NULL;
+ }
+ if (_previous != NULL) {
+ *_previous = NULL;
+ }
+
+ blob = ldb_msg_find_ldb_val(msg, "trustAuthIncoming");
+ if (blob == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_ACCOUNT_DISABLED;
+ }
+
+ /* ldb_val is equivalent to DATA_BLOB */
+ ndr_err = ndr_pull_struct_blob_all(blob, frame, &incoming,
+ (ndr_pull_flags_fn_t)ndr_pull_trustAuthInOutBlob);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ for (i = 0; i < incoming.current.count; i++) {
+ struct AuthenticationInformation *a =
+ &incoming.current.array[i];
+
+ if (current != NULL) {
+ break;
+ }
+
+ switch (a->AuthType) {
+ case TRUST_AUTH_TYPE_NONE:
+ case TRUST_AUTH_TYPE_VERSION:
+ break;
+ case TRUST_AUTH_TYPE_NT4OWF:
+ current = &a->AuthInfo.nt4owf.password;
+ break;
+ case TRUST_AUTH_TYPE_CLEAR:
+ mdfour(__current.hash,
+ a->AuthInfo.clear.password,
+ a->AuthInfo.clear.size);
+ current = &__current;
+ break;
+ }
+ }
+
+ if (current == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ for (i = 0; i < incoming.previous.count; i++) {
+ struct AuthenticationInformation *a =
+ &incoming.previous.array[i];
+
+ if (previous != NULL) {
+ break;
+ }
+
+ switch (a->AuthType) {
+ case TRUST_AUTH_TYPE_NONE:
+ case TRUST_AUTH_TYPE_VERSION:
+ break;
+ case TRUST_AUTH_TYPE_NT4OWF:
+ previous = &a->AuthInfo.nt4owf.password;
+ break;
+ case TRUST_AUTH_TYPE_CLEAR:
+ mdfour(__previous.hash,
+ a->AuthInfo.clear.password,
+ a->AuthInfo.clear.size);
+ previous = &__previous;
+ break;
+ }
+ }
+
+ if (previous == NULL) {
+ previous = current;
+ }
+
+ if (_current != NULL) {
+ *_current = talloc_memdup(mem_ctx, current, sizeof(*current));
+ if (*_current == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ }
+ if (_previous != NULL) {
+ *_previous =
+ talloc_memdup(mem_ctx, previous, sizeof(*previous));
+ if (*_previous == NULL) {
+ if (_current != NULL) {
+ TALLOC_FREE(*_current);
+ }
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ }
+ ZERO_STRUCTP(current);
+ ZERO_STRUCTP(previous);
+ TALLOC_FREE(frame);
+ return NT_STATUS_OK;
+}
+
+NTSTATUS dsdb_trust_search_tdos(struct ldb_context *sam_ctx,
+ const char *exclude,
+ const char * const *attrs,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_result **res)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ int ret;
+ struct ldb_dn *system_dn = NULL;
+ const char *filter = NULL;
+ char *exclude_encoded = NULL;
+
+ *res = NULL;
+
+ system_dn = samdb_system_container_dn(sam_ctx, frame);
+ if (system_dn == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ if (exclude != NULL) {
+ exclude_encoded = ldb_binary_encode_string(frame, exclude);
+ if (exclude_encoded == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ filter = talloc_asprintf(frame,
+ "(&(objectClass=trustedDomain)"
+ "(!(|(trustPartner=%s)(flatName=%s)))"
+ ")",
+ exclude_encoded, exclude_encoded);
+ if (filter == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ } else {
+ filter = "(objectClass=trustedDomain)";
+ }
+
+ ret = dsdb_search(sam_ctx, mem_ctx, res,
+ system_dn,
+ LDB_SCOPE_ONELEVEL, attrs,
+ DSDB_SEARCH_NO_GLOBAL_CATALOG,
+ "%s", filter);
+ if (ret != LDB_SUCCESS) {
+ NTSTATUS status = dsdb_ldb_err_to_ntstatus(ret);
+ DEBUG(3, ("Failed to search for %s: %s - %s\n",
+ filter, nt_errstr(status), ldb_errstring(sam_ctx)));
+ TALLOC_FREE(frame);
+ return status;
+ }
+
+ TALLOC_FREE(frame);
+ return NT_STATUS_OK;
+}
+
+struct dsdb_trust_routing_domain;
+
+struct dsdb_trust_routing_table {
+ struct dsdb_trust_routing_domain *domains;
+};
+
+struct dsdb_trust_routing_domain {
+ struct dsdb_trust_routing_domain *prev, *next;
+
+ struct lsa_TrustDomainInfoInfoEx *tdo;
+
+ struct lsa_ForestTrustDomainInfo di;
+
+ struct lsa_ForestTrustInformation *fti;
+};
+
+NTSTATUS dsdb_trust_routing_table_load(struct ldb_context *sam_ctx,
+ TALLOC_CTX *mem_ctx,
+ struct dsdb_trust_routing_table **_table)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ struct dsdb_trust_routing_table *table;
+ struct dsdb_trust_routing_domain *d = NULL;
+ struct ldb_dn *domain_dn = NULL;
+ struct lsa_TrustDomainInfoInfoEx *root_trust_tdo = NULL;
+ struct lsa_TrustDomainInfoInfoEx *trust_parent_tdo = NULL;
+ struct lsa_TrustDomainInfoInfoEx *root_direction_tdo = NULL;
+ const char * const trusts_attrs[] = {
+ "securityIdentifier",
+ "flatName",
+ "trustPartner",
+ "trustAttributes",
+ "trustDirection",
+ "trustType",
+ "msDS-TrustForestTrustInfo",
+ NULL
+ };
+ struct ldb_result *trusts_res = NULL;
+ unsigned int i;
+ NTSTATUS status;
+
+ *_table = NULL;
+
+ domain_dn = ldb_get_default_basedn(sam_ctx);
+ if (domain_dn == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ table = talloc_zero(mem_ctx, struct dsdb_trust_routing_table);
+ if (table == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ talloc_steal(frame, table);
+
+ d = talloc_zero(table, struct dsdb_trust_routing_domain);
+ if (d == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ status = dsdb_trust_crossref_tdo_info(d, sam_ctx,
+ domain_dn, NULL,
+ &d->tdo,
+ &root_trust_tdo,
+ &trust_parent_tdo);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+
+ /*
+ * d->tdo should not be NULL of status above is 'NT_STATUS_OK'
+ * check is needed to satisfy clang static checker
+ */
+ if (d->tdo == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+ d->di.domain_sid = d->tdo->sid;
+ d->di.netbios_domain_name.string = d->tdo->netbios_name.string;
+ d->di.dns_domain_name.string = d->tdo->domain_name.string;
+
+ if (root_trust_tdo != NULL) {
+ root_direction_tdo = root_trust_tdo;
+ } else if (trust_parent_tdo != NULL) {
+ root_direction_tdo = trust_parent_tdo;
+ }
+
+ if (root_direction_tdo == NULL) {
+ /* we're the forest root */
+ status = dsdb_trust_xref_forest_info(d, sam_ctx, &d->fti);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ }
+
+ DLIST_ADD(table->domains, d);
+
+ status = dsdb_trust_search_tdos(sam_ctx, NULL, trusts_attrs,
+ frame, &trusts_res);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+
+ for (i = 0; i < trusts_res->count; i++) {
+ bool ok;
+ int cmp;
+
+ d = talloc_zero(table, struct dsdb_trust_routing_domain);
+ if (d == NULL) {
+ TALLOC_FREE(frame);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ status = dsdb_trust_parse_tdo_info(d,
+ trusts_res->msgs[i],
+ &d->tdo);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+
+ d->di.domain_sid = d->tdo->sid;
+ d->di.netbios_domain_name.string = d->tdo->netbios_name.string;
+ d->di.dns_domain_name.string = d->tdo->domain_name.string;
+
+ DLIST_ADD_END(table->domains, d);
+
+ if (d->tdo->trust_attributes & LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE) {
+ struct ForestTrustInfo *fti = NULL;
+
+ status = dsdb_trust_parse_forest_info(frame,
+ trusts_res->msgs[i],
+ &fti);
+ if (NT_STATUS_EQUAL(status, NT_STATUS_NOT_FOUND)) {
+ fti = NULL;
+ status = NT_STATUS_OK;
+ }
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+
+ if (fti == NULL) {
+ continue;
+ }
+
+ status = dsdb_trust_forest_info_to_lsa(d, fti, &d->fti);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+
+ continue;
+ }
+
+ if (!(d->tdo->trust_attributes & LSA_TRUST_ATTRIBUTE_WITHIN_FOREST)) {
+ continue;
+ }
+
+ if (root_direction_tdo == NULL) {
+ continue;
+ }
+
+ ok = dom_sid_equal(root_direction_tdo->sid, d->tdo->sid);
+ if (!ok) {
+ continue;
+ }
+
+ cmp = strcasecmp_m(root_direction_tdo->netbios_name.string,
+ d->tdo->netbios_name.string);
+ if (cmp != 0) {
+ continue;
+ }
+
+ cmp = strcasecmp_m(root_direction_tdo->domain_name.string,
+ d->tdo->domain_name.string);
+ if (cmp != 0) {
+ continue;
+ }
+
+ /* this our route to the forest root */
+ status = dsdb_trust_xref_forest_info(d, sam_ctx, &d->fti);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(frame);
+ return status;
+ }
+ }
+
+ *_table = talloc_move(mem_ctx, &table);
+ TALLOC_FREE(frame);
+ return NT_STATUS_OK;
+}
+
+static void dsdb_trust_update_best_tln(
+ const struct dsdb_trust_routing_domain **best_d,
+ const char **best_tln,
+ const struct dsdb_trust_routing_domain *d,
+ const char *tln)
+{
+ int cmp;
+
+ if (*best_tln == NULL) {
+ *best_tln = tln;
+ *best_d = d;
+ return;
+ }
+
+ cmp = dns_cmp(*best_tln, tln);
+ if (cmp != DNS_CMP_FIRST_IS_CHILD) {
+ return;
+ }
+
+ *best_tln = tln;
+ *best_d = d;
+}
+
+const struct lsa_TrustDomainInfoInfoEx *dsdb_trust_routing_by_name(
+ const struct dsdb_trust_routing_table *table,
+ const char *name)
+{
+ const struct dsdb_trust_routing_domain *best_d = NULL;
+ const char *best_tln = NULL;
+ const struct dsdb_trust_routing_domain *d = NULL;
+
+ if (name == NULL) {
+ return NULL;
+ }
+
+ for (d = table->domains; d != NULL; d = d->next) {
+ bool transitive = false;
+ bool allow_netbios = false;
+ bool exclude = false;
+ uint32_t i;
+
+ if (d->tdo->trust_type != LSA_TRUST_TYPE_UPLEVEL) {
+ /*
+ * Only uplevel trusts have top level names
+ */
+ continue;
+ }
+
+ if (d->tdo->trust_attributes & LSA_TRUST_ATTRIBUTE_WITHIN_FOREST) {
+ transitive = true;
+ }
+
+ if (d->tdo->trust_attributes & LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE) {
+ transitive = true;
+ }
+
+ if (d->tdo->trust_attributes & LSA_TRUST_ATTRIBUTE_NON_TRANSITIVE) {
+ transitive = false;
+ }
+
+ if (d->tdo->trust_type != LSA_TRUST_TYPE_UPLEVEL) {
+ transitive = false;
+ }
+
+ switch (d->tdo->trust_type) {
+ case LSA_TRUST_TYPE_UPLEVEL:
+ if (d->tdo->trust_attributes & LSA_TRUST_ATTRIBUTE_UPLEVEL_ONLY) {
+ break;
+ }
+ allow_netbios = true;
+ break;
+ case LSA_TRUST_TYPE_DOWNLEVEL:
+ allow_netbios = true;
+ break;
+ default:
+ allow_netbios = false;
+ break;
+ }
+
+ if (!transitive || d->fti == NULL) {
+ int cmp;
+
+ if (allow_netbios) {
+ cmp = dns_cmp(name, d->tdo->netbios_name.string);
+ if (cmp == DNS_CMP_MATCH) {
+ /*
+ * exact match
+ */
+ return d->tdo;
+ }
+ }
+
+ cmp = dns_cmp(name, d->tdo->domain_name.string);
+ if (cmp == DNS_CMP_MATCH) {
+ /*
+ * exact match
+ */
+ return d->tdo;
+ }
+ if (cmp != DNS_CMP_FIRST_IS_CHILD) {
+ continue;
+ }
+
+ if (!transitive) {
+ continue;
+ }
+
+ dsdb_trust_update_best_tln(&best_d, &best_tln, d,
+ d->tdo->domain_name.string);
+ continue;
+ }
+
+ exclude = dsdb_trust_find_tln_ex_match(d->fti, name);
+ if (exclude) {
+ continue;
+ }
+
+ for (i = 0; i < d->fti->count; i++ ) {
+ const struct lsa_ForestTrustRecord *f = d->fti->entries[i];
+ const struct lsa_ForestTrustDomainInfo *di = NULL;
+ const char *fti_nbt = NULL;
+ int cmp;
+
+ if (!allow_netbios) {
+ break;
+ }
+
+ if (f == NULL) {
+ /* broken record */
+ continue;
+ }
+
+ if (f->type != LSA_FOREST_TRUST_DOMAIN_INFO) {
+ continue;
+ }
+
+ if (f->flags & LSA_NB_DISABLED_MASK) {
+ /*
+ * any flag disables the entry.
+ */
+ continue;
+ }
+
+ di = &f->forest_trust_data.domain_info;
+ fti_nbt = di->netbios_domain_name.string;
+ if (fti_nbt == NULL) {
+ /* broken record */
+ continue;
+ }
+
+ cmp = dns_cmp(name, fti_nbt);
+ if (cmp == DNS_CMP_MATCH) {
+ /*
+ * exact match
+ */
+ return d->tdo;
+ }
+ }
+
+ for (i = 0; i < d->fti->count; i++ ) {
+ const struct lsa_ForestTrustRecord *f = d->fti->entries[i];
+ const union lsa_ForestTrustData *u = NULL;
+ const char *fti_tln = NULL;
+ int cmp;
+
+ if (f == NULL) {
+ /* broken record */
+ continue;
+ }
+
+ if (f->type != LSA_FOREST_TRUST_TOP_LEVEL_NAME) {
+ continue;
+ }
+
+ if (f->flags & LSA_TLN_DISABLED_MASK) {
+ /*
+ * any flag disables the entry.
+ */
+ continue;
+ }
+
+ u = &f->forest_trust_data;
+ fti_tln = u->top_level_name.string;
+ if (fti_tln == NULL) {
+ continue;
+ }
+
+ cmp = dns_cmp(name, fti_tln);
+ switch (cmp) {
+ case DNS_CMP_MATCH:
+ case DNS_CMP_FIRST_IS_CHILD:
+ dsdb_trust_update_best_tln(&best_d, &best_tln,
+ d, fti_tln);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ if (best_d != NULL) {
+ return best_d->tdo;
+ }
+
+ return NULL;
+}
+
+const struct lsa_TrustDomainInfoInfoEx *dsdb_trust_domain_by_sid(
+ const struct dsdb_trust_routing_table *table,
+ const struct dom_sid *sid,
+ const struct lsa_ForestTrustDomainInfo **pdi)
+{
+ const struct dsdb_trust_routing_domain *d = NULL;
+
+ if (pdi != NULL) {
+ *pdi = NULL;
+ }
+
+ if (sid == NULL) {
+ return NULL;
+ }
+
+ for (d = table->domains; d != NULL; d = d->next) {
+ bool transitive = false;
+ uint32_t i;
+
+ if (d->tdo->trust_attributes & LSA_TRUST_ATTRIBUTE_WITHIN_FOREST) {
+ transitive = true;
+ }
+
+ if (d->tdo->trust_attributes & LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE) {
+ transitive = true;
+ }
+
+ if (d->tdo->trust_attributes & LSA_TRUST_ATTRIBUTE_NON_TRANSITIVE) {
+ transitive = false;
+ }
+
+ if (d->tdo->trust_type != LSA_TRUST_TYPE_UPLEVEL) {
+ transitive = false;
+ }
+
+ if (!transitive || d->fti == NULL) {
+ bool match = false;
+
+ match = dom_sid_equal(d->di.domain_sid, sid);
+ if (match) {
+ /*
+ * exact match, it's the domain itself.
+ */
+ if (pdi != NULL) {
+ *pdi = &d->di;
+ }
+ return d->tdo;
+ }
+ continue;
+ }
+
+ for (i = 0; i < d->fti->count; i++ ) {
+ const struct lsa_ForestTrustRecord *f = d->fti->entries[i];
+ const struct lsa_ForestTrustDomainInfo *di = NULL;
+ const struct dom_sid *fti_sid = NULL;
+ bool match = false;
+
+ if (f == NULL) {
+ /* broken record */
+ continue;
+ }
+
+ if (f->type != LSA_FOREST_TRUST_DOMAIN_INFO) {
+ continue;
+ }
+
+ if (f->flags & LSA_SID_DISABLED_MASK) {
+ /*
+ * any flag disables the entry.
+ */
+ continue;
+ }
+
+ di = &f->forest_trust_data.domain_info;
+ fti_sid = di->domain_sid;
+ if (fti_sid == NULL) {
+ /* broken record */
+ continue;
+ }
+
+ match = dom_sid_equal(fti_sid, sid);
+ if (match) {
+ /*
+ * exact match, it's a domain in the forest.
+ */
+ if (pdi != NULL) {
+ *pdi = di;
+ }
+ return d->tdo;
+ }
+ }
+ }
+
+ return NULL;
+}
+
+const struct lsa_TrustDomainInfoInfoEx *dsdb_trust_domain_by_name(
+ const struct dsdb_trust_routing_table *table,
+ const char *name,
+ const struct lsa_ForestTrustDomainInfo **pdi)
+{
+ const struct dsdb_trust_routing_domain *d = NULL;
+
+ if (pdi != NULL) {
+ *pdi = NULL;
+ }
+
+ if (name == NULL) {
+ return NULL;
+ }
+
+ for (d = table->domains; d != NULL; d = d->next) {
+ bool transitive = false;
+ uint32_t i;
+
+ if (d->tdo->trust_attributes & LSA_TRUST_ATTRIBUTE_WITHIN_FOREST) {
+ transitive = true;
+ }
+
+ if (d->tdo->trust_attributes & LSA_TRUST_ATTRIBUTE_FOREST_TRANSITIVE) {
+ transitive = true;
+ }
+
+ if (d->tdo->trust_attributes & LSA_TRUST_ATTRIBUTE_NON_TRANSITIVE) {
+ transitive = false;
+ }
+
+ if (d->tdo->trust_type != LSA_TRUST_TYPE_UPLEVEL) {
+ transitive = false;
+ }
+
+ if (!transitive || d->fti == NULL) {
+ bool match = false;
+
+ match = strequal_m(d->di.netbios_domain_name.string,
+ name);
+ if (match) {
+ /*
+ * exact match for netbios name,
+ * it's the domain itself.
+ */
+ if (pdi != NULL) {
+ *pdi = &d->di;
+ }
+ return d->tdo;
+ }
+ match = strequal_m(d->di.dns_domain_name.string,
+ name);
+ if (match) {
+ /*
+ * exact match for dns name,
+ * it's the domain itself.
+ */
+ if (pdi != NULL) {
+ *pdi = &d->di;
+ }
+ return d->tdo;
+ }
+ continue;
+ }
+
+ for (i = 0; i < d->fti->count; i++ ) {
+ const struct lsa_ForestTrustRecord *f = d->fti->entries[i];
+ const struct lsa_ForestTrustDomainInfo *di = NULL;
+ bool match = false;
+
+ if (f == NULL) {
+ /* broken record */
+ continue;
+ }
+
+ if (f->type != LSA_FOREST_TRUST_DOMAIN_INFO) {
+ continue;
+ }
+ di = &f->forest_trust_data.domain_info;
+
+ if (!(f->flags & LSA_NB_DISABLED_MASK)) {
+ match = strequal_m(di->netbios_domain_name.string,
+ name);
+ if (match) {
+ /*
+ * exact match for netbios name,
+ * it's a domain in the forest.
+ */
+ if (pdi != NULL) {
+ *pdi = di;
+ }
+ return d->tdo;
+ }
+ }
+
+ if (!(f->flags & LSA_TLN_DISABLED_MASK)) {
+ match = strequal_m(di->dns_domain_name.string,
+ name);
+ if (match) {
+ /*
+ * exact match for dns name,
+ * it's a domain in the forest.
+ */
+ if (pdi != NULL) {
+ *pdi = di;
+ }
+ return d->tdo;
+ }
+ }
+ }
+ }
+
+ return NULL;
+}
diff --git a/source4/dsdb/dns/dns_update.c b/source4/dsdb/dns/dns_update.c
new file mode 100644
index 0000000..dd94564
--- /dev/null
+++ b/source4/dsdb/dns/dns_update.c
@@ -0,0 +1,464 @@
+/*
+ Unix SMB/CIFS Implementation.
+
+ DNS update service
+
+ 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/>.
+
+*/
+
+/*
+ this module auto-creates the named.conf.update file, which tells
+ bind9 what KRB5 principals it should accept for updates to our zone
+
+ It also uses the samba_dnsupdate script to auto-create the right DNS
+ names for ourselves as a DC in the domain, using TSIG-GSS
+ */
+
+#include "includes.h"
+#include "dsdb/samdb/samdb.h"
+#include "auth/auth.h"
+#include "samba/service.h"
+#include "lib/messaging/irpc.h"
+#include "param/param.h"
+#include "system/filesys.h"
+#include "dsdb/common/util.h"
+#include "libcli/composite/composite.h"
+#include "libcli/security/dom_sid.h"
+#include "librpc/gen_ndr/ndr_irpc.h"
+#include "libds/common/roles.h"
+
+NTSTATUS server_service_dnsupdate_init(TALLOC_CTX *);
+
+struct dnsupdate_service {
+ struct task_server *task;
+ struct auth_session_info *system_session_info;
+ struct ldb_context *samdb;
+
+ /* status for periodic config file update */
+ struct {
+ uint32_t interval;
+ struct tevent_timer *te;
+ struct tevent_req *subreq;
+ NTSTATUS status;
+ } confupdate;
+
+ /* status for periodic DNS name check */
+ struct {
+ uint32_t interval;
+ struct tevent_timer *te;
+ struct tevent_req *subreq;
+ struct tevent_req *spnreq;
+ NTSTATUS status;
+ } nameupdate;
+};
+
+/*
+ called when dns update script has finished
+ */
+static void dnsupdate_nameupdate_done(struct tevent_req *subreq)
+{
+ struct dnsupdate_service *service = tevent_req_callback_data(subreq,
+ struct dnsupdate_service);
+ int ret;
+ int sys_errno;
+
+ service->nameupdate.subreq = NULL;
+
+ ret = samba_runcmd_recv(subreq, &sys_errno);
+ TALLOC_FREE(subreq);
+
+ if (ret != 0) {
+ DBG_ERR("Failed DNS update with exit code %d\n",
+ sys_errno);
+ } else {
+ DEBUG(3,("Completed DNS update check OK\n"));
+ }
+}
+
+
+/*
+ called when spn update script has finished
+ */
+static void dnsupdate_spnupdate_done(struct tevent_req *subreq)
+{
+ struct dnsupdate_service *service = tevent_req_callback_data(subreq,
+ struct dnsupdate_service);
+ int ret;
+ int sys_errno;
+
+ service->nameupdate.spnreq = NULL;
+
+ ret = samba_runcmd_recv(subreq, &sys_errno);
+ TALLOC_FREE(subreq);
+ if (ret != 0) {
+ DEBUG(0,(__location__ ": Failed SPN update - with error code %d\n",
+ sys_errno));
+ } else {
+ DEBUG(3,("Completed SPN update check OK\n"));
+ }
+}
+
+/*
+ called every 'dnsupdate:name interval' seconds
+ */
+static void dnsupdate_check_names(struct dnsupdate_service *service)
+{
+ const char * const *dns_update_command = lpcfg_dns_update_command(service->task->lp_ctx);
+ const char * const *spn_update_command = lpcfg_spn_update_command(service->task->lp_ctx);
+
+ /* kill any existing child */
+ TALLOC_FREE(service->nameupdate.subreq);
+
+ DEBUG(3,("Calling DNS name update script\n"));
+ service->nameupdate.subreq = samba_runcmd_send(service,
+ service->task->event_ctx,
+ timeval_current_ofs(20, 0),
+ 2, 0,
+ dns_update_command,
+ NULL);
+ if (service->nameupdate.subreq == NULL) {
+ DEBUG(0,(__location__ ": samba_runcmd_send() failed with no memory\n"));
+ return;
+ }
+ tevent_req_set_callback(service->nameupdate.subreq,
+ dnsupdate_nameupdate_done,
+ service);
+
+ DEBUG(3,("Calling SPN name update script\n"));
+ service->nameupdate.spnreq = samba_runcmd_send(service,
+ service->task->event_ctx,
+ timeval_current_ofs(20, 0),
+ 2, 0,
+ spn_update_command,
+ NULL);
+ if (service->nameupdate.spnreq == NULL) {
+ DEBUG(0,(__location__ ": samba_runcmd_send() failed with no memory\n"));
+ return;
+ }
+ tevent_req_set_callback(service->nameupdate.spnreq,
+ dnsupdate_spnupdate_done,
+ service);
+}
+
+static NTSTATUS dnsupdate_nameupdate_schedule(struct dnsupdate_service *service);
+
+/*
+ called every 'dnsupdate:name interval' seconds
+ */
+static void dnsupdate_nameupdate_handler_te(struct tevent_context *ev, struct tevent_timer *te,
+ struct timeval t, void *ptr)
+{
+ struct dnsupdate_service *service = talloc_get_type(ptr, struct dnsupdate_service);
+
+ dnsupdate_check_names(service);
+ dnsupdate_nameupdate_schedule(service);
+}
+
+
+static NTSTATUS dnsupdate_nameupdate_schedule(struct dnsupdate_service *service)
+{
+ service->nameupdate.te = tevent_add_timer(service->task->event_ctx, service,
+ timeval_current_ofs(service->nameupdate.interval, 0),
+ dnsupdate_nameupdate_handler_te, service);
+ NT_STATUS_HAVE_NO_MEMORY(service->nameupdate.te);
+ return NT_STATUS_OK;
+}
+
+
+struct dnsupdate_RODC_state {
+ struct irpc_message *msg;
+ struct dnsupdate_RODC *r;
+ char *tmp_path;
+ char *tmp_path2;
+ int fd;
+};
+
+static int dnsupdate_RODC_destructor(struct dnsupdate_RODC_state *st)
+{
+ if (st->fd != -1) {
+ close(st->fd);
+ }
+ unlink(st->tmp_path);
+ if (st->tmp_path2 != NULL) {
+ unlink(st->tmp_path2);
+ }
+ return 0;
+}
+
+/*
+ called when the DNS update has completed
+ */
+static void dnsupdate_RODC_callback(struct tevent_req *req)
+{
+ struct dnsupdate_RODC_state *st =
+ tevent_req_callback_data(req,
+ struct dnsupdate_RODC_state);
+ int sys_errno;
+ int i, ret;
+
+ ret = samba_runcmd_recv(req, &sys_errno);
+ talloc_free(req);
+ if (ret != 0) {
+ st->r->out.result = map_nt_error_from_unix_common(sys_errno);
+ DEBUG(2,(__location__ ": RODC DNS Update failed: %s\n", nt_errstr(st->r->out.result)));
+ } else {
+ st->r->out.result = NT_STATUS_OK;
+ DEBUG(3,(__location__ ": RODC DNS Update OK\n"));
+ }
+
+ for (i=0; i<st->r->in.dns_names->count; i++) {
+ st->r->out.dns_names->names[i].status = NT_STATUS_V(st->r->out.result);
+ }
+
+ irpc_send_reply(st->msg, NT_STATUS_OK);
+}
+
+
+/**
+ * Called when we get a RODC DNS update request from the netlogon
+ * rpc server
+ */
+static NTSTATUS dnsupdate_dnsupdate_RODC(struct irpc_message *msg,
+ struct dnsupdate_RODC *r)
+{
+ struct dnsupdate_service *s = talloc_get_type(msg->private_data,
+ struct dnsupdate_service);
+ const char * const *dns_update_command = lpcfg_dns_update_command(s->task->lp_ctx);
+ struct dnsupdate_RODC_state *st;
+ struct tevent_req *req;
+ int i, ret;
+ struct GUID ntds_guid;
+ const char *site, *dnsdomain, *dnsforest, *ntdsguid;
+ const char *hostname = NULL;
+ struct ldb_dn *sid_dn;
+ const char *attrs[] = { "dNSHostName", NULL };
+ struct ldb_result *res;
+
+ st = talloc_zero(msg, struct dnsupdate_RODC_state);
+ if (!st) {
+ r->out.result = NT_STATUS_NO_MEMORY;
+ return NT_STATUS_OK;
+ }
+
+ st->r = r;
+ st->msg = msg;
+
+ st->tmp_path = smbd_tmp_path(st, s->task->lp_ctx, "rodcdns.XXXXXX");
+ if (!st->tmp_path) {
+ talloc_free(st);
+ r->out.result = NT_STATUS_NO_MEMORY;
+ return NT_STATUS_OK;
+ }
+
+ st->fd = mkstemp(st->tmp_path);
+ if (st->fd == -1) {
+ DEBUG(0,("Unable to create a temporary file for RODC dnsupdate\n"));
+ talloc_free(st);
+ r->out.result = NT_STATUS_INTERNAL_DB_CORRUPTION;
+ return NT_STATUS_OK;
+ }
+
+ talloc_set_destructor(st, dnsupdate_RODC_destructor);
+
+ st->tmp_path2 = talloc_asprintf(st, "%s.cache", st->tmp_path);
+ if (!st->tmp_path2) {
+ talloc_free(st);
+ r->out.result = NT_STATUS_NO_MEMORY;
+ return NT_STATUS_OK;
+ }
+
+ sid_dn = ldb_dn_new_fmt(st, s->samdb, "<SID=%s>", dom_sid_string(st, r->in.dom_sid));
+ if (!sid_dn) {
+ talloc_free(st);
+ r->out.result = NT_STATUS_NO_MEMORY;
+ return NT_STATUS_OK;
+ }
+
+ /* work out the site */
+ ret = samdb_find_site_for_computer(s->samdb, st, sid_dn, &site);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(2, (__location__ ": Unable to find site for computer %s\n",
+ ldb_dn_get_linearized(sid_dn)));
+ talloc_free(st);
+ r->out.result = NT_STATUS_NO_SUCH_USER;
+ return NT_STATUS_OK;
+ }
+
+ /* work out the ntdsguid */
+ ret = samdb_find_ntdsguid_for_computer(s->samdb, sid_dn, &ntds_guid);
+ ntdsguid = GUID_string(st, &ntds_guid);
+ if (ret != LDB_SUCCESS || !ntdsguid) {
+ DEBUG(2, (__location__ ": Unable to find NTDS GUID for computer %s\n",
+ ldb_dn_get_linearized(sid_dn)));
+ talloc_free(st);
+ r->out.result = NT_STATUS_NO_SUCH_USER;
+ return NT_STATUS_OK;
+ }
+
+
+ /* find dnsdomain and dnsforest */
+ dnsdomain = lpcfg_dnsdomain(s->task->lp_ctx);
+ dnsforest = dnsdomain;
+
+ /* find the hostname */
+ ret = dsdb_search_dn(s->samdb, st, &res, sid_dn, attrs, 0);
+ if (ret == LDB_SUCCESS) {
+ hostname = ldb_msg_find_attr_as_string(res->msgs[0], "dNSHostName", NULL);
+ }
+ if (ret != LDB_SUCCESS || !hostname) {
+ DEBUG(2, (__location__ ": Unable to find NTDS GUID for computer %s\n",
+ ldb_dn_get_linearized(sid_dn)));
+ talloc_free(st);
+ r->out.result = NT_STATUS_NO_SUCH_USER;
+ return NT_STATUS_OK;
+ }
+
+ for (i=0; i<st->r->in.dns_names->count; i++) {
+ struct NL_DNS_NAME_INFO *n = &r->in.dns_names->names[i];
+ switch (n->type) {
+ case NlDnsLdapAtSite:
+ dprintf(st->fd, "SRV _ldap._tcp.%s._sites.%s %s %u\n",
+ site, dnsdomain, hostname, n->port);
+ break;
+ case NlDnsGcAtSite:
+ dprintf(st->fd, "SRV _ldap._tcp.%s._sites.gc._msdcs.%s %s %u\n",
+ site, dnsdomain, hostname, n->port);
+ break;
+ case NlDnsDsaCname:
+ dprintf(st->fd, "CNAME %s._msdcs.%s %s\n",
+ ntdsguid, dnsforest, hostname);
+ break;
+ case NlDnsKdcAtSite:
+ dprintf(st->fd, "SRV _kerberos._tcp.%s._sites.dc._msdcs.%s %s %u\n",
+ site, dnsdomain, hostname, n->port);
+ break;
+ case NlDnsDcAtSite:
+ dprintf(st->fd, "SRV _ldap._tcp.%s._sites.dc._msdcs.%s %s %u\n",
+ site, dnsdomain, hostname, n->port);
+ break;
+ case NlDnsRfc1510KdcAtSite:
+ dprintf(st->fd, "SRV _kerberos._tcp.%s._sites.%s %s %u\n",
+ site, dnsdomain, hostname, n->port);
+ break;
+ case NlDnsGenericGcAtSite:
+ dprintf(st->fd, "SRV _gc._tcp.%s._sites.%s %s %u\n",
+ site, dnsforest, hostname, n->port);
+ break;
+ }
+ }
+
+ close(st->fd);
+ st->fd = -1;
+
+ DEBUG(3,("Calling RODC DNS name update script %s\n", st->tmp_path));
+ req = samba_runcmd_send(st,
+ s->task->event_ctx,
+ timeval_current_ofs(20, 0),
+ 2, 0,
+ dns_update_command,
+ "--update-list",
+ st->tmp_path,
+ "--update-cache",
+ st->tmp_path2,
+ NULL);
+ NT_STATUS_HAVE_NO_MEMORY(req);
+
+ /* setup the callback */
+ tevent_req_set_callback(req, dnsupdate_RODC_callback, st);
+
+ msg->defer_reply = true;
+
+ return NT_STATUS_OK;
+}
+
+/*
+ startup the dns update task
+*/
+static NTSTATUS dnsupdate_task_init(struct task_server *task)
+{
+ NTSTATUS status;
+ struct dnsupdate_service *service;
+
+ if (lpcfg_server_role(task->lp_ctx) != ROLE_ACTIVE_DIRECTORY_DC) {
+ /* not useful for non-DC */
+ return NT_STATUS_INVALID_DOMAIN_ROLE;
+ }
+
+ task_server_set_title(task, "task[dnsupdate]");
+
+ service = talloc_zero(task, struct dnsupdate_service);
+ if (!service) {
+ task_server_terminate(task, "dnsupdate_task_init: out of memory", true);
+ return NT_STATUS_NO_MEMORY;
+ }
+ service->task = task;
+ task->private_data = service;
+
+ service->system_session_info = system_session(service->task->lp_ctx);
+ if (!service->system_session_info) {
+ task_server_terminate(task,
+ "dnsupdate: Failed to obtain server credentials\n",
+ true);
+ return NT_STATUS_UNSUCCESSFUL;
+ }
+
+ service->samdb = samdb_connect(service,
+ service->task->event_ctx,
+ task->lp_ctx,
+ service->system_session_info,
+ NULL,
+ 0);
+ if (!service->samdb) {
+ task_server_terminate(task, "dnsupdate: Failed to connect to local samdb\n",
+ true);
+ return NT_STATUS_UNSUCCESSFUL;
+ }
+
+ service->nameupdate.interval = lpcfg_parm_int(task->lp_ctx, NULL,
+ "dnsupdate", "name interval", 600); /* in seconds */
+
+ dnsupdate_check_names(service);
+ status = dnsupdate_nameupdate_schedule(service);
+ if (!NT_STATUS_IS_OK(status)) {
+ task_server_terminate(task, talloc_asprintf(task,
+ "dnsupdate: Failed to nameupdate schedule: %s\n",
+ nt_errstr(status)), true);
+ return status;
+ }
+
+ irpc_add_name(task->msg_ctx, "dnsupdate");
+
+ IRPC_REGISTER(task->msg_ctx, irpc, DNSUPDATE_RODC,
+ dnsupdate_dnsupdate_RODC, service);
+
+ return NT_STATUS_OK;
+
+}
+
+/*
+ register ourselves as a available server
+*/
+NTSTATUS server_service_dnsupdate_init(TALLOC_CTX *ctx)
+{
+ static const struct service_details details = {
+ .inhibit_fork_on_accept = true,
+ .inhibit_pre_fork = true,
+ .task_init = dnsupdate_task_init,
+ .post_fork = NULL
+ };
+ return register_server_service(ctx, "dnsupdate", &details);
+}
diff --git a/source4/dsdb/kcc/garbage_collect_tombstones.c b/source4/dsdb/kcc/garbage_collect_tombstones.c
new file mode 100644
index 0000000..14ee2d4
--- /dev/null
+++ b/source4/dsdb/kcc/garbage_collect_tombstones.c
@@ -0,0 +1,347 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ handle removal of deleted objects
+
+ Copyright (C) 2009 Andrew Tridgell
+ Copyright (C) 2016 Andrew Bartlett
+ Copyright (C) 2016 Catalyst.NET Ltd
+
+ 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_errors.h>
+#include "../lib/util/dlinklist.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 "lib/util/dlinklist.h"
+#include "ldb.h"
+#include "dsdb/kcc/garbage_collect_tombstones.h"
+#include "lib/ldb-samba/ldb_matching_rules.h"
+#include "lib/util/time.h"
+
+static NTSTATUS garbage_collect_tombstones_part(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ struct dsdb_ldb_dn_list_node *part,
+ char *filter,
+ unsigned int *num_links_removed,
+ unsigned int *num_objects_removed,
+ struct dsdb_schema *schema,
+ const char **attrs,
+ char **error_string,
+ NTTIME expunge_time_nttime)
+{
+ int ret;
+ struct ldb_dn *do_dn;
+ struct ldb_result *res;
+ unsigned int i, j, k;
+ uint32_t flags;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ if (!tmp_ctx) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ ret = dsdb_get_deleted_objects_dn(samdb, tmp_ctx, part->dn, &do_dn);
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(tmp_ctx);
+ /* some partitions have no Deleted Objects
+ container */
+ return NT_STATUS_OK;
+ }
+
+ DBG_INFO("Doing a full scan on %s and looking for deleted objects\n",
+ ldb_dn_get_linearized(part->dn));
+
+ flags = DSDB_SEARCH_SHOW_RECYCLED |
+ DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT |
+ DSDB_SEARCH_REVEAL_INTERNALS;
+ ret = dsdb_search(samdb, tmp_ctx, &res, part->dn, LDB_SCOPE_SUBTREE,
+ attrs, flags, "%s", filter);
+
+ if (ret != LDB_SUCCESS) {
+ *error_string = talloc_asprintf(mem_ctx,
+ "Failed to search for deleted "
+ "objects in %s: %s",
+ ldb_dn_get_linearized(do_dn),
+ ldb_errstring(samdb));
+ TALLOC_FREE(tmp_ctx);
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ for (i=0; i<res->count; i++) {
+ struct ldb_message *cleanup_msg = NULL;
+ unsigned int num_modified = 0;
+
+ bool isDeleted = ldb_msg_find_attr_as_bool(res->msgs[i],
+ "isDeleted", false);
+ if (isDeleted) {
+ if (ldb_dn_compare(do_dn, res->msgs[i]->dn) == 0) {
+ /* Skip the Deleted Object Container */
+ continue;
+ }
+
+ ret = dsdb_delete(samdb, res->msgs[i]->dn,
+ DSDB_SEARCH_SHOW_RECYCLED
+ |DSDB_MODIFY_RELAX);
+ if (ret != LDB_SUCCESS) {
+ DBG_WARNING(__location__ ": Failed to remove "
+ "deleted object %s\n",
+ ldb_dn_get_linearized(res->
+ msgs[i]->dn));
+ } else {
+ DBG_INFO("Removed deleted object %s\n",
+ ldb_dn_get_linearized(res->
+ msgs[i]->dn));
+ (*num_objects_removed)++;
+ }
+ continue;
+ }
+
+ /* This must have a linked attribute */
+
+ /*
+ * From MS-ADTS 3.1.1.1.9 DCs, usn Counters, and
+ * the Originating Update Stamp
+ *
+ * "A link value r is deleted, but exists as a
+ * tombstone, if r.stamp.timeDeleted ≠ 0. When
+ * the current time minus r.stamp.timeDeleted
+ * exceeds the tombstone lifetime, the link
+ * value r is garbage-collected; that is,
+ * removed from its containing forward link
+ * attribute. "
+ */
+
+ for (j=0; j < res->msgs[i]->num_elements; j++) {
+ struct ldb_message_element *element = NULL;
+ /* TODO this is O(log n) per attribute with deleted values */
+ const struct dsdb_attribute *attrib = NULL;
+
+ element = &res->msgs[i]->elements[j];
+ attrib = dsdb_attribute_by_lDAPDisplayName(schema,
+ element->name);
+
+ /* This avoids parsing isDeleted as a link */
+ if (attrib == NULL ||
+ attrib->linkID == 0 ||
+ ((attrib->linkID & 1) == 1)) {
+ continue;
+ }
+
+ for (k = 0; k < element->num_values; k++) {
+ struct ldb_val *value = &element->values[k];
+ uint64_t whenChanged = 0;
+ NTSTATUS status;
+ struct dsdb_dn *dn;
+ struct ldb_message_element *cleanup_elem = NULL;
+ char *guid_search_str = NULL;
+ char *guid_buf_str = NULL;
+ struct ldb_val cleanup_val;
+ struct GUID_txt_buf buf_guid;
+ struct GUID guid;
+ const struct ldb_val *guid_blob;
+
+ if (dsdb_dn_is_deleted_val(value) == false) {
+ continue;
+ }
+
+ dn = dsdb_dn_parse(tmp_ctx, samdb,
+ &element->values[k],
+ attrib->syntax->ldap_oid);
+ if (dn == NULL) {
+ DBG_WARNING("Failed to parse linked attribute blob of "
+ "%s on %s while expunging expired links\n",
+ element->name,
+ ldb_dn_get_linearized(res->msgs[i]->dn));
+ continue;
+ }
+
+ status = dsdb_get_extended_dn_uint64(dn->dn,
+ &whenChanged,
+ "RMD_CHANGETIME");
+ if (!NT_STATUS_IS_OK(status)) {
+ DBG_WARNING("Error: RMD_CHANGETIME is missing on a forward link.\n");
+ talloc_free(dn);
+ continue;
+ }
+
+ if (whenChanged >= expunge_time_nttime) {
+ talloc_free(dn);
+ continue;
+ }
+
+ guid_blob = ldb_dn_get_extended_component(dn->dn, "GUID");
+ status = GUID_from_ndr_blob(guid_blob, &guid);
+ if (!NT_STATUS_IS_OK(status)) {
+ DBG_WARNING("Error: Invalid GUID on link target.\n");
+ talloc_free(dn);
+ continue;
+ }
+
+ guid_buf_str = GUID_buf_string(&guid, &buf_guid);
+ guid_search_str = talloc_asprintf(mem_ctx,
+ "<GUID=%s>;%s",
+ guid_buf_str,
+ dsdb_dn_get_linearized(mem_ctx, dn));
+ cleanup_val = data_blob_string_const(guid_search_str);
+
+ talloc_free(dn);
+
+ if (cleanup_msg == NULL) {
+ cleanup_msg = ldb_msg_new(mem_ctx);
+ if (cleanup_msg == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ cleanup_msg->dn = res->msgs[i]->dn;
+ }
+
+ ret = ldb_msg_add_value(cleanup_msg,
+ element->name,
+ &cleanup_val,
+ &cleanup_elem);
+ if (ret != LDB_SUCCESS) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ cleanup_elem->flags = LDB_FLAG_MOD_DELETE;
+ num_modified++;
+ }
+ }
+
+ if (num_modified > 0) {
+ ret = dsdb_modify(samdb, cleanup_msg,
+ DSDB_REPLMD_VANISH_LINKS);
+ if (ret != LDB_SUCCESS) {
+ DBG_WARNING(__location__ ": Failed to remove deleted object %s\n",
+ ldb_dn_get_linearized(res->msgs[i]->dn));
+ } else {
+ DBG_INFO("Removed deleted object %s\n",
+ ldb_dn_get_linearized(res->msgs[i]->dn));
+ *num_links_removed = *num_links_removed + num_modified;
+ }
+
+ }
+ }
+
+ TALLOC_FREE(tmp_ctx);
+ return NT_STATUS_OK;
+}
+
+/*
+ * Per MS-ADTS 3.1.1.5.5 Delete Operation
+ *
+ * "Tombstones are a type of deleted object distinguished from
+ * existing-objects by the presence of the isDeleted attribute with the
+ * value true."
+ *
+ * "After a time period at least as large as a tombstone lifetime, the
+ * tombstone is removed from the directory."
+ *
+ * The purpose of this routine is to remove such objects. It is
+ * called from a timed event in the KCC, and from samba-tool domain
+ * expunge tombstones.
+ *
+ * Additionally, linked attributes have similar properties.
+ */
+NTSTATUS dsdb_garbage_collect_tombstones(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ struct dsdb_ldb_dn_list_node *part,
+ time_t current_time,
+ uint32_t tombstoneLifetime,
+ unsigned int *num_objects_removed,
+ unsigned int *num_links_removed,
+ char **error_string)
+{
+ const char **attrs = NULL;
+ char *filter = NULL;
+ NTSTATUS status;
+ unsigned int i;
+ struct dsdb_attribute *next_attr;
+ unsigned int num_link_attrs;
+ struct dsdb_schema *schema = dsdb_get_schema(samdb, mem_ctx);
+ unsigned long long expunge_time = current_time - tombstoneLifetime*60*60*24;
+ char *expunge_time_string = ldb_timestring_utc(mem_ctx, expunge_time);
+ NTTIME expunge_time_nttime;
+ unix_to_nt_time(&expunge_time_nttime, expunge_time);
+
+ *num_objects_removed = 0;
+ *num_links_removed = 0;
+ *error_string = NULL;
+ num_link_attrs = 0;
+
+ /*
+ * This filter is a bit strange, but the idea is to filter for
+ * objects that need to have tombstones expunged without
+ * bringing a potentially large database all into memory. To
+ * do that, we could use callbacks, but instead we use a
+ * custom match rule to triage the objects during the search,
+ * and ideally avoid memory allocation for most of the
+ * un-matched objects.
+ *
+ * The parameter to DSDB_MATCH_FOR_EXPUNGE is the NTTIME, we
+ * return records with deleted links deleted before this time.
+ *
+ * We use a date comparison on whenChanged to avoid returning
+ * all isDeleted records
+ */
+
+ filter = talloc_asprintf(mem_ctx, "(|");
+ for (next_attr = schema->attributes; next_attr != NULL; next_attr = next_attr->next) {
+ if (next_attr->linkID != 0 && ((next_attr->linkID & 1) == 0)) {
+ num_link_attrs++;
+ filter = talloc_asprintf_append(filter,
+ "(%s:" DSDB_MATCH_FOR_EXPUNGE ":=%llu)",
+ next_attr->lDAPDisplayName,
+ (unsigned long long)expunge_time_nttime);
+ if (filter == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ }
+ }
+
+ attrs = talloc_array(mem_ctx, const char *, num_link_attrs + 2);
+ i = 0;
+ for (next_attr = schema->attributes; next_attr != NULL; next_attr = next_attr->next) {
+ if (next_attr->linkID != 0 && ((next_attr->linkID & 1) == 0)) {
+ attrs[i++] = next_attr->lDAPDisplayName;
+ }
+ }
+ attrs[i] = "isDeleted";
+ attrs[i+1] = NULL;
+
+ filter = talloc_asprintf_append(filter,
+ "(&(isDeleted=TRUE)(whenChanged<=%s)))",
+ expunge_time_string);
+ if (filter == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ for (; part != NULL; part = part->next) {
+ status = garbage_collect_tombstones_part(mem_ctx, samdb, part,
+ filter,
+ num_links_removed,
+ num_objects_removed,
+ schema, attrs,
+ error_string,
+ expunge_time_nttime);
+ if (!NT_STATUS_IS_OK(status)) {
+ return status;
+ }
+ }
+
+ return NT_STATUS_OK;
+}
diff --git a/source4/dsdb/kcc/garbage_collect_tombstones.h b/source4/dsdb/kcc/garbage_collect_tombstones.h
new file mode 100644
index 0000000..ce62f5d
--- /dev/null
+++ b/source4/dsdb/kcc/garbage_collect_tombstones.h
@@ -0,0 +1,34 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ handle removal of deleted objects
+
+ Copyright (C) 2009 Andrew Tridgell
+
+ 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 "param/param.h"
+#include "dsdb/samdb/samdb.h"
+#include "dsdb/common/util.h"
+
+
+NTSTATUS dsdb_garbage_collect_tombstones(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ struct dsdb_ldb_dn_list_node *part,
+ time_t current_time,
+ uint32_t tombstoneLifetime,
+ unsigned int *num_objects_removed,
+ unsigned int *num_links_removed,
+ char **error_string);
diff --git a/source4/dsdb/kcc/kcc_connection.c b/source4/dsdb/kcc/kcc_connection.c
new file mode 100644
index 0000000..78d853e
--- /dev/null
+++ b/source4/dsdb/kcc/kcc_connection.c
@@ -0,0 +1,252 @@
+/*
+ Unix SMB/CIFS implementation.
+ KCC service periodic handling
+
+ Copyright (C) Crístian Deives
+
+ 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 "lib/events/events.h"
+#include "dsdb/samdb/samdb.h"
+#include "auth/auth.h"
+#include "samba/service.h"
+#include "lib/messaging/irpc.h"
+#include "dsdb/kcc/kcc_service.h"
+#include "dsdb/kcc/kcc_connection.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.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"
+
+static int kccsrv_add_connection(struct kccsrv_service *s,
+ struct kcc_connection *conn)
+{
+ struct ldb_message *msg;
+ TALLOC_CTX *tmp_ctx;
+ struct ldb_dn *new_dn, *server_dn;
+ struct GUID guid;
+ /* struct ldb_val schedule_val; */
+ int ret;
+ bool ok;
+
+ tmp_ctx = talloc_new(s);
+ if (!tmp_ctx) {
+ DEBUG(0, ("failed to talloc\n"));
+ ret = LDB_ERR_OPERATIONS_ERROR;
+ goto done;
+ }
+ new_dn = samdb_ntds_settings_dn(s->samdb, tmp_ctx);
+ if (!new_dn) {
+ DEBUG(0, ("failed to find NTDS settings\n"));
+ ret = LDB_ERR_OPERATIONS_ERROR;
+ goto done;
+ }
+ new_dn = ldb_dn_copy(tmp_ctx, new_dn);
+ if (!new_dn) {
+ DEBUG(0, ("failed to copy NTDS settings\n"));
+ ret = LDB_ERR_OPERATIONS_ERROR;
+ goto done;
+ }
+ guid = GUID_random();
+ ok = ldb_dn_add_child_fmt(new_dn, "CN=%s", GUID_string(tmp_ctx, &guid));
+ if (!ok) {
+ DEBUG(0, ("failed to create nTDSConnection DN\n"));
+ ret = LDB_ERR_INVALID_DN_SYNTAX;
+ goto done;
+ }
+ ret = dsdb_find_dn_by_guid(s->samdb, tmp_ctx, &conn->dsa_guid, 0, &server_dn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0, ("failed to find fromServer DN '%s'\n",
+ GUID_string(tmp_ctx, &conn->dsa_guid)));
+ goto done;
+ }
+ /*schedule_val = data_blob_const(r1->schedule, sizeof(r1->schedule));*/
+
+ msg = ldb_msg_new(tmp_ctx);
+ msg->dn = new_dn;
+ ldb_msg_add_string(msg, "objectClass", "nTDSConnection");
+ ldb_msg_add_string(msg, "showInAdvancedViewOnly", "TRUE");
+ ldb_msg_add_string(msg, "enabledConnection", "TRUE");
+ ldb_msg_add_linearized_dn(msg, "fromServer", server_dn);
+ /* ldb_msg_add_value(msg, "schedule", &schedule_val, NULL); */
+
+ samdb_msg_add_uint(s->samdb, msg, msg,
+ "options", NTDSCONN_OPT_IS_GENERATED);
+
+ ret = ldb_add(s->samdb, msg);
+ if (ret == LDB_SUCCESS) {
+ DEBUG(2, ("added nTDSConnection object '%s'\n",
+ ldb_dn_get_linearized(new_dn)));
+ } else {
+ DEBUG(0, ("failed to add an nTDSConnection object: %s\n",
+ ldb_strerror(ret)));
+ }
+
+done:
+ talloc_free(tmp_ctx);
+ return ret;
+}
+
+static int kccsrv_delete_connection(struct kccsrv_service *s,
+ struct kcc_connection *conn)
+{
+ TALLOC_CTX *tmp_ctx;
+ struct ldb_dn *dn;
+ int ret;
+
+ tmp_ctx = talloc_new(s);
+ ret = dsdb_find_dn_by_guid(s->samdb, tmp_ctx, &conn->obj_guid, 0, &dn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0, ("failed to find nTDSConnection's DN: %s\n",
+ ldb_strerror(ret)));
+ goto done;
+ }
+
+ ret = ldb_delete(s->samdb, dn);
+ if (ret == LDB_SUCCESS) {
+ DEBUG(2, ("deleted nTDSConnection object '%s'\n",
+ ldb_dn_get_linearized(dn)));
+ } else {
+ DEBUG(0, ("failed to delete an nTDSConnection object: %s\n",
+ ldb_strerror(ret)));
+ }
+
+done:
+ talloc_free(tmp_ctx);
+ return ret;
+}
+
+void kccsrv_apply_connections(struct kccsrv_service *s,
+ struct kcc_connection_list *ntds_list,
+ struct kcc_connection_list *dsa_list)
+{
+ unsigned int i, j, deleted = 0, added = 0;
+ int ret;
+
+ /* XXX
+ *
+ * This routine is not respecting connections that the
+ * administrator can specifically create (NTDSCONN_OPT_IS_GENERATED
+ * bit will not be set)
+ */
+ for (i = 0; ntds_list && i < ntds_list->count; i++) {
+ struct kcc_connection *ntds = &ntds_list->servers[i];
+ for (j = 0; j < dsa_list->count; j++) {
+ struct kcc_connection *dsa = &dsa_list->servers[j];
+ if (GUID_equal(&ntds->dsa_guid, &dsa->dsa_guid)) {
+ break;
+ }
+ }
+ if (j == dsa_list->count) {
+ ret = kccsrv_delete_connection(s, ntds);
+ if (ret == LDB_SUCCESS) {
+ deleted++;
+ }
+ }
+ }
+ DEBUG(4, ("%d connections have been deleted\n", deleted));
+
+ for (i = 0; i < dsa_list->count; i++) {
+ struct kcc_connection *dsa = &dsa_list->servers[i];
+ for (j = 0; ntds_list && j < ntds_list->count; j++) {
+ struct kcc_connection *ntds = &ntds_list->servers[j];
+ if (GUID_equal(&dsa->dsa_guid, &ntds->dsa_guid)) {
+ break;
+ }
+ }
+ if (ntds_list == NULL || j == ntds_list->count) {
+ ret = kccsrv_add_connection(s, dsa);
+ if (ret == LDB_SUCCESS) {
+ added++;
+ }
+ }
+ }
+ DEBUG(4, ("%d connections have been added\n", added));
+}
+
+struct kcc_connection_list *kccsrv_find_connections(struct kccsrv_service *s,
+ TALLOC_CTX *mem_ctx)
+{
+ unsigned int i;
+ int ret;
+ struct ldb_dn *base_dn;
+ struct ldb_result *res;
+ const char *attrs[] = { "objectGUID", "fromServer", NULL };
+ struct kcc_connection_list *list;
+ TALLOC_CTX *tmp_ctx;
+
+ tmp_ctx = talloc_new(mem_ctx);
+ if (!tmp_ctx) {
+ DEBUG(0, ("failed to talloc\n"));
+ return NULL;
+ }
+
+ base_dn = samdb_ntds_settings_dn(s->samdb, tmp_ctx);
+ if (!base_dn) {
+ DEBUG(0, ("failed to find our own NTDS settings DN\n"));
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+
+ ret = ldb_search(s->samdb, tmp_ctx, &res, base_dn, LDB_SCOPE_ONELEVEL,
+ attrs, "objectClass=nTDSConnection");
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0, ("failed nTDSConnection search: %s\n",
+ ldb_strerror(ret)));
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+
+ list = talloc(tmp_ctx, struct kcc_connection_list);
+ if (!list) {
+ DEBUG(0, ("out of memory\n"));
+ return NULL;
+ }
+ list->servers = talloc_array(list, struct kcc_connection,
+ res->count);
+ if (!list->servers) {
+ DEBUG(0, ("out of memory\n"));
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+ list->count = 0;
+
+ for (i = 0; i < res->count; i++) {
+ struct ldb_dn *server_dn;
+
+ list->servers[i].obj_guid = samdb_result_guid(res->msgs[i],
+ "objectGUID");
+ server_dn = samdb_result_dn(s->samdb, mem_ctx, res->msgs[i],
+ "fromServer", NULL);
+ ret = dsdb_find_guid_by_dn(s->samdb, server_dn,
+ &list->servers[i].dsa_guid);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0, ("Failed to find connection server's GUID by "
+ "DN=%s: %s\n",
+ ldb_dn_get_linearized(server_dn),
+ ldb_strerror(ret)));
+ continue;
+ }
+ list->count++;
+ }
+ DEBUG(4, ("found %d existing nTDSConnection objects\n", list->count));
+ talloc_steal(mem_ctx, list);
+ talloc_free(tmp_ctx);
+ return list;
+}
diff --git a/source4/dsdb/kcc/kcc_connection.h b/source4/dsdb/kcc/kcc_connection.h
new file mode 100644
index 0000000..b15e8ec
--- /dev/null
+++ b/source4/dsdb/kcc/kcc_connection.h
@@ -0,0 +1,39 @@
+/*
+ Unix SMB/CIFS Implementation.
+
+ KCC service
+
+ Copyright (C) Crístian Deives 2009
+ based on drepl service code
+
+ 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/>.
+
+*/
+
+#ifndef _DSDB_REPL_KCC_CONNECTION_H_
+#define _DSDB_REPL_KCC_CONNECTION_H_
+
+struct kcc_connection {
+ struct GUID obj_guid;
+ struct GUID dsa_guid;
+ struct GUID invocation_id;
+ uint8_t schedule[84];
+};
+
+struct kcc_connection_list {
+ unsigned count;
+ struct kcc_connection *servers;
+};
+
+#endif /* _DSDB_REPL_KCC_CONNECTION_H_ */
diff --git a/source4/dsdb/kcc/kcc_drs_replica_info.c b/source4/dsdb/kcc/kcc_drs_replica_info.c
new file mode 100644
index 0000000..5975926
--- /dev/null
+++ b/source4/dsdb/kcc/kcc_drs_replica_info.c
@@ -0,0 +1,911 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ DRS Replica Information
+
+ Copyright (C) Erick Nogueira do Nascimento 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 "dsdb/samdb/samdb.h"
+#include "dsdb/common/proto.h"
+#include "auth/auth.h"
+#include "samba/service.h"
+#include "lib/events/events.h"
+#include "lib/messaging/irpc.h"
+#include "dsdb/kcc/kcc_service.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.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/common/util.h"
+
+
+/*
+ get the stamp values for the linked attribute 'linked_attr_name' of the object 'dn'
+*/
+static WERROR get_linked_attribute_value_stamp(TALLOC_CTX *mem_ctx, struct ldb_context *samdb,
+ struct ldb_dn *dn, const char *linked_attr_name,
+ uint32_t *attr_version, NTTIME *attr_change_time, uint32_t *attr_orig_usn)
+{
+ struct ldb_result *res;
+ int ret;
+ const char *attrs[2];
+ struct ldb_dn *attr_ext_dn;
+ NTSTATUS ntstatus;
+
+ attrs[0] = linked_attr_name;
+ attrs[1] = NULL;
+
+ ret = dsdb_search_dn(samdb, mem_ctx, &res, dn, attrs,
+ DSDB_SEARCH_SHOW_EXTENDED_DN | DSDB_SEARCH_REVEAL_INTERNALS);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0, (__location__ ": Failed search for attribute %s on %s\n",
+ linked_attr_name, ldb_dn_get_linearized(dn)));
+ return WERR_INTERNAL_ERROR;
+ }
+
+ attr_ext_dn = ldb_msg_find_attr_as_dn(samdb, mem_ctx, res->msgs[0], linked_attr_name);
+ if (!attr_ext_dn) {
+ DEBUG(0, (__location__ ": Failed search for attribute %s on %s\n",
+ linked_attr_name, ldb_dn_get_linearized(dn)));
+ return WERR_INTERNAL_ERROR;
+ }
+
+ DEBUG(0, ("linked_attr_name = %s, attr_ext_dn = %s", linked_attr_name,
+ ldb_dn_get_extended_linearized(mem_ctx, attr_ext_dn, 1)));
+
+ ntstatus = dsdb_get_extended_dn_uint32(attr_ext_dn, attr_version, "RMD_VERSION");
+ if (!NT_STATUS_IS_OK(ntstatus)) {
+ DEBUG(0, (__location__ ": Could not extract component %s from dn \"%s\"\n",
+ "RMD_VERSION", ldb_dn_get_extended_linearized(mem_ctx, attr_ext_dn, 1)));
+ return WERR_INTERNAL_ERROR;
+ }
+
+ ntstatus = dsdb_get_extended_dn_nttime(attr_ext_dn, attr_change_time, "RMD_CHANGETIME");
+ if (!NT_STATUS_IS_OK(ntstatus)) {
+ DEBUG(0, (__location__ ": Could not extract component %s from dn \"%s\"\n",
+ "RMD_CHANGETIME", ldb_dn_get_extended_linearized(mem_ctx, attr_ext_dn, 1)));
+ return WERR_INTERNAL_ERROR;
+ }
+
+ ntstatus = dsdb_get_extended_dn_uint32(attr_ext_dn, attr_version, "RMD_ORIGINATING_USN");
+ if (!NT_STATUS_IS_OK(ntstatus)) {
+ DEBUG(0, (__location__ ": Could not extract component %s from dn \"%s\"\n",
+ "RMD_ORIGINATING_USN", ldb_dn_get_extended_linearized(mem_ctx, attr_ext_dn, 1)));
+ return WERR_INTERNAL_ERROR;
+ }
+
+ return WERR_OK;
+}
+
+static WERROR get_repl_prop_metadata_ctr(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ struct ldb_dn *dn,
+ struct replPropertyMetaDataBlob *obj_metadata_ctr)
+{
+ int ret;
+ struct ldb_result *res;
+ const char *attrs[] = { "replPropertyMetaData", NULL };
+ const struct ldb_val *omd_value;
+ enum ndr_err_code ndr_err;
+
+ ret = ldb_search(samdb, mem_ctx, &res, dn, LDB_SCOPE_BASE, attrs, NULL);
+ if (ret != LDB_SUCCESS || res->count != 1) {
+ DEBUG(0, (__location__ ": Failed search for replPropertyMetaData attribute on %s\n",
+ ldb_dn_get_linearized(dn)));
+ return WERR_INTERNAL_ERROR;
+ }
+
+ 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(dn)));
+ talloc_free(res);
+ return WERR_INTERNAL_ERROR;
+ }
+
+ ndr_err = ndr_pull_struct_blob(omd_value, mem_ctx,
+ obj_metadata_ctr,
+ (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(dn)));
+ talloc_free(res);
+ return WERR_INTERNAL_ERROR;
+ }
+
+ talloc_free(res);
+ return WERR_OK;
+}
+
+/*
+ get the DN of the nTDSDSA object from the configuration partition
+ whose invocationId is 'invocation_id'
+ put the value on 'dn_str'
+*/
+static WERROR get_dn_from_invocation_id(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ struct GUID *invocation_id,
+ const char **dn_str)
+{
+ char *invocation_id_str;
+ const char *attrs_invocation[] = { NULL };
+ struct ldb_message *msg;
+ int ret;
+
+ invocation_id_str = GUID_string(mem_ctx, invocation_id);
+ W_ERROR_HAVE_NO_MEMORY(invocation_id_str);
+
+ ret = dsdb_search_one(samdb, invocation_id_str, &msg, ldb_get_config_basedn(samdb), LDB_SCOPE_SUBTREE,
+ attrs_invocation, 0, "(&(objectClass=nTDSDSA)(invocationId=%s))", invocation_id_str);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0, (__location__ ": Failed search for the object DN under %s whose invocationId is %s\n",
+ invocation_id_str, ldb_dn_get_linearized(ldb_get_config_basedn(samdb))));
+ talloc_free(invocation_id_str);
+ return WERR_INTERNAL_ERROR;
+ }
+
+ *dn_str = ldb_dn_alloc_linearized(mem_ctx, msg->dn);
+ talloc_free(invocation_id_str);
+ return WERR_OK;
+}
+
+/*
+ get metadata version 2 info for a specified object DN
+*/
+static WERROR kccdrs_replica_get_info_obj_metadata2(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ struct drsuapi_DsReplicaGetInfo *r,
+ union drsuapi_DsReplicaInfo *reply,
+ struct ldb_dn *dn,
+ uint32_t base_index)
+{
+ WERROR status;
+ struct replPropertyMetaDataBlob omd_ctr;
+ struct replPropertyMetaData1 *attr;
+ struct drsuapi_DsReplicaObjMetaData2Ctr *metadata2;
+ const struct dsdb_schema *schema;
+
+ uint32_t i, j;
+
+ DEBUG(0, ("kccdrs_replica_get_info_obj_metadata2() called\n"));
+
+ if (!dn) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ if (!ldb_dn_validate(dn)) {
+ return WERR_DS_DRA_BAD_DN;
+ }
+
+ status = get_repl_prop_metadata_ctr(mem_ctx, samdb, dn, &omd_ctr);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ schema = dsdb_get_schema(samdb, reply);
+ if (!schema) {
+ DEBUG(0,(__location__": Failed to get the schema\n"));
+ return WERR_INTERNAL_ERROR;
+ }
+
+ reply->objmetadata2 = talloc_zero(mem_ctx, struct drsuapi_DsReplicaObjMetaData2Ctr);
+ W_ERROR_HAVE_NO_MEMORY(reply->objmetadata2);
+ metadata2 = reply->objmetadata2;
+ metadata2->enumeration_context = 0;
+
+ /* For each replicated attribute of the object */
+ for (i = 0, j = 0; i < omd_ctr.ctr.ctr1.count; i++) {
+ const struct dsdb_attribute *schema_attr;
+ uint32_t attr_version;
+ NTTIME attr_change_time;
+ uint32_t attr_originating_usn = 0;
+
+ /*
+ attr := attrsSeq[i]
+ s := AttrStamp(object, attr)
+ */
+ /* get a reference to the attribute on 'omd_ctr' */
+ attr = &omd_ctr.ctr.ctr1.array[j];
+
+ schema_attr = dsdb_attribute_by_attributeID_id(schema, attr->attid);
+
+ DEBUG(0, ("attribute_id = %d, attribute_name: %s\n", attr->attid, schema_attr->lDAPDisplayName));
+
+ /*
+ if (attr in Link Attributes of object and
+ dwInVersion = 2 and DS_REPL_INFO_FLAG_IMPROVE_LINKED_ATTRS in msgIn.ulFlags)
+ */
+ if (schema_attr &&
+ schema_attr->linkID != 0 && /* Checks if attribute is a linked attribute */
+ (schema_attr->linkID % 2) == 0 && /* is it a forward link? only forward links have the LinkValueStamp */
+ r->in.level == 2 &&
+ (r->in.req->req2.flags & DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE)) /* on MS-DRSR it is DS_REPL_INFO_FLAG_IMPROVE_LINKED_ATTRS */
+ {
+ /*
+ ls := LinkValueStamp of the most recent
+ value change in object!attr
+ */
+ status = get_linked_attribute_value_stamp(mem_ctx, samdb, dn, schema_attr->lDAPDisplayName,
+ &attr_version, &attr_change_time, &attr_originating_usn);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ /*
+ Aligning to MS-DRSR 4.1.13.3:
+ 's' on the doc is 'attr->originating_change_time' here
+ 'ls' on the doc is 'attr_change_time' here
+ */
+
+ /* if (ls is more recent than s (based on order in which the change was applied on server)) then */
+ if (attr_change_time > attr->originating_change_time) {
+ /*
+ Improve the stamp with the link value stamp.
+ s.dwVersion := ls.dwVersion
+ s.timeChanged := ls.timeChanged
+ s.uuidOriginating := NULLGUID
+ s.usnOriginating := ls.usnOriginating
+ */
+ attr->version = attr_version;
+ attr->originating_change_time = attr_change_time;
+ attr->originating_invocation_id = GUID_zero();
+ attr->originating_usn = attr_originating_usn;
+ }
+ }
+
+ if (i < base_index) {
+ continue;
+ }
+
+ metadata2->array = talloc_realloc(mem_ctx, metadata2->array,
+ struct drsuapi_DsReplicaObjMetaData2, j + 1);
+ W_ERROR_HAVE_NO_MEMORY(metadata2->array);
+ metadata2->array[j].attribute_name = schema_attr->lDAPDisplayName;
+ metadata2->array[j].local_usn = attr->local_usn;
+ metadata2->array[j].originating_change_time = attr->originating_change_time;
+ metadata2->array[j].originating_invocation_id = attr->originating_invocation_id;
+ metadata2->array[j].originating_usn = attr->originating_usn;
+ metadata2->array[j].version = attr->version;
+
+ /*
+ originating_dsa_dn := GetDNFromInvocationID(originating_invocation_id)
+ GetDNFromInvocationID() should return the DN of the nTDSDSAobject that has the specified invocation ID
+ See MS-DRSR 4.1.13.3 and 4.1.13.2.1
+ */
+ status = get_dn_from_invocation_id(mem_ctx, samdb,
+ &attr->originating_invocation_id,
+ &metadata2->array[j].originating_dsa_dn);
+ W_ERROR_NOT_OK_RETURN(status);
+ j++;
+ metadata2->count = j;
+
+ }
+
+ return WERR_OK;
+}
+
+/*
+ get cursors info for a specified DN
+*/
+static WERROR kccdrs_replica_get_info_cursors(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ struct drsuapi_DsReplicaGetInfo *r,
+ union drsuapi_DsReplicaInfo *reply,
+ struct ldb_dn *dn)
+{
+ int ret;
+
+ if (!ldb_dn_validate(dn)) {
+ return WERR_INVALID_PARAMETER;
+ }
+ reply->cursors = talloc(mem_ctx, struct drsuapi_DsReplicaCursorCtr);
+ W_ERROR_HAVE_NO_MEMORY(reply->cursors);
+
+ reply->cursors->reserved = 0;
+
+ ret = dsdb_load_udv_v1(samdb, dn, reply->cursors, &reply->cursors->array, &reply->cursors->count);
+ if (ret != LDB_SUCCESS) {
+ return WERR_DS_DRA_BAD_NC;
+ }
+ return WERR_OK;
+}
+
+/*
+ get cursors2 info for a specified DN
+*/
+static WERROR kccdrs_replica_get_info_cursors2(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ struct drsuapi_DsReplicaGetInfo *r,
+ union drsuapi_DsReplicaInfo *reply,
+ struct ldb_dn *dn)
+{
+ int ret;
+
+ if (!ldb_dn_validate(dn)) {
+ return WERR_INVALID_PARAMETER;
+ }
+ reply->cursors2 = talloc(mem_ctx, struct drsuapi_DsReplicaCursor2Ctr);
+ W_ERROR_HAVE_NO_MEMORY(reply->cursors2);
+
+ ret = dsdb_load_udv_v2(samdb, dn, reply->cursors2, &reply->cursors2->array, &reply->cursors2->count);
+ if (ret != LDB_SUCCESS) {
+ return WERR_DS_DRA_BAD_NC;
+ }
+
+ reply->cursors2->enumeration_context = reply->cursors2->count;
+ return WERR_OK;
+}
+
+/*
+ get pending ops info for a specified DN
+*/
+static WERROR kccdrs_replica_get_info_pending_ops(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ struct drsuapi_DsReplicaGetInfo *r,
+ union drsuapi_DsReplicaInfo *reply,
+ struct ldb_dn *dn)
+{
+ struct timeval now = timeval_current();
+
+ if (!ldb_dn_validate(dn)) {
+ return WERR_INVALID_PARAMETER;
+ }
+ reply->pendingops = talloc(mem_ctx, struct drsuapi_DsReplicaOpCtr);
+ W_ERROR_HAVE_NO_MEMORY(reply->pendingops);
+
+ /* claim no pending ops for now */
+ reply->pendingops->time = timeval_to_nttime(&now);
+ reply->pendingops->count = 0;
+ reply->pendingops->array = NULL;
+
+ return WERR_OK;
+}
+
+struct ncList {
+ struct ldb_dn *dn;
+ struct ncList *prev, *next;
+};
+
+/*
+ Fill 'master_nc_list' with the master ncs hosted by this server
+*/
+static WERROR get_master_ncs(TALLOC_CTX *mem_ctx, struct ldb_context *samdb,
+ const char *ntds_guid_str, struct ncList **master_nc_list)
+{
+ const char *post_2003_attrs[] = { "msDS-hasMasterNCs", "hasPartialReplicaNCs", NULL };
+ const char *pre_2003_attrs[] = { "hasMasterNCs", "hasPartialReplicaNCs", NULL };
+ const char **attrs = post_2003_attrs;
+ struct ldb_result *res;
+ struct ncList *nc_list = NULL;
+ struct ncList *nc_list_elem;
+ int ret;
+ unsigned int i;
+ char *nc_str;
+
+ /* In W2003 and greater, msDS-hasMasterNCs attribute lists the writable NC replicas */
+ ret = ldb_search(samdb, mem_ctx, &res, ldb_get_config_basedn(samdb),
+ LDB_SCOPE_DEFAULT, post_2003_attrs, "(objectguid=%s)", ntds_guid_str);
+
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed objectguid search - %s\n", ldb_errstring(samdb)));
+
+ attrs = post_2003_attrs;
+ ret = ldb_search(samdb, mem_ctx, &res, ldb_get_config_basedn(samdb),
+ LDB_SCOPE_DEFAULT, pre_2003_attrs, "(objectguid=%s)", ntds_guid_str);
+ }
+
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed objectguid search - %s\n", ldb_errstring(samdb)));
+ return WERR_INTERNAL_ERROR;
+ }
+
+ if (res->count == 0) {
+ DEBUG(0,(__location__ ": Failed: objectguid=%s not found\n", ntds_guid_str));
+ return WERR_INTERNAL_ERROR;
+ }
+
+ for (i = 0; i < res->count; i++) {
+ struct ldb_message_element *msg_elem;
+ unsigned int k, a;
+
+ for (a=0; attrs[a]; a++) {
+ msg_elem = ldb_msg_find_element(res->msgs[i], attrs[a]);
+ if (!msg_elem || msg_elem->num_values == 0) {
+ continue;
+ }
+
+ for (k = 0; k < msg_elem->num_values; k++) {
+ /* copy the string on msg_elem->values[k]->data to nc_str */
+ nc_str = talloc_strndup(mem_ctx, (char *)msg_elem->values[k].data, msg_elem->values[k].length);
+ W_ERROR_HAVE_NO_MEMORY(nc_str);
+
+ nc_list_elem = talloc_zero(mem_ctx, struct ncList);
+ W_ERROR_HAVE_NO_MEMORY(nc_list_elem);
+ nc_list_elem->dn = ldb_dn_new(mem_ctx, samdb, nc_str);
+ W_ERROR_HAVE_NO_MEMORY(nc_list_elem);
+ DLIST_ADD(nc_list, nc_list_elem);
+ }
+ }
+ }
+
+ *master_nc_list = nc_list;
+ return WERR_OK;
+}
+
+/*
+ Fill 'nc_list' with the ncs list. (MS-DRSR 4.1.13.3)
+ if the object dn is specified, fill 'nc_list' only with this dn
+ otherwise, fill 'nc_list' with all master ncs hosted by this server
+*/
+static WERROR get_ncs_list(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ struct kccsrv_service *service,
+ const char *object_dn_str,
+ struct ncList **nc_list)
+{
+ WERROR status;
+ struct ncList *nc_list_elem;
+ struct ldb_dn *nc_dn;
+
+ if (object_dn_str != NULL) {
+ /* ncs := { object_dn } */
+ *nc_list = NULL;
+ nc_dn = ldb_dn_new(mem_ctx, samdb, object_dn_str);
+ nc_list_elem = talloc_zero(mem_ctx, struct ncList);
+ W_ERROR_HAVE_NO_MEMORY(nc_list_elem);
+ nc_list_elem->dn = nc_dn;
+ DLIST_ADD_END(*nc_list, nc_list_elem);
+ } else {
+ /* ncs := getNCs() from ldb database.
+ * getNCs() must return an array containing
+ * the DSNames of all NCs hosted by this
+ * server.
+ */
+ char *ntds_guid_str = GUID_string(mem_ctx, &service->ntds_guid);
+ W_ERROR_HAVE_NO_MEMORY(ntds_guid_str);
+ status = get_master_ncs(mem_ctx, samdb, ntds_guid_str, nc_list);
+ W_ERROR_NOT_OK_RETURN(status);
+ }
+
+ return WERR_OK;
+}
+
+/*
+ Copy the fields from 'reps1' to 'reps2', leaving zeroed the fields on
+ 'reps2' that aren't available on 'reps1'.
+*/
+static WERROR copy_repsfrom_1_to_2(TALLOC_CTX *mem_ctx,
+ struct repsFromTo2 **reps2,
+ struct repsFromTo1 *reps1)
+{
+ struct repsFromTo2* reps;
+
+ reps = talloc_zero(mem_ctx, struct repsFromTo2);
+ W_ERROR_HAVE_NO_MEMORY(reps);
+
+ reps->blobsize = reps1->blobsize;
+ reps->consecutive_sync_failures = reps1->consecutive_sync_failures;
+ reps->last_attempt = reps1->last_attempt;
+ reps->last_success = reps1->last_success;
+ reps->result_last_attempt = reps1->result_last_attempt;
+ reps->other_info = talloc_zero(mem_ctx, struct repsFromTo2OtherInfo);
+ W_ERROR_HAVE_NO_MEMORY(reps->other_info);
+ reps->other_info->dns_name1 = reps1->other_info->dns_name;
+ reps->replica_flags = reps1->replica_flags;
+ memcpy(reps->schedule, reps1->schedule, sizeof(reps1->schedule));
+ reps->reserved = reps1->reserved;
+ reps->highwatermark = reps1->highwatermark;
+ reps->source_dsa_obj_guid = reps1->source_dsa_obj_guid;
+ reps->source_dsa_invocation_id = reps1->source_dsa_invocation_id;
+ reps->transport_guid = reps1->transport_guid;
+
+ *reps2 = reps;
+ return WERR_OK;
+}
+
+static WERROR fill_neighbor_from_repsFrom(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ struct ldb_dn *nc_dn,
+ struct drsuapi_DsReplicaNeighbour *neigh,
+ struct repsFromTo2 *reps_from)
+{
+ struct ldb_dn *source_dsa_dn;
+ int ret;
+ struct ldb_dn *transport_obj_dn = NULL;
+
+ neigh->source_dsa_address = reps_from->other_info->dns_name1;
+ neigh->replica_flags = reps_from->replica_flags;
+ neigh->last_attempt = reps_from->last_attempt;
+ neigh->source_dsa_obj_guid = reps_from->source_dsa_obj_guid;
+
+ ret = dsdb_find_dn_by_guid(samdb, mem_ctx, &reps_from->source_dsa_obj_guid,
+ DSDB_SEARCH_SHOW_RECYCLED,
+ &source_dsa_dn);
+
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find DN for neighbor GUID %s\n",
+ GUID_string(mem_ctx, &reps_from->source_dsa_obj_guid)));
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ neigh->source_dsa_obj_dn = ldb_dn_get_linearized(source_dsa_dn);
+ neigh->naming_context_dn = ldb_dn_get_linearized(nc_dn);
+
+ if (dsdb_find_guid_by_dn(samdb, nc_dn,
+ &neigh->naming_context_obj_guid)
+ != LDB_SUCCESS) {
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ if (!GUID_all_zero(&reps_from->transport_guid)) {
+ ret = dsdb_find_dn_by_guid(samdb, mem_ctx, &reps_from->transport_guid,
+ DSDB_SEARCH_SHOW_RECYCLED,
+ &transport_obj_dn);
+ if (ret != LDB_SUCCESS) {
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+ }
+
+ neigh->transport_obj_dn = ldb_dn_get_linearized(transport_obj_dn);
+ neigh->source_dsa_invocation_id = reps_from->source_dsa_invocation_id;
+ neigh->transport_obj_guid = reps_from->transport_guid;
+ neigh->highest_usn = reps_from->highwatermark.highest_usn;
+ neigh->tmp_highest_usn = reps_from->highwatermark.tmp_highest_usn;
+ neigh->last_success = reps_from->last_success;
+ neigh->result_last_attempt = reps_from->result_last_attempt;
+ neigh->consecutive_sync_failures = reps_from->consecutive_sync_failures;
+ neigh->reserved = 0; /* Unused. MUST be 0. */
+
+ return WERR_OK;
+}
+
+/*
+ Get the inbound neighbours of this DC
+ See details on MS-DRSR 4.1.13.3, for infoType DS_REPL_INFO_NEIGHBORS
+*/
+static WERROR kccdrs_replica_get_info_neighbours(TALLOC_CTX *mem_ctx,
+ struct kccsrv_service *service,
+ struct ldb_context *samdb,
+ struct drsuapi_DsReplicaGetInfo *r,
+ union drsuapi_DsReplicaInfo *reply,
+ uint32_t base_index,
+ struct GUID req_src_dsa_guid,
+ const char *object_dn_str)
+{
+ WERROR status;
+ uint32_t i, j;
+ struct ldb_dn *nc_dn = NULL;
+ struct ncList *p_nc_list = NULL;
+ struct repsFromToBlob *reps_from_blob = NULL;
+ struct repsFromTo2 *reps_from = NULL;
+ uint32_t c_reps_from;
+ uint32_t i_rep;
+ struct ncList *nc_list = NULL;
+
+ status = get_ncs_list(mem_ctx, samdb, service, object_dn_str, &nc_list);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ i = j = 0;
+
+ reply->neighbours = talloc_zero(mem_ctx, struct drsuapi_DsReplicaNeighbourCtr);
+ W_ERROR_HAVE_NO_MEMORY(reply->neighbours);
+ reply->neighbours->reserved = 0;
+ reply->neighbours->count = 0;
+
+ /* foreach nc in ncs */
+ for (p_nc_list = nc_list; p_nc_list != NULL; p_nc_list = p_nc_list->next) {
+
+ nc_dn = p_nc_list->dn;
+
+ /* load the nc's repsFromTo blob */
+ status = dsdb_loadreps(samdb, mem_ctx, nc_dn, "repsFrom",
+ &reps_from_blob, &c_reps_from);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ /* foreach r in nc!repsFrom */
+ for (i_rep = 0; i_rep < c_reps_from; i_rep++) {
+
+ /* put all info on reps_from */
+ if (reps_from_blob[i_rep].version == 1) {
+ status = copy_repsfrom_1_to_2(mem_ctx, &reps_from,
+ &reps_from_blob[i_rep].ctr.ctr1);
+ W_ERROR_NOT_OK_RETURN(status);
+ } else { /* reps_from->version == 2 */
+ reps_from = &reps_from_blob[i_rep].ctr.ctr2;
+ }
+
+ if (GUID_all_zero(&req_src_dsa_guid) ||
+ GUID_equal(&req_src_dsa_guid,
+ &reps_from->source_dsa_obj_guid)) {
+
+ if (i >= base_index) {
+ struct drsuapi_DsReplicaNeighbour neigh;
+ ZERO_STRUCT(neigh);
+ status = fill_neighbor_from_repsFrom(mem_ctx, samdb,
+ nc_dn, &neigh,
+ reps_from);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ /* append the neighbour to the neighbours array */
+ reply->neighbours->array = talloc_realloc(mem_ctx,
+ reply->neighbours->array,
+ struct drsuapi_DsReplicaNeighbour,
+ reply->neighbours->count + 1);
+ reply->neighbours->array[reply->neighbours->count] = neigh;
+ reply->neighbours->count++;
+ j++;
+ }
+
+ i++;
+ }
+ }
+ }
+
+ return WERR_OK;
+}
+
+static WERROR fill_neighbor_from_repsTo(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb, struct ldb_dn *nc_dn,
+ struct drsuapi_DsReplicaNeighbour *neigh,
+ struct repsFromTo2 *reps_to)
+{
+ int ret;
+ struct ldb_dn *source_dsa_dn;
+
+ neigh->source_dsa_address = reps_to->other_info->dns_name1;
+ neigh->replica_flags = reps_to->replica_flags;
+ neigh->last_attempt = reps_to->last_attempt;
+ neigh->source_dsa_obj_guid = reps_to->source_dsa_obj_guid;
+
+ ret = dsdb_find_dn_by_guid(samdb, mem_ctx,
+ &reps_to->source_dsa_obj_guid,
+ DSDB_SEARCH_SHOW_RECYCLED,
+ &source_dsa_dn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find DN for neighbor GUID %s\n",
+ GUID_string(mem_ctx, &reps_to->source_dsa_obj_guid)));
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ neigh->source_dsa_obj_dn = ldb_dn_get_linearized(source_dsa_dn);
+ neigh->naming_context_dn = ldb_dn_get_linearized(nc_dn);
+
+ ret = dsdb_find_guid_by_dn(samdb, nc_dn,
+ &neigh->naming_context_obj_guid);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find GUID for DN %s\n",
+ ldb_dn_get_linearized(nc_dn)));
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ neigh->last_success = reps_to->last_success;
+ neigh->result_last_attempt = reps_to->result_last_attempt;
+ neigh->consecutive_sync_failures = reps_to->consecutive_sync_failures;
+ return WERR_OK;
+}
+
+/*
+ Get the outbound neighbours of this DC
+ See details on MS-DRSR 4.1.13.3, for infoType DS_REPL_INFO_REPSTO
+*/
+static WERROR kccdrs_replica_get_info_repsto(TALLOC_CTX *mem_ctx,
+ struct kccsrv_service *service,
+ struct ldb_context *samdb,
+ struct drsuapi_DsReplicaGetInfo *r,
+ union drsuapi_DsReplicaInfo *reply,
+ uint32_t base_index,
+ struct GUID req_src_dsa_guid,
+ const char *object_dn_str)
+{
+ WERROR status;
+ uint32_t i, j;
+ struct ncList *p_nc_list = NULL;
+ struct ldb_dn *nc_dn = NULL;
+ struct repsFromToBlob *reps_to_blob;
+ struct repsFromTo2 *reps_to;
+ uint32_t c_reps_to;
+ uint32_t i_rep;
+ struct ncList *nc_list = NULL;
+
+ status = get_ncs_list(mem_ctx, samdb, service, object_dn_str, &nc_list);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ i = j = 0;
+
+ reply->repsto = talloc_zero(mem_ctx, struct drsuapi_DsReplicaNeighbourCtr);
+ W_ERROR_HAVE_NO_MEMORY(reply->repsto);
+ reply->repsto->reserved = 0;
+ reply->repsto->count = 0;
+
+ /* foreach nc in ncs */
+ for (p_nc_list = nc_list; p_nc_list != NULL; p_nc_list = p_nc_list->next) {
+
+ nc_dn = p_nc_list->dn;
+
+ status = dsdb_loadreps(samdb, mem_ctx, nc_dn, "repsTo",
+ &reps_to_blob, &c_reps_to);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ /* foreach r in nc!repsTo */
+ for (i_rep = 0; i_rep < c_reps_to; i_rep++) {
+ struct drsuapi_DsReplicaNeighbour neigh;
+ ZERO_STRUCT(neigh);
+
+ /* put all info on reps_to */
+ if (reps_to_blob[i_rep].version == 1) {
+ status = copy_repsfrom_1_to_2(mem_ctx,
+ &reps_to,
+ &reps_to_blob[i_rep].ctr.ctr1);
+ W_ERROR_NOT_OK_RETURN(status);
+ } else { /* reps_to->version == 2 */
+ reps_to = &reps_to_blob[i_rep].ctr.ctr2;
+ }
+
+ if (i >= base_index) {
+ status = fill_neighbor_from_repsTo(mem_ctx,
+ samdb, nc_dn,
+ &neigh, reps_to);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ /* append the neighbour to the neighbours array */
+ reply->repsto->array = talloc_realloc(mem_ctx,
+ reply->repsto->array,
+ struct drsuapi_DsReplicaNeighbour,
+ reply->repsto->count + 1);
+ reply->repsto->array[reply->repsto->count++] = neigh;
+ j++;
+ }
+ i++;
+ }
+ }
+
+ return WERR_OK;
+}
+
+NTSTATUS kccdrs_replica_get_info(struct irpc_message *msg,
+ struct drsuapi_DsReplicaGetInfo *req)
+{
+ WERROR status;
+ struct drsuapi_DsReplicaGetInfoRequest1 *req1;
+ struct drsuapi_DsReplicaGetInfoRequest2 *req2;
+ uint32_t base_index;
+ union drsuapi_DsReplicaInfo *reply;
+ struct GUID req_src_dsa_guid;
+ const char *object_dn_str = NULL;
+ struct kccsrv_service *service;
+ struct ldb_context *samdb;
+ TALLOC_CTX *mem_ctx;
+ enum drsuapi_DsReplicaInfoType info_type;
+
+ service = talloc_get_type(msg->private_data, struct kccsrv_service);
+ samdb = service->samdb;
+ mem_ctx = talloc_new(msg);
+ NT_STATUS_HAVE_NO_MEMORY(mem_ctx);
+
+#if 0
+ NDR_PRINT_IN_DEBUG(drsuapi_DsReplicaGetInfo, req);
+#endif
+
+ /* check request version */
+ if (req->in.level != DRSUAPI_DS_REPLICA_GET_INFO &&
+ req->in.level != DRSUAPI_DS_REPLICA_GET_INFO2)
+ {
+ DEBUG(1,(__location__ ": Unsupported DsReplicaGetInfo level %u\n",
+ req->in.level));
+ status = WERR_REVISION_MISMATCH;
+ goto done;
+ }
+
+ if (req->in.level == DRSUAPI_DS_REPLICA_GET_INFO) {
+ req1 = &req->in.req->req1;
+ base_index = 0;
+ info_type = req1->info_type;
+ object_dn_str = req1->object_dn;
+ req_src_dsa_guid = req1->source_dsa_guid;
+ } else { /* r->in.level == DRSUAPI_DS_REPLICA_GET_INFO2 */
+ req2 = &req->in.req->req2;
+ if (req2->enumeration_context == 0xffffffff) {
+ /* no more data is available */
+ status = WERR_NO_MORE_ITEMS; /* on MS-DRSR it is ERROR_NO_MORE_ITEMS */
+ goto done;
+ }
+
+ base_index = req2->enumeration_context;
+ info_type = req2->info_type;
+ object_dn_str = req2->object_dn;
+ req_src_dsa_guid = req2->source_dsa_guid;
+ }
+
+ reply = req->out.info;
+ *req->out.info_type = info_type;
+
+ /* Based on the infoType requested, retrieve the corresponding
+ * information and construct the response message */
+ switch (info_type) {
+
+ case DRSUAPI_DS_REPLICA_INFO_NEIGHBORS:
+ status = kccdrs_replica_get_info_neighbours(mem_ctx, service, samdb, req,
+ reply, base_index, req_src_dsa_guid,
+ object_dn_str);
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_REPSTO:
+ status = kccdrs_replica_get_info_repsto(mem_ctx, service, samdb, req,
+ reply, base_index, req_src_dsa_guid,
+ object_dn_str);
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_CURSORS: /* On MS-DRSR it is DS_REPL_INFO_CURSORS_FOR_NC */
+ status = kccdrs_replica_get_info_cursors(mem_ctx, samdb, req, reply,
+ ldb_dn_new(mem_ctx, samdb, object_dn_str));
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_CURSORS2: /* On MS-DRSR it is DS_REPL_INFO_CURSORS_2_FOR_NC */
+ status = kccdrs_replica_get_info_cursors2(mem_ctx, samdb, req, reply,
+ ldb_dn_new(mem_ctx, samdb, object_dn_str));
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_PENDING_OPS:
+ status = kccdrs_replica_get_info_pending_ops(mem_ctx, samdb, req, reply,
+ ldb_dn_new(mem_ctx, samdb, object_dn_str));
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_CURSORS3: /* On MS-DRSR it is DS_REPL_INFO_CURSORS_3_FOR_NC */
+ status = WERR_NOT_SUPPORTED;
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_UPTODATE_VECTOR_V1: /* On MS-DRSR it is DS_REPL_INFO_UPTODATE_VECTOR_V1 */
+ status = WERR_NOT_SUPPORTED;
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_OBJ_METADATA: /* On MS-DRSR it is DS_REPL_INFO_METADATA_FOR_OBJ */
+ /*
+ * It should be too complicated to filter the metadata2 to remove the additional data
+ * as metadata2 is a superset of metadata
+ */
+ status = WERR_NOT_SUPPORTED;
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_OBJ_METADATA2: /* On MS-DRSR it is DS_REPL_INFO_METADATA_FOR_OBJ */
+ status = kccdrs_replica_get_info_obj_metadata2(mem_ctx, samdb, req, reply,
+ ldb_dn_new(mem_ctx, samdb, object_dn_str), base_index);
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_ATTRIBUTE_VALUE_METADATA: /* On MS-DRSR it is DS_REPL_INFO_METADATA_FOR_ATTR_VALUE */
+ status = WERR_NOT_SUPPORTED;
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_ATTRIBUTE_VALUE_METADATA2: /* On MS-DRSR it is DS_REPL_INFO_METADATA_2_FOR_ATTR_VALUE */
+ status = WERR_NOT_SUPPORTED;
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_KCC_DSA_CONNECT_FAILURES: /* On MS-DRSR it is DS_REPL_INFO_KCC_DSA_CONNECT_FAILURES */
+ status = WERR_NOT_SUPPORTED;
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_KCC_DSA_LINK_FAILURES: /* On MS-DRSR it is DS_REPL_INFO_KCC_LINK_FAILURES */
+ status = WERR_NOT_SUPPORTED;
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_CLIENT_CONTEXTS: /* On MS-DRSR it is DS_REPL_INFO_CLIENT_CONTEXTS */
+ status = WERR_NOT_SUPPORTED;
+ break;
+ case DRSUAPI_DS_REPLICA_INFO_SERVER_OUTGOING_CALLS: /* On MS-DRSR it is DS_REPL_INFO_SERVER_OUTGOING_CALLS */
+ status = WERR_NOT_SUPPORTED;
+ break;
+ default:
+ DEBUG(1,(__location__ ": Unsupported DsReplicaGetInfo info_type %u\n",
+ info_type));
+ status = WERR_INVALID_LEVEL;
+ break;
+ }
+
+done:
+ /* put the status on the result field of the reply */
+ req->out.result = status;
+#if 0
+ NDR_PRINT_OUT_DEBUG(drsuapi_DsReplicaGetInfo, req);
+#endif
+ return NT_STATUS_OK;
+}
diff --git a/source4/dsdb/kcc/kcc_periodic.c b/source4/dsdb/kcc/kcc_periodic.c
new file mode 100644
index 0000000..7f0a532
--- /dev/null
+++ b/source4/dsdb/kcc/kcc_periodic.c
@@ -0,0 +1,825 @@
+/*
+ Unix SMB/CIFS Implementation.
+ KCC service periodic handling
+
+ Copyright (C) Andrew Tridgell 2009
+ based on repl service code
+
+ 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 "lib/events/events.h"
+#include "dsdb/samdb/samdb.h"
+#include "auth/auth.h"
+#include "samba/service.h"
+#include "lib/messaging/irpc.h"
+#include "dsdb/kcc/kcc_connection.h"
+#include "dsdb/kcc/kcc_service.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.h"
+#include "librpc/gen_ndr/ndr_misc.h"
+#include "librpc/gen_ndr/ndr_drsuapi.h"
+#include "librpc/gen_ndr/ndr_drsblobs.h"
+#include "librpc/gen_ndr/ndr_irpc_c.h"
+#include "param/param.h"
+#include "dsdb/common/util.h"
+
+/*
+ * see if two repsFromToBlob blobs are for the same source DSA
+ */
+static bool kccsrv_same_source_dsa(struct repsFromToBlob *r1, struct repsFromToBlob *r2)
+{
+ return GUID_equal(&r1->ctr.ctr1.source_dsa_obj_guid,
+ &r2->ctr.ctr1.source_dsa_obj_guid);
+}
+
+/*
+ * see if a repsFromToBlob is in a list
+ */
+static bool reps_in_list(struct repsFromToBlob *r, struct repsFromToBlob *reps, uint32_t count)
+{
+ uint32_t i;
+ for (i=0; i<count; i++) {
+ if (kccsrv_same_source_dsa(r, &reps[i])) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/*
+ make sure we only add repsFrom entries for DCs who are masters for
+ the partition
+ */
+static bool check_MasterNC(struct kccsrv_service *service, struct dsdb_ldb_dn_list_node *p, struct repsFromToBlob *r,
+ struct ldb_result *res)
+{
+ struct repsFromTo1 *r1 = &r->ctr.ctr1;
+ struct GUID invocation_id = r1->source_dsa_invocation_id;
+ unsigned int i, j;
+ TALLOC_CTX *tmp_ctx;
+
+ /* we are expecting only version 1 */
+ SMB_ASSERT(r->version == 1);
+
+ tmp_ctx = talloc_new(p);
+ if (!tmp_ctx) {
+ return false;
+ }
+
+ for (i=0; i<res->count; i++) {
+ struct ldb_message *msg = res->msgs[i];
+ struct ldb_message_element *el;
+ struct ldb_dn *dn;
+
+ struct GUID id2 = samdb_result_guid(msg, "invocationID");
+ if (GUID_all_zero(&id2) ||
+ !GUID_equal(&invocation_id, &id2)) {
+ continue;
+ }
+
+ el = ldb_msg_find_element(msg, "msDS-hasMasterNCs");
+ if (!el || el->num_values == 0) {
+ el = ldb_msg_find_element(msg, "hasMasterNCs");
+ if (!el || el->num_values == 0) {
+ continue;
+ }
+ }
+ for (j=0; j<el->num_values; j++) {
+ dn = ldb_dn_from_ldb_val(tmp_ctx, service->samdb, &el->values[j]);
+ if (!ldb_dn_validate(dn)) {
+ talloc_free(dn);
+ continue;
+ }
+ if (ldb_dn_compare(dn, p->dn) == 0) {
+ DEBUG(5,("%s %s match on %s in %s\n",
+ r1->other_info->dns_name,
+ el->name,
+ ldb_dn_get_linearized(dn),
+ ldb_dn_get_linearized(msg->dn)));
+ talloc_free(tmp_ctx);
+ return true;
+ }
+ talloc_free(dn);
+ }
+ }
+ talloc_free(tmp_ctx);
+ return false;
+}
+
+struct kccsrv_notify_drepl_server_state {
+ struct dreplsrv_refresh r;
+};
+
+static void kccsrv_notify_drepl_server_done(struct tevent_req *subreq);
+
+/**
+ * Force dreplsrv to update its state as topology is changed
+ */
+static void kccsrv_notify_drepl_server(struct kccsrv_service *s,
+ TALLOC_CTX *mem_ctx)
+{
+ struct kccsrv_notify_drepl_server_state *state;
+ struct dcerpc_binding_handle *irpc_handle;
+ struct tevent_req *subreq;
+
+ state = talloc_zero(s, struct kccsrv_notify_drepl_server_state);
+ if (state == NULL) {
+ return;
+ }
+
+ irpc_handle = irpc_binding_handle_by_name(state, s->task->msg_ctx,
+ "dreplsrv", &ndr_table_irpc);
+ if (irpc_handle == NULL) {
+ /* dreplsrv is not running yet */
+ TALLOC_FREE(state);
+ return;
+ }
+
+ subreq = dcerpc_dreplsrv_refresh_r_send(state, s->task->event_ctx,
+ irpc_handle, &state->r);
+ if (subreq == NULL) {
+ TALLOC_FREE(state);
+ return;
+ }
+ tevent_req_set_callback(subreq, kccsrv_notify_drepl_server_done, state);
+}
+
+static void kccsrv_notify_drepl_server_done(struct tevent_req *subreq)
+{
+ struct kccsrv_notify_drepl_server_state *state =
+ tevent_req_callback_data(subreq,
+ struct kccsrv_notify_drepl_server_state);
+
+ dcerpc_dreplsrv_refresh_r_recv(subreq, state);
+ TALLOC_FREE(subreq);
+
+ /* we don't care about errors */
+ TALLOC_FREE(state);
+}
+
+uint32_t kccsrv_replica_flags(struct kccsrv_service *s)
+{
+ if (s->am_rodc) {
+ return DRSUAPI_DRS_INIT_SYNC |
+ DRSUAPI_DRS_PER_SYNC |
+ DRSUAPI_DRS_ADD_REF |
+ DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING |
+ DRSUAPI_DRS_NONGC_RO_REP;
+ }
+ return DRSUAPI_DRS_INIT_SYNC |
+ DRSUAPI_DRS_PER_SYNC |
+ DRSUAPI_DRS_ADD_REF |
+ DRSUAPI_DRS_WRIT_REP;
+}
+
+/*
+ * add any missing repsFrom structures to our partitions
+ */
+NTSTATUS kccsrv_add_repsFrom(struct kccsrv_service *s, TALLOC_CTX *mem_ctx,
+ struct repsFromToBlob *reps, uint32_t count,
+ struct ldb_result *res)
+{
+ struct dsdb_ldb_dn_list_node *p;
+ bool notify_dreplsrv = false;
+ uint32_t replica_flags = kccsrv_replica_flags(s);
+
+ /* update the repsFrom on all partitions */
+ for (p=s->partitions; p; p=p->next) {
+ struct repsFromToBlob *our_reps;
+ uint32_t our_count;
+ WERROR werr;
+ uint32_t i, j;
+ bool modified = false;
+
+ werr = dsdb_loadreps(s->samdb, mem_ctx, p->dn, "repsFrom", &our_reps, &our_count);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0,(__location__ ": Failed to load repsFrom from %s - %s\n",
+ ldb_dn_get_linearized(p->dn), ldb_errstring(s->samdb)));
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ /* see if the entry already exists */
+ for (i=0; i<count; i++) {
+ for (j=0; j<our_count; j++) {
+ if (kccsrv_same_source_dsa(&reps[i], &our_reps[j])) {
+ /* we already have this one -
+ check the replica_flags are right */
+ if (replica_flags != our_reps[j].ctr.ctr1.replica_flags) {
+ /* we need to update the old one with
+ * the new flags
+ */
+ our_reps[j].ctr.ctr1.replica_flags = replica_flags;
+ modified = true;
+ }
+ break;
+ }
+ }
+ if (j == our_count) {
+ /* we don't have the new one - add it
+ * if it is a master
+ */
+ if (res && !check_MasterNC(s, p, &reps[i], res)) {
+ /* its not a master, we don't
+ want to pull from it */
+ continue;
+ }
+ /* we need to add it to our repsFrom */
+ our_reps = talloc_realloc(mem_ctx, our_reps, struct repsFromToBlob, our_count+1);
+ NT_STATUS_HAVE_NO_MEMORY(our_reps);
+ our_reps[our_count] = reps[i];
+ our_reps[our_count].ctr.ctr1.replica_flags = replica_flags;
+ our_count++;
+ modified = true;
+ DEBUG(4,(__location__ ": Added repsFrom for %s\n",
+ reps[i].ctr.ctr1.other_info->dns_name));
+ }
+ }
+
+ /* remove any stale ones */
+ for (i=0; i<our_count; i++) {
+ if (!reps_in_list(&our_reps[i], reps, count) ||
+ (res && !check_MasterNC(s, p, &our_reps[i], res))) {
+ DEBUG(4,(__location__ ": Removed repsFrom for %s\n",
+ our_reps[i].ctr.ctr1.other_info->dns_name));
+ memmove(&our_reps[i], &our_reps[i+1], (our_count-(i+1))*sizeof(our_reps[0]));
+ our_count--;
+ i--;
+ modified = true;
+ }
+ }
+
+ if (modified) {
+ werr = dsdb_savereps(s->samdb, mem_ctx, p->dn, "repsFrom", our_reps, our_count);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0,(__location__ ": Failed to save repsFrom to %s - %s\n",
+ ldb_dn_get_linearized(p->dn), ldb_errstring(s->samdb)));
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+ /* dreplsrv should refresh its state */
+ notify_dreplsrv = true;
+ }
+
+ /* remove stale repsTo entries */
+ modified = false;
+ werr = dsdb_loadreps(s->samdb, mem_ctx, p->dn, "repsTo", &our_reps, &our_count);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0,(__location__ ": Failed to load repsTo from %s - %s\n",
+ ldb_dn_get_linearized(p->dn), ldb_errstring(s->samdb)));
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ /* remove any stale ones */
+ for (i=0; i<our_count; i++) {
+ if (!reps_in_list(&our_reps[i], reps, count)) {
+ DEBUG(4,(__location__ ": Removed repsTo for %s\n",
+ our_reps[i].ctr.ctr1.other_info->dns_name));
+ memmove(&our_reps[i], &our_reps[i+1], (our_count-(i+1))*sizeof(our_reps[0]));
+ our_count--;
+ i--;
+ modified = true;
+ }
+ }
+
+ if (modified) {
+ werr = dsdb_savereps(s->samdb, mem_ctx, p->dn, "repsTo", our_reps, our_count);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0,(__location__ ": Failed to save repsTo to %s - %s\n",
+ ldb_dn_get_linearized(p->dn), ldb_errstring(s->samdb)));
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+ /* dreplsrv should refresh its state */
+ notify_dreplsrv = true;
+ }
+ }
+
+ /* notify dreplsrv toplogy has changed */
+ if (notify_dreplsrv) {
+ kccsrv_notify_drepl_server(s, mem_ctx);
+ }
+
+ return NT_STATUS_OK;
+
+}
+
+
+/*
+ form a unique list of DNs from a search result and a given set of attributes
+ */
+static int kccsrv_dn_list(struct ldb_context *ldb, struct ldb_result *res,
+ TALLOC_CTX *mem_ctx,
+ const char **attrs,
+ struct ldb_dn ***dn_list, int *dn_count)
+{
+ int i;
+ struct ldb_dn **nc_list = NULL;
+ int nc_count = 0;
+
+ nc_list = talloc_array(mem_ctx, struct ldb_dn *, 0);
+ if (nc_list == NULL) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ /* gather up a list of all NCs in this forest */
+ for (i=0; i<res->count; i++) {
+ struct ldb_message *msg = res->msgs[i];
+ int j;
+ for (j=0; attrs[j]; j++) {
+ struct ldb_message_element *el;
+ int k;
+
+ el = ldb_msg_find_element(msg, attrs[j]);
+ if (el == NULL) continue;
+ for (k=0; k<el->num_values; k++) {
+ struct ldb_dn *dn;
+ dn = ldb_dn_from_ldb_val(nc_list, ldb, &el->values[k]);
+ if (dn != NULL) {
+ int l;
+ for (l=0; l<nc_count; l++) {
+ if (ldb_dn_compare(nc_list[l], dn) == 0) break;
+ }
+ if (l < nc_count) continue;
+ nc_list = talloc_realloc(mem_ctx, nc_list, struct ldb_dn *, nc_count+1);
+ if (nc_list == NULL) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ nc_list[nc_count] = dn;
+ nc_count++;
+ }
+ }
+ }
+ }
+
+ (*dn_list) = nc_list;
+ (*dn_count) = nc_count;
+ return LDB_SUCCESS;
+}
+
+
+/*
+ look for any additional global catalog partitions that we should be
+ replicating (by looking for msDS-HasDomainNCs), and add them to our
+ hasPartialReplicaNCs NTDS attribute
+ */
+static int kccsrv_gc_update(struct kccsrv_service *s, struct ldb_result *res)
+{
+ int i;
+ struct ldb_dn **nc_list = NULL;
+ int nc_count = 0;
+ struct ldb_dn **our_nc_list = NULL;
+ int our_nc_count = 0;
+ const char *attrs1[] = { "msDS-hasMasterNCs", "hasMasterNCs", "msDS-HasDomainNCs", NULL };
+ const char *attrs2[] = { "msDS-hasMasterNCs", "hasMasterNCs", "msDS-HasDomainNCs", "hasPartialReplicaNCs", NULL };
+ int ret;
+ TALLOC_CTX *tmp_ctx = talloc_new(res);
+ struct ldb_result *res2;
+ struct ldb_message *msg;
+
+ /* get a complete list of NCs for the forest */
+ ret = kccsrv_dn_list(s->samdb, res, tmp_ctx, attrs1, &nc_list, &nc_count);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1,("Failed to get NC list for GC update - %s\n", ldb_errstring(s->samdb)));
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ /* get a list of what NCs we are already replicating */
+ ret = dsdb_search_dn(s->samdb, tmp_ctx, &res2, samdb_ntds_settings_dn(s->samdb, tmp_ctx), attrs2, 0);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1,("Failed to get our NC list attributes for GC update - %s\n", ldb_errstring(s->samdb)));
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ ret = kccsrv_dn_list(s->samdb, res2, tmp_ctx, attrs2, &our_nc_list, &our_nc_count);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1,("Failed to get our NC list for GC update - %s\n", ldb_errstring(s->samdb)));
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ msg = ldb_msg_new(tmp_ctx);
+ if (msg == NULL) {
+ talloc_free(tmp_ctx);
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ msg->dn = res2->msgs[0]->dn;
+
+ /* see if we are missing any */
+ for (i=0; i<nc_count; i++) {
+ int j;
+ for (j=0; j<our_nc_count; j++) {
+ if (ldb_dn_compare(nc_list[i], our_nc_list[j]) == 0) break;
+ }
+ if (j == our_nc_count) {
+ /* its a new one */
+ ret = ldb_msg_add_string(msg, "hasPartialReplicaNCs",
+ ldb_dn_get_extended_linearized(msg, nc_list[i], 1));
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ }
+ }
+
+ if (msg->num_elements == 0) {
+ /* none to add */
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+ }
+
+ if (s->am_rodc) {
+ DEBUG(5, ("%d partial replica should be added but we are RODC so we skip\n", msg->num_elements));
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+ }
+
+ msg->elements[0].flags = LDB_FLAG_MOD_ADD;
+
+ ret = dsdb_modify(s->samdb, msg, 0);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,("Failed to add hasPartialReplicaNCs - %s\n",
+ ldb_errstring(s->samdb)));
+ }
+
+ talloc_free(tmp_ctx);
+ return ret;
+}
+
+
+/*
+ this is the core of our initial simple KCC
+ We just add a repsFrom entry for all DCs we find that have nTDSDSA
+ objects, except for ourselves
+ */
+NTSTATUS kccsrv_simple_update(struct kccsrv_service *s, TALLOC_CTX *mem_ctx)
+{
+ struct ldb_result *res;
+ unsigned int i;
+ int ret;
+ const char *attrs[] = { "objectGUID", "invocationID", "msDS-hasMasterNCs", "hasMasterNCs", "msDS-HasDomainNCs", NULL };
+ struct repsFromToBlob *reps = NULL;
+ uint32_t count = 0;
+ struct kcc_connection_list *ntds_conn, *dsa_conn;
+
+ ret = dsdb_search(s->samdb, mem_ctx, &res, s->config_dn, LDB_SCOPE_SUBTREE,
+ attrs, DSDB_SEARCH_SHOW_EXTENDED_DN, "objectClass=nTDSDSA");
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed nTDSDSA search - %s\n", ldb_errstring(s->samdb)));
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ if (samdb_is_gc(s->samdb)) {
+ kccsrv_gc_update(s, res);
+ }
+
+ /* get the current list of connections */
+ ntds_conn = kccsrv_find_connections(s, mem_ctx);
+
+ dsa_conn = talloc_zero(mem_ctx, struct kcc_connection_list);
+
+ for (i=0; i<res->count; i++) {
+ struct repsFromTo1 *r1;
+ struct GUID ntds_guid, invocation_id;
+
+ ntds_guid = samdb_result_guid(res->msgs[i], "objectGUID");
+ if (GUID_equal(&ntds_guid, &s->ntds_guid)) {
+ /* don't replicate with ourselves */
+ continue;
+ }
+
+ invocation_id = samdb_result_guid(res->msgs[i], "invocationID");
+
+ reps = talloc_realloc(mem_ctx, reps, struct repsFromToBlob, count+1);
+ NT_STATUS_HAVE_NO_MEMORY(reps);
+
+ ZERO_STRUCT(reps[count]);
+ reps[count].version = 1;
+ r1 = &reps[count].ctr.ctr1;
+
+ r1->other_info = talloc_zero(reps, struct repsFromTo1OtherInfo);
+ r1->other_info->dns_name = samdb_ntds_msdcs_dns_name(s->samdb, reps, &ntds_guid);
+ r1->source_dsa_obj_guid = ntds_guid;
+ r1->source_dsa_invocation_id = invocation_id;
+ r1->replica_flags = kccsrv_replica_flags(s);
+ memset(r1->schedule, 0x11, sizeof(r1->schedule));
+
+ dsa_conn->servers = talloc_realloc(dsa_conn, dsa_conn->servers,
+ struct kcc_connection,
+ dsa_conn->count + 1);
+ NT_STATUS_HAVE_NO_MEMORY(dsa_conn->servers);
+ dsa_conn->servers[dsa_conn->count].dsa_guid = r1->source_dsa_obj_guid;
+ dsa_conn->count++;
+
+ count++;
+ }
+
+ kccsrv_apply_connections(s, ntds_conn, dsa_conn);
+
+ return kccsrv_add_repsFrom(s, mem_ctx, reps, count, res);
+}
+
+
+static void kccsrv_periodic_run(struct kccsrv_service *service);
+
+static void kccsrv_periodic_handler_te(struct tevent_context *ev, struct tevent_timer *te,
+ struct timeval t, void *ptr)
+{
+ struct kccsrv_service *service = talloc_get_type(ptr, struct kccsrv_service);
+ WERROR status;
+
+ service->periodic.te = NULL;
+
+ kccsrv_periodic_run(service);
+
+ status = kccsrv_periodic_schedule(service, service->periodic.interval);
+ if (!W_ERROR_IS_OK(status)) {
+ task_server_terminate(service->task, win_errstr(status), true);
+ return;
+ }
+}
+
+WERROR kccsrv_periodic_schedule(struct kccsrv_service *service, uint32_t next_interval)
+{
+ TALLOC_CTX *tmp_mem;
+ struct tevent_timer *new_te;
+ struct timeval next_time;
+
+ /* prevent looping */
+ if (next_interval == 0) next_interval = 1;
+
+ next_time = timeval_current_ofs(next_interval, 50);
+
+ if (service->periodic.te) {
+ /*
+ * if the timestamp of the new event is higher,
+ * as current next we don't need to reschedule
+ */
+ if (timeval_compare(&next_time, &service->periodic.next_event) > 0) {
+ return WERR_OK;
+ }
+ }
+
+ /* reset the next scheduled timestamp */
+ service->periodic.next_event = next_time;
+
+ new_te = tevent_add_timer(service->task->event_ctx, service,
+ service->periodic.next_event,
+ kccsrv_periodic_handler_te, service);
+ W_ERROR_HAVE_NO_MEMORY(new_te);
+
+ tmp_mem = talloc_new(service);
+ DEBUG(4,("kccsrv_periodic_schedule(%u) %sscheduled for: %s\n",
+ next_interval,
+ (service->periodic.te?"re":""),
+ nt_time_string(tmp_mem, timeval_to_nttime(&next_time))));
+ talloc_free(tmp_mem);
+
+ talloc_free(service->periodic.te);
+ service->periodic.te = new_te;
+
+ return WERR_OK;
+}
+
+/*
+ * Check to see if any dns entries need scavenging. This only occurs if aging
+ * is enabled in general ("zone scavenging" lpcfg) and on the zone
+ * (zone->fAging is true).
+ */
+static NTSTATUS kccsrv_dns_zone_scavenging(
+ struct kccsrv_service *s,
+ TALLOC_CTX *mem_ctx)
+{
+
+ time_t current_time = time(NULL);
+ time_t dns_scavenge_interval;
+ NTSTATUS status;
+ char *error_string = NULL;
+
+ /*
+ * Only perform zone scavenging if it's been enabled.
+ * (it still might be disabled on all zones).
+ */
+ if (!lpcfg_dns_zone_scavenging(s->task->lp_ctx)) {
+ DBG_INFO("DNS scavenging not enabled\n");
+ return NT_STATUS_OK;
+ }
+
+ dns_scavenge_interval = lpcfg_parm_int(s->task->lp_ctx,
+ NULL,
+ "dnsserver",
+ "scavenging_interval",
+ 2 * 60 * 60);
+ if ((current_time - s->last_dns_scavenge) > dns_scavenge_interval) {
+ s->last_dns_scavenge = current_time;
+ status = dns_tombstone_records(mem_ctx, s->samdb,
+ &error_string);
+ if (!NT_STATUS_IS_OK(status)) {
+ const char *err = NULL;
+ if (error_string != NULL) {
+ err = error_string;
+ } else {
+ err = nt_errstr(status);
+ }
+ DBG_ERR("DNS record scavenging process failed: %s\n",
+ err);
+ return status;
+ }
+ }
+ DBG_INFO("Successfully tombstoned stale DNS records\n");
+ return NT_STATUS_OK;
+}
+/*
+ * check to see if any dns tombstones should be deleted. This is not optional
+ * ([MS-DNSP] "DsTombstoneInterval") -- stale tombstones are useless clutter.
+ *
+ * Windows does it daily at 2am; we do it roughly daily at an uncontrolled
+ * time.
+ */
+static NTSTATUS kccsrv_dns_zone_tombstone_deletion(struct kccsrv_service *s,
+ TALLOC_CTX *mem_ctx)
+{
+ time_t current_time = time(NULL);
+ NTSTATUS status;
+ char *error_string = NULL;
+ time_t dns_collection_interval =
+ lpcfg_parm_int(s->task->lp_ctx,
+ NULL,
+ "dnsserver",
+ "tombstone_collection_interval",
+ 24 * 60 * 60);
+
+ if ((current_time - s->last_dns_tombstone_collection) >
+ dns_collection_interval) {
+ s->last_dns_tombstone_collection = current_time;
+ status = dns_delete_tombstones(mem_ctx, s->samdb,
+ &error_string);
+ if (!NT_STATUS_IS_OK(status)) {
+ const char *err = NULL;
+ if (error_string != NULL) {
+ err = error_string;
+ } else {
+ err = nt_errstr(status);
+ }
+ DBG_ERR("DNS tombstone deletion failed: %s\n", err);
+ return status;
+ }
+ }
+ DBG_INFO("Successfully deleted DNS tombstones\n");
+ return NT_STATUS_OK;
+}
+
+/*
+ check to see if any deleted objects need scavenging
+ */
+static NTSTATUS kccsrv_check_deleted(struct kccsrv_service *s, TALLOC_CTX *mem_ctx)
+{
+ time_t current_time = time(NULL);
+ time_t interval = lpcfg_parm_int(
+ s->task->lp_ctx, NULL, "kccsrv", "check_deleted_interval", 86400);
+ uint32_t tombstoneLifetime;
+ int ret;
+ unsigned int num_objects_removed = 0;
+ unsigned int num_links_removed = 0;
+ NTSTATUS status;
+ char *error_string = NULL;
+
+ if (current_time - s->last_deleted_check < interval) {
+ return NT_STATUS_OK;
+ }
+
+ ret = dsdb_tombstone_lifetime(s->samdb, &tombstoneLifetime);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1,(__location__ ": Failed to get tombstone lifetime\n"));
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ s->last_deleted_check = current_time;
+
+ status = dsdb_garbage_collect_tombstones(mem_ctx, s->samdb,
+ s->partitions,
+ current_time, tombstoneLifetime,
+ &num_objects_removed,
+ &num_links_removed,
+ &error_string);
+
+ if (NT_STATUS_IS_OK(status)) {
+ DEBUG(5, ("garbage_collect_tombstones: Removed %u tombstone objects "
+ "and %u tombstone links successfully\n",
+ num_objects_removed, num_links_removed));
+ } else {
+ DEBUG(2, ("garbage_collect_tombstones: Failure removing tombstone "
+ "objects and links after removing %u tombstone objects "
+ "and %u tombstone links successfully: %s\n",
+ num_objects_removed, num_links_removed,
+ error_string ? error_string : nt_errstr(status)));
+ }
+ return status;
+}
+
+static void kccsrv_periodic_run(struct kccsrv_service *service)
+{
+ TALLOC_CTX *mem_ctx;
+ NTSTATUS status;
+
+ DEBUG(4,("kccsrv_periodic_run(): update\n"));
+
+ mem_ctx = talloc_new(service);
+
+ if (service->samba_kcc_code)
+ status = kccsrv_samba_kcc(service);
+ else {
+ status = kccsrv_simple_update(service, mem_ctx);
+ if (!NT_STATUS_IS_OK(status))
+ DEBUG(0,("kccsrv_simple_update failed - %s\n",
+ nt_errstr(status)));
+ }
+
+ status = kccsrv_check_deleted(service, mem_ctx);
+ if (!NT_STATUS_IS_OK(status)) {
+ DEBUG(0,("kccsrv_check_deleted failed - %s\n", nt_errstr(status)));
+ }
+ status = kccsrv_dns_zone_scavenging(service, mem_ctx);
+ if (!NT_STATUS_IS_OK(status)) {
+ DBG_ERR("kccsrv_dns_zone_aging failed - %s\n",
+ nt_errstr(status));
+ }
+ status = kccsrv_dns_zone_tombstone_deletion(service, mem_ctx);
+ if (!NT_STATUS_IS_OK(status)) {
+ DBG_ERR("kccsrv_dns_zone_tombstone_scavenging failed - %s\n",
+ nt_errstr(status));
+ }
+ talloc_free(mem_ctx);
+}
+
+/* Called when samba_kcc script has finished
+ */
+static void samba_kcc_done(struct tevent_req *subreq)
+{
+ struct kccsrv_service *service =
+ tevent_req_callback_data(subreq, struct kccsrv_service);
+ int rc;
+ int sys_errno;
+
+ service->periodic.subreq = NULL;
+
+ rc = samba_runcmd_recv(subreq, &sys_errno);
+ TALLOC_FREE(subreq);
+
+ if (rc != 0)
+ service->periodic.status =
+ map_nt_error_from_unix_common(sys_errno);
+ else
+ service->periodic.status = NT_STATUS_OK;
+
+ if (!NT_STATUS_IS_OK(service->periodic.status))
+ DEBUG(0,(__location__ ": Failed samba_kcc - %s\n",
+ nt_errstr(service->periodic.status)));
+ else
+ DEBUG(3,("Completed samba_kcc OK\n"));
+}
+
+/* Invocation of the samba_kcc python script for replication
+ * topology generation.
+ */
+NTSTATUS kccsrv_samba_kcc(struct kccsrv_service *service)
+{
+ NTSTATUS status = NT_STATUS_OK;
+ const char * const *samba_kcc_command =
+ lpcfg_samba_kcc_command(service->task->lp_ctx);
+
+ /* kill any existing child */
+ TALLOC_FREE(service->periodic.subreq);
+
+ DEBUG(2, ("Calling samba_kcc script\n"));
+ service->periodic.subreq = samba_runcmd_send(service,
+ service->task->event_ctx,
+ timeval_current_ofs(40, 0),
+ 2, 0, samba_kcc_command, NULL);
+
+ if (service->periodic.subreq == NULL) {
+ status = NT_STATUS_NO_MEMORY;
+ goto xerror;
+ }
+ tevent_req_set_callback(service->periodic.subreq,
+ samba_kcc_done, service);
+
+xerror:
+ if (!NT_STATUS_IS_OK(status))
+ DEBUG(0,(__location__ ": failed - %s\n", nt_errstr(status)));
+ return status;
+}
diff --git a/source4/dsdb/kcc/kcc_service.c b/source4/dsdb/kcc/kcc_service.c
new file mode 100644
index 0000000..066cc07
--- /dev/null
+++ b/source4/dsdb/kcc/kcc_service.c
@@ -0,0 +1,364 @@
+/*
+ Unix SMB/CIFS Implementation.
+
+ KCC service
+
+ Copyright (C) Andrew Tridgell 2009
+ based on repl service code
+
+ 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/samdb/samdb.h"
+#include "auth/auth.h"
+#include "samba/service.h"
+#include "lib/events/events.h"
+#include "lib/messaging/irpc.h"
+#include "dsdb/kcc/kcc_service.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.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 "libds/common/roles.h"
+
+/*
+ establish system creds
+ */
+static WERROR kccsrv_init_creds(struct kccsrv_service *service)
+{
+ service->system_session_info = system_session(service->task->lp_ctx);
+ if (!service->system_session_info) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ return WERR_OK;
+}
+
+/*
+ connect to the local SAM
+ */
+static WERROR kccsrv_connect_samdb(struct kccsrv_service *service, struct loadparm_context *lp_ctx)
+{
+ const struct GUID *ntds_guid;
+
+ service->samdb = samdb_connect(service,
+ service->task->event_ctx,
+ lp_ctx,
+ service->system_session_info,
+ NULL,
+ 0);
+ if (!service->samdb) {
+ return WERR_DS_UNAVAILABLE;
+ }
+
+ ntds_guid = samdb_ntds_objectGUID(service->samdb);
+ if (!ntds_guid) {
+ DBG_ERR("Failed to determine own NTDS objectGUID\n");
+ return WERR_DS_UNAVAILABLE;
+ }
+
+ service->ntds_guid = *ntds_guid;
+
+ if (samdb_rodc(service->samdb, &service->am_rodc) != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to determine RODC status\n"));
+ return WERR_DS_UNAVAILABLE;
+ }
+
+ return WERR_OK;
+}
+
+
+/*
+ load our local partition list
+ */
+static WERROR kccsrv_load_partitions(struct kccsrv_service *s)
+{
+ struct ldb_dn *basedn;
+ struct ldb_result *r;
+ struct ldb_message_element *el;
+ static const char *attrs[] = { "namingContexts", "configurationNamingContext", NULL };
+ unsigned int i;
+ int ret;
+
+ basedn = ldb_dn_new(s, s->samdb, NULL);
+ W_ERROR_HAVE_NO_MEMORY(basedn);
+
+ ret = ldb_search(s->samdb, s, &r, basedn, LDB_SCOPE_BASE, attrs,
+ "(objectClass=*)");
+ talloc_free(basedn);
+ if (ret != LDB_SUCCESS) {
+ return WERR_FOOBAR;
+ } else if (r->count != 1) {
+ talloc_free(r);
+ return WERR_FOOBAR;
+ }
+
+ el = ldb_msg_find_element(r->msgs[0], "namingContexts");
+ if (!el) {
+ return WERR_FOOBAR;
+ }
+
+ for (i=0; i < el->num_values; i++) {
+ const char *v = (const char *)el->values[i].data;
+ struct ldb_dn *pdn;
+ struct dsdb_ldb_dn_list_node *p;
+
+ pdn = ldb_dn_new(s, s->samdb, v);
+ if (!ldb_dn_validate(pdn)) {
+ return WERR_FOOBAR;
+ }
+
+ p = talloc_zero(s, struct dsdb_ldb_dn_list_node);
+ W_ERROR_HAVE_NO_MEMORY(p);
+
+ p->dn = talloc_steal(p, pdn);
+
+ DLIST_ADD(s->partitions, p);
+
+ DEBUG(2, ("kccsrv_partition[%s] loaded\n", v));
+ }
+
+ el = ldb_msg_find_element(r->msgs[0], "configurationNamingContext");
+ if (!el) {
+ return WERR_FOOBAR;
+ }
+ s->config_dn = ldb_dn_new(s, s->samdb, (const char *)el->values[0].data);
+ if (!ldb_dn_validate(s->config_dn)) {
+ return WERR_FOOBAR;
+ }
+
+ talloc_free(r);
+
+ return WERR_OK;
+}
+
+
+struct kcc_manual_runcmd_state {
+ struct irpc_message *msg;
+ struct drsuapi_DsExecuteKCC *r;
+ struct kccsrv_service *service;
+};
+
+
+/*
+ * Called when samba_kcc script has finished
+ */
+static void manual_samba_kcc_done(struct tevent_req *subreq)
+{
+ struct kcc_manual_runcmd_state *st =
+ tevent_req_callback_data(subreq,
+ struct kcc_manual_runcmd_state);
+ int rc;
+ int sys_errno;
+ NTSTATUS status;
+
+ st->service->periodic.subreq = NULL;
+
+ rc = samba_runcmd_recv(subreq, &sys_errno);
+ TALLOC_FREE(subreq);
+
+ if (rc != 0) {
+ status = map_nt_error_from_unix_common(sys_errno);
+ } else {
+ status = NT_STATUS_OK;
+ }
+
+ if (!NT_STATUS_IS_OK(status)) {
+ DEBUG(0,(__location__ ": Failed manual run of samba_kcc - %s\n",
+ nt_errstr(status)));
+ } else {
+ DEBUG(3,("Completed manual run of samba_kcc OK\n"));
+ }
+
+ if (!(st->r->in.req->ctr1.flags & DRSUAPI_DS_EXECUTE_KCC_ASYNCHRONOUS_OPERATION)) {
+ irpc_send_reply(st->msg, status);
+ }
+}
+
+static NTSTATUS kccsrv_execute_kcc(struct irpc_message *msg, struct drsuapi_DsExecuteKCC *r)
+{
+ TALLOC_CTX *mem_ctx;
+ NTSTATUS status = NT_STATUS_OK;
+ struct kccsrv_service *service = talloc_get_type(msg->private_data, struct kccsrv_service);
+
+ const char * const *samba_kcc_command;
+ struct kcc_manual_runcmd_state *st;
+
+ if (!service->samba_kcc_code) {
+ mem_ctx = talloc_new(service);
+
+ status = kccsrv_simple_update(service, mem_ctx);
+ if (!NT_STATUS_IS_OK(status)) {
+ DEBUG(0,("kccsrv_execute_kcc failed - %s\n",
+ nt_errstr(status)));
+ }
+ talloc_free(mem_ctx);
+
+ return NT_STATUS_OK;
+ }
+
+ /* Invocation of the samba_kcc python script for replication
+ * topology generation.
+ */
+
+ samba_kcc_command =
+ lpcfg_samba_kcc_command(service->task->lp_ctx);
+
+ st = talloc(msg, struct kcc_manual_runcmd_state);
+ if (st == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ st->msg = msg;
+ st->r = r;
+ st->service = service;
+
+ /* don't run at the same time as an existing child */
+ if (service->periodic.subreq) {
+ status = NT_STATUS_DS_BUSY;
+ return status;
+ }
+
+ DEBUG(2, ("Calling samba_kcc script\n"));
+ service->periodic.subreq = samba_runcmd_send(service,
+ service->task->event_ctx,
+ timeval_current_ofs(40, 0),
+ 2, 0, samba_kcc_command, NULL);
+
+ if (service->periodic.subreq == NULL) {
+ status = NT_STATUS_NO_MEMORY;
+ DEBUG(0,(__location__ ": failed - %s\n", nt_errstr(status)));
+ return status;
+ } else {
+ tevent_req_set_callback(service->periodic.subreq,
+ manual_samba_kcc_done, st);
+ }
+
+ if (r->in.req->ctr1.flags & DRSUAPI_DS_EXECUTE_KCC_ASYNCHRONOUS_OPERATION) {
+ /* This actually means reply right away, let it run in the background */
+ } else {
+ /* mark the request as replied async, the caller wants to know when this is finished */
+ msg->defer_reply = true;
+ }
+ return status;
+
+}
+
+static NTSTATUS kccsrv_replica_get_info(struct irpc_message *msg, struct drsuapi_DsReplicaGetInfo *r)
+{
+ return kccdrs_replica_get_info(msg, r);
+}
+
+/*
+ startup the kcc service task
+*/
+static NTSTATUS kccsrv_task_init(struct task_server *task)
+{
+ WERROR status;
+ struct kccsrv_service *service;
+ uint32_t periodic_startup_interval;
+
+ switch (lpcfg_server_role(task->lp_ctx)) {
+ case ROLE_STANDALONE:
+ task_server_terminate(task, "kccsrv: no KCC required in standalone configuration", false);
+ return NT_STATUS_INVALID_DOMAIN_ROLE;
+ case ROLE_DOMAIN_MEMBER:
+ task_server_terminate(task, "kccsrv: no KCC required in domain member configuration", false);
+ return NT_STATUS_INVALID_DOMAIN_ROLE;
+ case ROLE_ACTIVE_DIRECTORY_DC:
+ /* Yes, we want a KCC */
+ break;
+ }
+
+ task_server_set_title(task, "task[kccsrv]");
+
+ service = talloc_zero(task, struct kccsrv_service);
+ if (!service) {
+ task_server_terminate(task, "kccsrv_task_init: out of memory", true);
+ return NT_STATUS_NO_MEMORY;
+ }
+ service->task = task;
+ service->startup_time = timeval_current();
+ task->private_data = service;
+
+ status = kccsrv_init_creds(service);
+ if (!W_ERROR_IS_OK(status)) {
+ task_server_terminate(task,
+ talloc_asprintf(task,
+ "kccsrv: Failed to obtain server credentials: %s\n",
+ win_errstr(status)), true);
+ return werror_to_ntstatus(status);
+ }
+
+ status = kccsrv_connect_samdb(service, task->lp_ctx);
+ if (!W_ERROR_IS_OK(status)) {
+ task_server_terminate(task, talloc_asprintf(task,
+ "kccsrv: Failed to connect to local samdb: %s\n",
+ win_errstr(status)), true);
+ return werror_to_ntstatus(status);
+ }
+
+ status = kccsrv_load_partitions(service);
+ if (!W_ERROR_IS_OK(status)) {
+ task_server_terminate(task, talloc_asprintf(task,
+ "kccsrv: Failed to load partitions: %s\n",
+ win_errstr(status)), true);
+ return werror_to_ntstatus(status);
+ }
+
+ periodic_startup_interval =
+ lpcfg_parm_int(task->lp_ctx, NULL, "kccsrv",
+ "periodic_startup_interval", 15); /* in seconds */
+ service->periodic.interval =
+ lpcfg_parm_int(task->lp_ctx, NULL, "kccsrv",
+ "periodic_interval", 300); /* in seconds */
+
+ /* (kccsrv:samba_kcc=true) will run newer samba_kcc replication
+ * topology generation code.
+ */
+ service->samba_kcc_code = lpcfg_parm_bool(task->lp_ctx, NULL,
+ "kccsrv", "samba_kcc", true);
+
+ status = kccsrv_periodic_schedule(service, periodic_startup_interval);
+ if (!W_ERROR_IS_OK(status)) {
+ task_server_terminate(task, talloc_asprintf(task,
+ "kccsrv: Failed to periodic schedule: %s\n",
+ win_errstr(status)), true);
+ return werror_to_ntstatus(status);
+ }
+
+ irpc_add_name(task->msg_ctx, "kccsrv");
+
+ IRPC_REGISTER(task->msg_ctx, drsuapi, DRSUAPI_DSEXECUTEKCC, kccsrv_execute_kcc, service);
+ IRPC_REGISTER(task->msg_ctx, drsuapi, DRSUAPI_DSREPLICAGETINFO, kccsrv_replica_get_info, service);
+ return NT_STATUS_OK;
+}
+
+/*
+ register ourselves as a available server
+*/
+NTSTATUS server_service_kcc_init(TALLOC_CTX *ctx)
+{
+ static const struct service_details details = {
+ .inhibit_fork_on_accept = true,
+ .inhibit_pre_fork = true,
+ .task_init = kccsrv_task_init,
+ .post_fork = NULL
+ };
+ return register_server_service(ctx, "kcc", &details);
+}
diff --git a/source4/dsdb/kcc/kcc_service.h b/source4/dsdb/kcc/kcc_service.h
new file mode 100644
index 0000000..d702b5a
--- /dev/null
+++ b/source4/dsdb/kcc/kcc_service.h
@@ -0,0 +1,101 @@
+/*
+ Unix SMB/CIFS Implementation.
+
+ KCC service
+
+ Copyright (C) Andrew Tridgell 2009
+ based on drepl service code
+
+ 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/>.
+
+*/
+
+#ifndef _DSDB_REPL_KCC_SERVICE_H_
+#define _DSDB_REPL_KCC_SERVICE_H_
+
+#include "librpc/gen_ndr/ndr_drsuapi_c.h"
+#include "dsdb/common/util.h"
+
+struct kccsrv_service {
+ /* the whole kcc service is in one task */
+ struct task_server *task;
+
+ /* the time the service was started */
+ struct timeval startup_time;
+
+ /* dn of our configuration partition */
+ struct ldb_dn *config_dn;
+
+ /*
+ * system session info
+ * with machine account credentials
+ */
+ struct auth_session_info *system_session_info;
+
+ /* list of local partitions */
+ struct dsdb_ldb_dn_list_node *partitions;
+
+ /*
+ * a connection to the local samdb
+ */
+ struct ldb_context *samdb;
+
+ /* the guid of our NTDS Settings object, which never changes! */
+ struct GUID ntds_guid;
+
+ /* some stuff for periodic processing */
+ struct {
+ /*
+ * the interval between to periodic runs
+ */
+ uint32_t interval;
+
+ /*
+ * the timestamp for the next event,
+ * this is the timestamp passed to event_add_timed()
+ */
+ struct timeval next_event;
+
+ /* here we have a reference to the timed event the schedules the periodic stuff */
+ struct tevent_timer *te;
+
+ /* samba_runcmd_send service for samba_kcc */
+ struct tevent_req *subreq;
+
+ /* return status of samba_kcc */
+ NTSTATUS status;
+
+ } periodic;
+
+ time_t last_deleted_check;
+
+ time_t last_dns_scavenge;
+
+ time_t last_dns_tombstone_collection;
+
+ time_t last_full_scan_deleted_check;
+
+ bool am_rodc;
+
+ /* run new samba_kcc topology generator code */
+ bool samba_kcc_code;
+};
+
+struct kcc_connection_list;
+
+#include "dsdb/kcc/garbage_collect_tombstones.h"
+#include "dsdb/kcc/scavenge_dns_records.h"
+#include "dsdb/kcc/kcc_service_proto.h"
+
+#endif /* _DSDB_REPL_KCC_SERVICE_H_ */
diff --git a/source4/dsdb/kcc/scavenge_dns_records.c b/source4/dsdb/kcc/scavenge_dns_records.c
new file mode 100644
index 0000000..f41250c
--- /dev/null
+++ b/source4/dsdb/kcc/scavenge_dns_records.c
@@ -0,0 +1,531 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ DNS tombstoning routines
+
+ 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 "includes.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.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 "lib/util/dlinklist.h"
+#include "ldb.h"
+#include "dsdb/kcc/scavenge_dns_records.h"
+#include "lib/ldb-samba/ldb_matching_rules.h"
+#include "lib/util/time.h"
+#include "dns_server/dnsserver_common.h"
+#include "librpc/gen_ndr/ndr_dnsp.h"
+#include "param/param.h"
+
+#include "librpc/gen_ndr/ndr_misc.h"
+#include "librpc/gen_ndr/ndr_drsuapi.h"
+#include "librpc/gen_ndr/ndr_drsblobs.h"
+
+/*
+ * Copy only non-expired dns records from one message element to another.
+ */
+static NTSTATUS copy_current_records(TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *old_el,
+ struct ldb_message_element *el,
+ uint32_t dns_timestamp)
+{
+ unsigned int i;
+ struct dnsp_DnssrvRpcRecord rec;
+ enum ndr_err_code ndr_err;
+
+ el->values = talloc_zero_array(mem_ctx, struct ldb_val,
+ old_el->num_values);
+ if (el->values == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ for (i = 0; i < old_el->num_values; i++) {
+ ndr_err = ndr_pull_struct_blob(
+ &(old_el->values[i]),
+ mem_ctx,
+ &rec,
+ (ndr_pull_flags_fn_t)ndr_pull_dnsp_DnssrvRpcRecord);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ DBG_ERR("Failed to pull dns rec blob.\n");
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+ if (rec.dwTimeStamp > dns_timestamp ||
+ rec.dwTimeStamp == 0) {
+ el->values[el->num_values] = old_el->values[i];
+ el->num_values++;
+ }
+ }
+
+ return NT_STATUS_OK;
+}
+
+/*
+ * Check all records in a zone and tombstone them if they're expired.
+ */
+static NTSTATUS dns_tombstone_records_zone(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ struct dns_server_zone *zone,
+ uint32_t dns_timestamp,
+ NTTIME entombed_time,
+ char **error_string)
+{
+ WERROR werr;
+ NTSTATUS status;
+ unsigned int i;
+ struct dnsserver_zoneinfo *zi = NULL;
+ struct ldb_result *res = NULL;
+ struct ldb_message_element *el = NULL;
+ struct ldb_message_element *tombstone_el = NULL;
+ struct ldb_message_element *old_el = NULL;
+ struct ldb_message *new_msg = NULL;
+ enum ndr_err_code ndr_err;
+ int ret;
+ struct GUID guid;
+ struct GUID_txt_buf buf_guid;
+ const char *attrs[] = {"dnsRecord",
+ "dNSTombstoned",
+ "objectGUID",
+ NULL};
+
+ struct ldb_val true_val = {
+ .data = discard_const_p(uint8_t, "TRUE"),
+ .length = 4
+ };
+
+ struct ldb_val tombstone_blob;
+ struct dnsp_DnssrvRpcRecord tombstone_struct = {
+ .wType = DNS_TYPE_TOMBSTONE,
+ .data = {.EntombedTime = entombed_time}
+ };
+
+ ndr_err = ndr_push_struct_blob(
+ &tombstone_blob,
+ mem_ctx,
+ &tombstone_struct,
+ (ndr_push_flags_fn_t)ndr_push_dnsp_DnssrvRpcRecord);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ *error_string = discard_const_p(char,
+ "Failed to push TOMBSTONE"
+ "dnsp_DnssrvRpcRecord\n");
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ *error_string = NULL;
+
+ /* Get NoRefreshInterval and RefreshInterval from zone properties.*/
+ zi = talloc(mem_ctx, struct dnsserver_zoneinfo);
+ if (zi == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ werr = dns_get_zone_properties(samdb, mem_ctx, zone->dn, zi);
+ if (W_ERROR_EQUAL(DNS_ERR(NOTZONE), werr)) {
+ return NT_STATUS_PROPSET_NOT_FOUND;
+ } else if (!W_ERROR_IS_OK(werr)) {
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ /* Subtract them from current time to get the earliest possible.
+ * timestamp allowed for a non-expired DNS record. */
+ dns_timestamp -= zi->dwNoRefreshInterval + zi->dwRefreshInterval;
+
+ /* Custom match gets dns records in the zone with dwTimeStamp < t. */
+ ret = ldb_search(samdb,
+ mem_ctx,
+ &res,
+ zone->dn,
+ LDB_SCOPE_SUBTREE,
+ attrs,
+ "(&(objectClass=dnsNode)"
+ "(&(!(dnsTombstoned=TRUE))"
+ "(dnsRecord:" DSDB_MATCH_FOR_DNS_TO_TOMBSTONE_TIME
+ ":=%"PRIu32")))",
+ dns_timestamp);
+ if (ret != LDB_SUCCESS) {
+ *error_string = talloc_asprintf(mem_ctx,
+ "Failed to search for dns "
+ "objects in zone %s: %s",
+ ldb_dn_get_linearized(zone->dn),
+ ldb_errstring(samdb));
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ /*
+ * Do a constrained update on each expired DNS node. To do a constrained
+ * update we leave the dnsRecord element as is, and just change the flag
+ * to MOD_DELETE, then add a new element with the changes we want. LDB
+ * will run the deletion first, and bail out if a binary comparison
+ * between the attribute we pass and the one in the database shows a
+ * change. This prevents race conditions.
+ */
+ for (i = 0; i < res->count; i++) {
+ new_msg = ldb_msg_copy(mem_ctx, res->msgs[i]);
+ if (new_msg == NULL) {
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ old_el = ldb_msg_find_element(new_msg, "dnsRecord");
+ if (old_el == NULL) {
+ TALLOC_FREE(new_msg);
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+ old_el->flags = LDB_FLAG_MOD_DELETE;
+
+ ret = ldb_msg_add_empty(
+ new_msg, "dnsRecord", LDB_FLAG_MOD_ADD, &el);
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(new_msg);
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ status = copy_current_records(new_msg, old_el, el, dns_timestamp);
+
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(new_msg);
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ /* If nothing was expired, do nothing. */
+ if (el->num_values == old_el->num_values &&
+ el->num_values != 0) {
+ TALLOC_FREE(new_msg);
+ continue;
+ }
+
+ /*
+ * If everything was expired, we tombstone the node, which
+ * involves adding a tombstone dnsRecord and a 'dnsTombstoned:
+ * TRUE' attribute. That is, we want to end up with this:
+ *
+ * objectClass: dnsNode
+ * dnsRecord: { .wType = DNSTYPE_TOMBSTONE,
+ * .data.EntombedTime = <now> }
+ * dnsTombstoned: TRUE
+ *
+ * and no other dnsRecords.
+ */
+ if (el->num_values == 0) {
+ struct ldb_val *vals = talloc_realloc(new_msg->elements,
+ el->values,
+ struct ldb_val,
+ 1);
+ if (!vals) {
+ TALLOC_FREE(new_msg);
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+ el->values = vals;
+ el->values[0] = tombstone_blob;
+ el->num_values = 1;
+
+ tombstone_el = ldb_msg_find_element(new_msg,
+ "dnsTombstoned");
+
+ if (tombstone_el == NULL) {
+ ret = ldb_msg_add_value(new_msg,
+ "dnsTombstoned",
+ &true_val,
+ &tombstone_el);
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(new_msg);
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+ tombstone_el->flags = LDB_FLAG_MOD_ADD;
+ } else {
+ if (tombstone_el->num_values != 1) {
+ vals = talloc_realloc(
+ new_msg->elements,
+ tombstone_el->values,
+ struct ldb_val,
+ 1);
+ if (!vals) {
+ TALLOC_FREE(new_msg);
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+ tombstone_el->values = vals;
+ tombstone_el->num_values = 1;
+ }
+ tombstone_el->flags = LDB_FLAG_MOD_REPLACE;
+ tombstone_el->values[0] = true_val;
+ }
+ } else {
+ /*
+ * Do not change the status of dnsTombstoned if we
+ * found any live records. If it exists, its value
+ * will be the harmless "FALSE", which is what we end
+ * up with when a tombstoned record is untombstoned.
+ * (in dns_common_replace).
+ */
+ ldb_msg_remove_attr(new_msg,
+ "dnsTombstoned");
+ }
+
+ /* Set DN to the GUID in case the object was moved. */
+ el = ldb_msg_find_element(new_msg, "objectGUID");
+ if (el == NULL) {
+ TALLOC_FREE(new_msg);
+ *error_string =
+ talloc_asprintf(mem_ctx,
+ "record has no objectGUID "
+ "in zone %s",
+ ldb_dn_get_linearized(zone->dn));
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ status = GUID_from_ndr_blob(el->values, &guid);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(new_msg);
+ *error_string =
+ discard_const_p(char, "Error: Invalid GUID.\n");
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ GUID_buf_string(&guid, &buf_guid);
+ new_msg->dn =
+ ldb_dn_new_fmt(mem_ctx, samdb, "<GUID=%s>", buf_guid.buf);
+
+ /* Remove the GUID so we're not trying to modify it. */
+ ldb_msg_remove_attr(new_msg, "objectGUID");
+
+ ret = ldb_modify(samdb, new_msg);
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(new_msg);
+ *error_string =
+ talloc_asprintf(mem_ctx,
+ "Failed to modify dns record "
+ "in zone %s: %s",
+ ldb_dn_get_linearized(zone->dn),
+ ldb_errstring(samdb));
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+ TALLOC_FREE(new_msg);
+ }
+
+ return NT_STATUS_OK;
+}
+
+/*
+ * Tombstone all expired DNS records.
+ */
+NTSTATUS dns_tombstone_records(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ char **error_string)
+{
+ struct dns_server_zone *zones = NULL;
+ struct dns_server_zone *z = NULL;
+ NTSTATUS ret;
+ uint32_t dns_timestamp;
+ NTTIME entombed_time;
+ TALLOC_CTX *tmp_ctx = NULL;
+ time_t unix_now = time(NULL);
+
+ unix_to_nt_time(&entombed_time, unix_now);
+ dns_timestamp = unix_to_dns_timestamp(unix_now);
+
+ tmp_ctx = talloc_new(mem_ctx);
+ if (tmp_ctx == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ ret = dns_common_zones(samdb, tmp_ctx, NULL, &zones);
+ if (!NT_STATUS_IS_OK(ret)) {
+ TALLOC_FREE(tmp_ctx);
+ return ret;
+ }
+
+ for (z = zones; z; z = z->next) {
+ ret = dns_tombstone_records_zone(tmp_ctx,
+ samdb,
+ z,
+ dns_timestamp,
+ entombed_time,
+ error_string);
+ if (NT_STATUS_EQUAL(ret, NT_STATUS_PROPSET_NOT_FOUND)) {
+ continue;
+ } else if (!NT_STATUS_IS_OK(ret)) {
+ TALLOC_FREE(tmp_ctx);
+ return ret;
+ }
+ }
+ TALLOC_FREE(tmp_ctx);
+ return NT_STATUS_OK;
+}
+
+/*
+ * Delete all DNS tombstones that have been around for longer than the server
+ * property 'dns_tombstone_interval' which we store in smb.conf, which
+ * corresponds to DsTombstoneInterval in [MS-DNSP] 3.1.1.1.1 "DNS Server
+ * Integer Properties".
+ */
+NTSTATUS dns_delete_tombstones(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ char **error_string)
+{
+ struct dns_server_zone *zones = NULL;
+ struct dns_server_zone *z = NULL;
+ int ret, i;
+ NTSTATUS status;
+ uint32_t current_time;
+ uint32_t tombstone_interval;
+ uint32_t tombstone_hours;
+ NTTIME tombstone_nttime;
+ enum ndr_err_code ndr_err;
+ struct ldb_result *res = NULL;
+ TALLOC_CTX *tmp_ctx = NULL;
+ struct loadparm_context *lp_ctx = NULL;
+ struct ldb_message_element *el = NULL;
+ struct dnsp_DnssrvRpcRecord rec = {0};
+ const char *attrs[] = {"dnsRecord", "dNSTombstoned", NULL};
+
+ current_time = unix_to_dns_timestamp(time(NULL));
+
+ lp_ctx = (struct loadparm_context *)ldb_get_opaque(samdb, "loadparm");
+ tombstone_interval = lpcfg_parm_ulong(lp_ctx, NULL,
+ "dnsserver",
+ "dns_tombstone_interval",
+ 24 * 14);
+
+ tombstone_hours = current_time - tombstone_interval;
+ status = dns_timestamp_to_nt_time(&tombstone_nttime,
+ tombstone_hours);
+
+ if (!NT_STATUS_IS_OK(status)) {
+ DBG_ERR("DNS timestamp exceeds NTTIME epoch.\n");
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ tmp_ctx = talloc_new(mem_ctx);
+ if (tmp_ctx == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+ status = dns_common_zones(samdb, tmp_ctx, NULL, &zones);
+ if (!NT_STATUS_IS_OK(status)) {
+ TALLOC_FREE(tmp_ctx);
+ return status;
+ }
+
+ for (z = zones; z; z = z->next) {
+ /*
+ * This can load a very large set, but on the
+ * assumption that the number of tombstones is
+ * relatively small compared with the number of active
+ * records, and that this is an indexed lookup, this
+ * should be OK. We can make a match rule if
+ * returning the set of tombstones becomes an issue.
+ */
+
+ ret = ldb_search(samdb,
+ tmp_ctx,
+ &res,
+ z->dn,
+ LDB_SCOPE_SUBTREE,
+ attrs,
+ "(&(objectClass=dnsNode)(dNSTombstoned=TRUE))");
+
+ if (ret != LDB_SUCCESS) {
+ *error_string =
+ talloc_asprintf(mem_ctx,
+ "Failed to "
+ "search for tombstoned "
+ "dns objects in zone %s: %s",
+ ldb_dn_get_linearized(z->dn),
+ ldb_errstring(samdb));
+ TALLOC_FREE(tmp_ctx);
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ for (i = 0; i < res->count; i++) {
+ struct ldb_message *msg = res->msgs[i];
+ el = ldb_msg_find_element(msg, "dnsRecord");
+ if (el == NULL) {
+ DBG_ERR("The tombstoned dns node %s has no dns "
+ "records, which should not happen.\n",
+ ldb_dn_get_linearized(msg->dn)
+ );
+ continue;
+ }
+ /*
+ * Below we assume the element has one value, which we
+ * expect because when we tombstone a node we remove
+ * all the records except for the tombstone.
+ */
+ if (el->num_values != 1) {
+ DBG_ERR("The tombstoned dns node %s has %u "
+ "dns records, expected one.\n",
+ ldb_dn_get_linearized(msg->dn),
+ el->num_values
+ );
+ continue;
+ }
+
+ ndr_err = ndr_pull_struct_blob(
+ el->values,
+ tmp_ctx,
+ &rec,
+ (ndr_pull_flags_fn_t)ndr_pull_dnsp_DnssrvRpcRecord);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ TALLOC_FREE(tmp_ctx);
+ DBG_ERR("Failed to pull dns rec blob.\n");
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+
+ if (rec.wType != DNS_TYPE_TOMBSTONE) {
+ DBG_ERR("A tombstoned dnsNode has non-tombstoned"
+ " records, which should not happen.\n");
+ continue;
+ }
+
+ if (rec.data.EntombedTime > tombstone_nttime) {
+ continue;
+ }
+ /*
+ * Between 4.9 and 4.14 in some places we saved the
+ * tombstone time as hours since the start of 1601,
+ * not in NTTIME ten-millionths of a second units.
+ *
+ * We can accommodate these bad values by noting that
+ * all the realistic timestamps in that measurement
+ * fall within the first *second* of NTTIME, that is,
+ * before 1601-01-01 00:00:01; and that these
+ * timestamps are not realistic for NTTIME timestamps.
+ *
+ * Calculation: there are roughly 365.25 * 24 = 8766
+ * hours per year, and < 500 years since 1601, so
+ * 4383000 would be a fine threshold. We round up to
+ * the crore-second (c. 2741CE) in honour of NTTIME.
+ */
+ if ((rec.data.EntombedTime < 10000000) &&
+ (rec.data.EntombedTime > tombstone_hours)) {
+ continue;
+ }
+
+ ret = dsdb_delete(samdb, msg->dn, 0);
+ if (ret != LDB_ERR_NO_SUCH_OBJECT &&
+ ret != LDB_SUCCESS) {
+ TALLOC_FREE(tmp_ctx);
+ DBG_ERR("Failed to delete dns node \n");
+ return NT_STATUS_INTERNAL_ERROR;
+ }
+ }
+
+ }
+ TALLOC_FREE(tmp_ctx);
+ return NT_STATUS_OK;
+}
diff --git a/source4/dsdb/kcc/scavenge_dns_records.h b/source4/dsdb/kcc/scavenge_dns_records.h
new file mode 100644
index 0000000..e065fed
--- /dev/null
+++ b/source4/dsdb/kcc/scavenge_dns_records.h
@@ -0,0 +1,36 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ DNS tombstoning routines
+
+ 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 "param/param.h"
+#include "dsdb/samdb/samdb.h"
+#include "dsdb/common/util.h"
+#include "dns_server/dnsserver_common.h"
+
+NTSTATUS dns_tombstone_records(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ char **error_string);
+
+NTSTATUS dns_delete_tombstones(TALLOC_CTX *mem_ctx,
+ struct ldb_context *samdb,
+ char **error_string);
+NTSTATUS remove_expired_records(TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *el,
+ NTTIME t);
diff --git a/source4/dsdb/pydsdb.c b/source4/dsdb/pydsdb.c
new file mode 100644
index 0000000..aac311a
--- /dev/null
+++ b/source4/dsdb/pydsdb.c
@@ -0,0 +1,1837 @@
+/*
+ Unix SMB/CIFS implementation.
+ Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2007-2010
+ 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/>.
+*/
+
+#include "lib/replace/system/python.h"
+#include "python/py3compat.h"
+#include "includes.h"
+#include <ldb.h>
+#include <pyldb.h>
+#include "dsdb/samdb/samdb.h"
+#include "libcli/security/security.h"
+#include "librpc/ndr/libndr.h"
+#include "system/kerberos.h"
+#include "auth/kerberos/kerberos.h"
+#include "librpc/rpc/pyrpc_util.h"
+#include "lib/policy/policy.h"
+#include "param/pyparam.h"
+#include "lib/util/dlinklist.h"
+#include "dsdb/kcc/garbage_collect_tombstones.h"
+#include "dsdb/kcc/scavenge_dns_records.h"
+#include "libds/common/flag_mapping.h"
+
+#undef strcasecmp
+
+/* FIXME: These should be in a header file somewhere */
+#define PyErr_LDB_OR_RAISE(py_ldb, ldb) \
+ if (!py_check_dcerpc_type(py_ldb, "ldb", "Ldb")) { \
+ PyErr_SetString(PyExc_TypeError, "Ldb connection object required"); \
+ return NULL; \
+ } \
+ ldb = pyldb_Ldb_AS_LDBCONTEXT(py_ldb);
+
+#define PyErr_LDB_DN_OR_RAISE(py_ldb_dn, dn) \
+ if (!py_check_dcerpc_type(py_ldb_dn, "ldb", "Dn")) { \
+ PyErr_SetString(PyExc_TypeError, "ldb Dn object required"); \
+ return NULL; \
+ } \
+ dn = pyldb_Dn_AS_DN(py_ldb_dn);
+
+static PyObject *py_ldb_get_exception(void)
+{
+ PyObject *mod = PyImport_ImportModule("ldb");
+ PyObject *result = NULL;
+ if (mod == NULL)
+ return NULL;
+
+ result = PyObject_GetAttrString(mod, "LdbError");
+ Py_CLEAR(mod);
+ return result;
+}
+
+static void PyErr_SetLdbError(PyObject *error, int ret, struct ldb_context *ldb_ctx)
+{
+ if (ret == LDB_ERR_PYTHON_EXCEPTION)
+ return; /* Python exception should already be set, just keep that */
+
+ PyErr_SetObject(error,
+ Py_BuildValue(discard_const_p(char, "(i,s)"), ret,
+ ldb_ctx == NULL?ldb_strerror(ret):ldb_errstring(ldb_ctx)));
+}
+
+static PyObject *py_samdb_server_site_name(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb, *result;
+ struct ldb_context *ldb;
+ const char *site;
+ TALLOC_CTX *mem_ctx;
+
+ if (!PyArg_ParseTuple(args, "O", &py_ldb))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ mem_ctx = talloc_new(NULL);
+ if (mem_ctx == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ site = samdb_server_site_name(ldb, mem_ctx);
+ if (site == NULL) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to find server site");
+ talloc_free(mem_ctx);
+ return NULL;
+ }
+
+ result = PyUnicode_FromString(site);
+ talloc_free(mem_ctx);
+ return result;
+}
+
+static PyObject *py_dsdb_convert_schema_to_openldap(PyObject *self,
+ PyObject *args)
+{
+ char *target_str, *mapping;
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ PyObject *ret;
+ char *retstr;
+
+ if (!PyArg_ParseTuple(args, "Oss", &py_ldb, &target_str, &mapping))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ retstr = dsdb_convert_schema_to_openldap(ldb, target_str, mapping);
+ if (retstr == NULL) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "dsdb_convert_schema_to_openldap failed");
+ return NULL;
+ }
+
+ ret = PyUnicode_FromString(retstr);
+ talloc_free(retstr);
+ return ret;
+}
+
+static PyObject *py_samdb_set_domain_sid(PyLdbObject *self, PyObject *args)
+{
+ PyObject *py_ldb, *py_sid;
+ struct ldb_context *ldb;
+ struct dom_sid *sid;
+ bool ret;
+ const char *sid_str = NULL;
+
+ if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_sid))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ sid_str = PyUnicode_AsUTF8(py_sid);
+ if (sid_str == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ sid = dom_sid_parse_talloc(NULL, sid_str);
+ if (sid == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ ret = samdb_set_domain_sid(ldb, sid);
+ talloc_free(sid);
+ if (!ret) {
+ PyErr_SetString(PyExc_RuntimeError, "set_domain_sid failed");
+ return NULL;
+ }
+ Py_RETURN_NONE;
+}
+
+static PyObject *py_samdb_set_ntds_settings_dn(PyLdbObject *self, PyObject *args)
+{
+ PyObject *py_ldb, *py_ntds_settings_dn;
+ struct ldb_context *ldb;
+ struct ldb_dn *ntds_settings_dn;
+ TALLOC_CTX *tmp_ctx;
+ bool ret;
+
+ if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_ntds_settings_dn))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ tmp_ctx = talloc_new(NULL);
+ if (tmp_ctx == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ if (!pyldb_Object_AsDn(tmp_ctx, py_ntds_settings_dn, ldb, &ntds_settings_dn)) {
+ /* exception thrown by "pyldb_Object_AsDn" */
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+
+ ret = samdb_set_ntds_settings_dn(ldb, ntds_settings_dn);
+ talloc_free(tmp_ctx);
+ if (!ret) {
+ PyErr_SetString(PyExc_RuntimeError, "set_ntds_settings_dn failed");
+ return NULL;
+ }
+ Py_RETURN_NONE;
+}
+
+static PyObject *py_samdb_get_domain_sid(PyLdbObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ const struct dom_sid *sid;
+ struct dom_sid_buf buf;
+ PyObject *ret;
+
+ if (!PyArg_ParseTuple(args, "O", &py_ldb))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ sid = samdb_domain_sid(ldb);
+ if (!sid) {
+ PyErr_SetString(PyExc_RuntimeError, "samdb_domain_sid failed");
+ return NULL;
+ }
+
+ ret = PyUnicode_FromString(dom_sid_str_buf(sid, &buf));
+ return ret;
+}
+
+static PyObject *py_samdb_ntds_invocation_id(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb, *result;
+ struct ldb_context *ldb;
+ const struct GUID *guid;
+ char *retstr;
+
+ if (!PyArg_ParseTuple(args, "O", &py_ldb)) {
+ return NULL;
+ }
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ guid = samdb_ntds_invocation_id(ldb);
+ if (guid == NULL) {
+ PyErr_SetString(PyExc_RuntimeError,
+ "Failed to find NTDS invocation ID");
+ return NULL;
+ }
+
+ retstr = GUID_string(NULL, guid);
+ if (retstr == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+ result = PyUnicode_FromString(retstr);
+ talloc_free(retstr);
+ return result;
+}
+
+static PyObject *py_dsdb_get_oid_from_attid(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ uint32_t attid;
+ struct dsdb_schema *schema;
+ const char *oid;
+ PyObject *ret;
+ WERROR status;
+ TALLOC_CTX *mem_ctx;
+
+ if (!PyArg_ParseTuple(args, "OI", &py_ldb, &attid))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ mem_ctx = talloc_new(NULL);
+ if (!mem_ctx) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ schema = dsdb_get_schema(ldb, mem_ctx);
+ if (!schema) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to find a schema from ldb \n");
+ talloc_free(mem_ctx);
+ return NULL;
+ }
+
+ status = dsdb_schema_pfm_oid_from_attid(schema->prefixmap, attid,
+ mem_ctx, &oid);
+ if (!W_ERROR_IS_OK(status)) {
+ PyErr_SetWERROR(status);
+ talloc_free(mem_ctx);
+ return NULL;
+ }
+
+ ret = PyUnicode_FromString(oid);
+
+ talloc_free(mem_ctx);
+
+ return ret;
+}
+
+
+static PyObject *py_dsdb_get_attid_from_lDAPDisplayName(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb, *is_schema_nc;
+ struct ldb_context *ldb;
+ struct dsdb_schema *schema;
+ const char *ldap_display_name;
+ bool schema_nc = false;
+ const struct dsdb_attribute *a;
+ uint32_t attid;
+
+ if (!PyArg_ParseTuple(args, "OsO", &py_ldb, &ldap_display_name, &is_schema_nc))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ if (is_schema_nc) {
+ if (!PyBool_Check(is_schema_nc)) {
+ PyErr_SetString(PyExc_TypeError, "Expected boolean is_schema_nc");
+ return NULL;
+ }
+ if (is_schema_nc == Py_True) {
+ schema_nc = true;
+ }
+ }
+
+ schema = dsdb_get_schema(ldb, NULL);
+
+ if (!schema) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to find a schema from ldb");
+ return NULL;
+ }
+
+ a = dsdb_attribute_by_lDAPDisplayName(schema, ldap_display_name);
+ if (a == NULL) {
+ PyErr_Format(PyExc_KeyError, "Failed to find attribute '%s'", ldap_display_name);
+ return NULL;
+ }
+
+ attid = dsdb_attribute_get_attid(a, schema_nc);
+
+ return PyLong_FromUnsignedLong(attid);
+}
+
+/*
+ return the systemFlags as int from the attribute name
+ */
+static PyObject *py_dsdb_get_systemFlags_from_lDAPDisplayName(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ struct dsdb_schema *schema;
+ const char *ldap_display_name;
+ const struct dsdb_attribute *attribute;
+
+ if (!PyArg_ParseTuple(args, "Os", &py_ldb, &ldap_display_name))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ schema = dsdb_get_schema(ldb, NULL);
+
+ if (!schema) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to find a schema from ldb");
+ return NULL;
+ }
+
+ attribute = dsdb_attribute_by_lDAPDisplayName(schema, ldap_display_name);
+ if (attribute == NULL) {
+ PyErr_Format(PyExc_KeyError, "Failed to find attribute '%s'", ldap_display_name);
+ return NULL;
+ }
+
+ return PyLong_FromLong(attribute->systemFlags);
+}
+
+/*
+ return the linkID from the attribute name
+ */
+static PyObject *py_dsdb_get_linkId_from_lDAPDisplayName(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ struct dsdb_schema *schema;
+ const char *ldap_display_name;
+ const struct dsdb_attribute *attribute;
+
+ if (!PyArg_ParseTuple(args, "Os", &py_ldb, &ldap_display_name))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ schema = dsdb_get_schema(ldb, NULL);
+
+ if (!schema) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to find a schema from ldb");
+ return NULL;
+ }
+
+ attribute = dsdb_attribute_by_lDAPDisplayName(schema, ldap_display_name);
+ if (attribute == NULL) {
+ PyErr_Format(PyExc_KeyError, "Failed to find attribute '%s'", ldap_display_name);
+ return NULL;
+ }
+
+ return PyLong_FromLong(attribute->linkID);
+}
+
+/*
+ return the backlink attribute name (if any) for an attribute
+ */
+static PyObject *py_dsdb_get_backlink_from_lDAPDisplayName(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ struct dsdb_schema *schema;
+ const char *ldap_display_name;
+ const struct dsdb_attribute *attribute, *target_attr;
+
+ if (!PyArg_ParseTuple(args, "Os", &py_ldb, &ldap_display_name))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ schema = dsdb_get_schema(ldb, NULL);
+
+ if (!schema) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to find a schema from ldb");
+ return NULL;
+ }
+
+ attribute = dsdb_attribute_by_lDAPDisplayName(schema, ldap_display_name);
+ if (attribute == NULL) {
+ PyErr_Format(PyExc_KeyError, "Failed to find attribute '%s'", ldap_display_name);
+ return NULL;
+ }
+
+ if (attribute->linkID == 0) {
+ Py_RETURN_NONE;
+ }
+
+ target_attr = dsdb_attribute_by_linkID(schema, attribute->linkID ^ 1);
+ if (target_attr == NULL) {
+ /* when we add pseudo-backlinks we'll need to handle
+ them here */
+ Py_RETURN_NONE;
+ }
+
+ return PyUnicode_FromString(target_attr->lDAPDisplayName);
+}
+
+
+static PyObject *py_dsdb_get_lDAPDisplayName_by_attid(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ struct dsdb_schema *schema;
+ const struct dsdb_attribute *a;
+ uint32_t attid;
+
+ if (!PyArg_ParseTuple(args, "OI", &py_ldb, &attid))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ schema = dsdb_get_schema(ldb, NULL);
+
+ if (!schema) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to find a schema from ldb");
+ return NULL;
+ }
+
+ a = dsdb_attribute_by_attributeID_id(schema, attid);
+ if (a == NULL) {
+ PyErr_Format(PyExc_KeyError, "Failed to find attribute '0x%08x'", attid);
+ return NULL;
+ }
+
+ return PyUnicode_FromString(a->lDAPDisplayName);
+}
+
+
+/*
+ return the attribute syntax oid as a string from the attribute name
+ */
+static PyObject *py_dsdb_get_syntax_oid_from_lDAPDisplayName(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ struct dsdb_schema *schema;
+ const char *ldap_display_name;
+ const struct dsdb_attribute *attribute;
+
+ if (!PyArg_ParseTuple(args, "Os", &py_ldb, &ldap_display_name))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ schema = dsdb_get_schema(ldb, NULL);
+
+ if (!schema) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to find a schema from ldb");
+ return NULL;
+ }
+
+ attribute = dsdb_attribute_by_lDAPDisplayName(schema, ldap_display_name);
+ if (attribute == NULL) {
+ PyErr_Format(PyExc_KeyError, "Failed to find attribute '%s'", ldap_display_name);
+ return NULL;
+ }
+
+ return PyUnicode_FromString(attribute->syntax->ldap_oid);
+}
+
+/*
+ convert a python string to a DRSUAPI drsuapi_DsReplicaAttribute attribute
+ */
+static PyObject *py_dsdb_DsReplicaAttribute(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb, *el_list, *ret;
+ struct ldb_context *ldb;
+ char *ldap_display_name;
+ const struct dsdb_attribute *a;
+ struct dsdb_schema *schema;
+ struct dsdb_syntax_ctx syntax_ctx;
+ struct ldb_message_element *el;
+ struct drsuapi_DsReplicaAttribute *attr;
+ TALLOC_CTX *tmp_ctx;
+ WERROR werr;
+ Py_ssize_t i;
+
+ if (!PyArg_ParseTuple(args, "OsO", &py_ldb, &ldap_display_name, &el_list)) {
+ return NULL;
+ }
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ schema = dsdb_get_schema(ldb, NULL);
+ if (!schema) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to find a schema from ldb");
+ return NULL;
+ }
+
+ a = dsdb_attribute_by_lDAPDisplayName(schema, ldap_display_name);
+ if (a == NULL) {
+ PyErr_Format(PyExc_KeyError, "Failed to find attribute '%s'", ldap_display_name);
+ return NULL;
+ }
+
+ dsdb_syntax_ctx_init(&syntax_ctx, ldb, schema);
+ syntax_ctx.is_schema_nc = false;
+
+ tmp_ctx = talloc_new(ldb);
+ if (tmp_ctx == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ /* If we were not given an LdbMessageElement */
+ if (!PyList_Check(el_list)) {
+ if (!py_check_dcerpc_type(el_list, "ldb", "MessageElement")) {
+ PyErr_SetString(py_ldb_get_exception(),
+ "list of strings or ldb MessageElement object required");
+ return NULL;
+ }
+ /*
+ * NOTE:
+ * el may not be a valid talloc context, it
+ * could be part of an array
+ */
+ el = pyldb_MessageElement_AsMessageElement(el_list);
+ } else {
+ el = talloc_zero(tmp_ctx, struct ldb_message_element);
+ if (el == NULL) {
+ PyErr_NoMemory();
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+
+ el->name = ldap_display_name;
+ el->num_values = PyList_Size(el_list);
+
+ el->values = talloc_array(el, struct ldb_val, el->num_values);
+ if (el->values == NULL) {
+ PyErr_NoMemory();
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+
+ for (i = 0; i < el->num_values; i++) {
+ PyObject *item = PyList_GetItem(el_list, i);
+ if (!(PyBytes_Check(item))) {
+ PyErr_Format(PyExc_TypeError,
+ "ldif_element type should be bytes"
+ );
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+ el->values[i].data =
+ (uint8_t *)PyBytes_AsString(item);
+ el->values[i].length = PyBytes_Size(item);
+ }
+ }
+
+ attr = talloc_zero(tmp_ctx, struct drsuapi_DsReplicaAttribute);
+ if (attr == NULL) {
+ PyErr_NoMemory();
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+
+ werr = a->syntax->ldb_to_drsuapi(&syntax_ctx, a, el, attr, attr);
+ PyErr_WERROR_NOT_OK_RAISE(werr);
+
+ ret = py_return_ndr_struct("samba.dcerpc.drsuapi", "DsReplicaAttribute", attr, attr);
+
+ talloc_free(tmp_ctx);
+
+ return ret;
+}
+
+
+/*
+ normalise a ldb attribute list
+ */
+static PyObject *py_dsdb_normalise_attributes(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb, *el_list, *py_ret;
+ struct ldb_context *ldb;
+ char *ldap_display_name;
+ const struct dsdb_attribute *a;
+ struct dsdb_schema *schema;
+ struct dsdb_syntax_ctx syntax_ctx;
+ struct ldb_message_element *el, *new_el;
+ struct drsuapi_DsReplicaAttribute *attr;
+ PyLdbMessageElementObject *ret;
+ TALLOC_CTX *tmp_ctx;
+ WERROR werr;
+ Py_ssize_t i;
+ PyTypeObject *py_type = NULL;
+ PyObject *module = NULL;
+
+ if (!PyArg_ParseTuple(args, "OsO", &py_ldb, &ldap_display_name, &el_list)) {
+ return NULL;
+ }
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ schema = dsdb_get_schema(ldb, NULL);
+ if (!schema) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to find a schema from ldb");
+ return NULL;
+ }
+
+ a = dsdb_attribute_by_lDAPDisplayName(schema, ldap_display_name);
+ if (a == NULL) {
+ PyErr_Format(PyExc_KeyError, "Failed to find attribute '%s'", ldap_display_name);
+ return NULL;
+ }
+
+ dsdb_syntax_ctx_init(&syntax_ctx, ldb, schema);
+ syntax_ctx.is_schema_nc = false;
+
+ tmp_ctx = talloc_new(ldb);
+ if (tmp_ctx == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ if (!PyList_Check(el_list)) {
+ if (!py_check_dcerpc_type(el_list, "ldb", "MessageElement")) {
+ PyErr_SetString(py_ldb_get_exception(),
+ "list of strings or ldb MessageElement object required");
+ return NULL;
+ }
+ /*
+ * NOTE:
+ * el may not be a valid talloc context, it
+ * could be part of an array
+ */
+ el = pyldb_MessageElement_AsMessageElement(el_list);
+ } else {
+ el = talloc_zero(tmp_ctx, struct ldb_message_element);
+ if (el == NULL) {
+ PyErr_NoMemory();
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+
+ el->name = ldap_display_name;
+ el->num_values = PyList_Size(el_list);
+
+ el->values = talloc_array(el, struct ldb_val, el->num_values);
+ if (el->values == NULL) {
+ PyErr_NoMemory();
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+
+ for (i = 0; i < el->num_values; i++) {
+ PyObject *item = PyList_GetItem(el_list, i);
+ if (!PyBytes_Check(item)) {
+ PyErr_Format(PyExc_TypeError,
+ "ldif_element type should be bytes"
+ );
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+ el->values[i].data = (uint8_t *)PyBytes_AsString(item);
+ el->values[i].length = PyBytes_Size(item);
+ }
+ }
+
+ new_el = talloc_zero(tmp_ctx, struct ldb_message_element);
+ if (new_el == NULL) {
+ PyErr_NoMemory();
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+
+ /* Normalise "objectClass" attribute if needed */
+ if (ldb_attr_cmp(a->lDAPDisplayName, "objectClass") == 0) {
+ int iret;
+ iret = dsdb_sort_objectClass_attr(ldb, schema, el, new_el, new_el);
+ if (iret != LDB_SUCCESS) {
+ PyErr_SetString(PyExc_RuntimeError, ldb_errstring(ldb));
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+ }
+
+ /* first run ldb_to_drsuapi, then convert back again. This has
+ * the effect of normalising the attributes
+ */
+
+ attr = talloc_zero(tmp_ctx, struct drsuapi_DsReplicaAttribute);
+ if (attr == NULL) {
+ PyErr_NoMemory();
+ talloc_free(tmp_ctx);
+ return NULL;
+ }
+
+ werr = a->syntax->ldb_to_drsuapi(&syntax_ctx, a, el, attr, attr);
+ PyErr_WERROR_NOT_OK_RAISE(werr);
+
+ /* now convert back again */
+ werr = a->syntax->drsuapi_to_ldb(&syntax_ctx, a, attr, new_el, new_el);
+ PyErr_WERROR_NOT_OK_RAISE(werr);
+
+ module = PyImport_ImportModule("ldb");
+ if (module == NULL) {
+ return NULL;
+ }
+
+ py_type = (PyTypeObject *)PyObject_GetAttrString(module, "MessageElement");
+ if (py_type == NULL) {
+ Py_DECREF(module);
+ return NULL;
+ }
+
+ Py_CLEAR(module);
+
+ py_ret = py_type->tp_alloc(py_type, 0);
+ Py_CLEAR(py_type);
+ if (py_ret == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+ ret = (PyLdbMessageElementObject *)py_ret;
+
+ ret->mem_ctx = talloc_new(NULL);
+ if (talloc_reference(ret->mem_ctx, new_el) == NULL) {
+ Py_CLEAR(py_ret);
+ PyErr_NoMemory();
+ return NULL;
+ }
+ ret->el = new_el;
+
+ talloc_free(tmp_ctx);
+
+ return py_ret;
+}
+
+
+static PyObject *py_dsdb_set_ntds_invocation_id(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb, *py_guid;
+ bool ret;
+ struct GUID guid;
+ struct ldb_context *ldb;
+ if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_guid))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+ GUID_from_string(PyUnicode_AsUTF8(py_guid), &guid);
+
+ if (GUID_all_zero(&guid)) {
+ PyErr_SetString(PyExc_RuntimeError, "set_ntds_invocation_id rejected due to all-zero invocation ID");
+ return NULL;
+ }
+
+ ret = samdb_set_ntds_invocation_id(ldb, &guid);
+ if (!ret) {
+ PyErr_SetString(PyExc_RuntimeError, "set_ntds_invocation_id failed");
+ return NULL;
+ }
+ Py_RETURN_NONE;
+}
+
+static PyObject *py_samdb_ntds_objectGUID(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb, *result;
+ struct ldb_context *ldb;
+ const struct GUID *guid;
+ char *retstr;
+
+ if (!PyArg_ParseTuple(args, "O", &py_ldb)) {
+ return NULL;
+ }
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ guid = samdb_ntds_objectGUID(ldb);
+ if (guid == NULL) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to find NTDS GUID");
+ return NULL;
+ }
+
+ retstr = GUID_string(NULL, guid);
+ if (retstr == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+ result = PyUnicode_FromString(retstr);
+ talloc_free(retstr);
+ return result;
+}
+
+static PyObject *py_dsdb_set_global_schema(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ int ret;
+ if (!PyArg_ParseTuple(args, "O", &py_ldb))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ ret = dsdb_set_global_schema(ldb);
+ PyErr_LDB_ERROR_IS_ERR_RAISE(py_ldb_get_exception(), ret, ldb);
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *py_dsdb_load_partition_usn(PyObject *self, PyObject *args)
+{
+ PyObject *py_dn, *py_ldb, *result;
+ struct ldb_dn *dn;
+ uint64_t highest_uSN, urgent_uSN;
+ struct ldb_context *ldb;
+ TALLOC_CTX *mem_ctx;
+ int ret;
+
+ if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_dn)) {
+ return NULL;
+ }
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ mem_ctx = talloc_new(NULL);
+ if (mem_ctx == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+
+ if (!pyldb_Object_AsDn(mem_ctx, py_dn, ldb, &dn)) {
+ talloc_free(mem_ctx);
+ return NULL;
+ }
+
+ ret = dsdb_load_partition_usn(ldb, dn, &highest_uSN, &urgent_uSN);
+ if (ret != LDB_SUCCESS) {
+ PyErr_Format(PyExc_RuntimeError,
+ "Failed to load partition [%s] uSN - %s",
+ ldb_dn_get_linearized(dn),
+ ldb_errstring(ldb));
+ talloc_free(mem_ctx);
+ return NULL;
+ }
+
+ talloc_free(mem_ctx);
+
+ result = Py_BuildValue(
+ "{s:l, s:l}",
+ "uSNHighest", (uint64_t)highest_uSN,
+ "uSNUrgent", (uint64_t)urgent_uSN);
+
+ return result;
+}
+
+static PyObject *py_dsdb_set_am_rodc(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ bool ret;
+ struct ldb_context *ldb;
+ int py_val;
+
+ if (!PyArg_ParseTuple(args, "Oi", &py_ldb, &py_val))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+ ret = samdb_set_am_rodc(ldb, (bool)py_val);
+ if (!ret) {
+ PyErr_SetString(PyExc_RuntimeError, "set_am_rodc failed");
+ return NULL;
+ }
+ Py_RETURN_NONE;
+}
+
+static PyObject *py_dsdb_set_schema_from_ldif(PyObject *self, PyObject *args)
+{
+ WERROR result;
+ char *pf, *df, *dn;
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+
+ if (!PyArg_ParseTuple(args, "Osss", &py_ldb, &pf, &df, &dn))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ result = dsdb_set_schema_from_ldif(ldb, pf, df, dn);
+ PyErr_WERROR_NOT_OK_RAISE(result);
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *py_dsdb_set_schema_from_ldb(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ PyObject *py_from_ldb;
+ struct ldb_context *from_ldb;
+ struct dsdb_schema *schema;
+ int ret;
+ char write_indices_and_attributes = SCHEMA_WRITE;
+ if (!PyArg_ParseTuple(args, "OO|b",
+ &py_ldb, &py_from_ldb, &write_indices_and_attributes))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ PyErr_LDB_OR_RAISE(py_from_ldb, from_ldb);
+
+ schema = dsdb_get_schema(from_ldb, NULL);
+ if (!schema) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to set find a schema on 'from' ldb!\n");
+ return NULL;
+ }
+
+ ret = dsdb_reference_schema(ldb, schema, write_indices_and_attributes);
+ PyErr_LDB_ERROR_IS_ERR_RAISE(py_ldb_get_exception(), ret, ldb);
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *py_dsdb_write_prefixes_from_schema_to_ldb(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ WERROR result;
+ struct dsdb_schema *schema;
+
+ if (!PyArg_ParseTuple(args, "O", &py_ldb))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ schema = dsdb_get_schema(ldb, NULL);
+ if (!schema) {
+ PyErr_SetString(PyExc_RuntimeError, "Failed to set find a schema on ldb!\n");
+ return NULL;
+ }
+
+ result = dsdb_write_prefixes_from_schema_to_ldb(NULL, ldb, schema);
+ PyErr_WERROR_NOT_OK_RAISE(result);
+
+ Py_RETURN_NONE;
+}
+
+
+static PyObject *py_dsdb_get_partitions_dn(PyObject *self, PyObject *args)
+{
+ struct ldb_context *ldb;
+ struct ldb_dn *dn;
+ PyObject *py_ldb, *ret;
+
+ if (!PyArg_ParseTuple(args, "O", &py_ldb))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ dn = samdb_partitions_dn(ldb, NULL);
+ if (dn == NULL) {
+ PyErr_NoMemory();
+ return NULL;
+ }
+ ret = pyldb_Dn_FromDn(dn);
+ talloc_free(dn);
+ return ret;
+}
+
+
+static PyObject *py_dsdb_get_nc_root(PyObject *self, PyObject *args)
+{
+ struct ldb_context *ldb;
+ struct ldb_dn *dn, *nc_root;
+ PyObject *py_ldb, *py_ldb_dn, *py_nc_root;
+ int ret;
+
+ if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_ldb_dn))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+ PyErr_LDB_DN_OR_RAISE(py_ldb_dn, dn);
+
+ ret = dsdb_find_nc_root(ldb, ldb, dn, &nc_root);
+ PyErr_LDB_ERROR_IS_ERR_RAISE(py_ldb_get_exception(), ret, ldb);
+
+ py_nc_root = pyldb_Dn_FromDn(nc_root);
+ talloc_unlink(ldb, nc_root);
+ return py_nc_root;
+}
+
+static PyObject *py_dsdb_get_wellknown_dn(PyObject *self, PyObject *args)
+{
+ struct ldb_context *ldb;
+ struct ldb_dn *nc_dn, *wk_dn;
+ char *wkguid;
+ PyObject *py_ldb, *py_nc_dn, *py_wk_dn;
+ int ret;
+
+ if (!PyArg_ParseTuple(args, "OOs", &py_ldb, &py_nc_dn, &wkguid))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+ PyErr_LDB_DN_OR_RAISE(py_nc_dn, nc_dn);
+
+ ret = dsdb_wellknown_dn(ldb, ldb, nc_dn, wkguid, &wk_dn);
+ if (ret == LDB_ERR_NO_SUCH_OBJECT) {
+ PyErr_Format(PyExc_KeyError, "Failed to find well known DN for GUID %s", wkguid);
+ return NULL;
+ }
+
+ PyErr_LDB_ERROR_IS_ERR_RAISE(py_ldb_get_exception(), ret, ldb);
+
+ py_wk_dn = pyldb_Dn_FromDn(wk_dn);
+ talloc_unlink(ldb, wk_dn);
+ return py_wk_dn;
+}
+
+
+/*
+ call into samdb_rodc()
+ */
+static PyObject *py_dsdb_am_rodc(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ int ret;
+ bool am_rodc;
+
+ if (!PyArg_ParseTuple(args, "O", &py_ldb))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ ret = samdb_rodc(ldb, &am_rodc);
+ if (ret != LDB_SUCCESS) {
+ PyErr_SetString(PyExc_RuntimeError, ldb_errstring(ldb));
+ return NULL;
+ }
+
+ return PyBool_FromLong(am_rodc);
+}
+
+/*
+ call into samdb_is_pdc()
+ */
+static PyObject *py_dsdb_am_pdc(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ bool am_pdc;
+
+ if (!PyArg_ParseTuple(args, "O", &py_ldb))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ am_pdc = samdb_is_pdc(ldb);
+ return PyBool_FromLong(am_pdc);
+}
+
+/*
+ call DSDB_EXTENDED_CREATE_OWN_RID_SET to get a new RID set for this server
+ */
+static PyObject *py_dsdb_create_own_rid_set(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ int ret;
+ struct ldb_result *ext_res;
+
+ if (!PyArg_ParseTuple(args, "O", &py_ldb))
+ return NULL;
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ /*
+ * Run DSDB_EXTENDED_CREATE_OWN_RID_SET to get a RID set
+ */
+
+ ret = ldb_extended(ldb, DSDB_EXTENDED_CREATE_OWN_RID_SET, NULL, &ext_res);
+
+ PyErr_LDB_ERROR_IS_ERR_RAISE(py_ldb_get_exception(), ret, ldb);
+
+ TALLOC_FREE(ext_res);
+
+ Py_RETURN_NONE;
+}
+
+/*
+ call DSDB_EXTENDED_ALLOCATE_RID to get a new RID set for this server
+ */
+static PyObject *py_dsdb_allocate_rid(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ struct ldb_context *ldb;
+ int ret;
+ uint32_t rid;
+ struct ldb_result *ext_res = NULL;
+ struct dsdb_extended_allocate_rid *rid_return = NULL;
+ if (!PyArg_ParseTuple(args, "O", &py_ldb)) {
+ return NULL;
+ }
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ rid_return = talloc_zero(ldb, struct dsdb_extended_allocate_rid);
+ if (rid_return == NULL) {
+ return PyErr_NoMemory();
+ }
+
+ /*
+ * Run DSDB_EXTENDED_ALLOCATE_RID to get a new RID
+ */
+
+ ret = ldb_extended(ldb, DSDB_EXTENDED_ALLOCATE_RID, rid_return, &ext_res);
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(rid_return);
+ TALLOC_FREE(ext_res);
+ PyErr_LDB_ERROR_IS_ERR_RAISE(py_ldb_get_exception(), ret, ldb);
+ }
+
+ rid = rid_return->rid;
+ TALLOC_FREE(rid_return);
+ TALLOC_FREE(ext_res);
+
+ return PyLong_FromLong(rid);
+}
+
+#ifdef AD_DC_BUILD_IS_ENABLED
+static PyObject *py_dns_delete_tombstones(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ NTSTATUS status;
+ struct ldb_context *ldb = NULL;
+ TALLOC_CTX *mem_ctx = NULL;
+ char *error_string = NULL;
+
+ if (!PyArg_ParseTuple(args, "O", &py_ldb)) {
+ return NULL;
+ }
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ mem_ctx = talloc_new(ldb);
+ if (mem_ctx == NULL) {
+ return PyErr_NoMemory();
+ }
+
+ status = dns_delete_tombstones(mem_ctx, ldb, &error_string);
+
+ if (!NT_STATUS_IS_OK(status)) {
+ if (error_string) {
+ PyErr_Format(PyExc_RuntimeError, "%s", error_string);
+ } else {
+ PyErr_SetNTSTATUS(status);
+ }
+ TALLOC_FREE(mem_ctx);
+ return NULL;
+ }
+
+ TALLOC_FREE(mem_ctx);
+ Py_RETURN_NONE;
+}
+
+static PyObject *py_scavenge_dns_records(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb;
+ NTSTATUS status;
+ struct ldb_context *ldb = NULL;
+ TALLOC_CTX *mem_ctx = NULL;
+ char *error_string = NULL;
+
+ if (!PyArg_ParseTuple(args, "O", &py_ldb)) {
+ return NULL;
+ }
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ mem_ctx = talloc_new(ldb);
+ if (mem_ctx == NULL) {
+ return PyErr_NoMemory();
+ }
+
+ status = dns_tombstone_records(mem_ctx, ldb, &error_string);
+
+ if (!NT_STATUS_IS_OK(status)) {
+ if (error_string) {
+ PyErr_Format(PyExc_RuntimeError, "%s", error_string);
+ } else {
+ PyErr_SetNTSTATUS(status);
+ }
+ TALLOC_FREE(mem_ctx);
+ return NULL;
+ }
+
+ TALLOC_FREE(mem_ctx);
+ Py_RETURN_NONE;
+}
+
+static PyObject *py_dsdb_garbage_collect_tombstones(PyObject *self, PyObject *args)
+{
+ PyObject *py_ldb, *py_list_dn;
+ struct ldb_context *ldb = NULL;
+ Py_ssize_t i;
+ Py_ssize_t length;
+ long long _current_time, _tombstone_lifetime = LLONG_MAX;
+ uint32_t tombstone_lifetime32;
+ struct dsdb_ldb_dn_list_node *part = NULL;
+ time_t current_time, tombstone_lifetime;
+ TALLOC_CTX *mem_ctx = NULL;
+ NTSTATUS status;
+ unsigned int num_objects_removed = 0;
+ unsigned int num_links_removed = 0;
+ char *error_string = NULL;
+
+ if (!PyArg_ParseTuple(args, "OOL|L", &py_ldb,
+ &py_list_dn, &_current_time, &_tombstone_lifetime)) {
+ return NULL;
+ }
+
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ mem_ctx = talloc_new(ldb);
+ if (mem_ctx == NULL) {
+ return PyErr_NoMemory();
+ }
+
+ current_time = _current_time;
+
+ if (_tombstone_lifetime == LLONG_MAX) {
+ int ret = dsdb_tombstone_lifetime(ldb, &tombstone_lifetime32);
+ if (ret != LDB_SUCCESS) {
+ PyErr_Format(PyExc_RuntimeError,
+ "Failed to get tombstone lifetime: %s",
+ ldb_errstring(ldb));
+ TALLOC_FREE(mem_ctx);
+ return NULL;
+ }
+ tombstone_lifetime = tombstone_lifetime32;
+ } else {
+ tombstone_lifetime = _tombstone_lifetime;
+ }
+
+ if (!PyList_Check(py_list_dn)) {
+ PyErr_SetString(PyExc_TypeError, "A list of DNs were expected");
+ TALLOC_FREE(mem_ctx);
+ return NULL;
+ }
+
+ length = PyList_GET_SIZE(py_list_dn);
+
+ for (i = 0; i < length; i++) {
+ const char *part_str = PyUnicode_AsUTF8(PyList_GetItem(py_list_dn, i));
+ struct ldb_dn *p;
+ struct dsdb_ldb_dn_list_node *node;
+
+ if (part_str == NULL) {
+ TALLOC_FREE(mem_ctx);
+ return PyErr_NoMemory();
+ }
+
+ p = ldb_dn_new(mem_ctx, ldb, part_str);
+ if (p == NULL) {
+ PyErr_Format(PyExc_RuntimeError, "Failed to parse DN %s", part_str);
+ TALLOC_FREE(mem_ctx);
+ return NULL;
+ }
+ node = talloc_zero(mem_ctx, struct dsdb_ldb_dn_list_node);
+ node->dn = p;
+
+ DLIST_ADD_END(part, node);
+ }
+
+ status = dsdb_garbage_collect_tombstones(mem_ctx, ldb,
+ part, current_time,
+ tombstone_lifetime,
+ &num_objects_removed,
+ &num_links_removed,
+ &error_string);
+
+ if (!NT_STATUS_IS_OK(status)) {
+ if (error_string) {
+ PyErr_Format(PyExc_RuntimeError, "%s", error_string);
+ } else {
+ PyErr_SetNTSTATUS(status);
+ }
+ TALLOC_FREE(mem_ctx);
+ return NULL;
+ }
+
+ TALLOC_FREE(mem_ctx);
+
+ return Py_BuildValue("(II)", num_objects_removed,
+ num_links_removed);
+}
+#endif
+
+static PyObject *py_dsdb_load_udv_v2(PyObject *self, PyObject *args)
+{
+ uint32_t count;
+ int ret, i;
+ bool ok;
+ PyObject *py_ldb = NULL, *py_dn = NULL, *pylist = NULL;
+ struct ldb_context *samdb = NULL;
+ struct ldb_dn *dn = NULL;
+ struct drsuapi_DsReplicaCursor2 *cursors = NULL;
+ TALLOC_CTX *tmp_ctx = NULL;
+
+ if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_dn)) {
+ return NULL;
+ }
+
+ PyErr_LDB_OR_RAISE(py_ldb, samdb);
+
+ tmp_ctx = talloc_new(samdb);
+ if (tmp_ctx == NULL) {
+ return PyErr_NoMemory();
+ }
+
+ ok = pyldb_Object_AsDn(tmp_ctx, py_dn, samdb, &dn);
+ if (!ok) {
+ TALLOC_FREE(tmp_ctx);
+ return NULL;
+ }
+
+ ret = dsdb_load_udv_v2(samdb, dn, tmp_ctx, &cursors, &count);
+ if (ret != LDB_SUCCESS) {
+ TALLOC_FREE(tmp_ctx);
+ PyErr_SetString(PyExc_RuntimeError,
+ "Failed to load udv from ldb");
+ return NULL;
+ }
+
+ pylist = PyList_New(count);
+ if (pylist == NULL) {
+ TALLOC_FREE(tmp_ctx);
+ return PyErr_NoMemory();
+ }
+
+ for (i = 0; i < count; i++) {
+ PyObject *py_cursor;
+ struct drsuapi_DsReplicaCursor2 *cursor;
+ cursor = talloc(tmp_ctx, struct drsuapi_DsReplicaCursor2);
+ if (cursor == NULL) {
+ TALLOC_FREE(tmp_ctx);
+ return PyErr_NoMemory();
+ }
+ *cursor = cursors[i];
+
+ py_cursor = py_return_ndr_struct("samba.dcerpc.drsuapi",
+ "DsReplicaCursor2",
+ cursor, cursor);
+ if (py_cursor == NULL) {
+ TALLOC_FREE(tmp_ctx);
+ return PyErr_NoMemory();
+ }
+
+ PyList_SetItem(pylist, i, py_cursor);
+ }
+
+ TALLOC_FREE(tmp_ctx);
+ return pylist;
+}
+
+static PyObject *py_dsdb_user_account_control_flag_bit_to_string(PyObject *self, PyObject *args)
+{
+ const char *str;
+ long long uf;
+ if (!PyArg_ParseTuple(args, "L", &uf)) {
+ return NULL;
+ }
+
+ if (uf > UINT32_MAX) {
+ return PyErr_Format(PyExc_OverflowError, "No UF_ flags are over UINT32_MAX");
+ }
+ if (uf < 0) {
+ return PyErr_Format(PyExc_KeyError, "No UF_ flags are less then zero");
+ }
+
+ str = dsdb_user_account_control_flag_bit_to_string(uf);
+ if (str == NULL) {
+ return PyErr_Format(PyExc_KeyError,
+ "No such UF_ flag 0x%08x",
+ (unsigned int)uf);
+ }
+ return PyUnicode_FromString(str);
+}
+
+static PyObject *py_dsdb_check_and_update_fl(PyObject *self, PyObject *args)
+{
+ TALLOC_CTX *frame = NULL;
+
+ PyObject *py_ldb = NULL, *py_lp = NULL;
+ struct ldb_context *ldb = NULL;
+ struct loadparm_context *lp_ctx = NULL;
+
+ int ret;
+
+ if (!PyArg_ParseTuple(args, "OO", &py_ldb, &py_lp)) {
+ return NULL;
+ }
+
+ PyErr_LDB_OR_RAISE(py_ldb, ldb);
+
+ frame = talloc_stackframe();
+
+ lp_ctx = lpcfg_from_py_object(frame, py_lp);
+ if (lp_ctx == NULL) {
+ TALLOC_FREE(frame);
+ return NULL;
+ }
+
+ ret = dsdb_check_and_update_fl(ldb, lp_ctx);
+ TALLOC_FREE(frame);
+
+ PyErr_LDB_ERROR_IS_ERR_RAISE(py_ldb_get_exception(), ret, ldb);
+
+ Py_RETURN_NONE;
+}
+
+static PyObject *py_dsdb_dc_operatingSystemVersion(PyObject *self, PyObject *args)
+{
+ const char *str = NULL;
+ int dc_level = 0;
+
+ if (!PyArg_ParseTuple(args, "i", &dc_level)) {
+ return NULL;
+ }
+
+ str = dsdb_dc_operatingSystemVersion(dc_level);
+ if (str == NULL) {
+ return PyErr_Format(PyExc_KeyError,
+ "dsdb_dc_operatingSystemVersion(%d) failed",
+ dc_level);
+ }
+
+ return PyUnicode_FromString(str);
+}
+
+static PyMethodDef py_dsdb_methods[] = {
+ { "_samdb_server_site_name", (PyCFunction)py_samdb_server_site_name,
+ METH_VARARGS, "Get the server site name as a string"},
+ { "_dsdb_convert_schema_to_openldap",
+ (PyCFunction)py_dsdb_convert_schema_to_openldap, METH_VARARGS,
+ "dsdb_convert_schema_to_openldap(ldb, target_str, mapping) -> str\n"
+ "Create an OpenLDAP schema from a schema." },
+ { "_samdb_set_domain_sid", (PyCFunction)py_samdb_set_domain_sid,
+ METH_VARARGS,
+ "samdb_set_domain_sid(samdb, sid)\n"
+ "Set SID of domain to use." },
+ { "_samdb_get_domain_sid", (PyCFunction)py_samdb_get_domain_sid,
+ METH_VARARGS,
+ "samdb_get_domain_sid(samdb)\n"
+ "Get SID of domain in use." },
+ { "_samdb_ntds_invocation_id", (PyCFunction)py_samdb_ntds_invocation_id,
+ METH_VARARGS, "get the NTDS invocation ID GUID as a string"},
+ { "_samdb_set_ntds_settings_dn", (PyCFunction)py_samdb_set_ntds_settings_dn,
+ METH_VARARGS,
+ "samdb_set_ntds_settings_dn(samdb, ntds_settings_dn)\n"
+ "Set NTDS Settings DN for this LDB (allows it to be set before the DB fully exists)." },
+ { "_dsdb_get_oid_from_attid", (PyCFunction)py_dsdb_get_oid_from_attid,
+ METH_VARARGS, NULL },
+ { "_dsdb_get_attid_from_lDAPDisplayName", (PyCFunction)py_dsdb_get_attid_from_lDAPDisplayName,
+ METH_VARARGS, NULL },
+ { "_dsdb_get_syntax_oid_from_lDAPDisplayName", (PyCFunction)py_dsdb_get_syntax_oid_from_lDAPDisplayName,
+ METH_VARARGS, NULL },
+ { "_dsdb_get_systemFlags_from_lDAPDisplayName", (PyCFunction)py_dsdb_get_systemFlags_from_lDAPDisplayName,
+ METH_VARARGS, NULL },
+ { "_dsdb_get_linkId_from_lDAPDisplayName", (PyCFunction)py_dsdb_get_linkId_from_lDAPDisplayName,
+ METH_VARARGS, NULL },
+ { "_dsdb_get_lDAPDisplayName_by_attid", (PyCFunction)py_dsdb_get_lDAPDisplayName_by_attid,
+ METH_VARARGS, NULL },
+ { "_dsdb_get_backlink_from_lDAPDisplayName", (PyCFunction)py_dsdb_get_backlink_from_lDAPDisplayName,
+ METH_VARARGS, NULL },
+ { "_dsdb_set_ntds_invocation_id",
+ (PyCFunction)py_dsdb_set_ntds_invocation_id, METH_VARARGS,
+ NULL },
+ { "_samdb_ntds_objectGUID", (PyCFunction)py_samdb_ntds_objectGUID,
+ METH_VARARGS, "get the NTDS objectGUID as a string"},
+ { "_dsdb_set_global_schema", (PyCFunction)py_dsdb_set_global_schema,
+ METH_VARARGS, NULL },
+ { "_dsdb_load_partition_usn", (PyCFunction)py_dsdb_load_partition_usn,
+ METH_VARARGS,
+ "get uSNHighest and uSNUrgent from the partition @REPLCHANGED"},
+ { "_dsdb_set_am_rodc",
+ (PyCFunction)py_dsdb_set_am_rodc, METH_VARARGS,
+ NULL },
+ { "_am_rodc",
+ (PyCFunction)py_dsdb_am_rodc, METH_VARARGS,
+ NULL },
+ { "_am_pdc",
+ (PyCFunction)py_dsdb_am_pdc, METH_VARARGS,
+ NULL },
+ { "_dsdb_set_schema_from_ldif", (PyCFunction)py_dsdb_set_schema_from_ldif, METH_VARARGS,
+ NULL },
+ { "_dsdb_set_schema_from_ldb", (PyCFunction)py_dsdb_set_schema_from_ldb, METH_VARARGS,
+ NULL },
+ { "_dsdb_write_prefixes_from_schema_to_ldb", (PyCFunction)py_dsdb_write_prefixes_from_schema_to_ldb, METH_VARARGS,
+ NULL },
+ { "_dsdb_get_partitions_dn", (PyCFunction)py_dsdb_get_partitions_dn, METH_VARARGS, NULL },
+ { "_dsdb_get_nc_root", (PyCFunction)py_dsdb_get_nc_root, METH_VARARGS, NULL },
+ { "_dsdb_get_wellknown_dn", (PyCFunction)py_dsdb_get_wellknown_dn, METH_VARARGS, NULL },
+ { "_dsdb_DsReplicaAttribute", (PyCFunction)py_dsdb_DsReplicaAttribute, METH_VARARGS, NULL },
+ { "_dsdb_normalise_attributes", (PyCFunction)py_dsdb_normalise_attributes, METH_VARARGS, NULL },
+#ifdef AD_DC_BUILD_IS_ENABLED
+ { "_dsdb_garbage_collect_tombstones", (PyCFunction)py_dsdb_garbage_collect_tombstones, METH_VARARGS,
+ "_dsdb_kcc_check_deleted(samdb, [dn], current_time, tombstone_lifetime)"
+ " -> (num_objects_expunged, num_links_expunged)" },
+ { "_scavenge_dns_records", (PyCFunction)py_scavenge_dns_records,
+ METH_VARARGS, NULL},
+ { "_dns_delete_tombstones", (PyCFunction)py_dns_delete_tombstones,
+ METH_VARARGS, NULL},
+#endif
+ { "_dsdb_create_own_rid_set", (PyCFunction)py_dsdb_create_own_rid_set, METH_VARARGS,
+ "_dsdb_create_own_rid_set(samdb)"
+ " -> None" },
+ { "_dsdb_allocate_rid", (PyCFunction)py_dsdb_allocate_rid, METH_VARARGS,
+ "_dsdb_allocate_rid(samdb)"
+ " -> RID" },
+ { "_dsdb_load_udv_v2", (PyCFunction)py_dsdb_load_udv_v2, METH_VARARGS, NULL },
+ { "user_account_control_flag_bit_to_string",
+ (PyCFunction)py_dsdb_user_account_control_flag_bit_to_string,
+ METH_VARARGS,
+ "user_account_control_flag_bit_to_string(bit)"
+ " -> string name" },
+ { "check_and_update_fl",
+ (PyCFunction)py_dsdb_check_and_update_fl,
+ METH_VARARGS,
+ "check_and_update_fl(ldb, lp) -> None\n"
+ "Hook to run in testing the code run on samba server startup "
+ "to validate and update DC functional levels"},
+ { "dc_operatingSystemVersion",
+ (PyCFunction)py_dsdb_dc_operatingSystemVersion,
+ METH_VARARGS,
+ "dsdb_dc_operatingSystemVersion(dc_level)"
+ " -> string name" },
+ {0}
+};
+
+static struct PyModuleDef moduledef = {
+ PyModuleDef_HEAD_INIT,
+ .m_name = "dsdb",
+ .m_doc = "Python bindings for the directory service databases.",
+ .m_size = -1,
+ .m_methods = py_dsdb_methods,
+};
+
+MODULE_INIT_FUNC(dsdb)
+{
+ PyObject *m;
+
+ m = PyModule_Create(&moduledef);
+
+ if (m == NULL)
+ return NULL;
+
+#define ADD_DSDB_FLAG(val) PyModule_AddObject(m, #val, PyLong_FromLong(val))
+
+ /* "userAccountControl" flags */
+ ADD_DSDB_FLAG(UF_NORMAL_ACCOUNT);
+ ADD_DSDB_FLAG(UF_TEMP_DUPLICATE_ACCOUNT);
+ ADD_DSDB_FLAG(UF_SERVER_TRUST_ACCOUNT);
+ ADD_DSDB_FLAG(UF_WORKSTATION_TRUST_ACCOUNT);
+ ADD_DSDB_FLAG(UF_INTERDOMAIN_TRUST_ACCOUNT);
+ ADD_DSDB_FLAG(UF_PASSWD_NOTREQD);
+ ADD_DSDB_FLAG(UF_ACCOUNTDISABLE);
+
+ ADD_DSDB_FLAG(UF_SCRIPT);
+ ADD_DSDB_FLAG(UF_ACCOUNTDISABLE);
+ ADD_DSDB_FLAG(UF_00000004);
+ ADD_DSDB_FLAG(UF_HOMEDIR_REQUIRED);
+ ADD_DSDB_FLAG(UF_LOCKOUT);
+ ADD_DSDB_FLAG(UF_PASSWD_NOTREQD);
+ ADD_DSDB_FLAG(UF_PASSWD_CANT_CHANGE);
+ ADD_DSDB_FLAG(UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED);
+ ADD_DSDB_FLAG(UF_TEMP_DUPLICATE_ACCOUNT);
+ ADD_DSDB_FLAG(UF_NORMAL_ACCOUNT);
+ ADD_DSDB_FLAG(UF_00000400);
+ ADD_DSDB_FLAG(UF_INTERDOMAIN_TRUST_ACCOUNT);
+ ADD_DSDB_FLAG(UF_WORKSTATION_TRUST_ACCOUNT);
+ ADD_DSDB_FLAG(UF_SERVER_TRUST_ACCOUNT);
+ ADD_DSDB_FLAG(UF_00004000);
+ ADD_DSDB_FLAG(UF_00008000);
+ ADD_DSDB_FLAG(UF_DONT_EXPIRE_PASSWD);
+ ADD_DSDB_FLAG(UF_MNS_LOGON_ACCOUNT);
+ ADD_DSDB_FLAG(UF_SMARTCARD_REQUIRED);
+ ADD_DSDB_FLAG(UF_TRUSTED_FOR_DELEGATION);
+ ADD_DSDB_FLAG(UF_NOT_DELEGATED);
+ ADD_DSDB_FLAG(UF_USE_DES_KEY_ONLY);
+ ADD_DSDB_FLAG(UF_DONT_REQUIRE_PREAUTH);
+ ADD_DSDB_FLAG(UF_PASSWORD_EXPIRED);
+ ADD_DSDB_FLAG(UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION);
+ ADD_DSDB_FLAG(UF_NO_AUTH_DATA_REQUIRED);
+ ADD_DSDB_FLAG(UF_PARTIAL_SECRETS_ACCOUNT);
+ ADD_DSDB_FLAG(UF_USE_AES_KEYS);
+
+ /* groupType flags */
+ ADD_DSDB_FLAG(GTYPE_SECURITY_BUILTIN_LOCAL_GROUP);
+ ADD_DSDB_FLAG(GTYPE_SECURITY_GLOBAL_GROUP);
+ ADD_DSDB_FLAG(GTYPE_SECURITY_DOMAIN_LOCAL_GROUP);
+ ADD_DSDB_FLAG(GTYPE_SECURITY_UNIVERSAL_GROUP);
+ ADD_DSDB_FLAG(GTYPE_DISTRIBUTION_GLOBAL_GROUP);
+ ADD_DSDB_FLAG(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP);
+ ADD_DSDB_FLAG(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP);
+
+ /* "sAMAccountType" flags */
+ ADD_DSDB_FLAG(ATYPE_NORMAL_ACCOUNT);
+ ADD_DSDB_FLAG(ATYPE_WORKSTATION_TRUST);
+ ADD_DSDB_FLAG(ATYPE_INTERDOMAIN_TRUST);
+ ADD_DSDB_FLAG(ATYPE_SECURITY_GLOBAL_GROUP);
+ ADD_DSDB_FLAG(ATYPE_SECURITY_LOCAL_GROUP);
+ ADD_DSDB_FLAG(ATYPE_SECURITY_UNIVERSAL_GROUP);
+ ADD_DSDB_FLAG(ATYPE_DISTRIBUTION_GLOBAL_GROUP);
+ ADD_DSDB_FLAG(ATYPE_DISTRIBUTION_LOCAL_GROUP);
+ ADD_DSDB_FLAG(ATYPE_DISTRIBUTION_UNIVERSAL_GROUP);
+
+ /* "domainFunctionality", "forestFunctionality" flags in the rootDSE */
+ ADD_DSDB_FLAG(DS_DOMAIN_FUNCTION_2000);
+ ADD_DSDB_FLAG(DS_DOMAIN_FUNCTION_2003_MIXED);
+ ADD_DSDB_FLAG(DS_DOMAIN_FUNCTION_2003);
+ ADD_DSDB_FLAG(DS_DOMAIN_FUNCTION_2008);
+ ADD_DSDB_FLAG(DS_DOMAIN_FUNCTION_2008_R2);
+ ADD_DSDB_FLAG(DS_DOMAIN_FUNCTION_2012);
+ ADD_DSDB_FLAG(DS_DOMAIN_FUNCTION_2012_R2);
+ ADD_DSDB_FLAG(DS_DOMAIN_FUNCTION_2016);
+
+ /* nc replica flags */
+ ADD_DSDB_FLAG(INSTANCE_TYPE_IS_NC_HEAD);
+ ADD_DSDB_FLAG(INSTANCE_TYPE_UNINSTANT);
+ ADD_DSDB_FLAG(INSTANCE_TYPE_WRITE);
+ ADD_DSDB_FLAG(INSTANCE_TYPE_NC_ABOVE);
+ ADD_DSDB_FLAG(INSTANCE_TYPE_NC_COMING);
+ ADD_DSDB_FLAG(INSTANCE_TYPE_NC_GOING);
+
+ /* "systemFlags" */
+ ADD_DSDB_FLAG(SYSTEM_FLAG_CR_NTDS_NC);
+ ADD_DSDB_FLAG(SYSTEM_FLAG_CR_NTDS_DOMAIN);
+ ADD_DSDB_FLAG(SYSTEM_FLAG_CR_NTDS_NOT_GC_REPLICATED);
+ ADD_DSDB_FLAG(SYSTEM_FLAG_SCHEMA_BASE_OBJECT);
+ ADD_DSDB_FLAG(SYSTEM_FLAG_ATTR_IS_RDN);
+ ADD_DSDB_FLAG(SYSTEM_FLAG_DISALLOW_MOVE_ON_DELETE);
+ ADD_DSDB_FLAG(SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE);
+ ADD_DSDB_FLAG(SYSTEM_FLAG_DOMAIN_DISALLOW_RENAME);
+ ADD_DSDB_FLAG(SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE);
+ ADD_DSDB_FLAG(SYSTEM_FLAG_CONFIG_ALLOW_MOVE);
+ ADD_DSDB_FLAG(SYSTEM_FLAG_CONFIG_ALLOW_RENAME);
+ ADD_DSDB_FLAG(SYSTEM_FLAG_DISALLOW_DELETE);
+
+ /* Kerberos encryption type constants */
+ ADD_DSDB_FLAG(ENC_ALL_TYPES);
+ ADD_DSDB_FLAG(ENC_CRC32);
+ ADD_DSDB_FLAG(ENC_RSA_MD5);
+ ADD_DSDB_FLAG(ENC_RC4_HMAC_MD5);
+ ADD_DSDB_FLAG(ENC_HMAC_SHA1_96_AES128);
+ ADD_DSDB_FLAG(ENC_HMAC_SHA1_96_AES256);
+ ADD_DSDB_FLAG(ENC_HMAC_SHA1_96_AES256_SK);
+
+ ADD_DSDB_FLAG(SEARCH_FLAG_ATTINDEX);
+ ADD_DSDB_FLAG(SEARCH_FLAG_PDNTATTINDEX);
+ ADD_DSDB_FLAG(SEARCH_FLAG_ANR);
+ ADD_DSDB_FLAG(SEARCH_FLAG_PRESERVEONDELETE);
+ ADD_DSDB_FLAG(SEARCH_FLAG_COPY);
+ ADD_DSDB_FLAG(SEARCH_FLAG_TUPLEINDEX);
+ ADD_DSDB_FLAG(SEARCH_FLAG_SUBTREEATTRINDEX);
+ ADD_DSDB_FLAG(SEARCH_FLAG_CONFIDENTIAL);
+ ADD_DSDB_FLAG(SEARCH_FLAG_NEVERVALUEAUDIT);
+ ADD_DSDB_FLAG(SEARCH_FLAG_RODC_ATTRIBUTE);
+
+ ADD_DSDB_FLAG(DS_FLAG_ATTR_NOT_REPLICATED);
+ ADD_DSDB_FLAG(DS_FLAG_ATTR_REQ_PARTIAL_SET_MEMBER);
+ ADD_DSDB_FLAG(DS_FLAG_ATTR_IS_CONSTRUCTED);
+
+ ADD_DSDB_FLAG(DS_NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED);
+ ADD_DSDB_FLAG(DS_NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED);
+ ADD_DSDB_FLAG(DS_NTDSSETTINGS_OPT_IS_TOPL_MIN_HOPS_DISABLED);
+ ADD_DSDB_FLAG(DS_NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED);
+ ADD_DSDB_FLAG(DS_NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED);
+ ADD_DSDB_FLAG(DS_NTDSSETTINGS_OPT_IS_GROUP_CACHING_ENABLED);
+ ADD_DSDB_FLAG(DS_NTDSSETTINGS_OPT_FORCE_KCC_WHISTLER_BEHAVIOR);
+ ADD_DSDB_FLAG(DS_NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED);
+ ADD_DSDB_FLAG(DS_NTDSSETTINGS_OPT_IS_SCHEDULE_HASHING_ENABLED);
+ ADD_DSDB_FLAG(DS_NTDSSETTINGS_OPT_IS_REDUNDANT_SERVER_TOPOLOGY_ENABLED);
+
+ ADD_DSDB_FLAG(DS_NTDSDSA_OPT_IS_GC);
+ ADD_DSDB_FLAG(DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL);
+ ADD_DSDB_FLAG(DS_NTDSDSA_OPT_DISABLE_OUTBOUND_REPL);
+ ADD_DSDB_FLAG(DS_NTDSDSA_OPT_DISABLE_NTDSCONN_XLATE);
+ ADD_DSDB_FLAG(DS_NTDSDSA_OPT_DISABLE_SPN_REGISTRATION);
+
+ /* dsHeuristics character indexes (see MS-ADTS 7.1.1.2.4.1.2) */
+ ADD_DSDB_FLAG(DS_HR_SUPFIRSTLASTANR);
+ ADD_DSDB_FLAG(DS_HR_SUPLASTFIRSTANR);
+ ADD_DSDB_FLAG(DS_HR_DOLISTOBJECT);
+ ADD_DSDB_FLAG(DS_HR_DONICKRES);
+ ADD_DSDB_FLAG(DS_HR_LDAP_USEPERMMOD);
+ ADD_DSDB_FLAG(DS_HR_HIDEDSID);
+ ADD_DSDB_FLAG(DS_HR_BLOCK_ANONYMOUS_OPS);
+ ADD_DSDB_FLAG(DS_HR_ALLOW_ANON_NSPI);
+ ADD_DSDB_FLAG(DS_HR_USER_PASSWORD_SUPPORT);
+ ADD_DSDB_FLAG(DS_HR_TENTH_CHAR);
+ ADD_DSDB_FLAG(DS_HR_SPECIFY_GUID_ON_ADD);
+ ADD_DSDB_FLAG(DS_HR_NO_STANDARD_SD);
+ ADD_DSDB_FLAG(DS_HR_ALLOW_NONSECURE_PWD_OPS);
+ ADD_DSDB_FLAG(DS_HR_NO_PROPAGATE_ON_NOCHANGE);
+ ADD_DSDB_FLAG(DS_HR_COMPUTE_ANR_STATS);
+ ADD_DSDB_FLAG(DS_HR_ADMINSDEXMASK);
+ ADD_DSDB_FLAG(DS_HR_KVNOEMUW2K);
+
+ ADD_DSDB_FLAG(DS_HR_TWENTIETH_CHAR);
+ ADD_DSDB_FLAG(DS_HR_ATTR_AUTHZ_ON_LDAP_ADD);
+ ADD_DSDB_FLAG(DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS);
+ ADD_DSDB_FLAG(DS_HR_THIRTIETH_CHAR);
+ ADD_DSDB_FLAG(DS_HR_FOURTIETH_CHAR);
+ ADD_DSDB_FLAG(DS_HR_FIFTIETH_CHAR);
+ ADD_DSDB_FLAG(DS_HR_SIXTIETH_CHAR);
+ ADD_DSDB_FLAG(DS_HR_SEVENTIETH_CHAR);
+ ADD_DSDB_FLAG(DS_HR_EIGHTIETH_CHAR);
+ ADD_DSDB_FLAG(DS_HR_NINETIETH_CHAR);
+
+ ADD_DSDB_FLAG(NTDSCONN_KCC_GC_TOPOLOGY);
+ ADD_DSDB_FLAG(NTDSCONN_KCC_RING_TOPOLOGY);
+ ADD_DSDB_FLAG(NTDSCONN_KCC_MINIMIZE_HOPS_TOPOLOGY);
+ ADD_DSDB_FLAG(NTDSCONN_KCC_STALE_SERVERS_TOPOLOGY);
+ ADD_DSDB_FLAG(NTDSCONN_KCC_OSCILLATING_CONNECTION_TOPOLOGY);
+ ADD_DSDB_FLAG(NTDSCONN_KCC_INTERSITE_GC_TOPOLOGY);
+ ADD_DSDB_FLAG(NTDSCONN_KCC_INTERSITE_TOPOLOGY);
+ ADD_DSDB_FLAG(NTDSCONN_KCC_SERVER_FAILOVER_TOPOLOGY);
+ ADD_DSDB_FLAG(NTDSCONN_KCC_SITE_FAILOVER_TOPOLOGY);
+ ADD_DSDB_FLAG(NTDSCONN_KCC_REDUNDANT_SERVER_TOPOLOGY);
+
+ ADD_DSDB_FLAG(NTDSCONN_OPT_IS_GENERATED);
+ ADD_DSDB_FLAG(NTDSCONN_OPT_TWOWAY_SYNC);
+ ADD_DSDB_FLAG(NTDSCONN_OPT_OVERRIDE_NOTIFY_DEFAULT);
+ ADD_DSDB_FLAG(NTDSCONN_OPT_USE_NOTIFY);
+ ADD_DSDB_FLAG(NTDSCONN_OPT_DISABLE_INTERSITE_COMPRESSION);
+ ADD_DSDB_FLAG(NTDSCONN_OPT_USER_OWNED_SCHEDULE);
+ ADD_DSDB_FLAG(NTDSCONN_OPT_RODC_TOPOLOGY);
+
+ /* Site Link Object options */
+ ADD_DSDB_FLAG(NTDSSITELINK_OPT_USE_NOTIFY);
+ ADD_DSDB_FLAG(NTDSSITELINK_OPT_TWOWAY_SYNC);
+ ADD_DSDB_FLAG(NTDSSITELINK_OPT_DISABLE_COMPRESSION);
+
+ /* GPO policy flags */
+ ADD_DSDB_FLAG(GPLINK_OPT_DISABLE);
+ ADD_DSDB_FLAG(GPLINK_OPT_ENFORCE);
+ ADD_DSDB_FLAG(GPO_FLAG_USER_DISABLE);
+ ADD_DSDB_FLAG(GPO_FLAG_MACHINE_DISABLE);
+ ADD_DSDB_FLAG(GPO_INHERIT);
+ ADD_DSDB_FLAG(GPO_BLOCK_INHERITANCE);
+
+#define ADD_DSDB_STRING(val) PyModule_AddObject(m, #val, PyUnicode_FromString(val))
+
+ ADD_DSDB_STRING(DSDB_SYNTAX_BINARY_DN);
+ ADD_DSDB_STRING(DSDB_SYNTAX_STRING_DN);
+ ADD_DSDB_STRING(DSDB_SYNTAX_OR_NAME);
+ ADD_DSDB_STRING(DSDB_CONTROL_DBCHECK);
+ ADD_DSDB_STRING(DSDB_CONTROL_DBCHECK_MODIFY_RO_REPLICA);
+ ADD_DSDB_STRING(DSDB_CONTROL_DBCHECK_FIX_DUPLICATE_LINKS);
+ ADD_DSDB_STRING(DSDB_CONTROL_DBCHECK_FIX_LINK_DN_NAME);
+ ADD_DSDB_STRING(DSDB_CONTROL_DBCHECK_FIX_LINK_DN_SID);
+ ADD_DSDB_STRING(DSDB_CONTROL_REPLMD_VANISH_LINKS);
+ ADD_DSDB_STRING(DSDB_CONTROL_PERMIT_INTERDOMAIN_TRUST_UAC_OID);
+ ADD_DSDB_STRING(DSDB_CONTROL_SKIP_DUPLICATES_CHECK_OID);
+ ADD_DSDB_STRING(DSDB_CONTROL_BYPASS_PASSWORD_HASH_OID);
+ ADD_DSDB_STRING(DSDB_CONTROL_INVALID_NOT_IMPLEMENTED);
+
+ ADD_DSDB_STRING(DS_GUID_COMPUTERS_CONTAINER);
+ ADD_DSDB_STRING(DS_GUID_DELETED_OBJECTS_CONTAINER);
+ ADD_DSDB_STRING(DS_GUID_DOMAIN_CONTROLLERS_CONTAINER);
+ ADD_DSDB_STRING(DS_GUID_FOREIGNSECURITYPRINCIPALS_CONTAINER);
+ ADD_DSDB_STRING(DS_GUID_INFRASTRUCTURE_CONTAINER);
+ ADD_DSDB_STRING(DS_GUID_LOSTANDFOUND_CONTAINER);
+ ADD_DSDB_STRING(DS_GUID_MICROSOFT_PROGRAM_DATA_CONTAINER);
+ ADD_DSDB_STRING(DS_GUID_NTDS_QUOTAS_CONTAINER);
+ ADD_DSDB_STRING(DS_GUID_PROGRAM_DATA_CONTAINER);
+ ADD_DSDB_STRING(DS_GUID_SYSTEMS_CONTAINER);
+ ADD_DSDB_STRING(DS_GUID_USERS_CONTAINER);
+ ADD_DSDB_STRING(DS_GUID_MANAGED_SERVICE_ACCOUNTS_CONTAINER);
+
+ ADD_DSDB_STRING(DS_GUID_SCHEMA_ATTR_DEPARTMENT);
+ ADD_DSDB_STRING(DS_GUID_SCHEMA_ATTR_DNS_HOST_NAME);
+ ADD_DSDB_STRING(DS_GUID_SCHEMA_ATTR_INSTANCE_TYPE);
+ ADD_DSDB_STRING(DS_GUID_SCHEMA_ATTR_MS_SFU_30);
+ ADD_DSDB_STRING(DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR);
+ ADD_DSDB_STRING(DS_GUID_SCHEMA_ATTR_PRIMARY_GROUP_ID);
+ ADD_DSDB_STRING(DS_GUID_SCHEMA_ATTR_SERVICE_PRINCIPAL_NAME);
+ ADD_DSDB_STRING(DS_GUID_SCHEMA_ATTR_USER_ACCOUNT_CONTROL);
+ ADD_DSDB_STRING(DS_GUID_SCHEMA_ATTR_USER_PASSWORD);
+ ADD_DSDB_STRING(DS_GUID_SCHEMA_CLASS_COMPUTER);
+ ADD_DSDB_STRING(DS_GUID_SCHEMA_CLASS_MANAGED_SERVICE_ACCOUNT);
+ ADD_DSDB_STRING(DS_GUID_SCHEMA_CLASS_USER);
+
+ ADD_DSDB_STRING(DSDB_FULL_JOIN_REPLICATION_COMPLETED_OPAQUE_NAME);
+
+ return m;
+}
diff --git a/source4/dsdb/repl/drepl_extended.c b/source4/dsdb/repl/drepl_extended.c
new file mode 100644
index 0000000..8b5bb6f
--- /dev/null
+++ b/source4/dsdb/repl/drepl_extended.c
@@ -0,0 +1,211 @@
+/*
+ Unix SMB/CIFS Implementation.
+
+ DSDB replication service - extended operation code
+
+ Copyright (C) Andrew Tridgell 2010
+ Copyright (C) Andrew Bartlett 2010
+ Copyright (C) Nadezhda Ivanova 2010
+
+ based on drepl_notify.c
+
+ 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 "samba/service.h"
+#include "dsdb/repl/drepl_service.h"
+#include "param/param.h"
+
+#undef DBGC_CLASS
+#define DBGC_CLASS DBGC_DRS_REPL
+
+
+/*
+ create the role owner source dsa structure
+
+ nc_dn: the DN of the subtree being replicated
+ source_dsa_dn: the DN of the server that we are replicating from
+ */
+static WERROR drepl_create_extended_source_dsa(struct dreplsrv_service *service,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_dn *nc_dn,
+ struct ldb_dn *source_dsa_dn,
+ uint64_t min_usn,
+ struct dreplsrv_partition_source_dsa **_sdsa)
+{
+ struct dreplsrv_partition_source_dsa *sdsa;
+ struct ldb_context *ldb = service->samdb;
+ int ret;
+ WERROR werr;
+ struct ldb_dn *nc_root;
+ struct dreplsrv_partition *p;
+
+ sdsa = talloc_zero(service, struct dreplsrv_partition_source_dsa);
+ W_ERROR_HAVE_NO_MEMORY(sdsa);
+
+ sdsa->partition = talloc_zero(sdsa, struct dreplsrv_partition);
+ if (!sdsa->partition) {
+ talloc_free(sdsa);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ sdsa->partition->dn = ldb_dn_copy(sdsa->partition, nc_dn);
+ if (!sdsa->partition->dn) {
+ talloc_free(sdsa);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ sdsa->partition->nc.dn = ldb_dn_alloc_linearized(sdsa->partition, nc_dn);
+ if (!sdsa->partition->nc.dn) {
+ talloc_free(sdsa);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ ret = dsdb_find_guid_by_dn(ldb, nc_dn, &sdsa->partition->nc.guid);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find GUID for %s\n",
+ ldb_dn_get_linearized(nc_dn)));
+ talloc_free(sdsa);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ sdsa->repsFrom1 = &sdsa->_repsFromBlob.ctr.ctr1;
+ ret = dsdb_find_guid_by_dn(ldb, source_dsa_dn, &sdsa->repsFrom1->source_dsa_obj_guid);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find objectGUID for %s\n",
+ ldb_dn_get_linearized(source_dsa_dn)));
+ talloc_free(sdsa);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ sdsa->repsFrom1->other_info = talloc_zero(sdsa, struct repsFromTo1OtherInfo);
+ if (!sdsa->repsFrom1->other_info) {
+ talloc_free(sdsa);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ sdsa->repsFrom1->other_info->dns_name = samdb_ntds_msdcs_dns_name(ldb,
+ sdsa->repsFrom1->other_info,
+ &sdsa->repsFrom1->source_dsa_obj_guid);
+ if (!sdsa->repsFrom1->other_info->dns_name) {
+ talloc_free(sdsa);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ werr = dreplsrv_out_connection_attach(service, sdsa->repsFrom1, &sdsa->conn);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0,(__location__ ": Failed to attach connection to %s\n",
+ ldb_dn_get_linearized(nc_dn)));
+ talloc_free(sdsa);
+ return werr;
+ }
+
+ ret = dsdb_find_nc_root(service->samdb, sdsa, nc_dn, &nc_root);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find nc_root for %s\n",
+ ldb_dn_get_linearized(nc_dn)));
+ talloc_free(sdsa);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ /* use the partition uptodateness vector */
+ ret = dsdb_load_udv_v2(service->samdb, nc_root, sdsa->partition,
+ &sdsa->partition->uptodatevector.cursors,
+ &sdsa->partition->uptodatevector.count);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to load UDV for %s\n",
+ ldb_dn_get_linearized(nc_root)));
+ talloc_free(sdsa);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ /* find the highwatermark from the partitions list */
+ for (p=service->partitions; p; p=p->next) {
+ if (ldb_dn_compare(p->dn, nc_root) == 0) {
+ struct dreplsrv_partition_source_dsa *s;
+ werr = dreplsrv_partition_source_dsa_by_guid(p,
+ &sdsa->repsFrom1->source_dsa_obj_guid,
+ &s);
+ if (W_ERROR_IS_OK(werr)) {
+ sdsa->repsFrom1->highwatermark = s->repsFrom1->highwatermark;
+ sdsa->repsFrom1->replica_flags = s->repsFrom1->replica_flags;
+ }
+ }
+ }
+
+ if (!service->am_rodc) {
+ sdsa->repsFrom1->replica_flags |= DRSUAPI_DRS_WRIT_REP;
+ }
+
+ *_sdsa = sdsa;
+ return WERR_OK;
+}
+
+struct extended_op_data {
+ dreplsrv_extended_callback_t callback;
+ void *callback_data;
+ struct dreplsrv_partition_source_dsa *sdsa;
+};
+
+/*
+ called when an extended op finishes
+ */
+static void extended_op_callback(struct dreplsrv_service *service,
+ WERROR err,
+ enum drsuapi_DsExtendedError exop_error,
+ void *cb_data)
+{
+ struct extended_op_data *data = talloc_get_type_abort(cb_data, struct extended_op_data);
+ talloc_unlink(data, data->sdsa);
+ data->callback(service, err, exop_error, data->callback_data);
+ talloc_free(data);
+}
+
+/*
+ schedule a getncchanges request to the role owner for an extended operation
+ */
+WERROR drepl_request_extended_op(struct dreplsrv_service *service,
+ struct ldb_dn *nc_dn,
+ struct ldb_dn *source_dsa_dn,
+ enum drsuapi_DsExtendedOperation extended_op,
+ uint64_t fsmo_info,
+ uint64_t min_usn,
+ dreplsrv_extended_callback_t callback,
+ void *callback_data)
+{
+ WERROR werr;
+ struct extended_op_data *data;
+
+ data = talloc(service, struct extended_op_data);
+ W_ERROR_HAVE_NO_MEMORY(data);
+
+ werr = drepl_create_extended_source_dsa(service, data, nc_dn, source_dsa_dn, min_usn, &data->sdsa);
+ W_ERROR_NOT_OK_RETURN(werr);
+
+ data->callback = callback;
+ data->callback_data = callback_data;
+
+ werr = dreplsrv_schedule_partition_pull_source(service, data->sdsa,
+ 0, extended_op, fsmo_info,
+ extended_op_callback, data);
+ if (!W_ERROR_IS_OK(werr)) {
+ talloc_free(data);
+ }
+
+ dreplsrv_run_pending_ops(service);
+
+ return werr;
+}
diff --git a/source4/dsdb/repl/drepl_fsmo.c b/source4/dsdb/repl/drepl_fsmo.c
new file mode 100644
index 0000000..3c3cbad
--- /dev/null
+++ b/source4/dsdb/repl/drepl_fsmo.c
@@ -0,0 +1,147 @@
+/*
+ Unix SMB/CIFS Implementation.
+
+ DSDB replication service - FSMO role change
+
+ Copyright (C) Nadezhda Ivanova 2010
+ Copyright (C) Andrew Tridgell 2010
+ Copyright (C) Andrew Bartlett 2010
+ Copyright (C) Anatoliy Atanasov 2010
+
+ based on drepl_ridalloc.c
+
+ 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/samdb/samdb.h"
+#include "samba/service.h"
+#include "dsdb/repl/drepl_service.h"
+#include "param/param.h"
+
+#undef DBGC_CLASS
+#define DBGC_CLASS DBGC_DRS_REPL
+
+struct fsmo_role_state {
+ struct irpc_message *msg;
+ struct drepl_takeFSMORole *r;
+};
+
+static void drepl_role_callback(struct dreplsrv_service *service,
+ WERROR werr,
+ enum drsuapi_DsExtendedError ext_err,
+ void *cb_data)
+{
+ struct fsmo_role_state *fsmo = talloc_get_type_abort(cb_data, struct fsmo_role_state);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(2,(__location__ ": Failed role transfer - %s - extended_ret[0x%X]\n",
+ win_errstr(werr), ext_err));
+ } else {
+ DEBUG(2,(__location__ ": Successful role transfer\n"));
+ }
+ fsmo->r->out.result = werr;
+ irpc_send_reply(fsmo->msg, NT_STATUS_OK);
+}
+
+/*
+ see which role is we are asked to assume, initialize data and send request
+ */
+NTSTATUS drepl_take_FSMO_role(struct irpc_message *msg,
+ struct drepl_takeFSMORole *r)
+{
+ struct dreplsrv_service *service = talloc_get_type(msg->private_data,
+ struct dreplsrv_service);
+ struct ldb_dn *role_owner_dn, *fsmo_role_dn;
+ TALLOC_CTX *tmp_ctx = talloc_new(service);
+ uint64_t fsmo_info = 0;
+ enum drsuapi_DsExtendedOperation extended_op = DRSUAPI_EXOP_NONE;
+ WERROR werr;
+ enum drepl_role_master role = r->in.role;
+ struct fsmo_role_state *fsmo;
+ bool is_us;
+ int ret;
+
+ werr = dsdb_get_fsmo_role_info(tmp_ctx, service->samdb, role,
+ &fsmo_role_dn, &role_owner_dn);
+ if (!W_ERROR_IS_OK(werr)) {
+ talloc_free(tmp_ctx);
+ r->out.result = werr;
+ return NT_STATUS_OK;
+ }
+
+ switch (role) {
+ case DREPL_NAMING_MASTER:
+ case DREPL_INFRASTRUCTURE_MASTER:
+ case DREPL_SCHEMA_MASTER:
+ extended_op = DRSUAPI_EXOP_FSMO_REQ_ROLE;
+ break;
+ case DREPL_RID_MASTER:
+ extended_op = DRSUAPI_EXOP_FSMO_RID_REQ_ROLE;
+ break;
+ case DREPL_PDC_MASTER:
+ extended_op = DRSUAPI_EXOP_FSMO_REQ_PDC;
+ break;
+ default:
+ DEBUG(0,("Unknown role %u in role transfer\n",
+ (unsigned)role));
+ /* IRPC messages are trusted, so this really should not happen */
+ smb_panic("Unknown role despite dsdb_get_fsmo_role_info success");
+ }
+
+ ret = samdb_dn_is_our_ntdsa(service->samdb, role_owner_dn, &is_us);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,("FSMO role check failed (failed to confirm if our ntdsDsa) for DN %s and owner %s \n",
+ ldb_dn_get_linearized(fsmo_role_dn),
+ ldb_dn_get_linearized(role_owner_dn)));
+ talloc_free(tmp_ctx);
+ r->out.result = WERR_DS_DRA_INTERNAL_ERROR;
+ return NT_STATUS_OK;
+ }
+
+ if (is_us) {
+ DEBUG(5,("FSMO role check failed, we already own DN %s with %s\n",
+ ldb_dn_get_linearized(fsmo_role_dn),
+ ldb_dn_get_linearized(role_owner_dn)));
+ r->out.result = WERR_OK;
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+ }
+
+ fsmo = talloc(msg, struct fsmo_role_state);
+ NT_STATUS_HAVE_NO_MEMORY(fsmo);
+
+ fsmo->msg = msg;
+ fsmo->r = r;
+
+ werr = drepl_request_extended_op(service,
+ fsmo_role_dn,
+ role_owner_dn,
+ extended_op,
+ fsmo_info,
+ 0,
+ drepl_role_callback,
+ fsmo);
+ if (!W_ERROR_IS_OK(werr)) {
+ r->out.result = werr;
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+ }
+
+ /* mark this message to be answered later */
+ msg->defer_reply = true;
+ dreplsrv_run_pending_ops(service);
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+}
diff --git a/source4/dsdb/repl/drepl_notify.c b/source4/dsdb/repl/drepl_notify.c
new file mode 100644
index 0000000..20be3b5
--- /dev/null
+++ b/source4/dsdb/repl/drepl_notify.c
@@ -0,0 +1,485 @@
+/*
+ Unix SMB/CIFS Implementation.
+
+ DSDB replication service periodic notification handling
+
+ Copyright (C) Andrew Tridgell 2009
+ based on drepl_periodic
+
+ 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 "lib/events/events.h"
+#include "dsdb/samdb/samdb.h"
+#include "auth/auth.h"
+#include "samba/service.h"
+#include "dsdb/repl/drepl_service.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.h"
+#include "librpc/gen_ndr/ndr_misc.h"
+#include "librpc/gen_ndr/ndr_drsuapi.h"
+#include "librpc/gen_ndr/ndr_drsblobs.h"
+#include "libcli/composite/composite.h"
+#include "../lib/util/tevent_ntstatus.h"
+
+#undef DBGC_CLASS
+#define DBGC_CLASS DBGC_DRS_REPL
+
+
+struct dreplsrv_op_notify_state {
+ struct tevent_context *ev;
+ struct dreplsrv_notify_operation *op;
+ void *ndr_struct_ptr;
+};
+
+static void dreplsrv_op_notify_connect_done(struct tevent_req *subreq);
+
+/*
+ start the ReplicaSync async call
+ */
+static struct tevent_req *dreplsrv_op_notify_send(TALLOC_CTX *mem_ctx,
+ struct tevent_context *ev,
+ struct dreplsrv_notify_operation *op)
+{
+ struct tevent_req *req;
+ struct dreplsrv_op_notify_state *state;
+ struct tevent_req *subreq;
+
+ req = tevent_req_create(mem_ctx, &state,
+ struct dreplsrv_op_notify_state);
+ if (req == NULL) {
+ return NULL;
+ }
+ state->ev = ev;
+ state->op = op;
+
+ subreq = dreplsrv_out_drsuapi_send(state,
+ ev,
+ op->source_dsa->conn);
+ if (tevent_req_nomem(subreq, req)) {
+ return tevent_req_post(req, ev);
+ }
+ tevent_req_set_callback(subreq, dreplsrv_op_notify_connect_done, req);
+
+ return req;
+}
+
+static void dreplsrv_op_notify_replica_sync_trigger(struct tevent_req *req);
+
+static void dreplsrv_op_notify_connect_done(struct tevent_req *subreq)
+{
+ struct tevent_req *req = tevent_req_callback_data(subreq,
+ struct tevent_req);
+ NTSTATUS status;
+
+ status = dreplsrv_out_drsuapi_recv(subreq);
+ TALLOC_FREE(subreq);
+ if (tevent_req_nterror(req, status)) {
+ return;
+ }
+
+ dreplsrv_op_notify_replica_sync_trigger(req);
+}
+
+static void dreplsrv_op_notify_replica_sync_done(struct tevent_req *subreq);
+
+static void dreplsrv_op_notify_replica_sync_trigger(struct tevent_req *req)
+{
+ struct dreplsrv_op_notify_state *state =
+ tevent_req_data(req,
+ struct dreplsrv_op_notify_state);
+ struct dreplsrv_partition *partition = state->op->source_dsa->partition;
+ struct dreplsrv_drsuapi_connection *drsuapi = state->op->source_dsa->conn->drsuapi;
+ struct drsuapi_DsReplicaSync *r;
+ struct tevent_req *subreq;
+
+ r = talloc_zero(state, struct drsuapi_DsReplicaSync);
+ if (tevent_req_nomem(r, req)) {
+ return;
+ }
+ r->in.req = talloc_zero(r, union drsuapi_DsReplicaSyncRequest);
+ if (tevent_req_nomem(r, req)) {
+ return;
+ }
+ r->in.bind_handle = &drsuapi->bind_handle;
+ r->in.level = 1;
+ r->in.req->req1.naming_context = &partition->nc;
+ r->in.req->req1.source_dsa_guid = state->op->service->ntds_guid;
+ r->in.req->req1.options =
+ DRSUAPI_DRS_ASYNC_OP |
+ DRSUAPI_DRS_UPDATE_NOTIFICATION |
+ DRSUAPI_DRS_WRIT_REP;
+
+ if (state->op->is_urgent) {
+ r->in.req->req1.options |= DRSUAPI_DRS_SYNC_URGENT;
+ }
+
+ state->ndr_struct_ptr = r;
+
+ if (DEBUGLVL(10)) {
+ NDR_PRINT_IN_DEBUG(drsuapi_DsReplicaSync, r);
+ }
+
+ subreq = dcerpc_drsuapi_DsReplicaSync_r_send(state,
+ state->ev,
+ drsuapi->drsuapi_handle,
+ r);
+ if (tevent_req_nomem(subreq, req)) {
+ return;
+ }
+ tevent_req_set_callback(subreq, dreplsrv_op_notify_replica_sync_done, req);
+}
+
+static void dreplsrv_op_notify_replica_sync_done(struct tevent_req *subreq)
+{
+ struct tevent_req *req =
+ tevent_req_callback_data(subreq,
+ struct tevent_req);
+ struct dreplsrv_op_notify_state *state =
+ tevent_req_data(req,
+ struct dreplsrv_op_notify_state);
+ struct drsuapi_DsReplicaSync *r = talloc_get_type(state->ndr_struct_ptr,
+ struct drsuapi_DsReplicaSync);
+ NTSTATUS status;
+
+ state->ndr_struct_ptr = NULL;
+
+ status = dcerpc_drsuapi_DsReplicaSync_r_recv(subreq, r);
+ TALLOC_FREE(subreq);
+ if (tevent_req_nterror(req, status)) {
+ return;
+ }
+
+ if (!W_ERROR_IS_OK(r->out.result)) {
+ status = werror_to_ntstatus(r->out.result);
+ tevent_req_nterror(req, status);
+ return;
+ }
+
+ tevent_req_done(req);
+}
+
+static NTSTATUS dreplsrv_op_notify_recv(struct tevent_req *req)
+{
+ return tevent_req_simple_recv_ntstatus(req);
+}
+
+/*
+ called when a notify operation has completed
+ */
+static void dreplsrv_notify_op_callback(struct tevent_req *subreq)
+{
+ struct dreplsrv_notify_operation *op =
+ tevent_req_callback_data(subreq,
+ struct dreplsrv_notify_operation);
+ NTSTATUS status;
+ struct dreplsrv_service *s = op->service;
+ WERROR werr;
+
+ status = dreplsrv_op_notify_recv(subreq);
+ werr = ntstatus_to_werror(status);
+ TALLOC_FREE(subreq);
+ if (!NT_STATUS_IS_OK(status)) {
+ DBG_INFO("dreplsrv_notify: Failed to send DsReplicaSync to %s for %s - %s : %s\n",
+ op->source_dsa->repsFrom1->other_info->dns_name,
+ ldb_dn_get_linearized(op->source_dsa->partition->dn),
+ nt_errstr(status), win_errstr(werr));
+ } else {
+ DBG_INFO("dreplsrv_notify: DsReplicaSync successfully sent to %s\n",
+ op->source_dsa->repsFrom1->other_info->dns_name);
+ op->source_dsa->notify_uSN = op->uSN;
+ }
+
+ drepl_reps_update(s, "repsTo", op->source_dsa->partition->dn,
+ &op->source_dsa->repsFrom1->source_dsa_obj_guid,
+ werr);
+
+ talloc_free(op);
+ s->ops.n_current = NULL;
+ dreplsrv_run_pending_ops(s);
+}
+
+/*
+ run any pending replica sync calls
+ */
+void dreplsrv_notify_run_ops(struct dreplsrv_service *s)
+{
+ struct dreplsrv_notify_operation *op;
+ struct tevent_req *subreq;
+
+ if (s->ops.n_current || s->ops.current) {
+ /* if there's still one running, we're done */
+ return;
+ }
+
+ if (!s->ops.notifies) {
+ /* if there're no pending operations, we're done */
+ return;
+ }
+
+ op = s->ops.notifies;
+ s->ops.n_current = op;
+ DLIST_REMOVE(s->ops.notifies, op);
+
+ subreq = dreplsrv_op_notify_send(op, s->task->event_ctx, op);
+ if (!subreq) {
+ DBG_ERR("dreplsrv_notify_run_ops: dreplsrv_op_notify_send[%s][%s] - no memory\n",
+ op->source_dsa->repsFrom1->other_info->dns_name,
+ ldb_dn_get_linearized(op->source_dsa->partition->dn));
+ return;
+ }
+ tevent_req_set_callback(subreq, dreplsrv_notify_op_callback, op);
+ DBG_INFO("started DsReplicaSync for %s to %s\n",
+ ldb_dn_get_linearized(op->source_dsa->partition->dn),
+ op->source_dsa->repsFrom1->other_info->dns_name);
+}
+
+
+/*
+ find a source_dsa for a given guid
+ */
+static struct dreplsrv_partition_source_dsa *dreplsrv_find_notify_dsa(struct dreplsrv_partition *p,
+ struct GUID *guid)
+{
+ struct dreplsrv_partition_source_dsa *s;
+
+ /* first check the sources list */
+ for (s=p->sources; s; s=s->next) {
+ if (GUID_equal(&s->repsFrom1->source_dsa_obj_guid, guid)) {
+ return s;
+ }
+ }
+
+ /* then the notifies list */
+ for (s=p->notifies; s; s=s->next) {
+ if (GUID_equal(&s->repsFrom1->source_dsa_obj_guid, guid)) {
+ return s;
+ }
+ }
+ return NULL;
+}
+
+
+/*
+ schedule a replicaSync message
+ */
+static WERROR dreplsrv_schedule_notify_sync(struct dreplsrv_service *service,
+ struct dreplsrv_partition *p,
+ struct repsFromToBlob *reps,
+ TALLOC_CTX *mem_ctx,
+ uint64_t uSN,
+ bool is_urgent,
+ uint32_t replica_flags)
+{
+ struct dreplsrv_notify_operation *op;
+ struct dreplsrv_partition_source_dsa *s;
+
+ s = dreplsrv_find_notify_dsa(p, &reps->ctr.ctr1.source_dsa_obj_guid);
+ if (s == NULL) {
+ DBG_ERR("Unable to find source_dsa for %s\n",
+ GUID_string(mem_ctx, &reps->ctr.ctr1.source_dsa_obj_guid));
+ return WERR_DS_UNAVAILABLE;
+ }
+
+ /* first try to find an existing notify operation */
+ for (op = service->ops.notifies; op; op = op->next) {
+ if (op->source_dsa != s) {
+ continue;
+ }
+
+ if (op->is_urgent != is_urgent) {
+ continue;
+ }
+
+ if (op->replica_flags != replica_flags) {
+ continue;
+ }
+
+ if (op->uSN < uSN) {
+ op->uSN = uSN;
+ }
+
+ /* reuse the notify operation, as it's not yet started */
+ return WERR_OK;
+ }
+
+ op = talloc_zero(mem_ctx, struct dreplsrv_notify_operation);
+ W_ERROR_HAVE_NO_MEMORY(op);
+
+ op->service = service;
+ op->source_dsa = s;
+ op->uSN = uSN;
+ op->is_urgent = is_urgent;
+ op->replica_flags = replica_flags;
+ op->schedule_time = time(NULL);
+
+ DLIST_ADD_END(service->ops.notifies, op);
+ talloc_steal(service, op);
+ return WERR_OK;
+}
+
+/*
+ see if a partition has a hugher uSN than what is in the repsTo and
+ if so then send a DsReplicaSync
+ */
+static WERROR dreplsrv_notify_check(struct dreplsrv_service *s,
+ struct dreplsrv_partition *p,
+ TALLOC_CTX *mem_ctx)
+{
+ uint32_t count=0;
+ struct repsFromToBlob *reps;
+ WERROR werr;
+ uint64_t uSNHighest;
+ uint64_t uSNUrgent;
+ uint32_t i;
+ int ret;
+
+ werr = dsdb_loadreps(s->samdb, mem_ctx, p->dn, "repsTo", &reps, &count);
+ if (!W_ERROR_IS_OK(werr)) {
+ DBG_ERR("Failed to load repsTo for %s\n",
+ ldb_dn_get_linearized(p->dn));
+ return werr;
+ }
+
+ /* loads the partition uSNHighest and uSNUrgent */
+ ret = dsdb_load_partition_usn(s->samdb, p->dn, &uSNHighest, &uSNUrgent);
+ if (ret != LDB_SUCCESS || uSNHighest == 0) {
+ /* nothing to do */
+ return WERR_OK;
+ }
+
+ /* see if any of our partners need some of our objects */
+ for (i=0; i<count; i++) {
+ struct dreplsrv_partition_source_dsa *sdsa;
+ uint32_t replica_flags;
+ sdsa = dreplsrv_find_notify_dsa(p, &reps[i].ctr.ctr1.source_dsa_obj_guid);
+ replica_flags = reps[i].ctr.ctr1.replica_flags;
+ if (sdsa == NULL) continue;
+ if (sdsa->notify_uSN < uSNHighest) {
+ /* we need to tell this partner to replicate
+ with us */
+ bool is_urgent = sdsa->notify_uSN < uSNUrgent;
+
+ /* check if urgent replication is needed */
+ werr = dreplsrv_schedule_notify_sync(s, p, &reps[i], mem_ctx,
+ uSNHighest, is_urgent, replica_flags);
+ if (!W_ERROR_IS_OK(werr)) {
+ DBG_ERR("Failed to setup notify to %s for %s\n",
+ reps[i].ctr.ctr1.other_info->dns_name,
+ ldb_dn_get_linearized(p->dn));
+ return werr;
+ }
+ DBG_DEBUG("queued DsReplicaSync for %s to %s "
+ "(urgent=%s) uSN=%llu:%llu\n",
+ ldb_dn_get_linearized(p->dn),
+ reps[i].ctr.ctr1.other_info->dns_name,
+ is_urgent?"true":"false",
+ (unsigned long long)sdsa->notify_uSN,
+ (unsigned long long)uSNHighest);
+ }
+ }
+
+ return WERR_OK;
+}
+
+/*
+ see if any of the partitions have changed, and if so then send a
+ DsReplicaSync to all the replica partners in the repsTo object
+ */
+static WERROR dreplsrv_notify_check_all(struct dreplsrv_service *s, TALLOC_CTX *mem_ctx)
+{
+ WERROR status;
+ struct dreplsrv_partition *p;
+
+ for (p = s->partitions; p; p = p->next) {
+ status = dreplsrv_notify_check(s, p, mem_ctx);
+ W_ERROR_NOT_OK_RETURN(status);
+ }
+
+ return WERR_OK;
+}
+
+static void dreplsrv_notify_run(struct dreplsrv_service *service);
+
+static void dreplsrv_notify_handler_te(struct tevent_context *ev, struct tevent_timer *te,
+ struct timeval t, void *ptr)
+{
+ struct dreplsrv_service *service = talloc_get_type(ptr, struct dreplsrv_service);
+ WERROR status;
+
+ service->notify.te = NULL;
+
+ dreplsrv_notify_run(service);
+
+ status = dreplsrv_notify_schedule(service, service->notify.interval);
+ if (!W_ERROR_IS_OK(status)) {
+ task_server_terminate(service->task, win_errstr(status), false);
+ return;
+ }
+}
+
+WERROR dreplsrv_notify_schedule(struct dreplsrv_service *service, uint32_t next_interval)
+{
+ TALLOC_CTX *tmp_mem;
+ struct tevent_timer *new_te;
+ struct timeval next_time;
+
+ /* prevent looping */
+ if (next_interval == 0) next_interval = 1;
+
+ next_time = timeval_current_ofs(next_interval, 50);
+
+ if (service->notify.te) {
+ /*
+ * if the timestamp of the new event is higher,
+ * as current next we don't need to reschedule
+ */
+ if (timeval_compare(&next_time, &service->notify.next_event) > 0) {
+ return WERR_OK;
+ }
+ }
+
+ /* reset the next scheduled timestamp */
+ service->notify.next_event = next_time;
+
+ new_te = tevent_add_timer(service->task->event_ctx, service,
+ service->notify.next_event,
+ dreplsrv_notify_handler_te, service);
+ W_ERROR_HAVE_NO_MEMORY(new_te);
+
+ tmp_mem = talloc_new(service);
+ DBG_DEBUG("dreplsrv_notify_schedule(%u) %sscheduled for: %s\n",
+ next_interval,
+ (service->notify.te?"re":""),
+ nt_time_string(tmp_mem, timeval_to_nttime(&next_time)));
+ talloc_free(tmp_mem);
+
+ talloc_free(service->notify.te);
+ service->notify.te = new_te;
+
+ return WERR_OK;
+}
+
+static void dreplsrv_notify_run(struct dreplsrv_service *service)
+{
+ TALLOC_CTX *mem_ctx;
+
+ mem_ctx = talloc_new(service);
+ dreplsrv_notify_check_all(service, mem_ctx);
+ talloc_free(mem_ctx);
+
+ dreplsrv_run_pending_ops(service);
+}
diff --git a/source4/dsdb/repl/drepl_out_helpers.c b/source4/dsdb/repl/drepl_out_helpers.c
new file mode 100644
index 0000000..d46b19e
--- /dev/null
+++ b/source4/dsdb/repl/drepl_out_helpers.c
@@ -0,0 +1,1356 @@
+/*
+ Unix SMB/CIFS Implementation.
+ DSDB replication service helper function for outgoing traffic
+
+ Copyright (C) Stefan Metzmacher 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 "dsdb/samdb/samdb.h"
+#include "auth/auth.h"
+#include "samba/service.h"
+#include "lib/events/events.h"
+#include "dsdb/repl/drepl_service.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.h"
+#include "librpc/gen_ndr/ndr_misc.h"
+#include "librpc/gen_ndr/ndr_drsuapi.h"
+#include "librpc/gen_ndr/ndr_drsblobs.h"
+#include "libcli/composite/composite.h"
+#include "auth/gensec/gensec.h"
+#include "param/param.h"
+#include "../lib/util/tevent_ntstatus.h"
+#include "libcli/security/security.h"
+
+#undef DBGC_CLASS
+#define DBGC_CLASS DBGC_DRS_REPL
+
+struct dreplsrv_out_drsuapi_state {
+ struct tevent_context *ev;
+
+ struct dreplsrv_out_connection *conn;
+
+ struct dreplsrv_drsuapi_connection *drsuapi;
+
+ struct drsuapi_DsBindInfoCtr bind_info_ctr;
+ struct drsuapi_DsBind bind_r;
+};
+
+static void dreplsrv_out_drsuapi_connect_done(struct composite_context *creq);
+
+struct tevent_req *dreplsrv_out_drsuapi_send(TALLOC_CTX *mem_ctx,
+ struct tevent_context *ev,
+ struct dreplsrv_out_connection *conn)
+{
+ struct tevent_req *req;
+ struct dreplsrv_out_drsuapi_state *state;
+ struct composite_context *creq;
+
+ req = tevent_req_create(mem_ctx, &state,
+ struct dreplsrv_out_drsuapi_state);
+ if (req == NULL) {
+ return NULL;
+ }
+
+ state->ev = ev;
+ state->conn = conn;
+ state->drsuapi = conn->drsuapi;
+
+ if (state->drsuapi != NULL) {
+ struct dcerpc_binding_handle *b =
+ state->drsuapi->pipe->binding_handle;
+ bool is_connected = dcerpc_binding_handle_is_connected(b);
+
+ if (is_connected) {
+ tevent_req_done(req);
+ return tevent_req_post(req, ev);
+ }
+
+ TALLOC_FREE(conn->drsuapi);
+ }
+
+ state->drsuapi = talloc_zero(state, struct dreplsrv_drsuapi_connection);
+ if (tevent_req_nomem(state->drsuapi, req)) {
+ return tevent_req_post(req, ev);
+ }
+
+ creq = dcerpc_pipe_connect_b_send(state, conn->binding, &ndr_table_drsuapi,
+ conn->service->system_session_info->credentials,
+ ev, conn->service->task->lp_ctx);
+ if (tevent_req_nomem(creq, req)) {
+ return tevent_req_post(req, ev);
+ }
+ composite_continue(NULL, creq, dreplsrv_out_drsuapi_connect_done, req);
+
+ return req;
+}
+
+static void dreplsrv_out_drsuapi_bind_done(struct tevent_req *subreq);
+
+static void dreplsrv_out_drsuapi_connect_done(struct composite_context *creq)
+{
+ struct tevent_req *req = talloc_get_type(creq->async.private_data,
+ struct tevent_req);
+ struct dreplsrv_out_drsuapi_state *state = tevent_req_data(req,
+ struct dreplsrv_out_drsuapi_state);
+ NTSTATUS status;
+ struct tevent_req *subreq;
+
+ status = dcerpc_pipe_connect_b_recv(creq,
+ state->drsuapi,
+ &state->drsuapi->pipe);
+ if (tevent_req_nterror(req, status)) {
+ return;
+ }
+
+ state->drsuapi->drsuapi_handle = state->drsuapi->pipe->binding_handle;
+
+ status = gensec_session_key(state->drsuapi->pipe->conn->security_state.generic_state,
+ state->drsuapi,
+ &state->drsuapi->gensec_skey);
+ if (tevent_req_nterror(req, status)) {
+ return;
+ }
+
+ state->bind_info_ctr.length = 28;
+ state->bind_info_ctr.info.info28 = state->conn->service->bind_info28;
+
+ state->bind_r.in.bind_guid = &state->conn->service->ntds_guid;
+ state->bind_r.in.bind_info = &state->bind_info_ctr;
+ state->bind_r.out.bind_handle = &state->drsuapi->bind_handle;
+
+ subreq = dcerpc_drsuapi_DsBind_r_send(state,
+ state->ev,
+ state->drsuapi->drsuapi_handle,
+ &state->bind_r);
+ if (tevent_req_nomem(subreq, req)) {
+ return;
+ }
+ tevent_req_set_callback(subreq, dreplsrv_out_drsuapi_bind_done, req);
+}
+
+static void dreplsrv_out_drsuapi_bind_done(struct tevent_req *subreq)
+{
+ struct tevent_req *req = tevent_req_callback_data(subreq,
+ struct tevent_req);
+ struct dreplsrv_out_drsuapi_state *state = tevent_req_data(req,
+ struct dreplsrv_out_drsuapi_state);
+ NTSTATUS status;
+
+ status = dcerpc_drsuapi_DsBind_r_recv(subreq, state);
+ TALLOC_FREE(subreq);
+ if (tevent_req_nterror(req, status)) {
+ return;
+ }
+
+ if (!W_ERROR_IS_OK(state->bind_r.out.result)) {
+ status = werror_to_ntstatus(state->bind_r.out.result);
+ tevent_req_nterror(req, status);
+ return;
+ }
+
+ ZERO_STRUCT(state->drsuapi->remote_info28);
+ if (state->bind_r.out.bind_info) {
+ struct drsuapi_DsBindInfo28 *info28;
+ info28 = &state->drsuapi->remote_info28;
+
+ switch (state->bind_r.out.bind_info->length) {
+ case 24: {
+ struct drsuapi_DsBindInfo24 *info24;
+ info24 = &state->bind_r.out.bind_info->info.info24;
+
+ info28->supported_extensions = info24->supported_extensions;
+ info28->site_guid = info24->site_guid;
+ info28->pid = info24->pid;
+ info28->repl_epoch = 0;
+ break;
+ }
+ case 28: {
+ *info28 = state->bind_r.out.bind_info->info.info28;
+ break;
+ }
+ case 32: {
+ struct drsuapi_DsBindInfo32 *info32;
+ info32 = &state->bind_r.out.bind_info->info.info32;
+
+ info28->supported_extensions = info32->supported_extensions;
+ info28->site_guid = info32->site_guid;
+ info28->pid = info32->pid;
+ info28->repl_epoch = info32->repl_epoch;
+ break;
+ }
+ case 48: {
+ struct drsuapi_DsBindInfo48 *info48;
+ info48 = &state->bind_r.out.bind_info->info.info48;
+
+ info28->supported_extensions = info48->supported_extensions;
+ info28->site_guid = info48->site_guid;
+ info28->pid = info48->pid;
+ info28->repl_epoch = info48->repl_epoch;
+ break;
+ }
+ case 52: {
+ struct drsuapi_DsBindInfo52 *info52;
+ info52 = &state->bind_r.out.bind_info->info.info52;
+
+ info28->supported_extensions = info52->supported_extensions;
+ info28->site_guid = info52->site_guid;
+ info28->pid = info52->pid;
+ info28->repl_epoch = info52->repl_epoch;
+ break;
+ }
+ default:
+ DEBUG(1, ("Warning: invalid info length in bind info: %d\n",
+ state->bind_r.out.bind_info->length));
+ break;
+ }
+ }
+
+ tevent_req_done(req);
+}
+
+NTSTATUS dreplsrv_out_drsuapi_recv(struct tevent_req *req)
+{
+ struct dreplsrv_out_drsuapi_state *state = tevent_req_data(req,
+ struct dreplsrv_out_drsuapi_state);
+ NTSTATUS status;
+
+ if (tevent_req_is_nterror(req, &status)) {
+ tevent_req_received(req);
+ return status;
+ }
+
+ state->conn->drsuapi = talloc_move(state->conn, &state->drsuapi);
+
+ tevent_req_received(req);
+ return NT_STATUS_OK;
+}
+
+struct dreplsrv_op_pull_source_schema_cycle {
+ struct repsFromTo1 repsFrom1;
+ size_t object_count;
+ struct drsuapi_DsReplicaObjectListItemEx *first_object;
+ struct drsuapi_DsReplicaObjectListItemEx *last_object;
+ uint32_t linked_attributes_count;
+ struct drsuapi_DsReplicaLinkedAttribute *linked_attributes;
+};
+
+struct dreplsrv_op_pull_source_state {
+ struct tevent_context *ev;
+ struct dreplsrv_out_operation *op;
+ void *ndr_struct_ptr;
+ /*
+ * Used when we have to re-try with a different NC, eg for
+ * EXOP retry or to get a current schema first
+ */
+ struct dreplsrv_partition_source_dsa *source_dsa_retry;
+ enum drsuapi_DsExtendedOperation extended_op_retry;
+ bool retry_started;
+ struct dreplsrv_op_pull_source_schema_cycle *schema_cycle;
+};
+
+static void dreplsrv_op_pull_source_connect_done(struct tevent_req *subreq);
+
+struct tevent_req *dreplsrv_op_pull_source_send(TALLOC_CTX *mem_ctx,
+ struct tevent_context *ev,
+ struct dreplsrv_out_operation *op)
+{
+ struct tevent_req *req;
+ struct dreplsrv_op_pull_source_state *state;
+ struct tevent_req *subreq;
+
+ req = tevent_req_create(mem_ctx, &state,
+ struct dreplsrv_op_pull_source_state);
+ if (req == NULL) {
+ return NULL;
+ }
+ state->ev = ev;
+ state->op = op;
+
+ subreq = dreplsrv_out_drsuapi_send(state, ev, op->source_dsa->conn);
+ if (tevent_req_nomem(subreq, req)) {
+ return tevent_req_post(req, ev);
+ }
+ tevent_req_set_callback(subreq, dreplsrv_op_pull_source_connect_done, req);
+
+ return req;
+}
+
+static bool dreplsrv_op_pull_source_detect_schema_cycle(struct tevent_req *req)
+{
+ struct dreplsrv_op_pull_source_state *state =
+ tevent_req_data(req,
+ struct dreplsrv_op_pull_source_state);
+ bool is_schema = false;
+
+ if (state->op->extended_op == DRSUAPI_EXOP_NONE) {
+ struct dreplsrv_out_operation *op = state->op;
+ struct dreplsrv_service *service = op->service;
+ struct ldb_dn *schema_dn = ldb_get_schema_basedn(service->samdb);
+ struct dreplsrv_partition *partition = op->source_dsa->partition;
+
+ is_schema = ldb_dn_compare(partition->dn, schema_dn) == 0;
+ }
+
+ if (is_schema) {
+ struct dreplsrv_op_pull_source_schema_cycle *sc;
+
+ sc = talloc_zero(state,
+ struct dreplsrv_op_pull_source_schema_cycle);
+ if (tevent_req_nomem(sc, req)) {
+ return false;
+ }
+ sc->repsFrom1 = *state->op->source_dsa->repsFrom1;
+
+ state->schema_cycle = sc;
+ }
+
+ return true;
+}
+
+static void dreplsrv_op_pull_source_get_changes_trigger(struct tevent_req *req);
+
+static void dreplsrv_op_pull_source_connect_done(struct tevent_req *subreq)
+{
+ struct tevent_req *req = tevent_req_callback_data(subreq,
+ struct tevent_req);
+ NTSTATUS status;
+ bool ok;
+
+ status = dreplsrv_out_drsuapi_recv(subreq);
+ TALLOC_FREE(subreq);
+ if (tevent_req_nterror(req, status)) {
+ return;
+ }
+
+ ok = dreplsrv_op_pull_source_detect_schema_cycle(req);
+ if (!ok) {
+ return;
+ }
+
+ dreplsrv_op_pull_source_get_changes_trigger(req);
+}
+
+static void dreplsrv_op_pull_source_get_changes_done(struct tevent_req *subreq);
+
+/*
+ get a RODC partial attribute set for a replication call
+ */
+static NTSTATUS dreplsrv_get_rodc_partial_attribute_set(struct dreplsrv_service *service,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsPartialAttributeSet **_pas,
+ struct drsuapi_DsReplicaOIDMapping_Ctr **pfm,
+ bool for_schema)
+{
+ struct drsuapi_DsPartialAttributeSet *pas;
+ struct dsdb_schema *schema;
+ uint32_t i;
+
+ pas = talloc_zero(mem_ctx, struct drsuapi_DsPartialAttributeSet);
+ NT_STATUS_HAVE_NO_MEMORY(pas);
+
+ schema = dsdb_get_schema(service->samdb, NULL);
+
+ pas->version = 1;
+ pas->attids = talloc_array(pas, enum drsuapi_DsAttributeId, schema->num_attributes);
+ if (pas->attids == NULL) {
+ TALLOC_FREE(pas);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ for (i=0; i<schema->num_attributes; i++) {
+ struct dsdb_attribute *a;
+ a = schema->attributes_by_attributeID_id[i];
+ if (a->systemFlags & (DS_FLAG_ATTR_NOT_REPLICATED | DS_FLAG_ATTR_IS_CONSTRUCTED)) {
+ continue;
+ }
+ if (a->searchFlags & SEARCH_FLAG_RODC_ATTRIBUTE) {
+ continue;
+ }
+ pas->attids[pas->num_attids] = dsdb_attribute_get_attid(a, for_schema);
+ pas->num_attids++;
+ }
+
+ pas->attids = talloc_realloc(pas, pas->attids, enum drsuapi_DsAttributeId, pas->num_attids);
+ if (pas->attids == NULL) {
+ TALLOC_FREE(pas);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ *_pas = pas;
+
+ if (pfm != NULL) {
+ dsdb_get_oid_mappings_drsuapi(schema, true, mem_ctx, pfm);
+ }
+
+ return NT_STATUS_OK;
+}
+
+
+/*
+ get a GC partial attribute set for a replication call
+ */
+static NTSTATUS dreplsrv_get_gc_partial_attribute_set(struct dreplsrv_service *service,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsPartialAttributeSet **_pas,
+ struct drsuapi_DsReplicaOIDMapping_Ctr **pfm)
+{
+ struct drsuapi_DsPartialAttributeSet *pas;
+ struct dsdb_schema *schema;
+ uint32_t i;
+
+ pas = talloc_zero(mem_ctx, struct drsuapi_DsPartialAttributeSet);
+ NT_STATUS_HAVE_NO_MEMORY(pas);
+
+ schema = dsdb_get_schema(service->samdb, NULL);
+
+ pas->version = 1;
+ pas->attids = talloc_array(pas, enum drsuapi_DsAttributeId, schema->num_attributes);
+ if (pas->attids == NULL) {
+ TALLOC_FREE(pas);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ for (i=0; i<schema->num_attributes; i++) {
+ struct dsdb_attribute *a;
+ a = schema->attributes_by_attributeID_id[i];
+ if (a->isMemberOfPartialAttributeSet) {
+ pas->attids[pas->num_attids] = dsdb_attribute_get_attid(a, false);
+ pas->num_attids++;
+ }
+ }
+
+ pas->attids = talloc_realloc(pas, pas->attids, enum drsuapi_DsAttributeId, pas->num_attids);
+ if (pas->attids == NULL) {
+ TALLOC_FREE(pas);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ *_pas = pas;
+
+ if (pfm != NULL) {
+ dsdb_get_oid_mappings_drsuapi(schema, true, mem_ctx, pfm);
+ }
+
+ return NT_STATUS_OK;
+}
+
+/*
+ convert from one udv format to the other
+ */
+static WERROR udv_convert(TALLOC_CTX *mem_ctx,
+ const struct replUpToDateVectorCtr2 *udv,
+ struct drsuapi_DsReplicaCursorCtrEx *udv_ex)
+{
+ uint32_t i;
+
+ udv_ex->version = 2;
+ udv_ex->reserved1 = 0;
+ udv_ex->reserved2 = 0;
+ udv_ex->count = udv->count;
+ udv_ex->cursors = talloc_array(mem_ctx, struct drsuapi_DsReplicaCursor, udv->count);
+ W_ERROR_HAVE_NO_MEMORY(udv_ex->cursors);
+
+ for (i=0; i<udv->count; i++) {
+ udv_ex->cursors[i].source_dsa_invocation_id = udv->cursors[i].source_dsa_invocation_id;
+ udv_ex->cursors[i].highest_usn = udv->cursors[i].highest_usn;
+ }
+
+ return WERR_OK;
+}
+
+
+static void dreplsrv_op_pull_source_get_changes_trigger(struct tevent_req *req)
+{
+ struct dreplsrv_op_pull_source_state *state = tevent_req_data(req,
+ struct dreplsrv_op_pull_source_state);
+ const struct repsFromTo1 *rf1 = state->op->source_dsa->repsFrom1;
+ struct dreplsrv_service *service = state->op->service;
+ struct dreplsrv_partition *partition = state->op->source_dsa->partition;
+ struct dreplsrv_drsuapi_connection *drsuapi = state->op->source_dsa->conn->drsuapi;
+ struct drsuapi_DsGetNCChanges *r;
+ struct drsuapi_DsReplicaCursorCtrEx *uptodateness_vector;
+ struct tevent_req *subreq;
+ struct drsuapi_DsPartialAttributeSet *pas = NULL;
+ NTSTATUS status;
+ uint32_t replica_flags;
+ struct drsuapi_DsReplicaHighWaterMark highwatermark;
+ struct drsuapi_DsReplicaOIDMapping_Ctr *mappings = NULL;
+ bool is_schema = false;
+
+ if (state->schema_cycle != NULL) {
+ is_schema = true;
+ rf1 = &state->schema_cycle->repsFrom1;
+ }
+
+ r = talloc(state, struct drsuapi_DsGetNCChanges);
+ if (tevent_req_nomem(r, req)) {
+ return;
+ }
+
+ r->out.level_out = talloc(r, uint32_t);
+ if (tevent_req_nomem(r->out.level_out, req)) {
+ return;
+ }
+ r->in.req = talloc(r, union drsuapi_DsGetNCChangesRequest);
+ if (tevent_req_nomem(r->in.req, req)) {
+ return;
+ }
+ r->out.ctr = talloc(r, union drsuapi_DsGetNCChangesCtr);
+ if (tevent_req_nomem(r->out.ctr, req)) {
+ return;
+ }
+
+ if (partition->uptodatevector.count != 0 &&
+ partition->uptodatevector_ex.count == 0) {
+ WERROR werr;
+ werr = udv_convert(partition, &partition->uptodatevector, &partition->uptodatevector_ex);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0,(__location__ ": Failed to convert UDV for %s : %s\n",
+ ldb_dn_get_linearized(partition->dn), win_errstr(werr)));
+ tevent_req_nterror(req, werror_to_ntstatus(werr));
+ return;
+ }
+ }
+
+ if (partition->uptodatevector_ex.count == 0) {
+ uptodateness_vector = NULL;
+ } else {
+ uptodateness_vector = &partition->uptodatevector_ex;
+ }
+
+ replica_flags = rf1->replica_flags;
+ highwatermark = rf1->highwatermark;
+
+ if (state->op->options & DRSUAPI_DRS_GET_ANC) {
+ replica_flags |= DRSUAPI_DRS_GET_ANC;
+ }
+
+ if (state->op->options & DRSUAPI_DRS_SYNC_FORCED) {
+ replica_flags |= DRSUAPI_DRS_SYNC_FORCED;
+ }
+
+ if (partition->partial_replica) {
+ status = dreplsrv_get_gc_partial_attribute_set(service, r,
+ &pas,
+ &mappings);
+ if (!NT_STATUS_IS_OK(status)) {
+ DEBUG(0,(__location__ ": Failed to construct GC partial attribute set : %s\n", nt_errstr(status)));
+ tevent_req_nterror(req, status);
+ return;
+ }
+ replica_flags &= ~DRSUAPI_DRS_WRIT_REP;
+ } else if (partition->rodc_replica || state->op->extended_op == DRSUAPI_EXOP_REPL_SECRET) {
+ status = dreplsrv_get_rodc_partial_attribute_set(service, r,
+ &pas,
+ &mappings,
+ is_schema);
+ if (!NT_STATUS_IS_OK(status)) {
+ DEBUG(0,(__location__ ": Failed to construct RODC partial attribute set : %s\n", nt_errstr(status)));
+ tevent_req_nterror(req, status);
+ return;
+ }
+ replica_flags &= ~DRSUAPI_DRS_WRIT_REP;
+ if (state->op->extended_op == DRSUAPI_EXOP_REPL_SECRET) {
+ replica_flags &= ~DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING;
+ } else {
+ replica_flags |= DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING;
+ }
+
+ /*
+ * As per MS-DRSR:
+ *
+ * 4.1.10.4
+ * Client Behavior When Sending the IDL_DRSGetNCChanges Request
+ *
+ * 4.1.10.4.1
+ * ReplicateNCRequestMsg
+ */
+ replica_flags |= DRSUAPI_DRS_GET_ALL_GROUP_MEMBERSHIP;
+ } else {
+ replica_flags |= DRSUAPI_DRS_GET_ALL_GROUP_MEMBERSHIP;
+ }
+
+ if (state->op->extended_op != DRSUAPI_EXOP_NONE) {
+ /*
+ * If it's an exop never set the ADD_REF even if it's in
+ * repsFrom flags.
+ */
+ replica_flags &= ~DRSUAPI_DRS_ADD_REF;
+ }
+
+ /* is this a full resync of all objects? */
+ if (state->op->options & DRSUAPI_DRS_FULL_SYNC_NOW) {
+ ZERO_STRUCT(highwatermark);
+ /* clear the FULL_SYNC_NOW option for subsequent
+ stages of the replication cycle */
+ state->op->options &= ~DRSUAPI_DRS_FULL_SYNC_NOW;
+ state->op->options |= DRSUAPI_DRS_FULL_SYNC_IN_PROGRESS;
+ replica_flags |= DRSUAPI_DRS_NEVER_SYNCED;
+ }
+ if (state->op->options & DRSUAPI_DRS_FULL_SYNC_IN_PROGRESS) {
+ uptodateness_vector = NULL;
+ }
+
+ r->in.bind_handle = &drsuapi->bind_handle;
+
+ if (drsuapi->remote_info28.supported_extensions & DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V10) {
+ r->in.level = 10;
+ r->in.req->req10.destination_dsa_guid = service->ntds_guid;
+ r->in.req->req10.source_dsa_invocation_id= rf1->source_dsa_invocation_id;
+ r->in.req->req10.naming_context = &partition->nc;
+ r->in.req->req10.highwatermark = highwatermark;
+ r->in.req->req10.uptodateness_vector = uptodateness_vector;
+ r->in.req->req10.replica_flags = replica_flags;
+ r->in.req->req10.max_object_count = 133;
+ r->in.req->req10.max_ndr_size = 1336811;
+ r->in.req->req10.extended_op = state->op->extended_op;
+ r->in.req->req10.fsmo_info = state->op->fsmo_info;
+ r->in.req->req10.partial_attribute_set = pas;
+ r->in.req->req10.partial_attribute_set_ex= NULL;
+ r->in.req->req10.mapping_ctr.num_mappings= mappings == NULL ? 0 : mappings->num_mappings;
+ r->in.req->req10.mapping_ctr.mappings = mappings == NULL ? NULL : mappings->mappings;
+
+ /* the only difference to v8 is the more_flags */
+ r->in.req->req10.more_flags = state->op->more_flags;
+
+ } else if (drsuapi->remote_info28.supported_extensions & DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8) {
+ r->in.level = 8;
+ r->in.req->req8.destination_dsa_guid = service->ntds_guid;
+ r->in.req->req8.source_dsa_invocation_id= rf1->source_dsa_invocation_id;
+ r->in.req->req8.naming_context = &partition->nc;
+ r->in.req->req8.highwatermark = highwatermark;
+ r->in.req->req8.uptodateness_vector = uptodateness_vector;
+ r->in.req->req8.replica_flags = replica_flags;
+ r->in.req->req8.max_object_count = 133;
+ r->in.req->req8.max_ndr_size = 1336811;
+ r->in.req->req8.extended_op = state->op->extended_op;
+ r->in.req->req8.fsmo_info = state->op->fsmo_info;
+ r->in.req->req8.partial_attribute_set = pas;
+ r->in.req->req8.partial_attribute_set_ex= NULL;
+ r->in.req->req8.mapping_ctr.num_mappings= mappings == NULL ? 0 : mappings->num_mappings;
+ r->in.req->req8.mapping_ctr.mappings = mappings == NULL ? NULL : mappings->mappings;
+ } else {
+ r->in.level = 5;
+ r->in.req->req5.destination_dsa_guid = service->ntds_guid;
+ r->in.req->req5.source_dsa_invocation_id= rf1->source_dsa_invocation_id;
+ r->in.req->req5.naming_context = &partition->nc;
+ r->in.req->req5.highwatermark = highwatermark;
+ r->in.req->req5.uptodateness_vector = uptodateness_vector;
+ r->in.req->req5.replica_flags = replica_flags;
+ r->in.req->req5.max_object_count = 133;
+ r->in.req->req5.max_ndr_size = 1336770;
+ r->in.req->req5.extended_op = state->op->extended_op;
+ r->in.req->req5.fsmo_info = state->op->fsmo_info;
+ }
+
+#if 0
+ NDR_PRINT_IN_DEBUG(drsuapi_DsGetNCChanges, r);
+#endif
+
+ state->ndr_struct_ptr = r;
+ subreq = dcerpc_drsuapi_DsGetNCChanges_r_send(state,
+ state->ev,
+ drsuapi->drsuapi_handle,
+ r);
+ if (tevent_req_nomem(subreq, req)) {
+ return;
+ }
+ tevent_req_set_callback(subreq, dreplsrv_op_pull_source_get_changes_done, req);
+}
+
+static void dreplsrv_op_pull_source_apply_changes_trigger(struct tevent_req *req,
+ struct drsuapi_DsGetNCChanges *r,
+ uint32_t ctr_level,
+ struct drsuapi_DsGetNCChangesCtr1 *ctr1,
+ struct drsuapi_DsGetNCChangesCtr6 *ctr6);
+
+static void dreplsrv_op_pull_source_get_changes_done(struct tevent_req *subreq)
+{
+ struct tevent_req *req = tevent_req_callback_data(subreq,
+ struct tevent_req);
+ struct dreplsrv_op_pull_source_state *state = tevent_req_data(req,
+ struct dreplsrv_op_pull_source_state);
+ NTSTATUS status;
+ struct drsuapi_DsGetNCChanges *r = talloc_get_type(state->ndr_struct_ptr,
+ struct drsuapi_DsGetNCChanges);
+ uint32_t ctr_level = 0;
+ struct drsuapi_DsGetNCChangesCtr1 *ctr1 = NULL;
+ struct drsuapi_DsGetNCChangesCtr6 *ctr6 = NULL;
+ enum drsuapi_DsExtendedError extended_ret = DRSUAPI_EXOP_ERR_NONE;
+ state->ndr_struct_ptr = NULL;
+
+ status = dcerpc_drsuapi_DsGetNCChanges_r_recv(subreq, r);
+ TALLOC_FREE(subreq);
+ if (tevent_req_nterror(req, status)) {
+ return;
+ }
+
+ if (!W_ERROR_IS_OK(r->out.result)) {
+ status = werror_to_ntstatus(r->out.result);
+ tevent_req_nterror(req, status);
+ return;
+ }
+
+ if (*r->out.level_out == 1) {
+ ctr_level = 1;
+ ctr1 = &r->out.ctr->ctr1;
+ } else if (*r->out.level_out == 2 &&
+ r->out.ctr->ctr2.mszip1.ts) {
+ ctr_level = 1;
+ ctr1 = &r->out.ctr->ctr2.mszip1.ts->ctr1;
+ } else if (*r->out.level_out == 6) {
+ ctr_level = 6;
+ ctr6 = &r->out.ctr->ctr6;
+ } else if (*r->out.level_out == 7 &&
+ r->out.ctr->ctr7.level == 6 &&
+ r->out.ctr->ctr7.type == DRSUAPI_COMPRESSION_TYPE_MSZIP &&
+ r->out.ctr->ctr7.ctr.mszip6.ts) {
+ ctr_level = 6;
+ ctr6 = &r->out.ctr->ctr7.ctr.mszip6.ts->ctr6;
+ } else if (*r->out.level_out == 7 &&
+ r->out.ctr->ctr7.level == 6 &&
+ r->out.ctr->ctr7.type == DRSUAPI_COMPRESSION_TYPE_WIN2K3_LZ77_DIRECT2 &&
+ r->out.ctr->ctr7.ctr.xpress6.ts) {
+ ctr_level = 6;
+ ctr6 = &r->out.ctr->ctr7.ctr.xpress6.ts->ctr6;
+ } else {
+ status = werror_to_ntstatus(WERR_BAD_NET_RESP);
+ tevent_req_nterror(req, status);
+ return;
+ }
+
+ if (!ctr1 && !ctr6) {
+ status = werror_to_ntstatus(WERR_BAD_NET_RESP);
+ tevent_req_nterror(req, status);
+ return;
+ }
+
+ if (ctr_level == 6) {
+ if (!W_ERROR_IS_OK(ctr6->drs_error)) {
+ status = werror_to_ntstatus(ctr6->drs_error);
+ tevent_req_nterror(req, status);
+ return;
+ }
+ extended_ret = ctr6->extended_ret;
+ }
+
+ if (ctr_level == 1) {
+ extended_ret = ctr1->extended_ret;
+ }
+
+ if (state->op->extended_op != DRSUAPI_EXOP_NONE) {
+ state->op->extended_ret = extended_ret;
+
+ if (extended_ret != DRSUAPI_EXOP_ERR_SUCCESS) {
+ status = NT_STATUS_UNSUCCESSFUL;
+ tevent_req_nterror(req, status);
+ return;
+ }
+ }
+
+ dreplsrv_op_pull_source_apply_changes_trigger(req, r, ctr_level, ctr1, ctr6);
+}
+
+/**
+ * If processing a chunk of replication data fails, check if it is due to a
+ * problem that can be fixed by setting extra flags in the GetNCChanges request,
+ * i.e. GET_ANC or GET_TGT.
+ * @returns NT_STATUS_OK if the request was retried, and an error code if not
+ */
+static NTSTATUS dreplsrv_op_pull_retry_with_flags(struct tevent_req *req,
+ WERROR error_code)
+{
+ struct dreplsrv_op_pull_source_state *state;
+ NTSTATUS nt_status = NT_STATUS_OK;
+
+ state = tevent_req_data(req, struct dreplsrv_op_pull_source_state);
+
+ /*
+ * Check if we failed to apply the records due to a missing parent or
+ * target object. If so, try again and ask for any missing parent/target
+ * objects to be included this time.
+ */
+ if (W_ERROR_EQUAL(error_code, WERR_DS_DRA_RECYCLED_TARGET)) {
+
+ if (state->op->more_flags & DRSUAPI_DRS_GET_TGT) {
+ DEBUG(1,("Missing target object despite setting DRSUAPI_DRS_GET_TGT flag\n"));
+ nt_status = NT_STATUS_INVALID_NETWORK_RESPONSE;
+ } else {
+ state->op->more_flags |= DRSUAPI_DRS_GET_TGT;
+ DEBUG(1,("Missing target object when we didn't set the DRSUAPI_DRS_GET_TGT flag, retrying\n"));
+ dreplsrv_op_pull_source_get_changes_trigger(req);
+ }
+ } else if (W_ERROR_EQUAL(error_code, WERR_DS_DRA_MISSING_PARENT)) {
+
+ if (state->op->options & DRSUAPI_DRS_GET_ANC) {
+ DEBUG(1,("Missing parent object despite setting DRSUAPI_DRS_GET_ANC flag\n"));
+ nt_status = NT_STATUS_INVALID_NETWORK_RESPONSE;
+ } else {
+ state->op->options |= DRSUAPI_DRS_GET_ANC;
+ DEBUG(4,("Missing parent object when we didn't set the DRSUAPI_DRS_GET_ANC flag, retrying\n"));
+ dreplsrv_op_pull_source_get_changes_trigger(req);
+ }
+ } else {
+ nt_status = werror_to_ntstatus(WERR_BAD_NET_RESP);
+ }
+
+ return nt_status;
+}
+
+
+static void dreplsrv_update_refs_trigger(struct tevent_req *req);
+
+static void dreplsrv_op_pull_source_apply_changes_trigger(struct tevent_req *req,
+ struct drsuapi_DsGetNCChanges *r,
+ uint32_t ctr_level,
+ struct drsuapi_DsGetNCChangesCtr1 *ctr1,
+ struct drsuapi_DsGetNCChangesCtr6 *ctr6)
+{
+ struct dreplsrv_op_pull_source_state *state = tevent_req_data(req,
+ struct dreplsrv_op_pull_source_state);
+ struct repsFromTo1 rf1 = *state->op->source_dsa->repsFrom1;
+ struct dreplsrv_service *service = state->op->service;
+ struct dreplsrv_partition *partition = state->op->source_dsa->partition;
+ struct dreplsrv_drsuapi_connection *drsuapi = state->op->source_dsa->conn->drsuapi;
+ struct ldb_dn *schema_dn = ldb_get_schema_basedn(service->samdb);
+ struct dreplsrv_op_pull_source_schema_cycle *sc = NULL;
+ struct dsdb_schema *schema;
+ struct dsdb_schema *working_schema = NULL;
+ const struct drsuapi_DsReplicaOIDMapping_Ctr *mapping_ctr;
+ uint32_t object_count;
+ struct drsuapi_DsReplicaObjectListItemEx *first_object;
+ uint32_t linked_attributes_count;
+ struct drsuapi_DsReplicaLinkedAttribute *linked_attributes;
+ const struct drsuapi_DsReplicaCursor2CtrEx *uptodateness_vector;
+ struct dsdb_extended_replicated_objects *objects;
+ bool more_data = false;
+ WERROR status;
+ NTSTATUS nt_status;
+ uint32_t dsdb_repl_flags = 0;
+ struct ldb_dn *nc_root = NULL;
+ bool was_schema = false;
+ int ret;
+
+ switch (ctr_level) {
+ case 1:
+ mapping_ctr = &ctr1->mapping_ctr;
+ object_count = ctr1->object_count;
+ first_object = ctr1->first_object;
+ linked_attributes_count = 0;
+ linked_attributes = NULL;
+ rf1.source_dsa_obj_guid = ctr1->source_dsa_guid;
+ rf1.source_dsa_invocation_id = ctr1->source_dsa_invocation_id;
+ rf1.highwatermark = ctr1->new_highwatermark;
+ uptodateness_vector = NULL; /* TODO: map it */
+ more_data = ctr1->more_data;
+ break;
+ case 6:
+ mapping_ctr = &ctr6->mapping_ctr;
+ object_count = ctr6->object_count;
+ first_object = ctr6->first_object;
+ linked_attributes_count = ctr6->linked_attributes_count;
+ linked_attributes = ctr6->linked_attributes;
+ rf1.source_dsa_obj_guid = ctr6->source_dsa_guid;
+ rf1.source_dsa_invocation_id = ctr6->source_dsa_invocation_id;
+ rf1.highwatermark = ctr6->new_highwatermark;
+ uptodateness_vector = ctr6->uptodateness_vector;
+ more_data = ctr6->more_data;
+ break;
+ default:
+ nt_status = werror_to_ntstatus(WERR_BAD_NET_RESP);
+ tevent_req_nterror(req, nt_status);
+ return;
+ }
+
+ /*
+ * We need to cache the schema changes until we replicated
+ * everything before we can apply the new schema.
+ */
+ if (state->schema_cycle != NULL) {
+ TALLOC_CTX *mem = NULL;
+ struct drsuapi_DsReplicaObjectListItemEx **ptr = NULL;
+ struct drsuapi_DsReplicaObjectListItemEx *l = NULL;
+
+ was_schema = true;
+ sc = state->schema_cycle;
+
+ sc->repsFrom1 = rf1;
+
+ if (sc->first_object == NULL) {
+ mem = sc;
+ ptr = &sc->first_object;
+ } else {
+ mem = sc->last_object;
+ ptr = &sc->last_object->next_object;
+ }
+ *ptr = talloc_move(mem, &first_object);
+ for (l = *ptr; l != NULL; l = l->next_object) {
+ sc->object_count++;
+ if (l->next_object == NULL) {
+ sc->last_object = l;
+ break;
+ }
+ }
+
+ if (sc->linked_attributes_count == 0) {
+ sc->linked_attributes = talloc_move(sc, &linked_attributes);
+ sc->linked_attributes_count = linked_attributes_count;
+ linked_attributes_count = 0;
+ } else if (linked_attributes_count > 0) {
+ struct drsuapi_DsReplicaLinkedAttribute *new_las = NULL;
+ struct drsuapi_DsReplicaLinkedAttribute *tmp_las = NULL;
+ uint64_t new_count;
+ uint64_t add_size;
+ uint32_t add_idx;
+
+ new_count = sc->linked_attributes_count;
+ new_count += linked_attributes_count;
+ if (new_count > UINT32_MAX) {
+ nt_status = werror_to_ntstatus(WERR_BAD_NET_RESP);
+ tevent_req_nterror(req, nt_status);
+ return;
+ }
+ add_size = linked_attributes_count;
+ add_size *= sizeof(linked_attributes[0]);
+ if (add_size > SIZE_MAX) {
+ nt_status = werror_to_ntstatus(WERR_BAD_NET_RESP);
+ tevent_req_nterror(req, nt_status);
+ return;
+ }
+ add_idx = sc->linked_attributes_count;
+
+ tmp_las = talloc_realloc(sc,
+ sc->linked_attributes,
+ struct drsuapi_DsReplicaLinkedAttribute,
+ new_count);
+ if (tevent_req_nomem(tmp_las, req)) {
+ return;
+ }
+ new_las = talloc_move(tmp_las, &linked_attributes);
+ memcpy(&tmp_las[add_idx], new_las, add_size);
+ sc->linked_attributes = tmp_las;
+ sc->linked_attributes_count = new_count;
+ linked_attributes_count = 0;
+ }
+
+ if (more_data) {
+ /* we don't need this structure anymore */
+ TALLOC_FREE(r);
+
+ dreplsrv_op_pull_source_get_changes_trigger(req);
+ return;
+ }
+
+ /* detach sc from state */
+ state->schema_cycle = NULL;
+ }
+
+ schema = dsdb_get_schema(service->samdb, state);
+ if (!schema) {
+ DEBUG(0,(__location__ ": Schema is not loaded yet!\n"));
+ tevent_req_nterror(req, NT_STATUS_INTERNAL_ERROR);
+ return;
+ }
+
+ /*
+ * Decide what working schema to use for object conversion.
+ * We won't need a working schema for empty replicas sent.
+ */
+ if (sc != NULL) {
+ first_object = talloc_move(r, &sc->first_object);
+ object_count = sc->object_count;
+ linked_attributes = talloc_move(r, &sc->linked_attributes);
+ linked_attributes_count = sc->linked_attributes_count;
+ TALLOC_FREE(sc);
+
+ if (first_object != NULL) {
+ /* create working schema to convert objects with */
+ status = dsdb_repl_make_working_schema(service->samdb,
+ schema,
+ mapping_ctr,
+ object_count,
+ first_object,
+ &drsuapi->gensec_skey,
+ state, &working_schema);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(0,("Failed to create working schema: %s\n",
+ win_errstr(status)));
+ tevent_req_nterror(req, NT_STATUS_INTERNAL_ERROR);
+ return;
+ }
+ }
+ }
+
+ if (partition->partial_replica || partition->rodc_replica) {
+ dsdb_repl_flags |= DSDB_REPL_FLAG_PARTIAL_REPLICA;
+ }
+ if (state->op->options & DRSUAPI_DRS_FULL_SYNC_IN_PROGRESS) {
+ dsdb_repl_flags |= DSDB_REPL_FLAG_PRIORITISE_INCOMING;
+ }
+ if (state->op->options & DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING) {
+ dsdb_repl_flags |= DSDB_REPL_FLAG_EXPECT_NO_SECRETS;
+ }
+ if (state->op->options & DRSUAPI_DRS_CRITICAL_ONLY ||
+ state->op->extended_op != DRSUAPI_EXOP_NONE) {
+ dsdb_repl_flags |= DSDB_REPL_FLAG_OBJECT_SUBSET;
+ }
+
+ if (state->op->more_flags & DRSUAPI_DRS_GET_TGT) {
+ dsdb_repl_flags |= DSDB_REPL_FLAG_TARGETS_UPTODATE;
+ }
+
+ if (state->op->extended_op != DRSUAPI_EXOP_NONE) {
+ ret = dsdb_find_nc_root(service->samdb, partition,
+ partition->dn, &nc_root);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find nc_root for %s\n",
+ ldb_dn_get_linearized(partition->dn)));
+ tevent_req_nterror(req, NT_STATUS_INTERNAL_ERROR);
+ return;
+ }
+ } else {
+ nc_root = partition->dn;
+ }
+
+ status = dsdb_replicated_objects_convert(service->samdb,
+ working_schema ? working_schema : schema,
+ nc_root,
+ mapping_ctr,
+ object_count,
+ first_object,
+ linked_attributes_count,
+ linked_attributes,
+ &rf1,
+ uptodateness_vector,
+ &drsuapi->gensec_skey,
+ dsdb_repl_flags,
+ state, &objects);
+
+ if (W_ERROR_EQUAL(status, WERR_DS_DRA_SCHEMA_MISMATCH)) {
+ struct dreplsrv_partition *p;
+ bool ok;
+
+ if (was_schema) {
+ nt_status = werror_to_ntstatus(WERR_BAD_NET_RESP);
+ DBG_ERR("Got mismatch for schema partition: %s/%s\n",
+ win_errstr(status), nt_errstr(nt_status));
+ tevent_req_nterror(req, nt_status);
+ return;
+ }
+
+ if (state->retry_started) {
+ nt_status = werror_to_ntstatus(WERR_BAD_NET_RESP);
+ DEBUG(0,("Failed to convert objects after retry: %s/%s\n",
+ win_errstr(status), nt_errstr(nt_status)));
+ tevent_req_nterror(req, nt_status);
+ return;
+ }
+
+ /*
+ * Change info sync or extended operation into a fetch
+ * of the schema partition, so we get all the schema
+ * objects we need.
+ *
+ * We don't want to re-do the remote exop,
+ * unless it was REPL_SECRET so we set the
+ * fallback operation to just be a fetch of
+ * the relevant partition.
+ */
+
+
+ if (state->op->extended_op == DRSUAPI_EXOP_REPL_SECRET) {
+ state->extended_op_retry = state->op->extended_op;
+ } else {
+ state->extended_op_retry = DRSUAPI_EXOP_NONE;
+ }
+ state->op->extended_op = DRSUAPI_EXOP_NONE;
+
+ if (ldb_dn_compare(nc_root, partition->dn) == 0) {
+ state->source_dsa_retry = state->op->source_dsa;
+ } else {
+ status = dreplsrv_partition_find_for_nc(service,
+ NULL, NULL,
+ ldb_dn_get_linearized(nc_root),
+ &p);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(2, ("Failed to find requested Naming Context for %s: %s\n",
+ ldb_dn_get_linearized(nc_root),
+ win_errstr(status)));
+ nt_status = werror_to_ntstatus(status);
+ tevent_req_nterror(req, nt_status);
+ return;
+ }
+ status = dreplsrv_partition_source_dsa_by_guid(p,
+ &state->op->source_dsa->repsFrom1->source_dsa_obj_guid,
+ &state->source_dsa_retry);
+
+ if (!W_ERROR_IS_OK(status)) {
+ struct GUID_txt_buf str;
+ DEBUG(2, ("Failed to find requested source DSA for %s and %s: %s\n",
+ ldb_dn_get_linearized(nc_root),
+ GUID_buf_string(&state->op->source_dsa->repsFrom1->source_dsa_obj_guid, &str),
+ win_errstr(status)));
+ nt_status = werror_to_ntstatus(status);
+ tevent_req_nterror(req, nt_status);
+ return;
+ }
+ }
+
+ /* Find schema naming context to be synchronized first */
+ status = dreplsrv_partition_find_for_nc(service,
+ NULL, NULL,
+ ldb_dn_get_linearized(schema_dn),
+ &p);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(2, ("Failed to find requested Naming Context for schema: %s\n",
+ win_errstr(status)));
+ nt_status = werror_to_ntstatus(status);
+ tevent_req_nterror(req, nt_status);
+ return;
+ }
+
+ status = dreplsrv_partition_source_dsa_by_guid(p,
+ &state->op->source_dsa->repsFrom1->source_dsa_obj_guid,
+ &state->op->source_dsa);
+ if (!W_ERROR_IS_OK(status)) {
+ struct GUID_txt_buf str;
+ DEBUG(2, ("Failed to find requested source DSA for %s and %s: %s\n",
+ ldb_dn_get_linearized(schema_dn),
+ GUID_buf_string(&state->op->source_dsa->repsFrom1->source_dsa_obj_guid, &str),
+ win_errstr(status)));
+ nt_status = werror_to_ntstatus(status);
+ tevent_req_nterror(req, nt_status);
+ return;
+ }
+ DEBUG(4,("Wrong schema when applying reply GetNCChanges, retrying\n"));
+
+ state->retry_started = true;
+
+ ok = dreplsrv_op_pull_source_detect_schema_cycle(req);
+ if (!ok) {
+ return;
+ }
+
+ dreplsrv_op_pull_source_get_changes_trigger(req);
+ return;
+
+ } else if (!W_ERROR_IS_OK(status)) {
+ nt_status = werror_to_ntstatus(WERR_BAD_NET_RESP);
+ DEBUG(0,("Failed to convert objects: %s/%s\n",
+ win_errstr(status), nt_errstr(nt_status)));
+ tevent_req_nterror(req, nt_status);
+ return;
+ }
+
+ status = dsdb_replicated_objects_commit(service->samdb,
+ working_schema,
+ objects,
+ &state->op->source_dsa->notify_uSN);
+ talloc_free(objects);
+
+ if (!W_ERROR_IS_OK(status)) {
+
+ /*
+ * Check if this error can be fixed by resending the GetNCChanges
+ * request with extra flags set (i.e. GET_ANC/GET_TGT)
+ */
+ nt_status = dreplsrv_op_pull_retry_with_flags(req, status);
+
+ if (NT_STATUS_IS_OK(nt_status)) {
+
+ /*
+ * We resent the request. Don't update the highwatermark,
+ * we'll start this part of the cycle again.
+ */
+ return;
+ }
+
+ DEBUG(0,("Failed to commit objects: %s/%s\n",
+ win_errstr(status), nt_errstr(nt_status)));
+ tevent_req_nterror(req, nt_status);
+ return;
+ }
+
+ if (state->op->extended_op == DRSUAPI_EXOP_NONE) {
+ /* if it applied fine, we need to update the highwatermark */
+ *state->op->source_dsa->repsFrom1 = rf1;
+ }
+
+ /* we don't need this maybe very large structure anymore */
+ TALLOC_FREE(r);
+
+ if (more_data) {
+ dreplsrv_op_pull_source_get_changes_trigger(req);
+ return;
+ }
+
+ /*
+ * If we had to divert via doing some other thing, such as
+ * pulling the schema, then go back and do the original
+ * operation once we are done.
+ */
+ if (state->source_dsa_retry != NULL) {
+ state->op->source_dsa = state->source_dsa_retry;
+ state->op->extended_op = state->extended_op_retry;
+ state->source_dsa_retry = NULL;
+ dreplsrv_op_pull_source_get_changes_trigger(req);
+ return;
+ }
+
+ if (state->op->extended_op != DRSUAPI_EXOP_NONE ||
+ state->op->service->am_rodc) {
+ /*
+ we don't do the UpdateRefs for extended ops or if we
+ are a RODC
+ */
+ tevent_req_done(req);
+ return;
+ }
+
+ /* now we need to update the repsTo record for this partition
+ on the server. These records are initially established when
+ we join the domain, but they quickly expire. We do it here
+ so we can use the already established DRSUAPI pipe
+ */
+ dreplsrv_update_refs_trigger(req);
+}
+
+static void dreplsrv_update_refs_done(struct tevent_req *subreq);
+
+/*
+ send a UpdateRefs request to refresh our repsTo record on the server
+ */
+static void dreplsrv_update_refs_trigger(struct tevent_req *req)
+{
+ struct dreplsrv_op_pull_source_state *state = tevent_req_data(req,
+ struct dreplsrv_op_pull_source_state);
+ struct dreplsrv_service *service = state->op->service;
+ struct dreplsrv_partition *partition = state->op->source_dsa->partition;
+ struct dreplsrv_drsuapi_connection *drsuapi = state->op->source_dsa->conn->drsuapi;
+ struct drsuapi_DsReplicaUpdateRefs *r;
+ char *ntds_dns_name;
+ struct tevent_req *subreq;
+
+ r = talloc(state, struct drsuapi_DsReplicaUpdateRefs);
+ if (tevent_req_nomem(r, req)) {
+ return;
+ }
+
+ ntds_dns_name = samdb_ntds_msdcs_dns_name(service->samdb, r, &service->ntds_guid);
+ if (tevent_req_nomem(ntds_dns_name, req)) {
+ talloc_free(r);
+ return;
+ }
+
+ r->in.bind_handle = &drsuapi->bind_handle;
+ r->in.level = 1;
+ r->in.req.req1.naming_context = &partition->nc;
+ r->in.req.req1.dest_dsa_dns_name = ntds_dns_name;
+ r->in.req.req1.dest_dsa_guid = service->ntds_guid;
+ r->in.req.req1.options = DRSUAPI_DRS_ADD_REF | DRSUAPI_DRS_DEL_REF;
+ if (!service->am_rodc) {
+ r->in.req.req1.options |= DRSUAPI_DRS_WRIT_REP;
+ }
+
+ state->ndr_struct_ptr = r;
+ subreq = dcerpc_drsuapi_DsReplicaUpdateRefs_r_send(state,
+ state->ev,
+ drsuapi->drsuapi_handle,
+ r);
+ if (tevent_req_nomem(subreq, req)) {
+ talloc_free(r);
+ return;
+ }
+ tevent_req_set_callback(subreq, dreplsrv_update_refs_done, req);
+}
+
+/*
+ receive a UpdateRefs reply
+ */
+static void dreplsrv_update_refs_done(struct tevent_req *subreq)
+{
+ struct tevent_req *req = tevent_req_callback_data(subreq,
+ struct tevent_req);
+ struct dreplsrv_op_pull_source_state *state = tevent_req_data(req,
+ struct dreplsrv_op_pull_source_state);
+ struct drsuapi_DsReplicaUpdateRefs *r = talloc_get_type(state->ndr_struct_ptr,
+ struct drsuapi_DsReplicaUpdateRefs);
+ NTSTATUS status;
+
+ state->ndr_struct_ptr = NULL;
+
+ status = dcerpc_drsuapi_DsReplicaUpdateRefs_r_recv(subreq, r);
+ TALLOC_FREE(subreq);
+ if (!NT_STATUS_IS_OK(status)) {
+ DEBUG(0,("UpdateRefs failed with %s\n",
+ nt_errstr(status)));
+ tevent_req_nterror(req, status);
+ return;
+ }
+
+ if (!W_ERROR_IS_OK(r->out.result)) {
+ status = werror_to_ntstatus(r->out.result);
+ DEBUG(0,("UpdateRefs failed with %s/%s for %s %s\n",
+ win_errstr(r->out.result),
+ nt_errstr(status),
+ r->in.req.req1.dest_dsa_dns_name,
+ r->in.req.req1.naming_context->dn));
+ /*
+ * TODO we are currently not sending the
+ * DsReplicaUpdateRefs at the correct moment,
+ * we do it just after a GetNcChanges which is
+ * not always correct.
+ * Especially when another DC is trying to demote
+ * it will sends us a DsReplicaSync that will trigger a getNcChanges
+ * this call will succeed but the DsRecplicaUpdateRefs that we send
+ * just after will not because the DC is in a demote state and
+ * will reply us a WERR_DS_DRA_BUSY, this error will cause us to
+ * answer to the DsReplicaSync with a non OK status, the other DC
+ * will stop the demote due to this error.
+ * In order to cope with this we will for the moment consider
+ * a DS_DRA_BUSY not as an error.
+ * It's not ideal but it should not have a too huge impact for
+ * running production as this error otherwise never happen and
+ * due to the fact the send a DsReplicaUpdateRefs after each getNcChanges
+ */
+ if (!W_ERROR_EQUAL(r->out.result, WERR_DS_DRA_BUSY)) {
+ tevent_req_nterror(req, status);
+ return;
+ }
+ }
+
+ DEBUG(4,("UpdateRefs OK for %s %s\n",
+ r->in.req.req1.dest_dsa_dns_name,
+ r->in.req.req1.naming_context->dn));
+
+ tevent_req_done(req);
+}
+
+WERROR dreplsrv_op_pull_source_recv(struct tevent_req *req)
+{
+ NTSTATUS status;
+
+ if (tevent_req_is_nterror(req, &status)) {
+ tevent_req_received(req);
+ return ntstatus_to_werror(status);
+ }
+
+ tevent_req_received(req);
+ return WERR_OK;
+}
+
diff --git a/source4/dsdb/repl/drepl_out_helpers.h b/source4/dsdb/repl/drepl_out_helpers.h
new file mode 100644
index 0000000..158eef4
--- /dev/null
+++ b/source4/dsdb/repl/drepl_out_helpers.h
@@ -0,0 +1,26 @@
+/*
+ Unix SMB/CIFS Implementation.
+ DSDB replication service helper function for outgoing traffic
+
+ Copyright (C) Stefan Metzmacher 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/>.
+
+*/
+
+#ifndef DREPL_OUT_HELPERS_H
+#define DREPL_OUT_HELPERS_H
+
+
+#endif /* DREPL_OUT_HELPERS_H */
diff --git a/source4/dsdb/repl/drepl_out_pull.c b/source4/dsdb/repl/drepl_out_pull.c
new file mode 100644
index 0000000..fe9bd60
--- /dev/null
+++ b/source4/dsdb/repl/drepl_out_pull.c
@@ -0,0 +1,260 @@
+/*
+ Unix SMB/CIFS Implementation.
+ DSDB replication service outgoing Pull-Replication
+
+ Copyright (C) Stefan Metzmacher 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 "dsdb/samdb/samdb.h"
+#include "auth/auth.h"
+#include "samba/service.h"
+#include "lib/events/events.h"
+#include "dsdb/repl/drepl_service.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.h"
+#include "librpc/gen_ndr/ndr_misc.h"
+#include "librpc/gen_ndr/ndr_drsuapi.h"
+#include "librpc/gen_ndr/ndr_drsblobs.h"
+#include "libcli/composite/composite.h"
+#include "libcli/security/security.h"
+
+#undef DBGC_CLASS
+#define DBGC_CLASS DBGC_DRS_REPL
+
+/*
+ update repsFrom/repsTo error information
+ */
+void drepl_reps_update(struct dreplsrv_service *s, const char *reps_attr,
+ struct ldb_dn *dn,
+ struct GUID *source_dsa_obj_guid, WERROR status)
+{
+ struct repsFromToBlob *reps;
+ uint32_t count, i;
+ WERROR werr;
+ TALLOC_CTX *tmp_ctx = talloc_new(s);
+ time_t t;
+ NTTIME now;
+
+ t = time(NULL);
+ unix_to_nt_time(&now, t);
+
+ werr = dsdb_loadreps(s->samdb, tmp_ctx, dn, reps_attr, &reps, &count);
+ if (!W_ERROR_IS_OK(werr)) {
+ talloc_free(tmp_ctx);
+ return;
+ }
+
+ for (i=0; i<count; i++) {
+ if (GUID_equal(source_dsa_obj_guid,
+ &reps[i].ctr.ctr1.source_dsa_obj_guid)) {
+ break;
+ }
+ }
+
+ if (i == count) {
+ /* no record to update */
+ talloc_free(tmp_ctx);
+ return;
+ }
+
+ /* only update the status fields */
+ reps[i].ctr.ctr1.last_attempt = now;
+ reps[i].ctr.ctr1.result_last_attempt = status;
+ if (W_ERROR_IS_OK(status)) {
+ reps[i].ctr.ctr1.last_success = now;
+ reps[i].ctr.ctr1.consecutive_sync_failures = 0;
+ } else {
+ reps[i].ctr.ctr1.consecutive_sync_failures++;
+ }
+
+ werr = dsdb_savereps(s->samdb, tmp_ctx, dn, reps_attr, reps, count);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(2,("drepl_reps_update: Failed to save %s for %s: %s\n",
+ reps_attr, ldb_dn_get_linearized(dn), win_errstr(werr)));
+ }
+ talloc_free(tmp_ctx);
+}
+
+WERROR dreplsrv_schedule_partition_pull_source(struct dreplsrv_service *s,
+ struct dreplsrv_partition_source_dsa *source,
+ uint32_t options,
+ enum drsuapi_DsExtendedOperation extended_op,
+ uint64_t fsmo_info,
+ dreplsrv_extended_callback_t callback,
+ void *cb_data)
+{
+ struct dreplsrv_out_operation *op;
+
+ op = talloc_zero(s, struct dreplsrv_out_operation);
+ W_ERROR_HAVE_NO_MEMORY(op);
+
+ op->service = s;
+ /*
+ * source may either be the long-term list of partners, or
+ * from dreplsrv_partition_source_dsa_temporary(). Because it
+ * can be either, we can't talloc_steal() it here, so we
+ * instead we reference it.
+ *
+ * We never talloc_free() the p->sources pointers - indeed we
+ * never remove them - and the temp source will otherwise go
+ * away with the msg it is allocated on.
+ *
+ * Finally the pointer created in drepl_request_extended_op()
+ * is removed with talloc_unlink().
+ *
+ */
+ op->source_dsa = talloc_reference(op, source);
+ if (!op->source_dsa) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ op->options = options;
+ op->extended_op = extended_op;
+ op->fsmo_info = fsmo_info;
+ op->callback = callback;
+ op->cb_data = cb_data;
+ op->schedule_time = time(NULL);
+ op->more_flags = 0;
+
+ DLIST_ADD_END(s->ops.pending, op);
+
+ return WERR_OK;
+}
+
+static WERROR dreplsrv_schedule_partition_pull(struct dreplsrv_service *s,
+ struct dreplsrv_partition *p,
+ TALLOC_CTX *mem_ctx)
+{
+ WERROR status;
+ struct dreplsrv_partition_source_dsa *cur;
+
+ for (cur = p->sources; cur; cur = cur->next) {
+ status = dreplsrv_schedule_partition_pull_source(s, cur,
+ 0, DRSUAPI_EXOP_NONE, 0,
+ NULL, NULL);
+ W_ERROR_NOT_OK_RETURN(status);
+ }
+
+ return WERR_OK;
+}
+
+WERROR dreplsrv_schedule_pull_replication(struct dreplsrv_service *s, TALLOC_CTX *mem_ctx)
+{
+ WERROR status;
+ struct dreplsrv_partition *p;
+
+ for (p = s->partitions; p; p = p->next) {
+ status = dreplsrv_schedule_partition_pull(s, p, mem_ctx);
+ W_ERROR_NOT_OK_RETURN(status);
+ }
+
+ return WERR_OK;
+}
+
+
+static void dreplsrv_pending_op_callback(struct tevent_req *subreq)
+{
+ struct dreplsrv_out_operation *op = tevent_req_callback_data(subreq,
+ struct dreplsrv_out_operation);
+ struct repsFromTo1 *rf = op->source_dsa->repsFrom1;
+ struct dreplsrv_service *s = op->service;
+ WERROR werr;
+
+ werr = dreplsrv_op_pull_source_recv(subreq);
+ TALLOC_FREE(subreq);
+
+ DEBUG(4,("dreplsrv_op_pull_source(%s) for %s\n", win_errstr(werr),
+ ldb_dn_get_linearized(op->source_dsa->partition->dn)));
+
+ if (op->extended_op == DRSUAPI_EXOP_NONE) {
+ drepl_reps_update(s, "repsFrom", op->source_dsa->partition->dn,
+ &rf->source_dsa_obj_guid, werr);
+ }
+
+ if (op->callback) {
+ op->callback(s, werr, op->extended_ret, op->cb_data);
+ }
+ talloc_free(op);
+ s->ops.current = NULL;
+ dreplsrv_run_pending_ops(s);
+}
+
+void dreplsrv_run_pull_ops(struct dreplsrv_service *s)
+{
+ struct dreplsrv_out_operation *op;
+ time_t t;
+ NTTIME now;
+ struct tevent_req *subreq;
+ WERROR werr;
+
+ if (s->ops.n_current || s->ops.current) {
+ /* if there's still one running, we're done */
+ return;
+ }
+
+ if (!s->ops.pending) {
+ /* if there're no pending operations, we're done */
+ return;
+ }
+
+ t = time(NULL);
+ unix_to_nt_time(&now, t);
+
+ op = s->ops.pending;
+ s->ops.current = op;
+ DLIST_REMOVE(s->ops.pending, op);
+
+ op->source_dsa->repsFrom1->last_attempt = now;
+
+ /* check if inbound replication is enabled */
+ if (!(op->options & DRSUAPI_DRS_SYNC_FORCED)) {
+ uint32_t rep_options;
+ if (samdb_ntds_options(op->service->samdb, &rep_options) != LDB_SUCCESS) {
+ werr = WERR_DS_DRA_INTERNAL_ERROR;
+ goto failed;
+ }
+
+ if ((rep_options & DS_NTDSDSA_OPT_DISABLE_INBOUND_REPL)) {
+ werr = WERR_DS_DRA_SINK_DISABLED;
+ goto failed;
+ }
+ }
+
+ subreq = dreplsrv_op_pull_source_send(op, s->task->event_ctx, op);
+ if (!subreq) {
+ werr = WERR_NOT_ENOUGH_MEMORY;
+ goto failed;
+ }
+
+ tevent_req_set_callback(subreq, dreplsrv_pending_op_callback, op);
+ return;
+
+failed:
+ if (op->extended_op == DRSUAPI_EXOP_NONE) {
+ drepl_reps_update(s, "repsFrom", op->source_dsa->partition->dn,
+ &op->source_dsa->repsFrom1->source_dsa_obj_guid, werr);
+ }
+ /* unblock queue processing */
+ s->ops.current = NULL;
+ /*
+ * let the callback do its job just like in any other failure situation
+ */
+ if (op->callback) {
+ op->callback(s, werr, op->extended_ret, op->cb_data);
+ }
+}
diff --git a/source4/dsdb/repl/drepl_partitions.c b/source4/dsdb/repl/drepl_partitions.c
new file mode 100644
index 0000000..c525329
--- /dev/null
+++ b/source4/dsdb/repl/drepl_partitions.c
@@ -0,0 +1,665 @@
+/*
+ Unix SMB/CIFS Implementation.
+ DSDB replication service
+
+ Copyright (C) Stefan Metzmacher 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 "dsdb/samdb/samdb.h"
+#include "auth/auth.h"
+#include "samba/service.h"
+#include "lib/events/events.h"
+#include "dsdb/repl/drepl_service.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.h"
+#include "librpc/gen_ndr/ndr_misc.h"
+#include "librpc/gen_ndr/ndr_drsuapi.h"
+#include "librpc/gen_ndr/ndr_drsblobs.h"
+#include "libcli/security/security.h"
+#include "param/param.h"
+#include "dsdb/common/util.h"
+
+#undef DBGC_CLASS
+#define DBGC_CLASS DBGC_DRS_REPL
+
+#undef strcasecmp
+
+/*
+ load the partitions list based on replicated NC attributes in our
+ NTDSDSA object
+ */
+WERROR dreplsrv_load_partitions(struct dreplsrv_service *s)
+{
+ WERROR status;
+ static const char *attrs[] = { "hasMasterNCs", "msDS-hasMasterNCs", "hasPartialReplicaNCs", "msDS-HasFullReplicaNCs", NULL };
+ unsigned int a;
+ int ret;
+ TALLOC_CTX *tmp_ctx;
+ struct ldb_result *res;
+ struct ldb_message_element *el;
+ struct ldb_dn *ntds_dn;
+
+ tmp_ctx = talloc_new(s);
+ W_ERROR_HAVE_NO_MEMORY(tmp_ctx);
+
+ ntds_dn = samdb_ntds_settings_dn(s->samdb, tmp_ctx);
+ if (!ntds_dn) {
+ DEBUG(1,(__location__ ": Unable to find ntds_dn: %s\n", ldb_errstring(s->samdb)));
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ ret = dsdb_search_dn(s->samdb, tmp_ctx, &res, ntds_dn, attrs, DSDB_SEARCH_SHOW_EXTENDED_DN);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1,("Searching for hasMasterNCs in NTDS DN failed: %s\n", ldb_errstring(s->samdb)));
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ for (a=0; attrs[a]; a++) {
+ int i;
+
+ el = ldb_msg_find_element(res->msgs[0], attrs[a]);
+ if (el == NULL) {
+ continue;
+ }
+ for (i=0; i<el->num_values; i++) {
+ struct ldb_dn *pdn;
+ struct dreplsrv_partition *p, *tp;
+ bool found;
+
+ pdn = ldb_dn_from_ldb_val(tmp_ctx, s->samdb, &el->values[i]);
+ if (pdn == NULL) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+ if (!ldb_dn_validate(pdn)) {
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ p = talloc_zero(s, struct dreplsrv_partition);
+ W_ERROR_HAVE_NO_MEMORY(p);
+
+ p->dn = talloc_steal(p, pdn);
+ p->service = s;
+
+ if (strcasecmp(attrs[a], "hasPartialReplicaNCs") == 0) {
+ p->partial_replica = true;
+ } else if (strcasecmp(attrs[a], "msDS-HasFullReplicaNCs") == 0) {
+ p->rodc_replica = true;
+ }
+
+ /* Do not add partitions more than once */
+ found = false;
+ for (tp = s->partitions; tp; tp = tp->next) {
+ if (ldb_dn_compare(tp->dn, p->dn) == 0) {
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ talloc_free(p);
+ continue;
+ }
+
+ DLIST_ADD(s->partitions, p);
+ DEBUG(2, ("dreplsrv_partition[%s] loaded\n", ldb_dn_get_linearized(p->dn)));
+ }
+ }
+
+ talloc_free(tmp_ctx);
+
+ status = dreplsrv_refresh_partitions(s);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ return WERR_OK;
+}
+
+/*
+ Check if particular SPN exists for an account
+ */
+static bool dreplsrv_spn_exists(struct ldb_context *samdb, struct ldb_dn *account_dn,
+ const char *principal_name)
+{
+ TALLOC_CTX *tmp_ctx;
+ const char *attrs_empty[] = { NULL };
+ int ret;
+ struct ldb_result *res;
+ const char *principal_name_encoded = NULL;
+
+ tmp_ctx = talloc_new(samdb);
+ if (tmp_ctx == NULL) {
+ return false;
+ }
+
+ principal_name_encoded = ldb_binary_encode_string(tmp_ctx, principal_name);
+ if (principal_name_encoded == NULL) {
+ talloc_free(tmp_ctx);
+ return false;
+ }
+
+ ret = dsdb_search(samdb, tmp_ctx, &res, account_dn, LDB_SCOPE_BASE, attrs_empty,
+ 0, "servicePrincipalName=%s",
+ principal_name_encoded);
+ if (ret != LDB_SUCCESS || res->count != 1) {
+ talloc_free(tmp_ctx);
+ return false;
+ }
+
+ talloc_free(tmp_ctx);
+ return true;
+}
+
+/*
+ work out the principal to use for DRS replication connections
+ */
+static NTSTATUS dreplsrv_get_target_principal(struct dreplsrv_service *s,
+ TALLOC_CTX *mem_ctx,
+ const struct repsFromTo1 *rft,
+ char **target_principal)
+{
+ TALLOC_CTX *tmp_ctx;
+ struct ldb_result *res;
+ const char *attrs_server[] = { "dNSHostName", "serverReference", NULL };
+ const char *attrs_ntds[] = { "msDS-HasDomainNCs", "hasMasterNCs", NULL };
+ int ret;
+ const char *hostname, *dnsdomain=NULL;
+ struct ldb_dn *ntds_dn, *server_dn, *computer_dn;
+ struct ldb_dn *forest_dn, *nc_dn;
+
+ *target_principal = NULL;
+
+ tmp_ctx = talloc_new(mem_ctx);
+
+ /* we need to find their hostname */
+ ret = dsdb_find_dn_by_guid(s->samdb, tmp_ctx, &rft->source_dsa_obj_guid, 0, &ntds_dn);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ /* its OK for their NTDSDSA DN not to be in our database */
+ return NT_STATUS_OK;
+ }
+
+ server_dn = ldb_dn_copy(tmp_ctx, ntds_dn);
+ if (server_dn == NULL) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+ }
+
+ /* strip off the NTDS Settings */
+ if (!ldb_dn_remove_child_components(server_dn, 1)) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+ }
+
+ ret = dsdb_search_dn(s->samdb, tmp_ctx, &res, server_dn, attrs_server, 0);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ /* its OK for their server DN not to be in our database */
+ return NT_STATUS_OK;
+ }
+
+ forest_dn = ldb_get_root_basedn(s->samdb);
+ if (forest_dn == NULL) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+ }
+
+ hostname = ldb_msg_find_attr_as_string(res->msgs[0], "dNSHostName", NULL);
+ computer_dn = ldb_msg_find_attr_as_dn(s->samdb, tmp_ctx, res->msgs[0], "serverReference");
+ if (hostname != NULL && computer_dn != NULL) {
+ char *local_principal;
+
+ /*
+ if we have the dNSHostName attribute then we can use
+ the GC/hostname/realm SPN. All DCs should have this SPN
+
+ Windows DC may set up it's dNSHostName before setting up
+ GC/xx/xx SPN. So make sure it exists, before using it.
+ */
+ local_principal = talloc_asprintf(mem_ctx, "GC/%s/%s",
+ hostname,
+ samdb_dn_to_dns_domain(tmp_ctx, forest_dn));
+ if (local_principal == NULL) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_NO_MEMORY;
+ }
+ if (dreplsrv_spn_exists(s->samdb, computer_dn, local_principal)) {
+ *target_principal = local_principal;
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+ }
+
+ talloc_free(local_principal);
+ }
+
+ /*
+ if we can't find the dNSHostName then we will try for the
+ E3514235-4B06-11D1-AB04-00C04FC2DCD2/${NTDSGUID}/${DNSDOMAIN}
+ SPN. To use that we need the DNS domain name of the target
+ DC. We find that by first looking for the msDS-HasDomainNCs
+ in the NTDSDSA object of the DC, and if we don't find that,
+ then we look for the hasMasterNCs attribute, and eliminate
+ the known schema and configuruation DNs. Despite how
+ bizarre this seems, Hongwei tells us that this is in fact
+ what windows does to find the SPN!!
+ */
+ ret = dsdb_search_dn(s->samdb, tmp_ctx, &res, ntds_dn, attrs_ntds, 0);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+ }
+
+ nc_dn = ldb_msg_find_attr_as_dn(s->samdb, tmp_ctx, res->msgs[0], "msDS-HasDomainNCs");
+ if (nc_dn != NULL) {
+ dnsdomain = samdb_dn_to_dns_domain(tmp_ctx, nc_dn);
+ }
+
+ if (dnsdomain == NULL) {
+ struct ldb_message_element *el;
+ int i;
+ el = ldb_msg_find_element(res->msgs[0], "hasMasterNCs");
+ for (i=0; el && i<el->num_values; i++) {
+ nc_dn = ldb_dn_from_ldb_val(tmp_ctx, s->samdb, &el->values[i]);
+ if (nc_dn == NULL ||
+ ldb_dn_compare(ldb_get_config_basedn(s->samdb), nc_dn) == 0 ||
+ ldb_dn_compare(ldb_get_schema_basedn(s->samdb), nc_dn) == 0) {
+ continue;
+ }
+ /* it must be a domain DN, get the equivalent
+ DNS domain name */
+ dnsdomain = samdb_dn_to_dns_domain(tmp_ctx, nc_dn);
+ break;
+ }
+ }
+
+ if (dnsdomain != NULL) {
+ *target_principal = talloc_asprintf(mem_ctx,
+ "E3514235-4B06-11D1-AB04-00C04FC2DCD2/%s/%s@%s",
+ GUID_string(tmp_ctx, &rft->source_dsa_obj_guid),
+ dnsdomain, dnsdomain);
+ }
+
+ talloc_free(tmp_ctx);
+ return NT_STATUS_OK;
+}
+
+
+WERROR dreplsrv_out_connection_attach(struct dreplsrv_service *s,
+ const struct repsFromTo1 *rft,
+ struct dreplsrv_out_connection **_conn)
+{
+ struct dreplsrv_out_connection *cur, *conn = NULL;
+ const char *hostname;
+
+ if (!rft->other_info) {
+ return WERR_FOOBAR;
+ }
+
+ if (!rft->other_info->dns_name) {
+ return WERR_FOOBAR;
+ }
+
+ hostname = rft->other_info->dns_name;
+
+ for (cur = s->connections; cur; cur = cur->next) {
+ const char *host;
+
+ host = dcerpc_binding_get_string_option(cur->binding, "host");
+ if (host == NULL) {
+ continue;
+ }
+
+ if (strcmp(host, hostname) == 0) {
+ conn = cur;
+ break;
+ }
+ }
+
+ if (!conn) {
+ NTSTATUS nt_status;
+ char *binding_str;
+ char *target_principal = NULL;
+
+ conn = talloc_zero(s, struct dreplsrv_out_connection);
+ W_ERROR_HAVE_NO_MEMORY(conn);
+
+ conn->service = s;
+
+ binding_str = talloc_asprintf(conn, "ncacn_ip_tcp:%s[krb5,seal]",
+ hostname);
+ W_ERROR_HAVE_NO_MEMORY(binding_str);
+ nt_status = dcerpc_parse_binding(conn, binding_str, &conn->binding);
+ talloc_free(binding_str);
+ if (!NT_STATUS_IS_OK(nt_status)) {
+ return ntstatus_to_werror(nt_status);
+ }
+
+ /* use the GC principal for DRS replication */
+ nt_status = dreplsrv_get_target_principal(s, conn->binding,
+ rft, &target_principal);
+ if (!NT_STATUS_IS_OK(nt_status)) {
+ return ntstatus_to_werror(nt_status);
+ }
+
+ nt_status = dcerpc_binding_set_string_option(conn->binding,
+ "target_principal",
+ target_principal);
+ TALLOC_FREE(target_principal);
+ if (!NT_STATUS_IS_OK(nt_status)) {
+ return ntstatus_to_werror(nt_status);
+ }
+
+ DLIST_ADD_END(s->connections, conn);
+
+ DEBUG(4,("dreplsrv_out_connection_attach(%s): create\n", hostname));
+ } else {
+ DEBUG(4,("dreplsrv_out_connection_attach(%s): attach\n", hostname));
+ }
+
+ *_conn = conn;
+ return WERR_OK;
+}
+
+/*
+ find an existing source dsa in a list
+ */
+static struct dreplsrv_partition_source_dsa *dreplsrv_find_source_dsa(struct dreplsrv_partition_source_dsa *list,
+ struct GUID *guid)
+{
+ struct dreplsrv_partition_source_dsa *s;
+ for (s=list; s; s=s->next) {
+ if (GUID_equal(&s->repsFrom1->source_dsa_obj_guid, guid)) {
+ return s;
+ }
+ }
+ return NULL;
+}
+
+
+
+static WERROR dreplsrv_partition_add_source_dsa(struct dreplsrv_service *s,
+ struct dreplsrv_partition *p,
+ struct dreplsrv_partition_source_dsa **listp,
+ struct dreplsrv_partition_source_dsa *check_list,
+ const struct ldb_val *val)
+{
+ WERROR status;
+ enum ndr_err_code ndr_err;
+ struct dreplsrv_partition_source_dsa *source, *s2;
+
+ source = talloc_zero(p, struct dreplsrv_partition_source_dsa);
+ W_ERROR_HAVE_NO_MEMORY(source);
+
+ ndr_err = ndr_pull_struct_blob(val, source,
+ &source->_repsFromBlob,
+ (ndr_pull_flags_fn_t)ndr_pull_repsFromToBlob);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ NTSTATUS nt_status = ndr_map_error2ntstatus(ndr_err);
+ talloc_free(source);
+ return ntstatus_to_werror(nt_status);
+ }
+ /* NDR_PRINT_DEBUG(repsFromToBlob, &source->_repsFromBlob); */
+ if (source->_repsFromBlob.version != 1) {
+ talloc_free(source);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ source->partition = p;
+ source->repsFrom1 = &source->_repsFromBlob.ctr.ctr1;
+
+ status = dreplsrv_out_connection_attach(s, source->repsFrom1, &source->conn);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ if (check_list &&
+ dreplsrv_find_source_dsa(check_list, &source->repsFrom1->source_dsa_obj_guid)) {
+ /* its in the check list, don't add it again */
+ talloc_free(source);
+ return WERR_OK;
+ }
+
+ /* re-use an existing source if found */
+ for (s2=*listp; s2; s2=s2->next) {
+ if (GUID_equal(&s2->repsFrom1->source_dsa_obj_guid,
+ &source->repsFrom1->source_dsa_obj_guid)) {
+ talloc_free(s2->repsFrom1->other_info);
+ *s2->repsFrom1 = *source->repsFrom1;
+ talloc_steal(s2, s2->repsFrom1->other_info);
+ talloc_free(source);
+ return WERR_OK;
+ }
+ }
+
+ DLIST_ADD_END(*listp, source);
+ return WERR_OK;
+}
+
+/**
+ * Find a partition when given a NC
+ * If the NC can't be found it will return BAD_NC
+ * Initial checks for invalid parameters have to be done beforehand
+ */
+WERROR dreplsrv_partition_find_for_nc(struct dreplsrv_service *s,
+ struct GUID *nc_guid,
+ struct dom_sid *nc_sid,
+ const char *nc_dn_str,
+ struct dreplsrv_partition **_p)
+{
+ struct dreplsrv_partition *p;
+ bool valid_sid, valid_guid;
+
+ SMB_ASSERT(_p);
+
+ valid_sid = nc_sid && !is_null_sid(nc_sid);
+ valid_guid = nc_guid && !GUID_all_zero(nc_guid);
+
+ if (!valid_sid && !valid_guid && (!nc_dn_str)) {
+ return WERR_DS_DRA_BAD_NC;
+ }
+
+ for (p = s->partitions; p; p = p->next) {
+ if ((valid_guid && GUID_equal(&p->nc.guid, nc_guid))
+ || strequal(p->nc.dn, nc_dn_str)
+ || (valid_sid && dom_sid_equal(&p->nc.sid, nc_sid)))
+ {
+ /* fill in the right guid and sid if possible */
+ if (nc_guid && !valid_guid) {
+ dsdb_get_extended_dn_guid(p->dn, nc_guid, "GUID");
+ }
+ if (nc_sid && !valid_sid) {
+ dsdb_get_extended_dn_sid(p->dn, nc_sid, "SID");
+ }
+ *_p = p;
+ return WERR_OK;
+ }
+ }
+
+ return WERR_DS_DRA_BAD_NC;
+}
+
+WERROR dreplsrv_partition_source_dsa_by_guid(struct dreplsrv_partition *p,
+ const struct GUID *dsa_guid,
+ struct dreplsrv_partition_source_dsa **_dsa)
+{
+ struct dreplsrv_partition_source_dsa *dsa;
+
+ SMB_ASSERT(dsa_guid != NULL);
+ SMB_ASSERT(!GUID_all_zero(dsa_guid));
+ SMB_ASSERT(_dsa);
+
+ for (dsa = p->sources; dsa; dsa = dsa->next) {
+ if (GUID_equal(dsa_guid, &dsa->repsFrom1->source_dsa_obj_guid)) {
+ *_dsa = dsa;
+ return WERR_OK;
+ }
+ }
+
+ return WERR_DS_DRA_NO_REPLICA;
+}
+
+WERROR dreplsrv_partition_source_dsa_by_dns(const struct dreplsrv_partition *p,
+ const char *dsa_dns,
+ struct dreplsrv_partition_source_dsa **_dsa)
+{
+ struct dreplsrv_partition_source_dsa *dsa;
+
+ SMB_ASSERT(dsa_dns != NULL);
+ SMB_ASSERT(_dsa);
+
+ for (dsa = p->sources; dsa; dsa = dsa->next) {
+ if (strequal(dsa_dns, dsa->repsFrom1->other_info->dns_name)) {
+ *_dsa = dsa;
+ return WERR_OK;
+ }
+ }
+
+ return WERR_DS_DRA_NO_REPLICA;
+}
+
+
+/*
+ create a temporary dsa structure for a replication. This is needed
+ for the initial replication of a new partition, such as when a new
+ domain NC is created and we are a global catalog server
+ */
+WERROR dreplsrv_partition_source_dsa_temporary(struct dreplsrv_partition *p,
+ TALLOC_CTX *mem_ctx,
+ const struct GUID *dsa_guid,
+ struct dreplsrv_partition_source_dsa **_dsa)
+{
+ struct dreplsrv_partition_source_dsa *dsa;
+ WERROR werr;
+
+ dsa = talloc_zero(mem_ctx, struct dreplsrv_partition_source_dsa);
+ W_ERROR_HAVE_NO_MEMORY(dsa);
+
+ dsa->partition = p;
+ dsa->repsFrom1 = &dsa->_repsFromBlob.ctr.ctr1;
+ dsa->repsFrom1->replica_flags = 0;
+ dsa->repsFrom1->source_dsa_obj_guid = *dsa_guid;
+
+ dsa->repsFrom1->other_info = talloc_zero(dsa, struct repsFromTo1OtherInfo);
+ W_ERROR_HAVE_NO_MEMORY(dsa->repsFrom1->other_info);
+
+ dsa->repsFrom1->other_info->dns_name = samdb_ntds_msdcs_dns_name(p->service->samdb,
+ dsa->repsFrom1->other_info, dsa_guid);
+ W_ERROR_HAVE_NO_MEMORY(dsa->repsFrom1->other_info->dns_name);
+
+ werr = dreplsrv_out_connection_attach(p->service, dsa->repsFrom1, &dsa->conn);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0,(__location__ ": Failed to attach connection to %s\n",
+ ldb_dn_get_linearized(p->dn)));
+ talloc_free(dsa);
+ return werr;
+ }
+
+ *_dsa = dsa;
+
+ return WERR_OK;
+}
+
+
+static WERROR dreplsrv_refresh_partition(struct dreplsrv_service *s,
+ struct dreplsrv_partition *p)
+{
+ WERROR status;
+ NTSTATUS ntstatus;
+ struct ldb_message_element *orf_el = NULL;
+ struct ldb_result *r = NULL;
+ unsigned int i;
+ int ret;
+ TALLOC_CTX *mem_ctx = talloc_new(p);
+ static const char *attrs[] = {
+ "repsFrom",
+ "repsTo",
+ NULL
+ };
+ struct ldb_dn *dn;
+
+ DEBUG(4, ("dreplsrv_refresh_partition(%s)\n",
+ ldb_dn_get_linearized(p->dn)));
+
+ ret = dsdb_search_dn(s->samdb, mem_ctx, &r, p->dn, attrs, DSDB_SEARCH_SHOW_EXTENDED_DN);
+ if (ret == LDB_ERR_NO_SUCH_OBJECT) {
+ /* we haven't replicated the partition yet, but we
+ * can fill in the guid, sid etc from the partition DN */
+ dn = p->dn;
+ } else if (ret != LDB_SUCCESS) {
+ talloc_free(mem_ctx);
+ return WERR_FOOBAR;
+ } else {
+ dn = r->msgs[0]->dn;
+ }
+
+ talloc_free(discard_const(p->nc.dn));
+ ZERO_STRUCT(p->nc);
+ p->nc.dn = ldb_dn_alloc_linearized(p, dn);
+ W_ERROR_HAVE_NO_MEMORY(p->nc.dn);
+ ntstatus = dsdb_get_extended_dn_guid(dn, &p->nc.guid, "GUID");
+ if (!NT_STATUS_IS_OK(ntstatus)) {
+ DEBUG(0,(__location__ ": unable to get GUID for %s: %s\n",
+ p->nc.dn, nt_errstr(ntstatus)));
+ talloc_free(mem_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+ dsdb_get_extended_dn_sid(dn, &p->nc.sid, "SID");
+
+ talloc_free(p->uptodatevector.cursors);
+ talloc_free(p->uptodatevector_ex.cursors);
+ ZERO_STRUCT(p->uptodatevector);
+ ZERO_STRUCT(p->uptodatevector_ex);
+
+ ret = dsdb_load_udv_v2(s->samdb, p->dn, p, &p->uptodatevector.cursors, &p->uptodatevector.count);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(4,(__location__ ": no UDV available for %s\n", ldb_dn_get_linearized(p->dn)));
+ }
+
+ status = WERR_OK;
+
+ if (r != NULL && (orf_el = ldb_msg_find_element(r->msgs[0], "repsFrom"))) {
+ for (i=0; i < orf_el->num_values; i++) {
+ status = dreplsrv_partition_add_source_dsa(s, p, &p->sources,
+ NULL, &orf_el->values[i]);
+ W_ERROR_NOT_OK_GOTO_DONE(status);
+ }
+ }
+
+ if (r != NULL && (orf_el = ldb_msg_find_element(r->msgs[0], "repsTo"))) {
+ for (i=0; i < orf_el->num_values; i++) {
+ status = dreplsrv_partition_add_source_dsa(s, p, &p->notifies,
+ p->sources, &orf_el->values[i]);
+ W_ERROR_NOT_OK_GOTO_DONE(status);
+ }
+ }
+
+done:
+ talloc_free(mem_ctx);
+ return status;
+}
+
+WERROR dreplsrv_refresh_partitions(struct dreplsrv_service *s)
+{
+ WERROR status;
+ struct dreplsrv_partition *p;
+
+ for (p = s->partitions; p; p = p->next) {
+ status = dreplsrv_refresh_partition(s, p);
+ W_ERROR_NOT_OK_RETURN(status);
+ }
+
+ return WERR_OK;
+}
diff --git a/source4/dsdb/repl/drepl_periodic.c b/source4/dsdb/repl/drepl_periodic.c
new file mode 100644
index 0000000..4cdc8cb
--- /dev/null
+++ b/source4/dsdb/repl/drepl_periodic.c
@@ -0,0 +1,157 @@
+/*
+ Unix SMB/CIFS Implementation.
+ DSDB replication service periodic handling
+
+ Copyright (C) Stefan Metzmacher 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 "lib/events/events.h"
+#include "dsdb/samdb/samdb.h"
+#include "auth/auth.h"
+#include "samba/service.h"
+#include "dsdb/repl/drepl_service.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.h"
+#include "librpc/gen_ndr/ndr_misc.h"
+#include "librpc/gen_ndr/ndr_drsuapi.h"
+#include "librpc/gen_ndr/ndr_drsblobs.h"
+
+#undef DBGC_CLASS
+#define DBGC_CLASS DBGC_DRS_REPL
+
+static void dreplsrv_periodic_run(struct dreplsrv_service *service);
+
+static void dreplsrv_periodic_handler_te(struct tevent_context *ev, struct tevent_timer *te,
+ struct timeval t, void *ptr)
+{
+ struct dreplsrv_service *service = talloc_get_type(ptr, struct dreplsrv_service);
+ WERROR status;
+
+ service->periodic.te = NULL;
+
+ dreplsrv_periodic_run(service);
+
+ status = dreplsrv_periodic_schedule(service, service->periodic.interval);
+ if (!W_ERROR_IS_OK(status)) {
+ task_server_terminate(service->task, win_errstr(status), false);
+ return;
+ }
+}
+
+WERROR dreplsrv_periodic_schedule(struct dreplsrv_service *service, uint32_t next_interval)
+{
+ TALLOC_CTX *tmp_mem;
+ struct tevent_timer *new_te;
+ struct timeval next_time;
+
+ /* prevent looping */
+ if (next_interval == 0) next_interval = 1;
+
+ next_time = timeval_current_ofs(next_interval, 50);
+
+ if (service->periodic.te) {
+ /*
+ * if the timestamp of the new event is higher,
+ * as current next we don't need to reschedule
+ */
+ if (timeval_compare(&next_time, &service->periodic.next_event) > 0) {
+ return WERR_OK;
+ }
+ }
+
+ /* reset the next scheduled timestamp */
+ service->periodic.next_event = next_time;
+
+ new_te = tevent_add_timer(service->task->event_ctx, service,
+ service->periodic.next_event,
+ dreplsrv_periodic_handler_te, service);
+ W_ERROR_HAVE_NO_MEMORY(new_te);
+
+ tmp_mem = talloc_new(service);
+ DEBUG(4,("dreplsrv_periodic_schedule(%u) %sscheduled for: %s\n",
+ next_interval,
+ (service->periodic.te?"re":""),
+ nt_time_string(tmp_mem, timeval_to_nttime(&next_time))));
+ talloc_free(tmp_mem);
+
+ talloc_free(service->periodic.te);
+ service->periodic.te = new_te;
+
+ return WERR_OK;
+}
+
+static void dreplsrv_periodic_run(struct dreplsrv_service *service)
+{
+ TALLOC_CTX *mem_ctx;
+
+ DEBUG(4,("dreplsrv_periodic_run(): schedule pull replication\n"));
+
+ /*
+ * KCC or some administrative tool
+ * might have changed Topology graph
+ * i.e. repsFrom/repsTo
+ */
+ dreplsrv_refresh_partitions(service);
+
+ mem_ctx = talloc_new(service);
+ dreplsrv_schedule_pull_replication(service, mem_ctx);
+ talloc_free(mem_ctx);
+
+ DEBUG(4,("dreplsrv_periodic_run(): run pending_ops memory=%u\n",
+ (unsigned)talloc_total_blocks(service)));
+
+ dreplsrv_ridalloc_check_rid_pool(service);
+
+ dreplsrv_run_pending_ops(service);
+}
+
+/*
+ run the next pending op, either a notify or a pull
+ */
+void dreplsrv_run_pending_ops(struct dreplsrv_service *s)
+{
+ if (!s->ops.notifies && !s->ops.pending) {
+ return;
+ }
+ if (!s->ops.notifies ||
+ (s->ops.pending &&
+ s->ops.notifies->schedule_time > s->ops.pending->schedule_time)) {
+ dreplsrv_run_pull_ops(s);
+ } else {
+ dreplsrv_notify_run_ops(s);
+ }
+}
+
+static void dreplsrv_pending_pull_handler_im(struct tevent_context *ev,
+ struct tevent_immediate *im,
+ void *ptr)
+{
+ struct dreplsrv_service *service = talloc_get_type(ptr, struct dreplsrv_service);
+
+ dreplsrv_run_pull_ops(service);
+}
+
+void dreplsrv_pendingops_schedule_pull_now(struct dreplsrv_service *service)
+{
+ tevent_schedule_immediate(service->pending.im, service->task->event_ctx,
+ dreplsrv_pending_pull_handler_im,
+ service);
+
+ return;
+}
+
diff --git a/source4/dsdb/repl/drepl_replica.c b/source4/dsdb/repl/drepl_replica.c
new file mode 100644
index 0000000..05d0683
--- /dev/null
+++ b/source4/dsdb/repl/drepl_replica.c
@@ -0,0 +1,62 @@
+/*
+ Unix SMB/CIFS Implementation.
+
+ DSDB replication service - DsReplica{Add,Del,Mod} handling
+
+ 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/>.
+
+*/
+
+#include "includes.h"
+#include "ldb_module.h"
+#include "dsdb/samdb/samdb.h"
+#include "samba/service.h"
+#include "dsdb/repl/drepl_service.h"
+#include "param/param.h"
+#include "librpc/gen_ndr/ndr_drsuapi.h"
+
+#undef DBGC_CLASS
+#define DBGC_CLASS DBGC_DRS_REPL
+
+/*
+ implement DsReplicaAdd (forwarded from DRS server)
+ */
+NTSTATUS drepl_replica_add(struct dreplsrv_service *service,
+ struct drsuapi_DsReplicaAdd *r)
+{
+ NDR_PRINT_FUNCTION_DEBUG(drsuapi_DsReplicaAdd, NDR_IN, r);
+ return NT_STATUS_NOT_IMPLEMENTED;
+}
+
+/*
+ implement DsReplicaDel (forwarded from DRS server)
+ */
+NTSTATUS drepl_replica_del(struct dreplsrv_service *service,
+ struct drsuapi_DsReplicaDel *r)
+{
+ NDR_PRINT_FUNCTION_DEBUG(drsuapi_DsReplicaDel, NDR_IN, r);
+ return NT_STATUS_NOT_IMPLEMENTED;
+}
+
+/*
+ implement DsReplicaMod (forwarded from DRS server)
+ */
+NTSTATUS drepl_replica_mod(struct dreplsrv_service *service,
+ struct drsuapi_DsReplicaMod *r)
+{
+ NDR_PRINT_FUNCTION_DEBUG(drsuapi_DsReplicaMod, NDR_IN, r);
+ return NT_STATUS_NOT_IMPLEMENTED;
+}
diff --git a/source4/dsdb/repl/drepl_ridalloc.c b/source4/dsdb/repl/drepl_ridalloc.c
new file mode 100644
index 0000000..529e4e3
--- /dev/null
+++ b/source4/dsdb/repl/drepl_ridalloc.c
@@ -0,0 +1,265 @@
+/*
+ Unix SMB/CIFS Implementation.
+
+ DSDB replication service - RID allocation code
+
+ Copyright (C) Andrew Tridgell 2010
+ Copyright (C) Andrew Bartlett 2010
+
+ based on drepl_notify.c
+
+ 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 "samba/service.h"
+#include "dsdb/repl/drepl_service.h"
+#include "param/param.h"
+
+#undef DBGC_CLASS
+#define DBGC_CLASS DBGC_DRS_REPL
+
+/*
+ called when a rid allocation request has completed
+ */
+static void drepl_new_rid_pool_callback(struct dreplsrv_service *service,
+ WERROR werr,
+ enum drsuapi_DsExtendedError ext_err,
+ void *cb_data)
+{
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0,(__location__ ": RID Manager failed RID allocation - %s - extended_ret[0x%X]\n",
+ win_errstr(werr), ext_err));
+ } else {
+ DEBUG(3,(__location__ ": RID Manager completed RID allocation OK\n"));
+ }
+
+ service->rid_alloc_in_progress = false;
+}
+
+/*
+ schedule a getncchanges request to the RID Manager to ask for a new
+ set of RIDs using DRSUAPI_EXOP_FSMO_RID_ALLOC
+ */
+static WERROR drepl_request_new_rid_pool(struct dreplsrv_service *service,
+ struct ldb_dn *rid_manager_dn, struct ldb_dn *fsmo_role_dn,
+ uint64_t alloc_pool)
+{
+ WERROR werr = drepl_request_extended_op(service,
+ rid_manager_dn,
+ fsmo_role_dn,
+ DRSUAPI_EXOP_FSMO_RID_ALLOC,
+ alloc_pool,
+ 0,
+ drepl_new_rid_pool_callback, NULL);
+ if (W_ERROR_IS_OK(werr)) {
+ service->rid_alloc_in_progress = true;
+ }
+ return werr;
+}
+
+
+/*
+ see if we are on the last pool we have
+ */
+static int drepl_ridalloc_pool_exhausted(struct ldb_context *ldb,
+ bool *exhausted,
+ uint64_t *_alloc_pool)
+{
+ struct ldb_dn *server_dn, *machine_dn, *rid_set_dn;
+ TALLOC_CTX *tmp_ctx = talloc_new(ldb);
+ uint64_t alloc_pool;
+ uint64_t prev_pool;
+ uint32_t prev_pool_lo, prev_pool_hi;
+ uint32_t next_rid;
+ static const char * const attrs[] = {
+ "rIDAllocationPool",
+ "rIDPreviousAllocationPool",
+ "rIDNextRid",
+ NULL
+ };
+ int ret;
+ struct ldb_result *res;
+
+ *exhausted = false;
+ *_alloc_pool = UINT64_MAX;
+
+ server_dn = ldb_dn_get_parent(tmp_ctx, samdb_ntds_settings_dn(ldb, tmp_ctx));
+ if (!server_dn) {
+ talloc_free(tmp_ctx);
+ return ldb_operr(ldb);
+ }
+
+ ret = samdb_reference_dn(ldb, tmp_ctx, server_dn, "serverReference", &machine_dn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find serverReference in %s - %s\n",
+ ldb_dn_get_linearized(server_dn), ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ ret = samdb_reference_dn(ldb, tmp_ctx, machine_dn, "rIDSetReferences", &rid_set_dn);
+ if (ret == LDB_ERR_NO_SUCH_ATTRIBUTE) {
+ *exhausted = true;
+ *_alloc_pool = 0;
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+ }
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find rIDSetReferences in %s - %s\n",
+ ldb_dn_get_linearized(machine_dn), ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ ret = ldb_search(ldb, tmp_ctx, &res, rid_set_dn, LDB_SCOPE_BASE, attrs, NULL);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to load RID Set attrs from %s - %s\n",
+ ldb_dn_get_linearized(rid_set_dn), ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ alloc_pool = ldb_msg_find_attr_as_uint64(res->msgs[0], "rIDAllocationPool", 0);
+ prev_pool = ldb_msg_find_attr_as_uint64(res->msgs[0], "rIDPreviousAllocationPool", 0);
+ prev_pool_lo = prev_pool & 0xFFFFFFFF;
+ prev_pool_hi = prev_pool >> 32;
+ next_rid = ldb_msg_find_attr_as_uint(res->msgs[0], "rIDNextRid", 0);
+
+ if (alloc_pool != prev_pool) {
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+ }
+
+ if (next_rid < (prev_pool_hi + prev_pool_lo)/2) {
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+ }
+
+ *exhausted = true;
+ *_alloc_pool = alloc_pool;
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+}
+
+
+/*
+ see if we are low on RIDs in the RID Set rIDAllocationPool. If we
+ are, then schedule a replication call with DRSUAPI_EXOP_FSMO_RID_ALLOC
+ to the RID Manager
+ */
+WERROR dreplsrv_ridalloc_check_rid_pool(struct dreplsrv_service *service)
+{
+ struct ldb_dn *rid_manager_dn, *fsmo_role_dn;
+ TALLOC_CTX *tmp_ctx = talloc_new(service);
+ struct ldb_context *ldb = service->samdb;
+ bool exhausted;
+ WERROR werr;
+ int ret;
+ uint64_t alloc_pool;
+ bool is_us;
+
+ if (service->am_rodc) {
+ talloc_free(tmp_ctx);
+ return WERR_OK;
+ }
+
+ if (service->rid_alloc_in_progress) {
+ talloc_free(tmp_ctx);
+ return WERR_OK;
+ }
+
+ /*
+ steps:
+ - find who the RID Manager is
+ - if we are the RID Manager then nothing to do
+ - find our RID Set object
+ - load rIDAllocationPool and rIDPreviousAllocationPool
+ - if rIDAllocationPool != rIDPreviousAllocationPool then
+ nothing to do
+ - schedule a getncchanges with DRSUAPI_EXOP_FSMO_RID_ALLOC
+ to the RID Manager
+ */
+
+ /* work out who is the RID Manager */
+ ret = samdb_rid_manager_dn(ldb, tmp_ctx, &rid_manager_dn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0, (__location__ ": Failed to find RID Manager object - %s\n", ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ /* find the DN of the RID Manager */
+ ret = samdb_reference_dn(ldb, tmp_ctx, rid_manager_dn, "fSMORoleOwner", &fsmo_role_dn);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find fSMORoleOwner in RID Manager object - %s\n",
+ ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ ret = samdb_dn_is_our_ntdsa(ldb, fsmo_role_dn, &is_us);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find determine if %s is our ntdsDsa object - %s\n",
+ ldb_dn_get_linearized(fsmo_role_dn), ldb_errstring(ldb)));
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ if (is_us) {
+ /* we are the RID Manager - no need to do a
+ DRSUAPI_EXOP_FSMO_RID_ALLOC */
+ talloc_free(tmp_ctx);
+ return WERR_OK;
+ }
+
+ ret = drepl_ridalloc_pool_exhausted(ldb, &exhausted, &alloc_pool);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ if (!exhausted) {
+ /* don't need a new pool */
+ talloc_free(tmp_ctx);
+ return WERR_OK;
+ }
+
+ DEBUG(2,(__location__ ": Requesting more RIDs from RID Manager\n"));
+
+ werr = drepl_request_new_rid_pool(service, rid_manager_dn, fsmo_role_dn, alloc_pool);
+ talloc_free(tmp_ctx);
+ return werr;
+}
+
+/* called by the samldb ldb module to tell us to ask for a new RID
+ pool */
+void dreplsrv_allocate_rid(struct imessaging_context *msg,
+ void *private_data,
+ uint32_t msg_type,
+ struct server_id server_id,
+ size_t num_fds,
+ int *fds,
+ DATA_BLOB *data)
+{
+ struct dreplsrv_service *service = talloc_get_type(private_data, struct dreplsrv_service);
+ if (num_fds != 0) {
+ DBG_WARNING("Received %zu fds, ignoring message\n", num_fds);
+ return;
+ }
+ dreplsrv_ridalloc_check_rid_pool(service);
+}
diff --git a/source4/dsdb/repl/drepl_secret.c b/source4/dsdb/repl/drepl_secret.c
new file mode 100644
index 0000000..47a8ca9
--- /dev/null
+++ b/source4/dsdb/repl/drepl_secret.c
@@ -0,0 +1,146 @@
+/*
+ Unix SMB/CIFS Implementation.
+
+ DSDB replication service - repl secret handling
+
+ Copyright (C) Andrew Tridgell 2010
+ Copyright (C) Andrew Bartlett 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 "samba/service.h"
+#include "dsdb/repl/drepl_service.h"
+#include "param/param.h"
+
+#undef DBGC_CLASS
+#define DBGC_CLASS DBGC_DRS_REPL
+
+struct repl_secret_state {
+ const char *user_dn;
+};
+
+/*
+ called when a repl secret has completed
+ */
+static void drepl_repl_secret_callback(struct dreplsrv_service *service,
+ WERROR werr,
+ enum drsuapi_DsExtendedError ext_err,
+ void *cb_data)
+{
+ struct repl_secret_state *state = talloc_get_type_abort(cb_data, struct repl_secret_state);
+ if (!W_ERROR_IS_OK(werr)) {
+ if (W_ERROR_EQUAL(werr, WERR_DS_DRA_SECRETS_DENIED)) {
+ DEBUG(3,(__location__ ": repl secret disallowed for user "
+ "%s - not in allowed replication group\n",
+ state->user_dn));
+ } else {
+ DEBUG(3,(__location__ ": repl secret failed for user %s - %s: extended_ret[0x%X]\n",
+ state->user_dn, win_errstr(werr), ext_err));
+ }
+ } else {
+ DEBUG(3,(__location__ ": repl secret completed OK for '%s'\n", state->user_dn));
+ }
+ talloc_free(state);
+}
+
+
+/**
+ * Called when the auth code wants us to try and replicate
+ * a users secrets
+ */
+void drepl_repl_secret(struct dreplsrv_service *service,
+ const char *user_dn)
+{
+ WERROR werr;
+ struct ldb_dn *nc_dn, *nc_root, *source_dsa_dn;
+ struct dreplsrv_partition *p;
+ struct GUID *source_dsa_guid;
+ struct repl_secret_state *state;
+ int ret;
+
+ state = talloc_zero(service, struct repl_secret_state);
+ if (state == NULL) {
+ /* nothing to do, no return value */
+ return;
+ }
+
+ /* keep a copy for logging in the callback */
+ state->user_dn = talloc_strdup(state, user_dn);
+
+ nc_dn = ldb_dn_new(state, service->samdb, user_dn);
+ if (!ldb_dn_validate(nc_dn)) {
+ DEBUG(0,(__location__ ": Failed to parse user_dn '%s'\n", user_dn));
+ talloc_free(state);
+ return;
+ }
+
+ /* work out which partition this is in */
+ ret = dsdb_find_nc_root(service->samdb, state, nc_dn, &nc_root);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to find nc_root for user_dn '%s'\n", user_dn));
+ talloc_free(state);
+ return;
+ }
+
+ /* find the partition in our list */
+ for (p=service->partitions; p; p=p->next) {
+ if (ldb_dn_compare(p->dn, nc_root) == 0) {
+ break;
+ }
+ }
+ if (p == NULL) {
+ DEBUG(0,(__location__ ": Failed to find partition for nc_root '%s'\n", ldb_dn_get_linearized(nc_root)));
+ talloc_free(state);
+ return;
+ }
+
+ if (p->sources == NULL) {
+ DEBUG(0,(__location__ ": No sources for nc_root '%s' for user_dn '%s'\n",
+ ldb_dn_get_linearized(nc_root), user_dn));
+ talloc_free(state);
+ return;
+ }
+
+ /* use the first source, for no particularly good reason */
+ source_dsa_guid = &p->sources->repsFrom1->source_dsa_obj_guid;
+
+ source_dsa_dn = ldb_dn_new(state, service->samdb,
+ talloc_asprintf(state, "<GUID=%s>",
+ GUID_string(state, source_dsa_guid)));
+ if (!ldb_dn_validate(source_dsa_dn)) {
+ DEBUG(0,(__location__ ": Invalid source DSA GUID '%s' for user_dn '%s'\n",
+ GUID_string(state, source_dsa_guid), user_dn));
+ talloc_free(state);
+ return;
+ }
+
+ werr = drepl_request_extended_op(service,
+ nc_dn,
+ source_dsa_dn,
+ DRSUAPI_EXOP_REPL_SECRET,
+ 0,
+ p->sources->repsFrom1->highwatermark.highest_usn,
+ drepl_repl_secret_callback, state);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(2,(__location__ ": Failed to setup secret replication for user_dn '%s'\n", user_dn));
+ talloc_free(state);
+ return;
+ }
+ DEBUG(3,(__location__ ": started secret replication for %s\n", user_dn));
+}
diff --git a/source4/dsdb/repl/drepl_service.c b/source4/dsdb/repl/drepl_service.c
new file mode 100644
index 0000000..02ece26
--- /dev/null
+++ b/source4/dsdb/repl/drepl_service.c
@@ -0,0 +1,545 @@
+/*
+ Unix SMB/CIFS Implementation.
+ DSDB replication service
+
+ Copyright (C) Stefan Metzmacher 2007
+ Copyright (C) Kamen Mazdrashki <kamenim@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/samdb/samdb.h"
+#include "auth/auth.h"
+#include "samba/service.h"
+#include "lib/events/events.h"
+#include "dsdb/repl/drepl_service.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.h"
+#include "librpc/gen_ndr/ndr_misc.h"
+#include "librpc/gen_ndr/ndr_drsuapi.h"
+#include "librpc/gen_ndr/ndr_drsblobs.h"
+#include "librpc/gen_ndr/ndr_irpc.h"
+#include "param/param.h"
+#include "libds/common/roles.h"
+
+#undef DBGC_CLASS
+#define DBGC_CLASS DBGC_DRS_REPL
+
+/**
+ * Call-back data for _drepl_replica_sync_done_cb()
+ */
+struct drepl_replica_sync_cb_data {
+ struct irpc_message *msg;
+ struct drsuapi_DsReplicaSync *r;
+
+ /* number of ops left to be completed */
+ int ops_count;
+
+ /* last failure error code */
+ WERROR werr_last_failure;
+};
+
+
+static WERROR dreplsrv_init_creds(struct dreplsrv_service *service)
+{
+ service->system_session_info = system_session(service->task->lp_ctx);
+ if (service->system_session_info == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dreplsrv_connect_samdb(struct dreplsrv_service *service, struct loadparm_context *lp_ctx)
+{
+ const struct GUID *ntds_guid;
+ struct drsuapi_DsBindInfo28 *bind_info28;
+
+ service->samdb = samdb_connect(service,
+ service->task->event_ctx,
+ lp_ctx,
+ service->system_session_info,
+ NULL,
+ 0);
+ if (!service->samdb) {
+ return WERR_DS_UNAVAILABLE;
+ }
+
+ ntds_guid = samdb_ntds_objectGUID(service->samdb);
+ if (!ntds_guid) {
+ return WERR_DS_UNAVAILABLE;
+ }
+ service->ntds_guid = *ntds_guid;
+
+ if (samdb_rodc(service->samdb, &service->am_rodc) != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed to determine RODC status\n"));
+ return WERR_DS_UNAVAILABLE;
+ }
+
+ bind_info28 = &service->bind_info28;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_BASE;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_ASYNC_REPLICATION;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_REMOVEAPI;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_MOVEREQ_V2;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GETCHG_COMPRESS;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V1;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_RESTORE_USN_OPTIMIZATION;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_KCC_EXECUTE;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_ADDENTRY_V2;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_LINKED_VALUE_REPLICATION;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V2;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_INSTANCE_TYPE_NOT_REQ_ON_MOD;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_CRYPTO_BIND;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GET_REPL_INFO;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_STRONG_ENCRYPTION;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V01;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_TRANSITIVE_MEMBERSHIP;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_ADD_SID_HISTORY;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_POST_BETA3;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V5;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GET_MEMBERSHIPS2;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V6;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_NONDOMAIN_NCS;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V5;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V6;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_ADDENTRYREPLY_V3;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V7;
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_VERIFY_OBJECT;
+#if 0 /* we don't support XPRESS compression yet */
+ bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_XPRESS_COMPRESS;
+#endif
+ /* TODO: fill in site_guid */
+ bind_info28->site_guid = GUID_zero();
+ /* TODO: find out how this is really triggered! */
+ bind_info28->pid = 0;
+ bind_info28->repl_epoch = 0;
+
+ return WERR_OK;
+}
+
+
+/**
+ * Callback for dreplsrv_out_operation operation completion.
+ *
+ * We just need to complete a waiting IRPC message here.
+ * In case pull operation has failed,
+ * caller of this callback will dump
+ * failure information.
+ *
+ * NOTE: cb_data is allocated in IRPC msg's context
+ * and will be freed during irpc_send_reply() call.
+ */
+static void _drepl_replica_sync_done_cb(struct dreplsrv_service *service,
+ WERROR werr,
+ enum drsuapi_DsExtendedError ext_err,
+ void *cb_data)
+{
+ struct drepl_replica_sync_cb_data *data = talloc_get_type(cb_data,
+ struct drepl_replica_sync_cb_data);
+ struct irpc_message *msg = data->msg;
+ struct drsuapi_DsReplicaSync *r = data->r;
+
+ /* store last bad result */
+ if (!W_ERROR_IS_OK(werr)) {
+ data->werr_last_failure = werr;
+ }
+
+ /* decrement pending ops count */
+ data->ops_count--;
+
+ if (data->ops_count == 0) {
+ /* Return result to client */
+ r->out.result = data->werr_last_failure;
+
+ /* complete IRPC message */
+ irpc_send_reply(msg, NT_STATUS_OK);
+ }
+}
+
+/**
+ * Helper to schedule a replication operation with a source DSA.
+ * If 'data' is valid pointer, then a callback
+ * for the operation is passed and 'data->msg' is
+ * marked as 'deferred' - defer_reply = true
+ */
+static WERROR _drepl_schedule_replication(struct dreplsrv_service *service,
+ struct dreplsrv_partition_source_dsa *dsa,
+ struct drsuapi_DsReplicaObjectIdentifier *nc,
+ uint32_t rep_options,
+ struct drepl_replica_sync_cb_data *data,
+ TALLOC_CTX *mem_ctx)
+{
+ WERROR werr;
+ dreplsrv_extended_callback_t fn_callback = NULL;
+
+ if (data) {
+ fn_callback = _drepl_replica_sync_done_cb;
+ }
+
+ /* schedule replication item */
+ werr = dreplsrv_schedule_partition_pull_source(service, dsa, rep_options,
+ DRSUAPI_EXOP_NONE, 0,
+ fn_callback, data);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0,("%s: failed setup of sync of partition (%s, %s, %s) - %s\n",
+ __FUNCTION__,
+ GUID_string(mem_ctx, &nc->guid),
+ nc->dn,
+ dsa->repsFrom1->other_info->dns_name,
+ win_errstr(werr)));
+ return werr;
+ }
+ /* log we've scheduled a replication item */
+ DEBUG(3,("%s: forcing sync of partition (%s, %s, %s)\n",
+ __FUNCTION__,
+ GUID_string(mem_ctx, &nc->guid),
+ nc->dn,
+ dsa->repsFrom1->other_info->dns_name));
+
+ /* mark IRPC message as deferred if necessary */
+ if (data) {
+ data->ops_count++;
+ data->msg->defer_reply = true;
+ }
+
+ return WERR_OK;
+}
+
+/*
+ DsReplicaSync messages from the DRSUAPI server are forwarded here
+ */
+static NTSTATUS drepl_replica_sync(struct irpc_message *msg,
+ struct drsuapi_DsReplicaSync *r)
+{
+ WERROR werr;
+ struct dreplsrv_partition *p;
+ struct drepl_replica_sync_cb_data *cb_data;
+ struct dreplsrv_partition_source_dsa *dsa;
+ struct drsuapi_DsReplicaSyncRequest1 *req1;
+ struct drsuapi_DsReplicaObjectIdentifier *nc;
+ struct dreplsrv_service *service = talloc_get_type(msg->private_data,
+ struct dreplsrv_service);
+
+#define REPLICA_SYNC_FAIL(_msg, _werr) do {\
+ if (!W_ERROR_IS_OK(_werr)) { \
+ DEBUG(0,(__location__ ": Failure - %s. werr = %s\n", \
+ _msg, win_errstr(_werr))); \
+ NDR_PRINT_IN_DEBUG(drsuapi_DsReplicaSync, r); \
+ } \
+ r->out.result = _werr; \
+ goto done;\
+ } while(0)
+
+
+ if (r->in.level != 1) {
+ REPLICA_SYNC_FAIL("Unsupported level",
+ WERR_DS_DRA_INVALID_PARAMETER);
+ }
+
+ req1 = &r->in.req->req1;
+ nc = req1->naming_context;
+
+ /* Check input parameters */
+ if (!nc) {
+ REPLICA_SYNC_FAIL("Invalid Naming Context",
+ WERR_DS_DRA_INVALID_PARAMETER);
+ }
+
+ /* Find Naming context to be synchronized */
+ werr = dreplsrv_partition_find_for_nc(service,
+ &nc->guid, &nc->sid, nc->dn,
+ &p);
+ if (!W_ERROR_IS_OK(werr)) {
+ REPLICA_SYNC_FAIL("Failed to find requested Naming Context",
+ werr);
+ }
+
+ /* should we process it asynchronously? */
+ if (req1->options & DRSUAPI_DRS_ASYNC_OP) {
+ cb_data = NULL;
+ } else {
+ cb_data = talloc_zero(msg, struct drepl_replica_sync_cb_data);
+ if (!cb_data) {
+ REPLICA_SYNC_FAIL("Not enough memory",
+ WERR_DS_DRA_INTERNAL_ERROR);
+ }
+
+ cb_data->msg = msg;
+ cb_data->r = r;
+ cb_data->werr_last_failure = WERR_OK;
+ }
+
+ /* collect source DSAs to sync with */
+ if (req1->options & DRSUAPI_DRS_SYNC_ALL) {
+ for (dsa = p->sources; dsa; dsa = dsa->next) {
+ /* schedule replication item */
+ werr = _drepl_schedule_replication(service, dsa, nc,
+ req1->options, cb_data, msg);
+ if (!W_ERROR_IS_OK(werr)) {
+ REPLICA_SYNC_FAIL("_drepl_schedule_replication() failed",
+ werr);
+ }
+ }
+ } else {
+ if (req1->options & DRSUAPI_DRS_SYNC_BYNAME) {
+ /* client should pass at least valid string */
+ if (!req1->source_dsa_dns) {
+ REPLICA_SYNC_FAIL("'source_dsa_dns' is not valid",
+ WERR_DS_DRA_INVALID_PARAMETER);
+ }
+
+ werr = dreplsrv_partition_source_dsa_by_dns(p,
+ req1->source_dsa_dns,
+ &dsa);
+ } else {
+ /* client should pass at least some GUID */
+ if (GUID_all_zero(&req1->source_dsa_guid)) {
+ REPLICA_SYNC_FAIL("'source_dsa_guid' is not valid",
+ WERR_DS_DRA_INVALID_PARAMETER);
+ }
+
+ werr = dreplsrv_partition_source_dsa_by_guid(p,
+ &req1->source_dsa_guid,
+ &dsa);
+ if (W_ERROR_EQUAL(werr, WERR_DS_DRA_NO_REPLICA)) {
+ /* we don't have this source setup as
+ a replication partner. Create a
+ temporary dsa structure for this
+ replication */
+ werr = dreplsrv_partition_source_dsa_temporary(p,
+ msg,
+ &req1->source_dsa_guid,
+ &dsa);
+ }
+ }
+ if (!W_ERROR_IS_OK(werr)) {
+ REPLICA_SYNC_FAIL("Failed to locate source DSA for given NC",
+ werr);
+ }
+
+ /* schedule replication item */
+ werr = _drepl_schedule_replication(service, dsa, nc,
+ req1->options, cb_data, msg);
+ if (!W_ERROR_IS_OK(werr)) {
+ REPLICA_SYNC_FAIL("_drepl_schedule_replication() failed",
+ werr);
+ }
+ }
+
+ /* if we got here, everything is OK */
+ r->out.result = WERR_OK;
+
+ /*
+ * schedule replication event to force
+ * replication as soon as possible
+ */
+ dreplsrv_pendingops_schedule_pull_now(service);
+
+done:
+ return NT_STATUS_OK;
+}
+
+/**
+ * Called when drplsrv should refresh its state.
+ * For example, when KCC change topology, dreplsrv
+ * should update its cache
+ *
+ * @param partition_dn If not empty/NULL, partition to update
+ */
+static NTSTATUS dreplsrv_refresh(struct irpc_message *msg,
+ struct dreplsrv_refresh *r)
+{
+ struct dreplsrv_service *s = talloc_get_type(msg->private_data,
+ struct dreplsrv_service);
+
+ r->out.result = dreplsrv_refresh_partitions(s);
+
+ return NT_STATUS_OK;
+}
+
+/**
+ * Called when the auth code wants us to try and replicate
+ * a users secrets
+ */
+static NTSTATUS drepl_trigger_repl_secret(struct irpc_message *msg,
+ struct drepl_trigger_repl_secret *r)
+{
+ struct dreplsrv_service *service = talloc_get_type(msg->private_data,
+ struct dreplsrv_service);
+
+
+ drepl_repl_secret(service, r->in.user_dn);
+
+ /* we are not going to be sending a reply to this request */
+ msg->no_reply = true;
+
+ return NT_STATUS_OK;
+}
+
+
+/*
+ DsReplicaAdd messages from the DRSUAPI server are forwarded here
+ */
+static NTSTATUS dreplsrv_replica_add(struct irpc_message *msg,
+ struct drsuapi_DsReplicaAdd *r)
+{
+ struct dreplsrv_service *service = talloc_get_type(msg->private_data,
+ struct dreplsrv_service);
+ return drepl_replica_add(service, r);
+}
+
+/*
+ DsReplicaDel messages from the DRSUAPI server are forwarded here
+ */
+static NTSTATUS dreplsrv_replica_del(struct irpc_message *msg,
+ struct drsuapi_DsReplicaDel *r)
+{
+ struct dreplsrv_service *service = talloc_get_type(msg->private_data,
+ struct dreplsrv_service);
+ return drepl_replica_del(service, r);
+}
+
+/*
+ DsReplicaMod messages from the DRSUAPI server are forwarded here
+ */
+static NTSTATUS dreplsrv_replica_mod(struct irpc_message *msg,
+ struct drsuapi_DsReplicaMod *r)
+{
+ struct dreplsrv_service *service = talloc_get_type(msg->private_data,
+ struct dreplsrv_service);
+ return drepl_replica_mod(service, r);
+}
+
+
+/*
+ startup the dsdb replicator service task
+*/
+static NTSTATUS dreplsrv_task_init(struct task_server *task)
+{
+ WERROR status;
+ struct dreplsrv_service *service;
+ uint32_t periodic_startup_interval;
+
+ switch (lpcfg_server_role(task->lp_ctx)) {
+ case ROLE_STANDALONE:
+ task_server_terminate(task, "dreplsrv: no DSDB replication required in standalone configuration",
+ false);
+ return NT_STATUS_INVALID_DOMAIN_ROLE;
+ case ROLE_DOMAIN_MEMBER:
+ task_server_terminate(task, "dreplsrv: no DSDB replication required in domain member configuration",
+ false);
+ return NT_STATUS_INVALID_DOMAIN_ROLE;
+ case ROLE_ACTIVE_DIRECTORY_DC:
+ /* Yes, we want DSDB replication */
+ break;
+ }
+
+ task_server_set_title(task, "task[dreplsrv]");
+
+ service = talloc_zero(task, struct dreplsrv_service);
+ if (!service) {
+ task_server_terminate(task, "dreplsrv_task_init: out of memory", true);
+ return NT_STATUS_NO_MEMORY;
+ }
+ service->task = task;
+ service->startup_time = timeval_current();
+ task->private_data = service;
+
+ status = dreplsrv_init_creds(service);
+ if (!W_ERROR_IS_OK(status)) {
+ task_server_terminate(task, talloc_asprintf(task,
+ "dreplsrv: Failed to obtain server credentials: %s\n",
+ win_errstr(status)), true);
+ return werror_to_ntstatus(status);
+ }
+
+ status = dreplsrv_connect_samdb(service, task->lp_ctx);
+ if (!W_ERROR_IS_OK(status)) {
+ task_server_terminate(task, talloc_asprintf(task,
+ "dreplsrv: Failed to connect to local samdb: %s\n",
+ win_errstr(status)), true);
+ return werror_to_ntstatus(status);
+ }
+
+ status = dreplsrv_load_partitions(service);
+ if (!W_ERROR_IS_OK(status)) {
+ task_server_terminate(task, talloc_asprintf(task,
+ "dreplsrv: Failed to load partitions: %s\n",
+ win_errstr(status)), true);
+ return werror_to_ntstatus(status);
+ }
+
+ periodic_startup_interval = lpcfg_parm_int(task->lp_ctx, NULL, "dreplsrv", "periodic_startup_interval", 15); /* in seconds */
+ service->periodic.interval = lpcfg_parm_int(task->lp_ctx, NULL, "dreplsrv", "periodic_interval", 300); /* in seconds */
+
+ status = dreplsrv_periodic_schedule(service, periodic_startup_interval);
+ if (!W_ERROR_IS_OK(status)) {
+ task_server_terminate(task, talloc_asprintf(task,
+ "dreplsrv: Failed to periodic schedule: %s\n",
+ win_errstr(status)), true);
+ return werror_to_ntstatus(status);
+ }
+
+ service->pending.im = tevent_create_immediate(service);
+ if (service->pending.im == NULL) {
+ task_server_terminate(task,
+ "dreplsrv: Failed to create immediate "
+ "task for future DsReplicaSync\n",
+ true);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ /* if we are a RODC then we do not send DSReplicaSync*/
+ if (!service->am_rodc) {
+ service->notify.interval = lpcfg_parm_int(task->lp_ctx, NULL, "dreplsrv",
+ "notify_interval", 5); /* in seconds */
+ status = dreplsrv_notify_schedule(service, service->notify.interval);
+ if (!W_ERROR_IS_OK(status)) {
+ task_server_terminate(task, talloc_asprintf(task,
+ "dreplsrv: Failed to setup notify schedule: %s\n",
+ win_errstr(status)), true);
+ return werror_to_ntstatus(status);
+ }
+ }
+
+ irpc_add_name(task->msg_ctx, "dreplsrv");
+
+ IRPC_REGISTER(task->msg_ctx, irpc, DREPLSRV_REFRESH, dreplsrv_refresh, service);
+ IRPC_REGISTER(task->msg_ctx, drsuapi, DRSUAPI_DSREPLICASYNC, drepl_replica_sync, service);
+ IRPC_REGISTER(task->msg_ctx, drsuapi, DRSUAPI_DSREPLICAADD, dreplsrv_replica_add, service);
+ IRPC_REGISTER(task->msg_ctx, drsuapi, DRSUAPI_DSREPLICADEL, dreplsrv_replica_del, service);
+ IRPC_REGISTER(task->msg_ctx, drsuapi, DRSUAPI_DSREPLICAMOD, dreplsrv_replica_mod, service);
+ IRPC_REGISTER(task->msg_ctx, irpc, DREPL_TAKEFSMOROLE, drepl_take_FSMO_role, service);
+ IRPC_REGISTER(task->msg_ctx, irpc, DREPL_TRIGGER_REPL_SECRET, drepl_trigger_repl_secret, service);
+ imessaging_register(task->msg_ctx, service, MSG_DREPL_ALLOCATE_RID, dreplsrv_allocate_rid);
+
+ return NT_STATUS_OK;
+}
+
+/*
+ register ourselves as a available server
+*/
+NTSTATUS server_service_drepl_init(TALLOC_CTX *ctx)
+{
+ static const struct service_details details = {
+ .inhibit_fork_on_accept = true,
+ .inhibit_pre_fork = true,
+ .task_init = dreplsrv_task_init,
+ .post_fork = NULL,
+ };
+ return register_server_service(ctx, "drepl", &details);
+}
diff --git a/source4/dsdb/repl/drepl_service.h b/source4/dsdb/repl/drepl_service.h
new file mode 100644
index 0000000..4451df9
--- /dev/null
+++ b/source4/dsdb/repl/drepl_service.h
@@ -0,0 +1,251 @@
+/*
+ Unix SMB/CIFS Implementation.
+ DSDB replication service
+
+ Copyright (C) Stefan Metzmacher 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/>.
+
+*/
+
+#ifndef _DSDB_REPL_DREPL_SERVICE_H_
+#define _DSDB_REPL_DREPL_SERVICE_H_
+
+#include "librpc/gen_ndr/ndr_drsuapi_c.h"
+
+struct dreplsrv_service;
+struct dreplsrv_partition;
+
+struct dreplsrv_drsuapi_connection {
+ /*
+ * this pipe pointer is also the indicator
+ * for a valid connection
+ */
+ struct dcerpc_pipe *pipe;
+ struct dcerpc_binding_handle *drsuapi_handle;
+
+ DATA_BLOB gensec_skey;
+ struct drsuapi_DsBindInfo28 remote_info28;
+ struct policy_handle bind_handle;
+};
+
+struct dreplsrv_out_connection {
+ struct dreplsrv_out_connection *prev, *next;
+
+ struct dreplsrv_service *service;
+
+ /*
+ * the binding for the outgoing connection
+ */
+ struct dcerpc_binding *binding;
+
+ /* the out going connection to the source dsa */
+ struct dreplsrv_drsuapi_connection *drsuapi;
+};
+
+struct dreplsrv_partition_source_dsa {
+ struct dreplsrv_partition_source_dsa *prev, *next;
+
+ struct dreplsrv_partition *partition;
+
+ /*
+ * the cached repsFrom value for this source dsa
+ *
+ * it needs to be updated after each DsGetNCChanges() call
+ * to the source dsa
+ *
+ * repsFrom1 == &_repsFromBlob.ctr.ctr1
+ */
+ struct repsFromToBlob _repsFromBlob;
+ struct repsFromTo1 *repsFrom1;
+
+ /* the last uSN when we sent a notify */
+ uint64_t notify_uSN;
+
+ /* the reference to the source_dsa and its outgoing connection */
+ struct dreplsrv_out_connection *conn;
+};
+
+struct dreplsrv_partition {
+ struct dreplsrv_partition *prev, *next;
+
+ struct dreplsrv_service *service;
+
+ /* the dn of the partition */
+ struct ldb_dn *dn;
+ struct drsuapi_DsReplicaObjectIdentifier nc;
+
+ /*
+ * up-to-date vector needs to be updated before and after each DsGetNCChanges() call
+ *
+ * - before: we need to use our own invocationId together with our highestCommittedUSN
+ * - after: we need to merge in the remote uptodatevector, to avoid reading it again
+ */
+ struct replUpToDateVectorCtr2 uptodatevector;
+ struct drsuapi_DsReplicaCursorCtrEx uptodatevector_ex;
+
+ /*
+ * a linked list of all source dsa's we replicate from
+ */
+ struct dreplsrv_partition_source_dsa *sources;
+
+ /*
+ * a linked list of all source dsa's we will notify,
+ * that are not also in sources
+ */
+ struct dreplsrv_partition_source_dsa *notifies;
+
+ bool partial_replica;
+ bool rodc_replica;
+};
+
+typedef void (*dreplsrv_extended_callback_t)(struct dreplsrv_service *,
+ WERROR,
+ enum drsuapi_DsExtendedError,
+ void *cb_data);
+
+struct dreplsrv_out_operation {
+ struct dreplsrv_out_operation *prev, *next;
+ time_t schedule_time;
+
+ struct dreplsrv_service *service;
+
+ struct dreplsrv_partition_source_dsa *source_dsa;
+
+ /* replication options - currently used by DsReplicaSync */
+ uint32_t options;
+ enum drsuapi_DsExtendedOperation extended_op;
+ uint64_t fsmo_info;
+ enum drsuapi_DsExtendedError extended_ret;
+ dreplsrv_extended_callback_t callback;
+ void *cb_data;
+ /* more replication flags - used by DsReplicaSync GET_TGT */
+ uint32_t more_flags;
+};
+
+struct dreplsrv_notify_operation {
+ struct dreplsrv_notify_operation *prev, *next;
+ time_t schedule_time;
+
+ struct dreplsrv_service *service;
+ uint64_t uSN;
+
+ struct dreplsrv_partition_source_dsa *source_dsa;
+ bool is_urgent;
+ uint32_t replica_flags;
+};
+
+struct dreplsrv_service {
+ /* the whole drepl service is in one task */
+ struct task_server *task;
+
+ /* the time the service was started */
+ struct timeval startup_time;
+
+ /*
+ * system session info
+ * with machine account credentials
+ */
+ struct auth_session_info *system_session_info;
+
+ /*
+ * a connection to the local samdb
+ */
+ struct ldb_context *samdb;
+
+ /* the guid of our NTDS Settings object, which never changes! */
+ struct GUID ntds_guid;
+ /*
+ * the struct holds the values used for outgoing DsBind() calls,
+ * so that we need to set them up only once
+ */
+ struct drsuapi_DsBindInfo28 bind_info28;
+
+ /* some stuff for periodic processing */
+ struct {
+ /*
+ * the interval between to periodic runs
+ */
+ uint32_t interval;
+
+ /*
+ * the timestamp for the next event,
+ * this is the timestamp passed to event_add_timed()
+ */
+ struct timeval next_event;
+
+ /* here we have a reference to the timed event the schedules the periodic stuff */
+ struct tevent_timer *te;
+ } periodic;
+
+ /* some stuff for running only the incoming notify ops */
+ struct {
+ /*
+ * here we have a reference to the immediate event that was
+ * scheduled from the DsReplicaSync
+ */
+ struct tevent_immediate *im;
+ } pending;
+
+ /* some stuff for notify processing */
+ struct {
+ /*
+ * the interval between notify runs
+ */
+ uint32_t interval;
+
+ /*
+ * the timestamp for the next event,
+ * this is the timestamp passed to event_add_timed()
+ */
+ struct timeval next_event;
+
+ /* here we have a reference to the timed event the schedules the notifies */
+ struct tevent_timer *te;
+ } notify;
+
+ /*
+ * the list of partitions we need to replicate
+ */
+ struct dreplsrv_partition *partitions;
+
+ /*
+ * the list of cached connections
+ */
+ struct dreplsrv_out_connection *connections;
+
+ struct {
+ /* the pointer to the current active operation */
+ struct dreplsrv_out_operation *current;
+
+ /* the list of pending operations */
+ struct dreplsrv_out_operation *pending;
+
+ /* the list of pending notify operations */
+ struct dreplsrv_notify_operation *notifies;
+
+ /* an active notify operation */
+ struct dreplsrv_notify_operation *n_current;
+ } ops;
+
+ bool rid_alloc_in_progress;
+
+ bool am_rodc;
+};
+
+#include "lib/messaging/irpc.h"
+#include "dsdb/repl/drepl_out_helpers.h"
+#include "dsdb/repl/drepl_service_proto.h"
+
+#endif /* _DSDB_REPL_DREPL_SERVICE_H_ */
diff --git a/source4/dsdb/repl/replicated_objects.c b/source4/dsdb/repl/replicated_objects.c
new file mode 100644
index 0000000..01adae2
--- /dev/null
+++ b/source4/dsdb/repl/replicated_objects.c
@@ -0,0 +1,1283 @@
+/*
+ Unix SMB/CIFS Implementation.
+ Helper functions for applying replicated objects
+
+ 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 "dsdb/samdb/samdb.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.h"
+#include "librpc/gen_ndr/ndr_misc.h"
+#include "librpc/gen_ndr/ndr_drsuapi.h"
+#include "librpc/gen_ndr/ndr_drsblobs.h"
+#include "../libcli/drsuapi/drsuapi.h"
+#include "libcli/auth/libcli_auth.h"
+#include "param/param.h"
+
+#undef DBGC_CLASS
+#define DBGC_CLASS DBGC_DRS_REPL
+
+static WERROR dsdb_repl_merge_working_schema(struct ldb_context *ldb,
+ struct dsdb_schema *dest_schema,
+ const struct dsdb_schema *ref_schema)
+{
+ const struct dsdb_class *cur_class = NULL;
+ const struct dsdb_attribute *cur_attr = NULL;
+ int ret;
+
+ for (cur_class = ref_schema->classes;
+ cur_class;
+ cur_class = cur_class->next)
+ {
+ const struct dsdb_class *tmp1;
+ struct dsdb_class *tmp2;
+
+ tmp1 = dsdb_class_by_governsID_id(dest_schema,
+ cur_class->governsID_id);
+ if (tmp1 != NULL) {
+ continue;
+ }
+
+ /*
+ * Do a shallow copy so that original next and prev are
+ * not modified, we don't need to do a deep copy
+ * as the rest won't be modified and this is for
+ * a short lived object.
+ */
+ tmp2 = talloc(dest_schema, struct dsdb_class);
+ if (tmp2 == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ *tmp2 = *cur_class;
+ DLIST_ADD(dest_schema->classes, tmp2);
+ }
+
+ for (cur_attr = ref_schema->attributes;
+ cur_attr;
+ cur_attr = cur_attr->next)
+ {
+ const struct dsdb_attribute *tmp1;
+ struct dsdb_attribute *tmp2;
+
+ tmp1 = dsdb_attribute_by_attributeID_id(dest_schema,
+ cur_attr->attributeID_id);
+ if (tmp1 != NULL) {
+ continue;
+ }
+
+ /*
+ * Do a shallow copy so that original next and prev are
+ * not modified, we don't need to do a deep copy
+ * as the rest won't be modified and this is for
+ * a short lived object.
+ */
+ tmp2 = talloc(dest_schema, struct dsdb_attribute);
+ if (tmp2 == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ *tmp2 = *cur_attr;
+ DLIST_ADD(dest_schema->attributes, tmp2);
+ }
+
+ ret = dsdb_setup_sorted_accessors(ldb, dest_schema);
+ if (LDB_SUCCESS != ret) {
+ DEBUG(0,("Failed to add new attribute to reference schema!\n"));
+ return WERR_INTERNAL_ERROR;
+ }
+
+ return WERR_OK;
+}
+
+WERROR dsdb_repl_resolve_working_schema(struct ldb_context *ldb,
+ struct dsdb_schema_prefixmap *pfm_remote,
+ uint32_t cycle_before_switching,
+ struct dsdb_schema *initial_schema,
+ struct dsdb_schema *resulting_schema,
+ uint32_t object_count,
+ const struct drsuapi_DsReplicaObjectListItemEx *first_object)
+{
+ struct schema_list {
+ struct schema_list *next, *prev;
+ const struct drsuapi_DsReplicaObjectListItemEx *obj;
+ };
+ struct schema_list *schema_list = NULL, *schema_list_item, *schema_list_next_item;
+ WERROR werr;
+ struct dsdb_schema *working_schema;
+ const struct drsuapi_DsReplicaObjectListItemEx *cur;
+ DATA_BLOB empty_key = data_blob_null;
+ int ret, pass_no;
+ uint32_t ignore_attids[] = {
+ DRSUAPI_ATTID_auxiliaryClass,
+ DRSUAPI_ATTID_mayContain,
+ DRSUAPI_ATTID_mustContain,
+ DRSUAPI_ATTID_possSuperiors,
+ DRSUAPI_ATTID_systemPossSuperiors,
+ DRSUAPI_ATTID_INVALID
+ };
+ TALLOC_CTX *frame = talloc_stackframe();
+
+ /* create a list of objects yet to be converted */
+ for (cur = first_object; cur; cur = cur->next_object) {
+ schema_list_item = talloc(frame, struct schema_list);
+ if (schema_list_item == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ schema_list_item->obj = cur;
+ DLIST_ADD_END(schema_list, schema_list_item);
+ }
+
+ /* resolve objects until all are resolved and in local schema */
+ pass_no = 1;
+ working_schema = initial_schema;
+
+ while (schema_list) {
+ uint32_t converted_obj_count = 0;
+ uint32_t failed_obj_count = 0;
+
+ if (resulting_schema != working_schema) {
+ /*
+ * If the selfmade schema is not the schema used to
+ * translate and validate replicated object,
+ * Which means that we are using the bootstrap schema
+ * Then we add attributes and classes that were already
+ * translated to the working schema, the idea is that
+ * we might need to add new attributes and classes
+ * to be able to translate critical replicated objects
+ * and without that we wouldn't be able to translate them
+ */
+ werr = dsdb_repl_merge_working_schema(ldb,
+ working_schema,
+ resulting_schema);
+ if (!W_ERROR_IS_OK(werr)) {
+ talloc_free(frame);
+ return werr;
+ }
+ }
+
+ for (schema_list_item = schema_list;
+ schema_list_item;
+ schema_list_item=schema_list_next_item) {
+ struct dsdb_extended_replicated_object object;
+
+ cur = schema_list_item->obj;
+
+ /*
+ * Save the next item, now we have saved out
+ * the current one, so we can DLIST_REMOVE it
+ * safely
+ */
+ schema_list_next_item = schema_list_item->next;
+
+ /*
+ * Convert the objects into LDB messages using the
+ * schema we have so far. It's ok if we fail to convert
+ * an object. We should convert more objects on next pass.
+ */
+ werr = dsdb_convert_object_ex(ldb, working_schema,
+ NULL,
+ pfm_remote,
+ cur, &empty_key,
+ ignore_attids,
+ 0,
+ schema_list_item, &object);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(4,("debug: Failed to convert schema "
+ "object %s into ldb msg, "
+ "will try during next loop\n",
+ cur->object.identifier->dn));
+
+ failed_obj_count++;
+ } else {
+ /*
+ * Convert the schema from ldb_message format
+ * (OIDs as OID strings) into schema, using
+ * the remote prefixMap
+ *
+ * It's not likely, but possible to get the
+ * same object twice and we should keep
+ * the last instance.
+ */
+ werr = dsdb_schema_set_el_from_ldb_msg_dups(ldb,
+ resulting_schema,
+ object.msg,
+ true);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(4,("debug: failed to convert "
+ "object %s into a schema element, "
+ "will try during next loop: %s\n",
+ ldb_dn_get_linearized(object.msg->dn),
+ win_errstr(werr)));
+ failed_obj_count++;
+ } else {
+ DEBUG(8,("Converted object %s into a schema element\n",
+ ldb_dn_get_linearized(object.msg->dn)));
+ DLIST_REMOVE(schema_list, schema_list_item);
+ TALLOC_FREE(schema_list_item);
+ converted_obj_count++;
+ }
+ }
+ }
+
+ DEBUG(4,("Schema load pass %d: converted %d, %d of %d objects left to be converted.\n",
+ pass_no, converted_obj_count, failed_obj_count, object_count));
+
+ /* check if we converted any objects in this pass */
+ if (converted_obj_count == 0) {
+ DEBUG(0,("Can't continue Schema load: "
+ "didn't manage to convert any objects: "
+ "all %d remaining of %d objects "
+ "failed to convert\n",
+ failed_obj_count, object_count));
+ talloc_free(frame);
+ return WERR_INTERNAL_ERROR;
+ }
+
+ /*
+ * Don't try to load the schema if there is missing object
+ * _and_ we are on the first pass as some critical objects
+ * might be missing.
+ */
+ if (failed_obj_count == 0 || pass_no > cycle_before_switching) {
+ /* prepare for another cycle */
+ working_schema = resulting_schema;
+
+ ret = dsdb_setup_sorted_accessors(ldb, working_schema);
+ if (LDB_SUCCESS != ret) {
+ DEBUG(0,("Failed to create schema-cache indexes!\n"));
+ talloc_free(frame);
+ return WERR_INTERNAL_ERROR;
+ }
+ }
+ pass_no++;
+ }
+
+ talloc_free(frame);
+ return WERR_OK;
+}
+
+/**
+ * Multi-pass working schema creation
+ * Function will:
+ * - shallow copy initial schema supplied
+ * - create a working schema in multiple passes
+ * until all objects are resolved
+ * Working schema is a schema with Attributes, Classes
+ * and indexes, but w/o subClassOf, possibleSupperiors etc.
+ * It is to be used just us cache for converting attribute values.
+ */
+WERROR dsdb_repl_make_working_schema(struct ldb_context *ldb,
+ const struct dsdb_schema *initial_schema,
+ const struct drsuapi_DsReplicaOIDMapping_Ctr *mapping_ctr,
+ uint32_t object_count,
+ const struct drsuapi_DsReplicaObjectListItemEx *first_object,
+ const DATA_BLOB *gensec_skey,
+ TALLOC_CTX *mem_ctx,
+ struct dsdb_schema **_schema_out)
+{
+ WERROR werr;
+ struct dsdb_schema_prefixmap *pfm_remote;
+ uint32_t r;
+ struct dsdb_schema *working_schema;
+
+ /* make a copy of the iniatial_scheam so we don't mess with it */
+ working_schema = dsdb_schema_copy_shallow(mem_ctx, ldb, initial_schema);
+ if (!working_schema) {
+ DEBUG(0,(__location__ ": schema copy failed!\n"));
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ working_schema->resolving_in_progress = true;
+
+ /* we are going to need remote prefixMap for decoding */
+ werr = dsdb_schema_pfm_from_drsuapi_pfm(mapping_ctr, true,
+ working_schema, &pfm_remote, NULL);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0,(__location__ ": Failed to decode remote prefixMap: %s\n",
+ win_errstr(werr)));
+ talloc_free(working_schema);
+ return werr;
+ }
+
+ for (r=0; r < pfm_remote->length; r++) {
+ const struct dsdb_schema_prefixmap_oid *rm = &pfm_remote->prefixes[r];
+ bool found_oid = false;
+ uint32_t l;
+
+ for (l=0; l < working_schema->prefixmap->length; l++) {
+ const struct dsdb_schema_prefixmap_oid *lm = &working_schema->prefixmap->prefixes[l];
+ int cmp;
+
+ cmp = data_blob_cmp(&rm->bin_oid, &lm->bin_oid);
+ if (cmp == 0) {
+ found_oid = true;
+ break;
+ }
+ }
+
+ if (found_oid) {
+ continue;
+ }
+
+ /*
+ * We prefer the same is as we got from the remote peer
+ * if there's no conflict.
+ */
+ werr = dsdb_schema_pfm_add_entry(working_schema->prefixmap,
+ rm->bin_oid, &rm->id, NULL);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0,(__location__ ": Failed to merge remote prefixMap: %s\n",
+ win_errstr(werr)));
+ talloc_free(working_schema);
+ return werr;
+ }
+ }
+
+ werr = dsdb_repl_resolve_working_schema(ldb,
+ pfm_remote,
+ 0, /* cycle_before_switching */
+ working_schema,
+ working_schema,
+ object_count,
+ first_object);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0, ("%s: dsdb_repl_resolve_working_schema() failed: %s\n",
+ __location__, win_errstr(werr)));
+ talloc_free(working_schema);
+ return werr;
+ }
+
+ working_schema->resolving_in_progress = false;
+
+ *_schema_out = working_schema;
+
+ return WERR_OK;
+}
+
+static bool dsdb_attid_in_list(const uint32_t attid_list[], uint32_t attid)
+{
+ const uint32_t *cur;
+ if (!attid_list) {
+ return false;
+ }
+ for (cur = attid_list; *cur != DRSUAPI_ATTID_INVALID; cur++) {
+ if (*cur == attid) {
+ return true;
+ }
+ }
+ return false;
+}
+
+WERROR dsdb_convert_object_ex(struct ldb_context *ldb,
+ const struct dsdb_schema *schema,
+ struct ldb_dn *partition_dn,
+ const struct dsdb_schema_prefixmap *pfm_remote,
+ const struct drsuapi_DsReplicaObjectListItemEx *in,
+ const DATA_BLOB *gensec_skey,
+ const uint32_t *ignore_attids,
+ uint32_t dsdb_repl_flags,
+ TALLOC_CTX *mem_ctx,
+ struct dsdb_extended_replicated_object *out)
+{
+ WERROR status = WERR_OK;
+ uint32_t i;
+ struct ldb_message *msg;
+ struct replPropertyMetaDataBlob *md;
+ int instanceType;
+ struct ldb_message_element *instanceType_e = NULL;
+ NTTIME whenChanged = 0;
+ time_t whenChanged_t;
+ const char *whenChanged_s;
+ struct dom_sid *sid = NULL;
+ uint32_t rid = 0;
+ uint32_t attr_count;
+
+ if (!in->object.identifier) {
+ return WERR_FOOBAR;
+ }
+
+ if (!in->object.identifier->dn || !in->object.identifier->dn[0]) {
+ return WERR_FOOBAR;
+ }
+
+ if (in->object.attribute_ctr.num_attributes != 0 && !in->meta_data_ctr) {
+ return WERR_FOOBAR;
+ }
+
+ if (in->object.attribute_ctr.num_attributes != in->meta_data_ctr->count) {
+ return WERR_FOOBAR;
+ }
+
+ sid = &in->object.identifier->sid;
+ if (sid->num_auths > 0) {
+ rid = sid->sub_auths[sid->num_auths - 1];
+ }
+
+ msg = ldb_msg_new(mem_ctx);
+ W_ERROR_HAVE_NO_MEMORY(msg);
+
+ msg->dn = ldb_dn_new(msg, ldb, in->object.identifier->dn);
+ W_ERROR_HAVE_NO_MEMORY(msg->dn);
+
+ msg->num_elements = in->object.attribute_ctr.num_attributes;
+ msg->elements = talloc_array(msg, struct ldb_message_element,
+ msg->num_elements);
+ W_ERROR_HAVE_NO_MEMORY(msg->elements);
+
+ md = talloc(mem_ctx, struct replPropertyMetaDataBlob);
+ W_ERROR_HAVE_NO_MEMORY(md);
+
+ md->version = 1;
+ md->reserved = 0;
+ md->ctr.ctr1.count = in->meta_data_ctr->count;
+ md->ctr.ctr1.reserved = 0;
+ md->ctr.ctr1.array = talloc_array(mem_ctx,
+ struct replPropertyMetaData1,
+ md->ctr.ctr1.count);
+ W_ERROR_HAVE_NO_MEMORY(md->ctr.ctr1.array);
+
+ for (i=0, attr_count=0; i < in->meta_data_ctr->count; i++, attr_count++) {
+ struct drsuapi_DsReplicaAttribute *a;
+ struct drsuapi_DsReplicaMetaData *d;
+ struct replPropertyMetaData1 *m;
+ struct ldb_message_element *e;
+ uint32_t j;
+
+ a = &in->object.attribute_ctr.attributes[i];
+ d = &in->meta_data_ctr->meta_data[i];
+ m = &md->ctr.ctr1.array[attr_count];
+ e = &msg->elements[attr_count];
+
+ if (dsdb_attid_in_list(ignore_attids, a->attid)) {
+ attr_count--;
+ continue;
+ }
+
+ if (GUID_all_zero(&d->originating_invocation_id)) {
+ status = WERR_DS_SRC_GUID_MISMATCH;
+ DEBUG(0, ("Refusing replication of object containing invalid zero invocationID on attribute %d of %s: %s\n",
+ a->attid,
+ ldb_dn_get_linearized(msg->dn),
+ win_errstr(status)));
+ return status;
+ }
+
+ if (a->attid == DRSUAPI_ATTID_instanceType) {
+ if (instanceType_e != NULL) {
+ return WERR_FOOBAR;
+ }
+ instanceType_e = e;
+ }
+
+ for (j=0; j<a->value_ctr.num_values; j++) {
+ status = drsuapi_decrypt_attribute(a->value_ctr.values[j].blob,
+ gensec_skey, rid,
+ dsdb_repl_flags, a);
+ if (!W_ERROR_IS_OK(status)) {
+ break;
+ }
+ }
+ if (W_ERROR_EQUAL(status, WERR_TOO_MANY_SECRETS)) {
+ WERROR get_name_status = dsdb_attribute_drsuapi_to_ldb(ldb, schema, pfm_remote,
+ a, msg->elements, e, NULL);
+ if (W_ERROR_IS_OK(get_name_status)) {
+ DEBUG(0, ("Unxpectedly got secret value %s on %s from DRS server\n",
+ e->name, ldb_dn_get_linearized(msg->dn)));
+ } else {
+ DEBUG(0, ("Unxpectedly got secret value on %s from DRS server\n",
+ ldb_dn_get_linearized(msg->dn)));
+ }
+ } else if (!W_ERROR_IS_OK(status)) {
+ return status;
+ }
+
+ /*
+ * This function also fills in the local attid value,
+ * based on comparing the remote and local prefixMap
+ * tables. If we don't convert the value, then we can
+ * have invalid values in the replPropertyMetaData we
+ * store on disk, as the prefixMap is per host, not
+ * per-domain. This may be why Microsoft added the
+ * msDS-IntID feature, however this is not used for
+ * extra attributes in the schema partition itself.
+ */
+ status = dsdb_attribute_drsuapi_to_ldb(ldb, schema, pfm_remote,
+ a, msg->elements, e,
+ &m->attid);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ m->version = d->version;
+ m->originating_change_time = d->originating_change_time;
+ m->originating_invocation_id = d->originating_invocation_id;
+ m->originating_usn = d->originating_usn;
+ m->local_usn = 0;
+
+ if (a->attid == DRSUAPI_ATTID_name) {
+ const struct ldb_val *rdn_val = ldb_dn_get_rdn_val(msg->dn);
+ if (rdn_val == NULL) {
+ DEBUG(0, ("Unxpectedly unable to get RDN from %s for validation\n",
+ ldb_dn_get_linearized(msg->dn)));
+ return WERR_FOOBAR;
+ }
+ if (e->num_values != 1) {
+ DEBUG(0, ("Unxpectedly got wrong number of attribute values (got %u, expected 1) when checking RDN against name of %s\n",
+ e->num_values,
+ ldb_dn_get_linearized(msg->dn)));
+ return WERR_FOOBAR;
+ }
+ if (data_blob_cmp(rdn_val,
+ &e->values[0]) != 0) {
+ DEBUG(0, ("Unxpectedly got mismatching RDN values when checking RDN against name of %s\n",
+ ldb_dn_get_linearized(msg->dn)));
+ return WERR_FOOBAR;
+ }
+ }
+ if (d->originating_change_time > whenChanged) {
+ whenChanged = d->originating_change_time;
+ }
+
+ }
+
+ msg->num_elements = attr_count;
+ md->ctr.ctr1.count = attr_count;
+
+ if (instanceType_e == NULL) {
+ return WERR_FOOBAR;
+ }
+
+ instanceType = ldb_msg_find_attr_as_int(msg, "instanceType", 0);
+
+ if ((instanceType & INSTANCE_TYPE_IS_NC_HEAD)
+ && partition_dn != NULL) {
+ int partition_dn_cmp = ldb_dn_compare(partition_dn, msg->dn);
+ if (partition_dn_cmp != 0) {
+ DEBUG(4, ("Remote server advised us of a new partition %s while processing %s, ignoring\n",
+ ldb_dn_get_linearized(msg->dn),
+ ldb_dn_get_linearized(partition_dn)));
+ return WERR_DS_ADD_REPLICA_INHIBITED;
+ }
+ }
+
+ if (dsdb_repl_flags & DSDB_REPL_FLAG_PARTIAL_REPLICA) {
+ /* the instanceType type for partial_replica
+ replication is sent via DRS with TYPE_WRITE set, but
+ must be used on the client with TYPE_WRITE removed
+ */
+ if (instanceType & INSTANCE_TYPE_WRITE) {
+ /*
+ * Make sure we do not change the order
+ * of msg->elements!
+ *
+ * That's why we use
+ * instanceType_e->num_values = 0
+ * instead of
+ * ldb_msg_remove_attr(msg, "instanceType");
+ */
+ struct ldb_message_element *e;
+
+ e = ldb_msg_find_element(msg, "instanceType");
+ if (e != instanceType_e) {
+ DEBUG(0,("instanceType_e[%p] changed to e[%p]\n",
+ instanceType_e, e));
+ return WERR_FOOBAR;
+ }
+
+ instanceType_e->num_values = 0;
+
+ instanceType &= ~INSTANCE_TYPE_WRITE;
+ if (ldb_msg_add_fmt(msg, "instanceType", "%d", instanceType) != LDB_SUCCESS) {
+ return WERR_INTERNAL_ERROR;
+ }
+ }
+ } else {
+ if (!(instanceType & INSTANCE_TYPE_WRITE)) {
+ DBG_ERR("Refusing to replicate %s from a read-only "
+ "replica into a read-write replica!\n",
+ ldb_dn_get_linearized(msg->dn));
+ return WERR_DS_DRA_SOURCE_IS_PARTIAL_REPLICA;
+ }
+ }
+
+ whenChanged_t = nt_time_to_unix(whenChanged);
+ whenChanged_s = ldb_timestring(msg, whenChanged_t);
+ W_ERROR_HAVE_NO_MEMORY(whenChanged_s);
+
+ out->object_guid = in->object.identifier->guid;
+
+ if (in->parent_object_guid == NULL) {
+ out->parent_guid = NULL;
+ } else {
+ out->parent_guid = talloc(mem_ctx, struct GUID);
+ W_ERROR_HAVE_NO_MEMORY(out->parent_guid);
+ *out->parent_guid = *in->parent_object_guid;
+ }
+
+ out->msg = msg;
+ out->when_changed = whenChanged_s;
+ out->meta_data = md;
+ return WERR_OK;
+}
+
+WERROR dsdb_replicated_objects_convert(struct ldb_context *ldb,
+ const struct dsdb_schema *schema,
+ struct ldb_dn *partition_dn,
+ const struct drsuapi_DsReplicaOIDMapping_Ctr *mapping_ctr,
+ uint32_t object_count,
+ const struct drsuapi_DsReplicaObjectListItemEx *first_object,
+ uint32_t linked_attributes_count,
+ const struct drsuapi_DsReplicaLinkedAttribute *linked_attributes,
+ const struct repsFromTo1 *source_dsa,
+ const struct drsuapi_DsReplicaCursor2CtrEx *uptodateness_vector,
+ const DATA_BLOB *gensec_skey,
+ uint32_t dsdb_repl_flags,
+ TALLOC_CTX *mem_ctx,
+ struct dsdb_extended_replicated_objects **objects)
+{
+ WERROR status;
+ struct dsdb_schema_prefixmap *pfm_remote;
+ struct dsdb_extended_replicated_objects *out;
+ const struct drsuapi_DsReplicaObjectListItemEx *cur;
+ struct dsdb_syntax_ctx syntax_ctx;
+ uint32_t i;
+
+ out = talloc_zero(mem_ctx, struct dsdb_extended_replicated_objects);
+ W_ERROR_HAVE_NO_MEMORY(out);
+ out->version = DSDB_EXTENDED_REPLICATED_OBJECTS_VERSION;
+ out->dsdb_repl_flags = dsdb_repl_flags;
+
+ /*
+ * Ensure schema is kept valid for as long as 'out'
+ * which may contain pointers to it
+ */
+ schema = talloc_reference(out, schema);
+ W_ERROR_HAVE_NO_MEMORY(schema);
+
+ status = dsdb_schema_pfm_from_drsuapi_pfm(mapping_ctr, true,
+ out, &pfm_remote, NULL);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(0,(__location__ ": Failed to decode remote prefixMap: %s\n",
+ win_errstr(status)));
+ talloc_free(out);
+ return status;
+ }
+
+ /* use default syntax conversion context */
+ dsdb_syntax_ctx_init(&syntax_ctx, ldb, schema);
+ syntax_ctx.pfm_remote = pfm_remote;
+
+ if (ldb_dn_compare(partition_dn, ldb_get_schema_basedn(ldb)) != 0) {
+ /*
+ * check for schema changes in case
+ * we are not replicating Schema NC
+ */
+ status = dsdb_schema_info_cmp(schema, mapping_ctr);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(4,("Can't replicate %s because remote schema has changed since we last replicated the schema\n",
+ ldb_dn_get_linearized(partition_dn)));
+ talloc_free(out);
+ return status;
+ }
+ }
+
+ out->partition_dn = partition_dn;
+
+ out->source_dsa = source_dsa;
+ out->uptodateness_vector= uptodateness_vector;
+
+ out->num_objects = 0;
+ out->objects = talloc_array(out,
+ struct dsdb_extended_replicated_object,
+ object_count);
+ W_ERROR_HAVE_NO_MEMORY_AND_FREE(out->objects, out);
+
+ for (i=0, cur = first_object; cur; cur = cur->next_object, i++) {
+ if (i == object_count) {
+ talloc_free(out);
+ return WERR_FOOBAR;
+ }
+
+ status = dsdb_convert_object_ex(ldb, schema, out->partition_dn,
+ pfm_remote,
+ cur, gensec_skey,
+ NULL,
+ dsdb_repl_flags,
+ out->objects,
+ &out->objects[out->num_objects]);
+
+ /*
+ * Check to see if we have been advised of a
+ * subdomain or new application partition. We don't
+ * want to start on that here, instead the caller
+ * should consider if it would like to replicate it
+ * based on the cross-ref object.
+ */
+ if (W_ERROR_EQUAL(status, WERR_DS_ADD_REPLICA_INHIBITED)) {
+ struct GUID_txt_buf guid_str;
+ DBG_ERR("Ignoring object outside partition %s %s: %s\n",
+ GUID_buf_string(&cur->object.identifier->guid,
+ &guid_str),
+ cur->object.identifier->dn,
+ win_errstr(status));
+ continue;
+ }
+
+ if (!W_ERROR_IS_OK(status)) {
+ talloc_free(out);
+ DEBUG(0,("Failed to convert object %s: %s\n",
+ cur->object.identifier->dn,
+ win_errstr(status)));
+ return status;
+ }
+
+ /* Assuming we didn't skip or error, increment the number of objects */
+ out->num_objects++;
+ }
+
+ DBG_INFO("Processed %"PRIu32" DRS objects, saw %"PRIu32" objects "
+ "and expected %"PRIu32" objects\n",
+ out->num_objects, i, object_count);
+
+ out->objects = talloc_realloc(out, out->objects,
+ struct dsdb_extended_replicated_object,
+ out->num_objects);
+ if (out->num_objects != 0 && out->objects == NULL) {
+ DBG_ERR("FAILURE: talloc_realloc() failed after "
+ "processing %"PRIu32" DRS objects!\n",
+ out->num_objects);
+ talloc_free(out);
+ return WERR_FOOBAR;
+ }
+ if (i != object_count) {
+ DBG_ERR("FAILURE: saw %"PRIu32" DRS objects, server said we "
+ "should expect to see %"PRIu32" objects!\n",
+ i, object_count);
+ talloc_free(out);
+ return WERR_FOOBAR;
+ }
+
+ out->linked_attributes = talloc_array(out,
+ struct drsuapi_DsReplicaLinkedAttribute,
+ linked_attributes_count);
+ W_ERROR_HAVE_NO_MEMORY_AND_FREE(out->linked_attributes, out);
+
+ for (i=0; i < linked_attributes_count; i++) {
+ const struct drsuapi_DsReplicaLinkedAttribute *ra = &linked_attributes[i];
+ struct drsuapi_DsReplicaLinkedAttribute *la = &out->linked_attributes[i];
+
+ if (ra->identifier == NULL) {
+ talloc_free(out);
+ return WERR_BAD_NET_RESP;
+ }
+
+ *la = *ra;
+
+ la->identifier = talloc_zero(out->linked_attributes,
+ struct drsuapi_DsReplicaObjectIdentifier);
+ W_ERROR_HAVE_NO_MEMORY_AND_FREE(la->identifier, out);
+
+ /*
+ * We typically only get the guid filled
+ * and the repl_meta_data module only cares abouf
+ * the guid.
+ */
+ la->identifier->guid = ra->identifier->guid;
+
+ if (ra->value.blob != NULL) {
+ la->value.blob = talloc_zero(out->linked_attributes,
+ DATA_BLOB);
+ W_ERROR_HAVE_NO_MEMORY_AND_FREE(la->value.blob, out);
+
+ if (ra->value.blob->length != 0) {
+ *la->value.blob = data_blob_dup_talloc(la->value.blob,
+ *ra->value.blob);
+ W_ERROR_HAVE_NO_MEMORY_AND_FREE(la->value.blob->data, out);
+ }
+ }
+
+ status = dsdb_attribute_drsuapi_remote_to_local(&syntax_ctx,
+ ra->attid,
+ &la->attid,
+ NULL);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(0,(__location__": linked_attribute[%u] attid 0x%08X not found: %s\n",
+ i, ra->attid, win_errstr(status)));
+ return status;
+ }
+ }
+
+ out->linked_attributes_count = linked_attributes_count;
+
+ /* free pfm_remote, we won't need it anymore */
+ talloc_free(pfm_remote);
+
+ *objects = out;
+ return WERR_OK;
+}
+
+/**
+ * Commits a list of replicated objects.
+ *
+ * @param working_schema dsdb_schema to be used for resolving
+ * Classes/Attributes during Schema replication. If not NULL,
+ * it will be set on ldb and used while committing replicated objects
+ */
+WERROR dsdb_replicated_objects_commit(struct ldb_context *ldb,
+ struct dsdb_schema *working_schema,
+ struct dsdb_extended_replicated_objects *objects,
+ uint64_t *notify_uSN)
+{
+ WERROR werr;
+ struct ldb_result *ext_res;
+ struct dsdb_schema *cur_schema = NULL;
+ struct dsdb_schema *new_schema = NULL;
+ int ret;
+ uint64_t seq_num1, seq_num2;
+ bool used_global_schema = false;
+
+ TALLOC_CTX *tmp_ctx = talloc_new(objects);
+ if (!tmp_ctx) {
+ DEBUG(0,("Failed to start talloc\n"));
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ /* wrap the extended operation in a transaction
+ See [MS-DRSR] 3.3.2 Transactions
+ */
+ ret = ldb_transaction_start(ldb);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ " Failed to start transaction: %s\n",
+ ldb_errstring(ldb)));
+ return WERR_FOOBAR;
+ }
+
+ ret = dsdb_load_partition_usn(ldb, objects->partition_dn, &seq_num1, NULL);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ " Failed to load partition uSN\n"));
+ ldb_transaction_cancel(ldb);
+ TALLOC_FREE(tmp_ctx);
+ return WERR_FOOBAR;
+ }
+
+ /*
+ * Set working_schema for ldb in case we are replicating from Schema NC.
+ * Schema won't be reloaded during Replicated Objects commit, as it is
+ * done in a transaction. So we need some way to search for newly
+ * added Classes and Attributes
+ */
+ if (working_schema) {
+ /* store current schema so we can fall back in case of failure */
+ cur_schema = dsdb_get_schema(ldb, tmp_ctx);
+ used_global_schema = dsdb_uses_global_schema(ldb);
+
+ ret = dsdb_reference_schema(ldb, working_schema, SCHEMA_MEMORY_ONLY);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ "Failed to reference working schema - %s\n",
+ ldb_strerror(ret)));
+ /* TODO: Map LDB Error to NTSTATUS? */
+ ldb_transaction_cancel(ldb);
+ TALLOC_FREE(tmp_ctx);
+ return WERR_INTERNAL_ERROR;
+ }
+ }
+
+ ret = ldb_extended(ldb, DSDB_EXTENDED_REPLICATED_OBJECTS_OID, objects, &ext_res);
+ if (ret != LDB_SUCCESS) {
+ /* restore previous schema */
+ if (used_global_schema) {
+ dsdb_set_global_schema(ldb);
+ } else if (cur_schema) {
+ dsdb_reference_schema(ldb, cur_schema, SCHEMA_MEMORY_ONLY);
+ }
+
+ if (W_ERROR_EQUAL(objects->error, WERR_DS_DRA_RECYCLED_TARGET)) {
+ DEBUG(3,("Missing target while attempting to apply records: %s\n",
+ ldb_errstring(ldb)));
+ } else if (W_ERROR_EQUAL(objects->error, WERR_DS_DRA_MISSING_PARENT)) {
+ DEBUG(3,("Missing parent while attempting to apply records: %s\n",
+ ldb_errstring(ldb)));
+ } else {
+ DEBUG(1,("Failed to apply records: %s: %s\n",
+ ldb_errstring(ldb), ldb_strerror(ret)));
+ }
+ ldb_transaction_cancel(ldb);
+ TALLOC_FREE(tmp_ctx);
+
+ if (!W_ERROR_IS_OK(objects->error)) {
+ return objects->error;
+ }
+ return WERR_FOOBAR;
+ }
+ talloc_free(ext_res);
+
+ /* Save our updated prefixMap and check the schema is good. */
+ if (working_schema) {
+ struct ldb_result *ext_res_2;
+
+ werr = dsdb_write_prefixes_from_schema_to_ldb(working_schema,
+ ldb,
+ working_schema);
+ if (!W_ERROR_IS_OK(werr)) {
+ /* restore previous schema */
+ if (used_global_schema) {
+ dsdb_set_global_schema(ldb);
+ } else if (cur_schema ) {
+ dsdb_reference_schema(ldb,
+ cur_schema,
+ SCHEMA_MEMORY_ONLY);
+ }
+ DEBUG(0,("Failed to save updated prefixMap: %s\n",
+ win_errstr(werr)));
+ ldb_transaction_cancel(ldb);
+ TALLOC_FREE(tmp_ctx);
+ return werr;
+ }
+
+ /*
+ * Use dsdb_schema_from_db through dsdb extended to check we
+ * can load the schema currently sitting in the transaction.
+ * We need this check because someone might have written to
+ * the schema or prefixMap before we started the transaction,
+ * which may have caused corruption.
+ */
+ ret = ldb_extended(ldb, DSDB_EXTENDED_SCHEMA_LOAD,
+ NULL, &ext_res_2);
+
+ if (ret != LDB_SUCCESS) {
+ if (used_global_schema) {
+ dsdb_set_global_schema(ldb);
+ } else if (cur_schema) {
+ dsdb_reference_schema(ldb, cur_schema, SCHEMA_MEMORY_ONLY);
+ }
+ DEBUG(0,("Corrupt schema write attempt detected, "
+ "aborting schema modification operation.\n"
+ "This probably happened due to bad timing of "
+ "another schema edit: %s (%s)\n",
+ ldb_errstring(ldb),
+ ldb_strerror(ret)));
+ ldb_transaction_cancel(ldb);
+ TALLOC_FREE(tmp_ctx);
+ return WERR_FOOBAR;
+ }
+ }
+
+ ret = ldb_transaction_prepare_commit(ldb);
+ if (ret != LDB_SUCCESS) {
+ /* restore previous schema */
+ if (used_global_schema) {
+ dsdb_set_global_schema(ldb);
+ } else if (cur_schema ) {
+ dsdb_reference_schema(ldb, cur_schema, SCHEMA_MEMORY_ONLY);
+ }
+ DBG_ERR(" Failed to prepare commit of transaction: %s (%s)\n",
+ ldb_errstring(ldb),
+ ldb_strerror(ret));
+ TALLOC_FREE(tmp_ctx);
+ return WERR_FOOBAR;
+ }
+
+ ret = dsdb_load_partition_usn(ldb, objects->partition_dn, &seq_num2, NULL);
+ if (ret != LDB_SUCCESS) {
+ /* restore previous schema */
+ if (used_global_schema) {
+ dsdb_set_global_schema(ldb);
+ } else if (cur_schema ) {
+ dsdb_reference_schema(ldb, cur_schema, SCHEMA_MEMORY_ONLY);
+ }
+ DEBUG(0,(__location__ " Failed to load partition uSN\n"));
+ ldb_transaction_cancel(ldb);
+ TALLOC_FREE(tmp_ctx);
+ return WERR_FOOBAR;
+ }
+
+ ret = ldb_transaction_commit(ldb);
+ if (ret != LDB_SUCCESS) {
+ /* restore previous schema */
+ if (used_global_schema) {
+ dsdb_set_global_schema(ldb);
+ } else if (cur_schema ) {
+ dsdb_reference_schema(ldb, cur_schema, SCHEMA_MEMORY_ONLY);
+ }
+ DEBUG(0,(__location__ " Failed to commit transaction\n"));
+ TALLOC_FREE(tmp_ctx);
+ return WERR_FOOBAR;
+ }
+
+ if (seq_num1 > *notify_uSN) {
+ /*
+ * A notify was already required before
+ * the current transaction.
+ */
+ } else if (objects->originating_updates) {
+ /*
+ * Applying the replicated changes
+ * required originating updates,
+ * so a notify is required.
+ */
+ } else {
+ /*
+ * There's no need to notify the
+ * server about the change we just from it.
+ */
+ *notify_uSN = seq_num2;
+ }
+
+ /*
+ * Reset the Schema used by ldb. This will lead to
+ * a schema cache being refreshed from database.
+ */
+ if (working_schema) {
+ /* Reload the schema */
+ new_schema = dsdb_get_schema(ldb, tmp_ctx);
+ /* TODO:
+ * If dsdb_get_schema() fails, we just fall back
+ * to what we had. However, the database is probably
+ * unable to operate for other users from this
+ * point... */
+ if (new_schema == NULL || new_schema == working_schema) {
+ DBG_ERR("Failed to re-load schema after commit of "
+ "transaction (working: %p/%"PRIu64", new: "
+ "%p/%"PRIu64")\n", new_schema,
+ new_schema != NULL ?
+ new_schema->metadata_usn : 0,
+ working_schema, working_schema->metadata_usn);
+ dsdb_reference_schema(ldb, cur_schema, SCHEMA_MEMORY_ONLY);
+ if (used_global_schema) {
+ dsdb_set_global_schema(ldb);
+ }
+ TALLOC_FREE(tmp_ctx);
+ return WERR_INTERNAL_ERROR;
+ } else if (used_global_schema) {
+ dsdb_make_schema_global(ldb, new_schema);
+ }
+ }
+
+ DEBUG(2,("Replicated %u objects (%u linked attributes) for %s\n",
+ objects->num_objects, objects->linked_attributes_count,
+ ldb_dn_get_linearized(objects->partition_dn)));
+
+ TALLOC_FREE(tmp_ctx);
+ return WERR_OK;
+}
+
+static WERROR dsdb_origin_object_convert(struct ldb_context *ldb,
+ const struct dsdb_schema *schema,
+ const struct drsuapi_DsReplicaObjectListItem *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message **_msg)
+{
+ WERROR status;
+ unsigned int i;
+ struct ldb_message *msg;
+
+ if (!in->object.identifier) {
+ return WERR_FOOBAR;
+ }
+
+ if (!in->object.identifier->dn || !in->object.identifier->dn[0]) {
+ return WERR_FOOBAR;
+ }
+
+ msg = ldb_msg_new(mem_ctx);
+ W_ERROR_HAVE_NO_MEMORY(msg);
+
+ msg->dn = ldb_dn_new(msg, ldb, in->object.identifier->dn);
+ W_ERROR_HAVE_NO_MEMORY(msg->dn);
+
+ msg->num_elements = in->object.attribute_ctr.num_attributes;
+ msg->elements = talloc_array(msg, struct ldb_message_element,
+ msg->num_elements);
+ W_ERROR_HAVE_NO_MEMORY(msg->elements);
+
+ for (i=0; i < msg->num_elements; i++) {
+ struct drsuapi_DsReplicaAttribute *a;
+ struct ldb_message_element *e;
+
+ a = &in->object.attribute_ctr.attributes[i];
+ e = &msg->elements[i];
+
+ status = dsdb_attribute_drsuapi_to_ldb(ldb, schema, schema->prefixmap,
+ a, msg->elements, e, NULL);
+ W_ERROR_NOT_OK_RETURN(status);
+ }
+
+
+ *_msg = msg;
+
+ return WERR_OK;
+}
+
+WERROR dsdb_origin_objects_commit(struct ldb_context *ldb,
+ TALLOC_CTX *mem_ctx,
+ const struct drsuapi_DsReplicaObjectListItem *first_object,
+ uint32_t *_num,
+ uint32_t dsdb_repl_flags,
+ struct drsuapi_DsReplicaObjectIdentifier2 **_ids)
+{
+ WERROR status;
+ const struct dsdb_schema *schema;
+ const struct drsuapi_DsReplicaObjectListItem *cur;
+ struct ldb_message **objects;
+ struct drsuapi_DsReplicaObjectIdentifier2 *ids;
+ uint32_t i;
+ uint32_t num_objects = 0;
+ const char * const attrs[] = {
+ "objectGUID",
+ "objectSid",
+ NULL
+ };
+ struct ldb_result *res;
+ int ret;
+
+ for (cur = first_object; cur; cur = cur->next_object) {
+ num_objects++;
+ }
+
+ if (num_objects == 0) {
+ return WERR_OK;
+ }
+
+ ret = ldb_transaction_start(ldb);
+ if (ret != LDB_SUCCESS) {
+ return WERR_DS_INTERNAL_FAILURE;
+ }
+
+ objects = talloc_array(mem_ctx, struct ldb_message *,
+ num_objects);
+ if (objects == NULL) {
+ status = WERR_NOT_ENOUGH_MEMORY;
+ goto cancel;
+ }
+
+ schema = dsdb_get_schema(ldb, objects);
+ if (!schema) {
+ return WERR_DS_SCHEMA_NOT_LOADED;
+ }
+
+ for (i=0, cur = first_object; cur; cur = cur->next_object, i++) {
+ status = dsdb_origin_object_convert(ldb, schema, cur,
+ objects, &objects[i]);
+ if (!W_ERROR_IS_OK(status)) {
+ goto cancel;
+ }
+ }
+
+ ids = talloc_array(mem_ctx,
+ struct drsuapi_DsReplicaObjectIdentifier2,
+ num_objects);
+ if (ids == NULL) {
+ status = WERR_NOT_ENOUGH_MEMORY;
+ goto cancel;
+ }
+
+ if (dsdb_repl_flags & DSDB_REPL_FLAG_ADD_NCNAME) {
+ /* check for possible NC creation */
+ for (i=0; i < num_objects; i++) {
+ struct ldb_message *msg = objects[i];
+ struct ldb_message_element *el;
+ struct ldb_dn *nc_dn;
+
+ if (ldb_msg_check_string_attribute(msg, "objectClass", "crossRef") == 0) {
+ continue;
+ }
+ el = ldb_msg_find_element(msg, "nCName");
+ if (el == NULL || el->num_values != 1) {
+ continue;
+ }
+ nc_dn = ldb_dn_from_ldb_val(objects, ldb, &el->values[0]);
+ if (!ldb_dn_validate(nc_dn)) {
+ continue;
+ }
+ ret = dsdb_create_partial_replica_NC(ldb, nc_dn);
+ if (ret != LDB_SUCCESS) {
+ status = WERR_DS_INTERNAL_FAILURE;
+ goto cancel;
+ }
+ }
+ }
+
+ for (i=0; i < num_objects; i++) {
+ struct dom_sid *sid = NULL;
+ struct ldb_request *add_req;
+
+ DEBUG(6,(__location__ ": adding %s\n",
+ ldb_dn_get_linearized(objects[i]->dn)));
+
+ ret = ldb_build_add_req(&add_req,
+ ldb,
+ objects,
+ objects[i],
+ NULL,
+ NULL,
+ ldb_op_default_callback,
+ NULL);
+ if (ret != LDB_SUCCESS) {
+ status = WERR_DS_INTERNAL_FAILURE;
+ goto cancel;
+ }
+
+ ret = ldb_request_add_control(add_req, LDB_CONTROL_RELAX_OID, true, NULL);
+ if (ret != LDB_SUCCESS) {
+ status = WERR_DS_INTERNAL_FAILURE;
+ goto cancel;
+ }
+
+ ret = ldb_request(ldb, add_req);
+ if (ret == LDB_SUCCESS) {
+ ret = ldb_wait(add_req->handle, LDB_WAIT_ALL);
+ }
+ if (ret != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Failed add of %s - %s\n",
+ ldb_dn_get_linearized(objects[i]->dn), ldb_errstring(ldb)));
+ status = WERR_DS_INTERNAL_FAILURE;
+ goto cancel;
+ }
+
+ talloc_free(add_req);
+
+ ret = ldb_search(ldb, objects, &res, objects[i]->dn,
+ LDB_SCOPE_BASE, attrs,
+ "(objectClass=*)");
+ if (ret != LDB_SUCCESS) {
+ status = WERR_DS_INTERNAL_FAILURE;
+ goto cancel;
+ }
+ ids[i].guid = samdb_result_guid(res->msgs[0], "objectGUID");
+ sid = samdb_result_dom_sid(objects, res->msgs[0], "objectSid");
+ if (sid) {
+ ids[i].sid = *sid;
+ } else {
+ ZERO_STRUCT(ids[i].sid);
+ }
+ }
+
+ ret = ldb_transaction_commit(ldb);
+ if (ret != LDB_SUCCESS) {
+ return WERR_DS_INTERNAL_FAILURE;
+ }
+
+ talloc_free(objects);
+
+ *_num = num_objects;
+ *_ids = ids;
+ return WERR_OK;
+
+cancel:
+ talloc_free(objects);
+ ldb_transaction_cancel(ldb);
+ return status;
+}
diff --git a/source4/dsdb/samdb.pc.in b/source4/dsdb/samdb.pc.in
new file mode 100644
index 0000000..691f73e
--- /dev/null
+++ b/source4/dsdb/samdb.pc.in
@@ -0,0 +1,10 @@
+prefix=@prefix@
+exec_prefix=@exec_prefix@
+libdir=@libdir@
+includedir=@includedir@
+
+Name: samdb
+Description: Sam Database
+Version: @PACKAGE_VERSION@
+Libs: @LIB_RPATH@ -L${libdir} -lsamdb
+Cflags: -I${includedir} -DHAVE_IMMEDIATE_STRUCTURES=1
diff --git a/source4/dsdb/samdb/cracknames.c b/source4/dsdb/samdb/cracknames.c
new file mode 100644
index 0000000..52f5e41
--- /dev/null
+++ b/source4/dsdb/samdb/cracknames.c
@@ -0,0 +1,1804 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ crachnames implementation for the drsuapi pipe
+ DsCrackNames()
+
+ Copyright (C) Stefan Metzmacher 2004
+ Copyright (C) Andrew Bartlett <abartlet@samba.org> 2004-2005
+ Copyright (C) Matthieu Patou <mat@matws.net> 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/>.
+*/
+
+#include "includes.h"
+#include "librpc/gen_ndr/drsuapi.h"
+#include "lib/events/events.h"
+#include <ldb.h>
+#include <ldb_errors.h>
+#include "auth/kerberos/kerberos.h"
+#include "libcli/ldap/ldap_ndr.h"
+#include "libcli/security/security.h"
+#include "auth/auth.h"
+#include "../lib/util/util_ldb.h"
+#include "dsdb/samdb/samdb.h"
+#include "dsdb/common/util.h"
+#include "param/param.h"
+
+#undef strcasecmp
+
+static WERROR DsCrackNameOneFilter(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx,
+ struct smb_krb5_context *smb_krb5_context,
+ uint32_t format_flags, enum drsuapi_DsNameFormat format_offered,
+ enum drsuapi_DsNameFormat format_desired,
+ struct ldb_dn *name_dn, const char *name,
+ const char *domain_filter, const char *result_filter,
+ struct drsuapi_DsNameInfo1 *info1, int scope, struct ldb_dn *search_dn);
+static WERROR DsCrackNameOneSyntactical(TALLOC_CTX *mem_ctx,
+ enum drsuapi_DsNameFormat format_offered,
+ enum drsuapi_DsNameFormat format_desired,
+ struct ldb_dn *name_dn, const char *name,
+ struct drsuapi_DsNameInfo1 *info1);
+
+static WERROR dns_domain_from_principal(TALLOC_CTX *mem_ctx, struct smb_krb5_context *smb_krb5_context,
+ const char *name,
+ struct drsuapi_DsNameInfo1 *info1)
+{
+ krb5_error_code ret;
+ krb5_principal principal;
+ /* perhaps it's a principal with a realm, so return the right 'domain only' response */
+ ret = krb5_parse_name_flags(smb_krb5_context->krb5_context, name,
+ KRB5_PRINCIPAL_PARSE_REQUIRE_REALM, &principal);
+ if (ret) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ }
+
+ info1->dns_domain_name = smb_krb5_principal_get_realm(
+ mem_ctx, smb_krb5_context->krb5_context, principal);
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+
+ W_ERROR_HAVE_NO_MEMORY(info1->dns_domain_name);
+
+ info1->status = DRSUAPI_DS_NAME_STATUS_DOMAIN_ONLY;
+ return WERR_OK;
+}
+
+static enum drsuapi_DsNameStatus LDB_lookup_spn_alias(struct ldb_context *ldb_ctx,
+ TALLOC_CTX *mem_ctx,
+ const char *alias_from,
+ char **alias_to)
+{
+ /*
+ * Some of the logic of this function is mirrored in find_spn_alias()
+ * in source4/dsdb.samdb/ldb_modules/samldb.c. If you change this to
+ * not return the first matched alias, you will need to rethink that
+ * function too.
+ */
+ unsigned int i;
+ int ret;
+ struct ldb_result *res;
+ struct ldb_message_element *spnmappings;
+ TALLOC_CTX *tmp_ctx;
+ struct ldb_dn *service_dn;
+ char *service_dn_str;
+
+ const char *directory_attrs[] = {
+ "sPNMappings",
+ NULL
+ };
+
+ tmp_ctx = talloc_new(mem_ctx);
+ if (!tmp_ctx) {
+ return DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ }
+
+ service_dn = ldb_dn_new(tmp_ctx, ldb_ctx, "CN=Directory Service,CN=Windows NT,CN=Services");
+ if ( ! ldb_dn_add_base(service_dn, ldb_get_config_basedn(ldb_ctx))) {
+ talloc_free(tmp_ctx);
+ return DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ }
+ service_dn_str = ldb_dn_alloc_linearized(tmp_ctx, service_dn);
+ if ( ! service_dn_str) {
+ talloc_free(tmp_ctx);
+ return DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ }
+
+ ret = ldb_search(ldb_ctx, tmp_ctx, &res, service_dn, LDB_SCOPE_BASE,
+ directory_attrs, "(objectClass=nTDSService)");
+
+ if (ret != LDB_SUCCESS && ret != LDB_ERR_NO_SUCH_OBJECT) {
+ DEBUG(1, ("ldb_search: dn: %s not found: %s\n", service_dn_str, ldb_errstring(ldb_ctx)));
+ talloc_free(tmp_ctx);
+ return DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ } else if (ret == LDB_ERR_NO_SUCH_OBJECT) {
+ DEBUG(1, ("ldb_search: dn: %s not found\n", service_dn_str));
+ talloc_free(tmp_ctx);
+ return DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ } else if (res->count != 1) {
+ DEBUG(1, ("ldb_search: dn: %s not found\n", service_dn_str));
+ talloc_free(tmp_ctx);
+ return DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ }
+
+ spnmappings = ldb_msg_find_element(res->msgs[0], "sPNMappings");
+ if (!spnmappings || spnmappings->num_values == 0) {
+ DEBUG(1, ("ldb_search: dn: %s no sPNMappings attribute\n", service_dn_str));
+ talloc_free(tmp_ctx);
+ return DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ }
+
+ for (i = 0; i < spnmappings->num_values; i++) {
+ char *mapping, *p, *str;
+ mapping = talloc_strdup(tmp_ctx,
+ (const char *)spnmappings->values[i].data);
+ if (!mapping) {
+ DEBUG(1, ("LDB_lookup_spn_alias: ldb_search: dn: %s did not have an sPNMapping\n", service_dn_str));
+ talloc_free(tmp_ctx);
+ return DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ }
+
+ /* C string manipulation sucks */
+
+ p = strchr(mapping, '=');
+ if (!p) {
+ DEBUG(1, ("ldb_search: dn: %s sPNMapping malformed: %s\n",
+ service_dn_str, mapping));
+ talloc_free(tmp_ctx);
+ return DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ }
+ p[0] = '\0';
+ p++;
+ do {
+ str = p;
+ p = strchr(p, ',');
+ if (p) {
+ p[0] = '\0';
+ p++;
+ }
+ if (strcasecmp(str, alias_from) == 0) {
+ *alias_to = mapping;
+ talloc_steal(mem_ctx, mapping);
+ talloc_free(tmp_ctx);
+ return DRSUAPI_DS_NAME_STATUS_OK;
+ }
+ } while (p);
+ }
+ DEBUG(4, ("LDB_lookup_spn_alias: no alias for service %s applicable\n", alias_from));
+ talloc_free(tmp_ctx);
+ return DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+}
+
+/* When cracking a ServicePrincipalName, many services may be served
+ * by the host/ servicePrincipalName. The incoming query is for cifs/
+ * but we translate it here, and search on host/. This is done after
+ * the cifs/ entry has been searched for, making this a fallback */
+
+static WERROR DsCrackNameSPNAlias(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx,
+ struct smb_krb5_context *smb_krb5_context,
+ uint32_t format_flags, enum drsuapi_DsNameFormat format_offered,
+ enum drsuapi_DsNameFormat format_desired,
+ const char *name, struct drsuapi_DsNameInfo1 *info1)
+{
+ WERROR wret;
+ krb5_error_code ret;
+ krb5_principal principal;
+ krb5_data component;
+ const char *service, *dns_name;
+ char *new_service;
+ char *new_princ;
+ enum drsuapi_DsNameStatus namestatus;
+
+ /* parse principal */
+ ret = krb5_parse_name_flags(smb_krb5_context->krb5_context,
+ name, KRB5_PRINCIPAL_PARSE_NO_REALM, &principal);
+ if (ret) {
+ DEBUG(2, ("Could not parse principal: %s: %s\n",
+ name, smb_get_krb5_error_message(smb_krb5_context->krb5_context,
+ ret, mem_ctx)));
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ /* grab cifs/, http/ etc */
+
+ ret = smb_krb5_princ_component(smb_krb5_context->krb5_context,
+ principal, 0, &component);
+ if (ret) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ return WERR_OK;
+ }
+ service = (const char *)component.data;
+ ret = smb_krb5_princ_component(smb_krb5_context->krb5_context,
+ principal, 1, &component);
+ if (ret) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ return WERR_OK;
+ }
+ dns_name = (const char *)component.data;
+
+ /* MAP it */
+ namestatus = LDB_lookup_spn_alias(sam_ctx, mem_ctx,
+ service, &new_service);
+
+ if (namestatus == DRSUAPI_DS_NAME_STATUS_NOT_FOUND) {
+ wret = WERR_OK;
+ info1->status = DRSUAPI_DS_NAME_STATUS_DOMAIN_ONLY;
+ info1->dns_domain_name = talloc_strdup(mem_ctx, dns_name);
+ if (!info1->dns_domain_name) {
+ wret = WERR_NOT_ENOUGH_MEMORY;
+ }
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ return wret;
+ } else if (namestatus != DRSUAPI_DS_NAME_STATUS_OK) {
+ info1->status = namestatus;
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ return WERR_OK;
+ }
+
+ /* reform principal */
+ new_princ = talloc_asprintf(mem_ctx, "%s/%s", new_service, dns_name);
+ if (!new_princ) {
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ wret = DsCrackNameOneName(sam_ctx, mem_ctx, format_flags, format_offered, format_desired,
+ new_princ, info1);
+ talloc_free(new_princ);
+ if (W_ERROR_IS_OK(wret) && (info1->status == DRSUAPI_DS_NAME_STATUS_NOT_FOUND)) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_DOMAIN_ONLY;
+ info1->dns_domain_name = talloc_strdup(mem_ctx, dns_name);
+ if (!info1->dns_domain_name) {
+ wret = WERR_NOT_ENOUGH_MEMORY;
+ }
+ }
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ return wret;
+}
+
+/* Subcase of CrackNames, for the userPrincipalName */
+
+static WERROR DsCrackNameUPN(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx,
+ struct smb_krb5_context *smb_krb5_context,
+ uint32_t format_flags, enum drsuapi_DsNameFormat format_offered,
+ enum drsuapi_DsNameFormat format_desired,
+ const char *name, struct drsuapi_DsNameInfo1 *info1)
+{
+ int ldb_ret;
+ WERROR status;
+ const char *domain_filter = NULL;
+ const char *result_filter = NULL;
+ krb5_error_code ret;
+ krb5_principal principal;
+ char *realm;
+ char *realm_encoded = NULL;
+ char *unparsed_name_short;
+ const char *unparsed_name_short_encoded = NULL;
+ const char *domain_attrs[] = { NULL };
+ struct ldb_result *domain_res = NULL;
+
+ /* Prevent recursion */
+ if (!name) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ }
+
+ ret = krb5_parse_name_flags(smb_krb5_context->krb5_context, name,
+ KRB5_PRINCIPAL_PARSE_REQUIRE_REALM, &principal);
+ if (ret) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ }
+
+ realm = smb_krb5_principal_get_realm(
+ mem_ctx, smb_krb5_context->krb5_context, principal);
+ if (realm == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ realm_encoded = ldb_binary_encode_string(mem_ctx, realm);
+ if (realm_encoded == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ ldb_ret = ldb_search(sam_ctx, mem_ctx, &domain_res,
+ samdb_partitions_dn(sam_ctx, mem_ctx),
+ LDB_SCOPE_ONELEVEL,
+ domain_attrs,
+ "(&(objectClass=crossRef)(|(dnsRoot=%s)(netbiosName=%s))"
+ "(systemFlags:"LDB_OID_COMPARATOR_AND":=%u))",
+ realm_encoded,
+ realm_encoded,
+ SYSTEM_FLAG_CR_NTDS_DOMAIN);
+ TALLOC_FREE(realm_encoded);
+ TALLOC_FREE(realm);
+
+ if (ldb_ret != LDB_SUCCESS) {
+ DEBUG(2, ("DsCrackNameUPN domain ref search failed: %s\n", ldb_errstring(sam_ctx)));
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ return WERR_OK;
+ }
+
+ switch (domain_res->count) {
+ case 1:
+ break;
+ case 0:
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ return dns_domain_from_principal(mem_ctx, smb_krb5_context,
+ name, info1);
+ default:
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE;
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ return WERR_OK;
+ }
+
+ /*
+ * The important thing here is that a samAccountName may have
+ * a space in it, and this must not be kerberos escaped to
+ * match this filter, so we specify
+ * KRB5_PRINCIPAL_UNPARSE_DISPLAY
+ */
+ ret = krb5_unparse_name_flags(smb_krb5_context->krb5_context, principal,
+ KRB5_PRINCIPAL_UNPARSE_NO_REALM |
+ KRB5_PRINCIPAL_UNPARSE_DISPLAY,
+ &unparsed_name_short);
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+
+ if (ret) {
+ free(unparsed_name_short);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ unparsed_name_short_encoded = ldb_binary_encode_string(mem_ctx, unparsed_name_short);
+ if (unparsed_name_short_encoded == NULL) {
+ free(unparsed_name_short);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ /* This may need to be extended for more userPrincipalName variations */
+ result_filter = talloc_asprintf(mem_ctx, "(&(samAccountName=%s)(objectClass=user))",
+ unparsed_name_short_encoded);
+
+ domain_filter = talloc_asprintf(mem_ctx, "(distinguishedName=%s)", ldb_dn_get_linearized(domain_res->msgs[0]->dn));
+
+ if (!result_filter || !domain_filter) {
+ free(unparsed_name_short);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ status = DsCrackNameOneFilter(sam_ctx, mem_ctx,
+ smb_krb5_context,
+ format_flags, format_offered, format_desired,
+ NULL, unparsed_name_short, domain_filter, result_filter,
+ info1, LDB_SCOPE_SUBTREE, NULL);
+ free(unparsed_name_short);
+
+ return status;
+}
+
+/*
+ * This function will workout the filtering parameter in order to be able to do
+ * the adapted search when the incoming format is format_functional.
+ * This boils down to defining the search_dn (passed as pointer to ldb_dn *) and the
+ * ldap filter request.
+ * Main input parameters are:
+ * * name, which is the portion of the functional name after the
+ * first '/'.
+ * * domain_filter, which is a ldap search filter used to find the NC DN given the
+ * function name to crack.
+ */
+static WERROR get_format_functional_filtering_param(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx,
+ char *name, struct drsuapi_DsNameInfo1 *info1,
+ struct ldb_dn **psearch_dn, const char *domain_filter, const char **presult_filter)
+{
+ struct ldb_result *domain_res = NULL;
+ const char * const domain_attrs[] = {"ncName", NULL};
+ struct ldb_dn *partitions_basedn = samdb_partitions_dn(sam_ctx, mem_ctx);
+ int ldb_ret;
+ char *account, *s, *result_filter = NULL;
+ struct ldb_dn *search_dn = NULL;
+
+ *psearch_dn = NULL;
+ *presult_filter = NULL;
+
+ ldb_ret = ldb_search(sam_ctx, mem_ctx, &domain_res,
+ partitions_basedn,
+ LDB_SCOPE_ONELEVEL,
+ domain_attrs,
+ "%s", domain_filter);
+
+ if (ldb_ret != LDB_SUCCESS) {
+ DEBUG(2, ("DsCrackNameOne domain ref search failed: %s\n", ldb_errstring(sam_ctx)));
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ return WERR_FOOBAR;
+ }
+
+ if (domain_res->count == 1) {
+ struct ldb_dn *tmp_dn = samdb_result_dn(sam_ctx, mem_ctx, domain_res->msgs[0], "ncName", NULL);
+ const char * const name_attrs[] = {"name", NULL};
+
+ account = name;
+ s = strchr(account, '/');
+ talloc_free(domain_res);
+ while(s) {
+ s[0] = '\0';
+ s++;
+
+ ldb_ret = ldb_search(sam_ctx, mem_ctx, &domain_res,
+ tmp_dn,
+ LDB_SCOPE_ONELEVEL,
+ name_attrs,
+ "name=%s", account);
+
+ if (ldb_ret != LDB_SUCCESS) {
+ DEBUG(2, ("DsCrackNameOne domain ref search failed: %s\n", ldb_errstring(sam_ctx)));
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ return WERR_OK;
+ }
+ talloc_free(tmp_dn);
+ switch (domain_res->count) {
+ case 1:
+ break;
+ case 0:
+ talloc_free(domain_res);
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ default:
+ talloc_free(domain_res);
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE;
+ return WERR_OK;
+ }
+
+ tmp_dn = talloc_steal(mem_ctx, domain_res->msgs[0]->dn);
+ talloc_free(domain_res);
+ search_dn = tmp_dn;
+ account = s;
+ s = strchr(account, '/');
+ }
+ account = ldb_binary_encode_string(mem_ctx, account);
+ W_ERROR_HAVE_NO_MEMORY(account);
+ result_filter = talloc_asprintf(mem_ctx, "(name=%s)",
+ account);
+ W_ERROR_HAVE_NO_MEMORY(result_filter);
+ }
+ *psearch_dn = search_dn;
+ *presult_filter = result_filter;
+ return WERR_OK;
+}
+
+/* Crack a single 'name', from format_offered into format_desired, returning the result in info1 */
+
+WERROR DsCrackNameOneName(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx,
+ uint32_t format_flags, enum drsuapi_DsNameFormat format_offered,
+ enum drsuapi_DsNameFormat format_desired,
+ const char *name, struct drsuapi_DsNameInfo1 *info1)
+{
+ krb5_error_code ret;
+ const char *domain_filter = NULL;
+ const char *result_filter = NULL;
+ struct ldb_dn *name_dn = NULL;
+ struct ldb_dn *search_dn = NULL;
+
+ struct smb_krb5_context *smb_krb5_context = NULL;
+ int scope = LDB_SCOPE_SUBTREE;
+
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ info1->dns_domain_name = NULL;
+ info1->result_name = NULL;
+
+ if (!name) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ /* TODO: - fill the correct names in all cases!
+ * - handle format_flags
+ */
+ if (format_desired == DRSUAPI_DS_NAME_FORMAT_UNKNOWN) {
+ return WERR_OK;
+ }
+ /* here we need to set the domain_filter and/or the result_filter */
+ switch (format_offered) {
+ case DRSUAPI_DS_NAME_FORMAT_UNKNOWN:
+ {
+ unsigned int i;
+ enum drsuapi_DsNameFormat formats[] = {
+ DRSUAPI_DS_NAME_FORMAT_FQDN_1779, DRSUAPI_DS_NAME_FORMAT_USER_PRINCIPAL,
+ DRSUAPI_DS_NAME_FORMAT_NT4_ACCOUNT, DRSUAPI_DS_NAME_FORMAT_CANONICAL,
+ DRSUAPI_DS_NAME_FORMAT_GUID, DRSUAPI_DS_NAME_FORMAT_DISPLAY,
+ DRSUAPI_DS_NAME_FORMAT_SERVICE_PRINCIPAL,
+ DRSUAPI_DS_NAME_FORMAT_SID_OR_SID_HISTORY,
+ DRSUAPI_DS_NAME_FORMAT_CANONICAL_EX
+ };
+ WERROR werr;
+ for (i=0; i < ARRAY_SIZE(formats); i++) {
+ werr = DsCrackNameOneName(sam_ctx, mem_ctx, format_flags, formats[i], format_desired, name, info1);
+ if (!W_ERROR_IS_OK(werr)) {
+ return werr;
+ }
+ if (info1->status != DRSUAPI_DS_NAME_STATUS_NOT_FOUND &&
+ (formats[i] != DRSUAPI_DS_NAME_FORMAT_CANONICAL ||
+ info1->status != DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR))
+ {
+ return werr;
+ }
+ }
+ return werr;
+ }
+
+ case DRSUAPI_DS_NAME_FORMAT_CANONICAL:
+ case DRSUAPI_DS_NAME_FORMAT_CANONICAL_EX:
+ {
+ char *str, *s, *account;
+ const char *str_encoded = NULL;
+ scope = LDB_SCOPE_ONELEVEL;
+
+ if (strlen(name) == 0) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ return WERR_OK;
+ }
+
+ str = talloc_strdup(mem_ctx, name);
+ W_ERROR_HAVE_NO_MEMORY(str);
+
+ if (format_offered == DRSUAPI_DS_NAME_FORMAT_CANONICAL_EX) {
+ /* Look backwards for the \n, and replace it with / */
+ s = strrchr(str, '\n');
+ if (!s) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ return WERR_OK;
+ }
+ s[0] = '/';
+ }
+
+ s = strchr(str, '/');
+ if (!s) {
+ /* there must be at least one / */
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ return WERR_OK;
+ }
+
+ s[0] = '\0';
+ s++;
+
+ str_encoded = ldb_binary_encode_string(mem_ctx, str);
+ if (str_encoded == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ domain_filter = talloc_asprintf(mem_ctx, "(&(objectClass=crossRef)(dnsRoot=%s)(systemFlags:%s:=%u))",
+ str_encoded,
+ LDB_OID_COMPARATOR_AND,
+ SYSTEM_FLAG_CR_NTDS_DOMAIN);
+ W_ERROR_HAVE_NO_MEMORY(domain_filter);
+
+ /* There may not be anything after the domain component (search for the domain itself) */
+ account = s;
+ if (account && *account) {
+ WERROR werr = get_format_functional_filtering_param(sam_ctx,
+ mem_ctx,
+ account,
+ info1,
+ &search_dn,
+ domain_filter,
+ &result_filter);
+ if (!W_ERROR_IS_OK(werr)) {
+ return werr;
+ }
+ if (info1->status != DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR)
+ return WERR_OK;
+ }
+ break;
+ }
+ case DRSUAPI_DS_NAME_FORMAT_NT4_ACCOUNT: {
+ char *p;
+ char *domain;
+ char *domain_encoded = NULL;
+ const char *account = NULL;
+
+ domain = talloc_strdup(mem_ctx, name);
+ W_ERROR_HAVE_NO_MEMORY(domain);
+
+ p = strchr(domain, '\\');
+ if (!p) {
+ /* invalid input format */
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ }
+ p[0] = '\0';
+
+ if (p[1]) {
+ account = &p[1];
+ }
+
+ domain_encoded = ldb_binary_encode_string(mem_ctx, domain);
+ if (domain_encoded == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ domain_filter = talloc_asprintf(mem_ctx,
+ "(&(objectClass=crossRef)(netbiosName=%s)(systemFlags:%s:=%u))",
+ domain_encoded,
+ LDB_OID_COMPARATOR_AND,
+ SYSTEM_FLAG_CR_NTDS_DOMAIN);
+ W_ERROR_HAVE_NO_MEMORY(domain_filter);
+ if (account) {
+ const char *account_encoded = NULL;
+
+ account_encoded = ldb_binary_encode_string(mem_ctx, account);
+ if (account_encoded == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ result_filter = talloc_asprintf(mem_ctx, "(sAMAccountName=%s)",
+ account_encoded);
+ W_ERROR_HAVE_NO_MEMORY(result_filter);
+ }
+
+ talloc_free(domain);
+ break;
+ }
+
+ /* A LDAP DN as a string */
+ case DRSUAPI_DS_NAME_FORMAT_FQDN_1779: {
+ domain_filter = NULL;
+ name_dn = ldb_dn_new(mem_ctx, sam_ctx, name);
+ if (! ldb_dn_validate(name_dn)) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ }
+ break;
+ }
+
+ /* A GUID as a string */
+ case DRSUAPI_DS_NAME_FORMAT_GUID: {
+ struct GUID guid;
+ char *ldap_guid;
+ NTSTATUS nt_status;
+ domain_filter = NULL;
+
+ nt_status = GUID_from_string(name, &guid);
+ if (!NT_STATUS_IS_OK(nt_status)) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ }
+
+ ldap_guid = ldap_encode_ndr_GUID(mem_ctx, &guid);
+ if (!ldap_guid) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ result_filter = talloc_asprintf(mem_ctx, "(objectGUID=%s)",
+ ldap_guid);
+ W_ERROR_HAVE_NO_MEMORY(result_filter);
+ break;
+ }
+ case DRSUAPI_DS_NAME_FORMAT_DISPLAY: {
+ const char *name_encoded = NULL;
+
+ domain_filter = NULL;
+
+ name_encoded = ldb_binary_encode_string(mem_ctx, name);
+ if (name_encoded == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ result_filter = talloc_asprintf(mem_ctx, "(|(displayName=%s)(samAccountName=%s))",
+ name_encoded,
+ name_encoded);
+ W_ERROR_HAVE_NO_MEMORY(result_filter);
+ break;
+ }
+
+ /* A S-1234-5678 style string */
+ case DRSUAPI_DS_NAME_FORMAT_SID_OR_SID_HISTORY: {
+ struct dom_sid *sid = dom_sid_parse_talloc(mem_ctx, name);
+ char *ldap_sid;
+
+ domain_filter = NULL;
+ if (!sid) {
+ info1->dns_domain_name = NULL;
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ }
+ ldap_sid = ldap_encode_ndr_dom_sid(mem_ctx,
+ sid);
+ if (!ldap_sid) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ result_filter = talloc_asprintf(mem_ctx, "(objectSid=%s)",
+ ldap_sid);
+ W_ERROR_HAVE_NO_MEMORY(result_filter);
+ break;
+ }
+ case DRSUAPI_DS_NAME_FORMAT_USER_PRINCIPAL: {
+ krb5_principal principal;
+ char *unparsed_name;
+ const char *unparsed_name_encoded = NULL;
+
+ ret = smb_krb5_init_context(mem_ctx,
+ (struct loadparm_context *)ldb_get_opaque(sam_ctx, "loadparm"),
+ &smb_krb5_context);
+
+ if (ret) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ /* Ensure we reject complete junk first */
+ ret = krb5_parse_name(smb_krb5_context->krb5_context, name, &principal);
+ if (ret) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ }
+
+ domain_filter = NULL;
+
+ /*
+ * By getting the unparsed name here, we ensure the
+ * escaping is removed correctly (and trust the client
+ * less). The important thing here is that a
+ * userPrincipalName may have a space in it, and this
+ * must not be kerberos escaped to match this filter,
+ * so we specify KRB5_PRINCIPAL_UNPARSE_DISPLAY
+ */
+ ret = krb5_unparse_name_flags(smb_krb5_context->krb5_context,
+ principal,
+ KRB5_PRINCIPAL_UNPARSE_DISPLAY,
+ &unparsed_name);
+ if (ret) {
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+
+ /* The ldb_binary_encode_string() here avoids LDAP filter injection attacks */
+ unparsed_name_encoded = ldb_binary_encode_string(mem_ctx, unparsed_name);
+ if (unparsed_name_encoded == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ result_filter = talloc_asprintf(mem_ctx, "(&(userPrincipalName=%s)(objectClass=user))",
+ unparsed_name_encoded);
+
+ free(unparsed_name);
+ W_ERROR_HAVE_NO_MEMORY(result_filter);
+ break;
+ }
+ case DRSUAPI_DS_NAME_FORMAT_SERVICE_PRINCIPAL: {
+ krb5_principal principal;
+ char *unparsed_name_short;
+ const char *unparsed_name_short_encoded = NULL;
+ bool principal_is_host = false;
+
+ ret = smb_krb5_init_context(mem_ctx,
+ (struct loadparm_context *)ldb_get_opaque(sam_ctx, "loadparm"),
+ &smb_krb5_context);
+
+ if (ret) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ ret = krb5_parse_name(smb_krb5_context->krb5_context, name, &principal);
+ if (ret == 0 &&
+ krb5_princ_size(smb_krb5_context->krb5_context,
+ principal) < 2) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ return WERR_OK;
+ } else if (ret == 0) {
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ }
+ ret = krb5_parse_name_flags(smb_krb5_context->krb5_context, name,
+ KRB5_PRINCIPAL_PARSE_NO_REALM, &principal);
+ if (ret) {
+ return dns_domain_from_principal(mem_ctx, smb_krb5_context,
+ name, info1);
+ }
+
+ domain_filter = NULL;
+
+ ret = krb5_unparse_name_flags(smb_krb5_context->krb5_context, principal,
+ KRB5_PRINCIPAL_UNPARSE_NO_REALM, &unparsed_name_short);
+ if (ret) {
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ unparsed_name_short_encoded = ldb_binary_encode_string(mem_ctx, unparsed_name_short);
+ if (unparsed_name_short_encoded == NULL) {
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ free(unparsed_name_short);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ if ((krb5_princ_size(smb_krb5_context->krb5_context, principal) == 2)) {
+ krb5_data component;
+
+ ret = smb_krb5_princ_component(smb_krb5_context->krb5_context,
+ principal, 0, &component);
+ if (ret) {
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ free(unparsed_name_short);
+ return WERR_INTERNAL_ERROR;
+ }
+
+ principal_is_host = strcasecmp(component.data, "host") == 0;
+ }
+
+ if (principal_is_host) {
+ /* the 'cn' attribute is just the leading part of the name */
+ krb5_data component;
+ char *computer_name;
+ const char *computer_name_encoded = NULL;
+ ret = smb_krb5_princ_component(
+ smb_krb5_context->krb5_context,
+ principal, 1, &component);
+ if (ret) {
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ free(unparsed_name_short);
+ return WERR_INTERNAL_ERROR;
+ }
+ computer_name = talloc_strndup(mem_ctx, (char *)component.data,
+ strcspn((char *)component.data, "."));
+ if (computer_name == NULL) {
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ free(unparsed_name_short);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ computer_name_encoded = ldb_binary_encode_string(mem_ctx, computer_name);
+ if (computer_name_encoded == NULL) {
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ free(unparsed_name_short);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ result_filter = talloc_asprintf(mem_ctx, "(|(&(servicePrincipalName=%s)(objectClass=user))(&(cn=%s)(objectClass=computer)))",
+ unparsed_name_short_encoded,
+ computer_name_encoded);
+ } else {
+ result_filter = talloc_asprintf(mem_ctx, "(&(servicePrincipalName=%s)(objectClass=user))",
+ unparsed_name_short_encoded);
+ }
+ krb5_free_principal(smb_krb5_context->krb5_context, principal);
+ free(unparsed_name_short);
+ W_ERROR_HAVE_NO_MEMORY(result_filter);
+
+ break;
+ }
+ default: {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ }
+ }
+
+ if (format_flags & DRSUAPI_DS_NAME_FLAG_SYNTACTICAL_ONLY) {
+ return DsCrackNameOneSyntactical(mem_ctx, format_offered, format_desired,
+ name_dn, name, info1);
+ }
+
+ return DsCrackNameOneFilter(sam_ctx, mem_ctx,
+ smb_krb5_context,
+ format_flags, format_offered, format_desired,
+ name_dn, name,
+ domain_filter, result_filter,
+ info1, scope, search_dn);
+}
+
+/* Subcase of CrackNames. It is possible to translate a LDAP-style DN
+ * (FQDN_1779) into a canonical name without actually searching the
+ * database */
+
+static WERROR DsCrackNameOneSyntactical(TALLOC_CTX *mem_ctx,
+ enum drsuapi_DsNameFormat format_offered,
+ enum drsuapi_DsNameFormat format_desired,
+ struct ldb_dn *name_dn, const char *name,
+ struct drsuapi_DsNameInfo1 *info1)
+{
+ char *cracked;
+ if (format_offered != DRSUAPI_DS_NAME_FORMAT_FQDN_1779) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NO_SYNTACTICAL_MAPPING;
+ return WERR_OK;
+ }
+
+ switch (format_desired) {
+ case DRSUAPI_DS_NAME_FORMAT_CANONICAL:
+ cracked = ldb_dn_canonical_string(mem_ctx, name_dn);
+ break;
+ case DRSUAPI_DS_NAME_FORMAT_CANONICAL_EX:
+ cracked = ldb_dn_canonical_ex_string(mem_ctx, name_dn);
+ break;
+ default:
+ info1->status = DRSUAPI_DS_NAME_STATUS_NO_SYNTACTICAL_MAPPING;
+ return WERR_OK;
+ }
+ info1->status = DRSUAPI_DS_NAME_STATUS_OK;
+ info1->result_name = cracked;
+ if (!cracked) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ return WERR_OK;
+}
+
+/* Given a filter for the domain, and one for the result, perform the
+ * ldb search. The format offered and desired flags change the
+ * behaviours, including what attributes to return.
+ *
+ * The smb_krb5_context is required because we use the krb5 libs for principal parsing
+ */
+
+static WERROR DsCrackNameOneFilter(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx,
+ struct smb_krb5_context *smb_krb5_context,
+ uint32_t format_flags, enum drsuapi_DsNameFormat format_offered,
+ enum drsuapi_DsNameFormat format_desired,
+ struct ldb_dn *name_dn, const char *name,
+ const char *domain_filter, const char *result_filter,
+ struct drsuapi_DsNameInfo1 *info1,
+ int scope, struct ldb_dn *search_dn)
+{
+ int ldb_ret;
+ struct ldb_result *domain_res = NULL;
+ const char * const *domain_attrs;
+ const char * const *result_attrs;
+ struct ldb_message **result_res = NULL;
+ struct ldb_message *result = NULL;
+ int i;
+ char *p;
+ struct ldb_dn *partitions_basedn = samdb_partitions_dn(sam_ctx, mem_ctx);
+
+ const char * const _domain_attrs_1779[] = { "ncName", "dnsRoot", NULL};
+ const char * const _result_attrs_null[] = { NULL };
+
+ const char * const _domain_attrs_canonical[] = { "ncName", "dnsRoot", NULL};
+ const char * const _result_attrs_canonical[] = { "canonicalName", NULL };
+
+ const char * const _domain_attrs_nt4[] = { "ncName", "dnsRoot", "nETBIOSName", NULL};
+ const char * const _result_attrs_nt4[] = { "sAMAccountName", "objectSid", "objectClass", NULL};
+
+ const char * const _domain_attrs_guid[] = { "ncName", "dnsRoot", NULL};
+ const char * const _result_attrs_guid[] = { "objectGUID", NULL};
+
+ const char * const _domain_attrs_upn[] = { "ncName", "dnsRoot", NULL};
+ const char * const _result_attrs_upn[] = { "userPrincipalName", NULL};
+
+ const char * const _domain_attrs_spn[] = { "ncName", "dnsRoot", NULL};
+ const char * const _result_attrs_spn[] = { "servicePrincipalName", NULL};
+
+ const char * const _domain_attrs_display[] = { "ncName", "dnsRoot", NULL};
+ const char * const _result_attrs_display[] = { "displayName", "samAccountName", NULL};
+
+ const char * const _domain_attrs_sid[] = { "ncName", "dnsRoot", NULL};
+ const char * const _result_attrs_sid[] = { "objectSid", NULL};
+
+ const char * const _domain_attrs_none[] = { "ncName", "dnsRoot" , NULL};
+ const char * const _result_attrs_none[] = { NULL};
+
+ /* here we need to set the attrs lists for domain and result lookups */
+ switch (format_desired) {
+ case DRSUAPI_DS_NAME_FORMAT_FQDN_1779:
+ case DRSUAPI_DS_NAME_FORMAT_CANONICAL_EX:
+ domain_attrs = _domain_attrs_1779;
+ result_attrs = _result_attrs_null;
+ break;
+ case DRSUAPI_DS_NAME_FORMAT_CANONICAL:
+ domain_attrs = _domain_attrs_canonical;
+ result_attrs = _result_attrs_canonical;
+ break;
+ case DRSUAPI_DS_NAME_FORMAT_NT4_ACCOUNT:
+ domain_attrs = _domain_attrs_nt4;
+ result_attrs = _result_attrs_nt4;
+ break;
+ case DRSUAPI_DS_NAME_FORMAT_GUID:
+ domain_attrs = _domain_attrs_guid;
+ result_attrs = _result_attrs_guid;
+ break;
+ case DRSUAPI_DS_NAME_FORMAT_DISPLAY:
+ domain_attrs = _domain_attrs_display;
+ result_attrs = _result_attrs_display;
+ break;
+ case DRSUAPI_DS_NAME_FORMAT_USER_PRINCIPAL:
+ domain_attrs = _domain_attrs_upn;
+ result_attrs = _result_attrs_upn;
+ break;
+ case DRSUAPI_DS_NAME_FORMAT_SERVICE_PRINCIPAL:
+ domain_attrs = _domain_attrs_spn;
+ result_attrs = _result_attrs_spn;
+ break;
+ case DRSUAPI_DS_NAME_FORMAT_SID_OR_SID_HISTORY:
+ domain_attrs = _domain_attrs_sid;
+ result_attrs = _result_attrs_sid;
+ break;
+ default:
+ domain_attrs = _domain_attrs_none;
+ result_attrs = _result_attrs_none;
+ break;
+ }
+
+ if (domain_filter) {
+ /* if we have a domain_filter look it up and set the result_basedn and the dns_domain_name */
+ ldb_ret = ldb_search(sam_ctx, mem_ctx, &domain_res,
+ partitions_basedn,
+ LDB_SCOPE_ONELEVEL,
+ domain_attrs,
+ "%s", domain_filter);
+
+ if (ldb_ret != LDB_SUCCESS) {
+ DEBUG(2, ("DsCrackNameOneFilter domain ref search failed: %s\n", ldb_errstring(sam_ctx)));
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ return WERR_OK;
+ }
+
+ switch (domain_res->count) {
+ case 1:
+ break;
+ case 0:
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ default:
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE;
+ return WERR_OK;
+ }
+
+ info1->dns_domain_name = ldb_msg_find_attr_as_string(domain_res->msgs[0], "dnsRoot", NULL);
+ W_ERROR_HAVE_NO_MEMORY(info1->dns_domain_name);
+ info1->status = DRSUAPI_DS_NAME_STATUS_DOMAIN_ONLY;
+ } else {
+ info1->dns_domain_name = NULL;
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ }
+
+ if (result_filter) {
+ int ret;
+ struct ldb_result *res;
+ uint32_t dsdb_flags = 0;
+ struct ldb_dn *real_search_dn = NULL;
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+
+ /*
+ * From 4.1.4.2.11 of MS-DRSR
+ * if DS_NAME_FLAG_GCVERIFY in flags then
+ * rt := select all O from all
+ * where attrValue in GetAttrVals(O, att, false)
+ * else
+ * rt := select all O from subtree DefaultNC()
+ * where attrValue in GetAttrVals(O, att, false)
+ * endif
+ * return rt
+ */
+ if (format_flags & DRSUAPI_DS_NAME_FLAG_GCVERIFY ||
+ format_offered == DRSUAPI_DS_NAME_FORMAT_GUID)
+ {
+ dsdb_flags = DSDB_SEARCH_SEARCH_ALL_PARTITIONS;
+ } else if (domain_res) {
+ if (!search_dn) {
+ struct ldb_dn *tmp_dn = samdb_result_dn(sam_ctx, mem_ctx, domain_res->msgs[0], "ncName", NULL);
+ real_search_dn = tmp_dn;
+ } else {
+ real_search_dn = search_dn;
+ }
+ } else {
+ real_search_dn = ldb_get_default_basedn(sam_ctx);
+ }
+ if (format_offered == DRSUAPI_DS_NAME_FORMAT_GUID){
+ dsdb_flags |= DSDB_SEARCH_SHOW_RECYCLED;
+ }
+ /* search with the 'phantom root' flag */
+ ret = dsdb_search(sam_ctx, mem_ctx, &res,
+ real_search_dn,
+ scope,
+ result_attrs,
+ dsdb_flags,
+ "%s", result_filter);
+ if (ret != LDB_SUCCESS) {
+ DEBUG(2, ("DsCrackNameOneFilter search from '%s' with flags 0x%08x failed: %s\n",
+ ldb_dn_get_linearized(real_search_dn),
+ dsdb_flags,
+ ldb_errstring(sam_ctx)));
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ return WERR_OK;
+ }
+
+ ldb_ret = res->count;
+ result_res = res->msgs;
+ } else if (format_offered == DRSUAPI_DS_NAME_FORMAT_FQDN_1779) {
+ ldb_ret = gendb_search_dn(sam_ctx, mem_ctx, name_dn, &result_res,
+ result_attrs);
+ } else if (domain_res) {
+ name_dn = samdb_result_dn(sam_ctx, mem_ctx, domain_res->msgs[0], "ncName", NULL);
+ ldb_ret = gendb_search_dn(sam_ctx, mem_ctx, name_dn, &result_res,
+ result_attrs);
+ } else {
+ /* Can't happen */
+ DEBUG(0, ("LOGIC ERROR: DsCrackNameOneFilter domain ref search not available: This can't happen...\n"));
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ return WERR_OK;
+ }
+
+ switch (ldb_ret) {
+ case 1:
+ result = result_res[0];
+ break;
+ case 0:
+ switch (format_offered) {
+ case DRSUAPI_DS_NAME_FORMAT_SERVICE_PRINCIPAL:
+ return DsCrackNameSPNAlias(sam_ctx, mem_ctx,
+ smb_krb5_context,
+ format_flags, format_offered, format_desired,
+ name, info1);
+
+ case DRSUAPI_DS_NAME_FORMAT_USER_PRINCIPAL:
+ return DsCrackNameUPN(sam_ctx, mem_ctx, smb_krb5_context,
+ format_flags, format_offered, format_desired,
+ name, info1);
+ default:
+ break;
+ }
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ case -1:
+ DEBUG(2, ("DsCrackNameOneFilter result search failed: %s\n", ldb_errstring(sam_ctx)));
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ return WERR_OK;
+ default:
+ switch (format_offered) {
+ case DRSUAPI_DS_NAME_FORMAT_CANONICAL:
+ case DRSUAPI_DS_NAME_FORMAT_CANONICAL_EX:
+ {
+ const char *canonical_name = NULL; /* Not required, but we get warnings... */
+ /* We may need to manually filter further */
+ for (i = 0; i < ldb_ret; i++) {
+ switch (format_offered) {
+ case DRSUAPI_DS_NAME_FORMAT_CANONICAL:
+ canonical_name = ldb_dn_canonical_string(mem_ctx, result_res[i]->dn);
+ break;
+ case DRSUAPI_DS_NAME_FORMAT_CANONICAL_EX:
+ canonical_name = ldb_dn_canonical_ex_string(mem_ctx, result_res[i]->dn);
+ break;
+ default:
+ break;
+ }
+ if (strcasecmp_m(canonical_name, name) == 0) {
+ result = result_res[i];
+ break;
+ }
+ }
+ if (!result) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ }
+ }
+ FALL_THROUGH;
+ default:
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE;
+ return WERR_OK;
+ }
+ }
+
+ info1->dns_domain_name = ldb_dn_canonical_string(mem_ctx, result->dn);
+ W_ERROR_HAVE_NO_MEMORY(info1->dns_domain_name);
+ p = strchr(info1->dns_domain_name, '/');
+ if (p) {
+ p[0] = '\0';
+ }
+
+ /* here we can use result and domain_res[0] */
+ switch (format_desired) {
+ case DRSUAPI_DS_NAME_FORMAT_FQDN_1779: {
+ info1->result_name = ldb_dn_alloc_linearized(mem_ctx, result->dn);
+ W_ERROR_HAVE_NO_MEMORY(info1->result_name);
+
+ info1->status = DRSUAPI_DS_NAME_STATUS_OK;
+ return WERR_OK;
+ }
+ case DRSUAPI_DS_NAME_FORMAT_CANONICAL: {
+ info1->result_name = ldb_msg_find_attr_as_string(result, "canonicalName", NULL);
+ info1->status = DRSUAPI_DS_NAME_STATUS_OK;
+ return WERR_OK;
+ }
+ case DRSUAPI_DS_NAME_FORMAT_CANONICAL_EX: {
+ /* Not in the virtual ldb attribute */
+ return DsCrackNameOneSyntactical(mem_ctx,
+ DRSUAPI_DS_NAME_FORMAT_FQDN_1779,
+ DRSUAPI_DS_NAME_FORMAT_CANONICAL_EX,
+ result->dn, name, info1);
+ }
+ case DRSUAPI_DS_NAME_FORMAT_NT4_ACCOUNT: {
+
+ const struct dom_sid *sid = samdb_result_dom_sid(mem_ctx, result, "objectSid");
+ const char *_acc = "", *_dom = "";
+ if (sid == NULL) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NO_MAPPING;
+ return WERR_OK;
+ }
+
+ if (samdb_find_attribute(sam_ctx, result, "objectClass",
+ "domain")) {
+ /* This can also find a DomainDNSZones entry,
+ * but it won't have the SID we just
+ * checked. */
+ ldb_ret = ldb_search(sam_ctx, mem_ctx, &domain_res,
+ partitions_basedn,
+ LDB_SCOPE_ONELEVEL,
+ domain_attrs,
+ "(ncName=%s)", ldb_dn_get_linearized(result->dn));
+
+ if (ldb_ret != LDB_SUCCESS) {
+ DEBUG(2, ("DsCrackNameOneFilter domain ref search failed: %s\n", ldb_errstring(sam_ctx)));
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ return WERR_OK;
+ }
+
+ switch (domain_res->count) {
+ case 1:
+ break;
+ case 0:
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ default:
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE;
+ return WERR_OK;
+ }
+ _dom = ldb_msg_find_attr_as_string(domain_res->msgs[0], "nETBIOSName", NULL);
+ W_ERROR_HAVE_NO_MEMORY(_dom);
+ } else {
+ _acc = ldb_msg_find_attr_as_string(result, "sAMAccountName", NULL);
+ if (!_acc) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NO_MAPPING;
+ return WERR_OK;
+ }
+ if (dom_sid_in_domain(&global_sid_Builtin, sid)) {
+ _dom = "BUILTIN";
+ } else {
+ const char *attrs[] = { NULL };
+ struct ldb_result *domain_res2;
+ struct dom_sid *dom_sid = dom_sid_dup(mem_ctx, sid);
+ if (!dom_sid) {
+ return WERR_OK;
+ }
+ dom_sid->num_auths--;
+ ldb_ret = ldb_search(sam_ctx, mem_ctx, &domain_res,
+ NULL,
+ LDB_SCOPE_BASE,
+ attrs,
+ "(&(objectSid=%s)(objectClass=domain))",
+ ldap_encode_ndr_dom_sid(mem_ctx, dom_sid));
+
+ if (ldb_ret != LDB_SUCCESS) {
+ DEBUG(2, ("DsCrackNameOneFilter domain search failed: %s\n", ldb_errstring(sam_ctx)));
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ return WERR_OK;
+ }
+
+ switch (domain_res->count) {
+ case 1:
+ break;
+ case 0:
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ default:
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE;
+ return WERR_OK;
+ }
+
+ ldb_ret = ldb_search(sam_ctx, mem_ctx, &domain_res2,
+ partitions_basedn,
+ LDB_SCOPE_ONELEVEL,
+ domain_attrs,
+ "(ncName=%s)", ldb_dn_get_linearized(domain_res->msgs[0]->dn));
+
+ if (ldb_ret != LDB_SUCCESS) {
+ DEBUG(2, ("DsCrackNameOneFilter domain ref search failed: %s\n", ldb_errstring(sam_ctx)));
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ return WERR_OK;
+ }
+
+ switch (domain_res2->count) {
+ case 1:
+ break;
+ case 0:
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ default:
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE;
+ return WERR_OK;
+ }
+ _dom = ldb_msg_find_attr_as_string(domain_res2->msgs[0], "nETBIOSName", NULL);
+ W_ERROR_HAVE_NO_MEMORY(_dom);
+ }
+ }
+
+ info1->result_name = talloc_asprintf(mem_ctx, "%s\\%s", _dom, _acc);
+ W_ERROR_HAVE_NO_MEMORY(info1->result_name);
+
+ info1->status = DRSUAPI_DS_NAME_STATUS_OK;
+ return WERR_OK;
+ }
+ case DRSUAPI_DS_NAME_FORMAT_GUID: {
+ struct GUID guid;
+
+ guid = samdb_result_guid(result, "objectGUID");
+
+ info1->result_name = GUID_string2(mem_ctx, &guid);
+ W_ERROR_HAVE_NO_MEMORY(info1->result_name);
+
+ info1->status = DRSUAPI_DS_NAME_STATUS_OK;
+ return WERR_OK;
+ }
+ case DRSUAPI_DS_NAME_FORMAT_DISPLAY: {
+ info1->result_name = ldb_msg_find_attr_as_string(result, "displayName", NULL);
+ if (!info1->result_name) {
+ info1->result_name = ldb_msg_find_attr_as_string(result, "sAMAccountName", NULL);
+ }
+ if (!info1->result_name) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ } else {
+ info1->status = DRSUAPI_DS_NAME_STATUS_OK;
+ }
+ return WERR_OK;
+ }
+ case DRSUAPI_DS_NAME_FORMAT_SERVICE_PRINCIPAL: {
+ struct ldb_message_element *el
+ = ldb_msg_find_element(result,
+ "servicePrincipalName");
+ if (el == NULL) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ return WERR_OK;
+ } else if (el->num_values > 1) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE;
+ return WERR_OK;
+ }
+
+ info1->result_name = ldb_msg_find_attr_as_string(result, "servicePrincipalName", NULL);
+ if (!info1->result_name) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NO_MAPPING;
+ } else {
+ info1->status = DRSUAPI_DS_NAME_STATUS_OK;
+ }
+ return WERR_OK;
+ }
+ case DRSUAPI_DS_NAME_FORMAT_DNS_DOMAIN: {
+ info1->dns_domain_name = NULL;
+ info1->status = DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR;
+ return WERR_OK;
+ }
+ case DRSUAPI_DS_NAME_FORMAT_SID_OR_SID_HISTORY: {
+ const struct dom_sid *sid = samdb_result_dom_sid(mem_ctx, result, "objectSid");
+
+ if (sid == NULL) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NO_MAPPING;
+ return WERR_OK;
+ }
+
+ info1->result_name = dom_sid_string(mem_ctx, sid);
+ W_ERROR_HAVE_NO_MEMORY(info1->result_name);
+
+ info1->status = DRSUAPI_DS_NAME_STATUS_OK;
+ return WERR_OK;
+ }
+ case DRSUAPI_DS_NAME_FORMAT_USER_PRINCIPAL: {
+ info1->result_name = ldb_msg_find_attr_as_string(result, "userPrincipalName", NULL);
+ if (!info1->result_name) {
+ info1->status = DRSUAPI_DS_NAME_STATUS_NO_MAPPING;
+ } else {
+ info1->status = DRSUAPI_DS_NAME_STATUS_OK;
+ }
+ return WERR_OK;
+ }
+ default:
+ info1->status = DRSUAPI_DS_NAME_STATUS_NO_MAPPING;
+ return WERR_OK;
+ }
+}
+
+/* Given a user Principal Name (such as foo@bar.com),
+ * return the user and domain DNs. This is used in the KDC to then
+ * return the Keys and evaluate policy */
+
+NTSTATUS crack_user_principal_name(struct ldb_context *sam_ctx,
+ TALLOC_CTX *mem_ctx,
+ const char *user_principal_name,
+ struct ldb_dn **user_dn,
+ struct ldb_dn **domain_dn)
+{
+ WERROR werr;
+ struct drsuapi_DsNameInfo1 info1;
+ werr = DsCrackNameOneName(sam_ctx, mem_ctx, 0,
+ DRSUAPI_DS_NAME_FORMAT_USER_PRINCIPAL,
+ DRSUAPI_DS_NAME_FORMAT_FQDN_1779,
+ user_principal_name,
+ &info1);
+ if (!W_ERROR_IS_OK(werr)) {
+ return werror_to_ntstatus(werr);
+ }
+ switch (info1.status) {
+ case DRSUAPI_DS_NAME_STATUS_OK:
+ break;
+ case DRSUAPI_DS_NAME_STATUS_NOT_FOUND:
+ case DRSUAPI_DS_NAME_STATUS_DOMAIN_ONLY:
+ case DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE:
+ return NT_STATUS_NO_SUCH_USER;
+ case DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR:
+ default:
+ return NT_STATUS_UNSUCCESSFUL;
+ }
+
+ *user_dn = ldb_dn_new(mem_ctx, sam_ctx, info1.result_name);
+
+ if (domain_dn) {
+ werr = DsCrackNameOneName(sam_ctx, mem_ctx, 0,
+ DRSUAPI_DS_NAME_FORMAT_CANONICAL,
+ DRSUAPI_DS_NAME_FORMAT_FQDN_1779,
+ talloc_asprintf(mem_ctx, "%s/",
+ info1.dns_domain_name),
+ &info1);
+ if (!W_ERROR_IS_OK(werr)) {
+ return werror_to_ntstatus(werr);
+ }
+ switch (info1.status) {
+ case DRSUAPI_DS_NAME_STATUS_OK:
+ break;
+ case DRSUAPI_DS_NAME_STATUS_NOT_FOUND:
+ case DRSUAPI_DS_NAME_STATUS_DOMAIN_ONLY:
+ case DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE:
+ return NT_STATUS_NO_SUCH_USER;
+ case DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR:
+ default:
+ return NT_STATUS_UNSUCCESSFUL;
+ }
+
+ *domain_dn = ldb_dn_new(mem_ctx, sam_ctx, info1.result_name);
+ }
+
+ return NT_STATUS_OK;
+}
+
+/* Given a Service Principal Name (such as host/foo.bar.com@BAR.COM),
+ * return the user and domain DNs. This is used in the KDC to then
+ * return the Keys and evaluate policy */
+
+NTSTATUS crack_service_principal_name(struct ldb_context *sam_ctx,
+ TALLOC_CTX *mem_ctx,
+ const char *service_principal_name,
+ struct ldb_dn **user_dn,
+ struct ldb_dn **domain_dn)
+{
+ WERROR werr;
+ struct drsuapi_DsNameInfo1 info1;
+ werr = DsCrackNameOneName(sam_ctx, mem_ctx, 0,
+ DRSUAPI_DS_NAME_FORMAT_SERVICE_PRINCIPAL,
+ DRSUAPI_DS_NAME_FORMAT_FQDN_1779,
+ service_principal_name,
+ &info1);
+ if (!W_ERROR_IS_OK(werr)) {
+ return werror_to_ntstatus(werr);
+ }
+ switch (info1.status) {
+ case DRSUAPI_DS_NAME_STATUS_OK:
+ break;
+ case DRSUAPI_DS_NAME_STATUS_NOT_FOUND:
+ case DRSUAPI_DS_NAME_STATUS_DOMAIN_ONLY:
+ case DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE:
+ return NT_STATUS_NO_SUCH_USER;
+ case DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR:
+ default:
+ return NT_STATUS_UNSUCCESSFUL;
+ }
+
+ *user_dn = ldb_dn_new(mem_ctx, sam_ctx, info1.result_name);
+
+ if (domain_dn) {
+ werr = DsCrackNameOneName(sam_ctx, mem_ctx, 0,
+ DRSUAPI_DS_NAME_FORMAT_CANONICAL,
+ DRSUAPI_DS_NAME_FORMAT_FQDN_1779,
+ talloc_asprintf(mem_ctx, "%s/",
+ info1.dns_domain_name),
+ &info1);
+ if (!W_ERROR_IS_OK(werr)) {
+ return werror_to_ntstatus(werr);
+ }
+ switch (info1.status) {
+ case DRSUAPI_DS_NAME_STATUS_OK:
+ break;
+ case DRSUAPI_DS_NAME_STATUS_NOT_FOUND:
+ case DRSUAPI_DS_NAME_STATUS_DOMAIN_ONLY:
+ case DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE:
+ return NT_STATUS_NO_SUCH_USER;
+ case DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR:
+ default:
+ return NT_STATUS_UNSUCCESSFUL;
+ }
+
+ *domain_dn = ldb_dn_new(mem_ctx, sam_ctx, info1.result_name);
+ }
+
+ return NT_STATUS_OK;
+}
+
+NTSTATUS crack_name_to_nt4_name(TALLOC_CTX *mem_ctx,
+ struct ldb_context *ldb,
+ enum drsuapi_DsNameFormat format_offered,
+ const char *name,
+ const char **nt4_domain, const char **nt4_account)
+{
+ WERROR werr;
+ struct drsuapi_DsNameInfo1 info1;
+ char *p;
+
+ /* Handle anonymous bind */
+ if (!name || !*name) {
+ *nt4_domain = "";
+ *nt4_account = "";
+ return NT_STATUS_OK;
+ }
+
+ werr = DsCrackNameOneName(ldb, mem_ctx, 0,
+ format_offered,
+ DRSUAPI_DS_NAME_FORMAT_NT4_ACCOUNT,
+ name,
+ &info1);
+ if (!W_ERROR_IS_OK(werr)) {
+ return werror_to_ntstatus(werr);
+ }
+ switch (info1.status) {
+ case DRSUAPI_DS_NAME_STATUS_OK:
+ break;
+ case DRSUAPI_DS_NAME_STATUS_NOT_FOUND:
+ case DRSUAPI_DS_NAME_STATUS_DOMAIN_ONLY:
+ case DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE:
+ return NT_STATUS_NO_SUCH_USER;
+ case DRSUAPI_DS_NAME_STATUS_RESOLVE_ERROR:
+ default:
+ return NT_STATUS_UNSUCCESSFUL;
+ }
+
+ *nt4_domain = talloc_strdup(mem_ctx, info1.result_name);
+ if (*nt4_domain == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ p = strchr(*nt4_domain, '\\');
+ if (!p) {
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+ p[0] = '\0';
+
+ *nt4_account = talloc_strdup(mem_ctx, &p[1]);
+ if (*nt4_account == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ return NT_STATUS_OK;
+}
+
+NTSTATUS crack_auto_name_to_nt4_name(TALLOC_CTX *mem_ctx,
+ struct ldb_context *ldb,
+ const char *name,
+ const char **nt4_domain,
+ const char **nt4_account)
+{
+ enum drsuapi_DsNameFormat format_offered = DRSUAPI_DS_NAME_FORMAT_UNKNOWN;
+
+ /* Handle anonymous bind */
+ if (!name || !*name) {
+ *nt4_domain = "";
+ *nt4_account = "";
+ return NT_STATUS_OK;
+ }
+
+ /*
+ * Here we only consider a subset of the possible name forms listed in
+ * [MS-ADTS] 5.1.1.1.1, and we don't retry with a different name form if
+ * the first attempt fails.
+ */
+
+ if (strchr_m(name, '=')) {
+ format_offered = DRSUAPI_DS_NAME_FORMAT_FQDN_1779;
+ } else if (strchr_m(name, '@')) {
+ format_offered = DRSUAPI_DS_NAME_FORMAT_USER_PRINCIPAL;
+ } else if (strchr_m(name, '\\')) {
+ format_offered = DRSUAPI_DS_NAME_FORMAT_NT4_ACCOUNT;
+ } else if (strchr_m(name, '\n')) {
+ format_offered = DRSUAPI_DS_NAME_FORMAT_CANONICAL_EX;
+ } else if (strchr_m(name, '/')) {
+ format_offered = DRSUAPI_DS_NAME_FORMAT_CANONICAL;
+ } else if ((name[0] == 'S' || name[0] == 's') && name[1] == '-') {
+ format_offered = DRSUAPI_DS_NAME_FORMAT_SID_OR_SID_HISTORY;
+ } else {
+ return NT_STATUS_NO_SUCH_USER;
+ }
+
+ return crack_name_to_nt4_name(mem_ctx, ldb, format_offered, name, nt4_domain, nt4_account);
+}
+
+
+WERROR dcesrv_drsuapi_ListRoles(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx,
+ const struct drsuapi_DsNameRequest1 *req1,
+ struct drsuapi_DsNameCtr1 **ctr1)
+{
+ struct drsuapi_DsNameInfo1 *names;
+ uint32_t i;
+ uint32_t count = 5;/*number of fsmo role owners we are going to return*/
+
+ *ctr1 = talloc(mem_ctx, struct drsuapi_DsNameCtr1);
+ W_ERROR_HAVE_NO_MEMORY(*ctr1);
+ names = talloc_array(mem_ctx, struct drsuapi_DsNameInfo1, count);
+ W_ERROR_HAVE_NO_MEMORY(names);
+
+ for (i = 0; i < count; i++) {
+ WERROR werr;
+ struct ldb_dn *role_owner_dn, *fsmo_role_dn, *server_dn;
+ werr = dsdb_get_fsmo_role_info(mem_ctx, sam_ctx, i,
+ &fsmo_role_dn, &role_owner_dn);
+ if(!W_ERROR_IS_OK(werr)) {
+ return werr;
+ }
+ server_dn = ldb_dn_copy(mem_ctx, role_owner_dn);
+ ldb_dn_remove_child_components(server_dn, 1);
+ names[i].status = DRSUAPI_DS_NAME_STATUS_OK;
+ names[i].dns_domain_name = samdb_dn_to_dnshostname(sam_ctx, mem_ctx,
+ server_dn);
+ if(!names[i].dns_domain_name) {
+ DEBUG(4, ("list_roles: Failed to find dNSHostName for server %s\n",
+ ldb_dn_get_linearized(server_dn)));
+ }
+ names[i].result_name = talloc_strdup(mem_ctx, ldb_dn_get_linearized(role_owner_dn));
+ }
+
+ (*ctr1)->count = count;
+ (*ctr1)->array = names;
+
+ return WERR_OK;
+}
+
+WERROR dcesrv_drsuapi_CrackNamesByNameFormat(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx,
+ const struct drsuapi_DsNameRequest1 *req1,
+ struct drsuapi_DsNameCtr1 **ctr1)
+{
+ struct drsuapi_DsNameInfo1 *names;
+ uint32_t i, count;
+ WERROR status;
+
+ *ctr1 = talloc_zero(mem_ctx, struct drsuapi_DsNameCtr1);
+ W_ERROR_HAVE_NO_MEMORY(*ctr1);
+
+ count = req1->count;
+ names = talloc_array(mem_ctx, struct drsuapi_DsNameInfo1, count);
+ W_ERROR_HAVE_NO_MEMORY(names);
+
+ for (i=0; i < count; i++) {
+ status = DsCrackNameOneName(sam_ctx, mem_ctx,
+ req1->format_flags,
+ req1->format_offered,
+ req1->format_desired,
+ req1->names[i].str,
+ &names[i]);
+ if (!W_ERROR_IS_OK(status)) {
+ return status;
+ }
+ }
+
+ (*ctr1)->count = count;
+ (*ctr1)->array = names;
+
+ return WERR_OK;
+}
+
+WERROR dcesrv_drsuapi_ListInfoServer(struct ldb_context *sam_ctx, TALLOC_CTX *mem_ctx,
+ const struct drsuapi_DsNameRequest1 *req1,
+ struct drsuapi_DsNameCtr1 **_ctr1)
+{
+ struct drsuapi_DsNameInfo1 *names;
+ struct ldb_result *res;
+ struct ldb_dn *server_dn, *dn;
+ struct drsuapi_DsNameCtr1 *ctr1;
+ int ret, i;
+ const char *str;
+ const char *attrs[] = {
+ "dNSHostName",
+ "serverReference",
+ NULL
+ };
+
+ *_ctr1 = NULL;
+
+ ctr1 = talloc_zero(mem_ctx, struct drsuapi_DsNameCtr1);
+ W_ERROR_HAVE_NO_MEMORY(ctr1);
+
+ /*
+ * No magic value here, we have to return 3 entries according to the
+ * MS-DRSR.pdf
+ */
+ ctr1->count = 3;
+ names = talloc_zero_array(ctr1, struct drsuapi_DsNameInfo1,
+ ctr1->count);
+ W_ERROR_HAVE_NO_MEMORY(names);
+ ctr1->array = names;
+
+ for (i=0; i < ctr1->count; i++) {
+ names[i].status = DRSUAPI_DS_NAME_STATUS_NOT_FOUND;
+ }
+ *_ctr1 = ctr1;
+
+ if (req1->count != 1) {
+ DEBUG(1, ("Expected a count of 1 for the ListInfoServer crackname \n"));
+ return WERR_OK;
+ }
+
+ if (req1->names[0].str == NULL) {
+ return WERR_OK;
+ }
+
+ server_dn = ldb_dn_new(mem_ctx, sam_ctx, req1->names[0].str);
+ W_ERROR_HAVE_NO_MEMORY(server_dn);
+
+ ret = ldb_search(sam_ctx, mem_ctx, &res, server_dn, LDB_SCOPE_ONELEVEL,
+ NULL, "(objectClass=nTDSDSA)");
+
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1, ("Search for objectClass=nTDSDSA "
+ "returned less than 1 objects\n"));
+ return WERR_OK;
+ }
+
+ if (res->count != 1) {
+ DEBUG(1, ("Search for objectClass=nTDSDSA "
+ "returned less than 1 objects\n"));
+ return WERR_OK;
+ }
+
+ if (res->msgs[0]->dn) {
+ names[0].result_name = ldb_dn_alloc_linearized(names, res->msgs[0]->dn);
+ W_ERROR_HAVE_NO_MEMORY(names[0].result_name);
+ names[0].status = DRSUAPI_DS_NAME_STATUS_OK;
+ }
+
+ talloc_free(res);
+
+ ret = ldb_search(sam_ctx, mem_ctx, &res, server_dn, LDB_SCOPE_BASE,
+ attrs, "(objectClass=*)");
+ if (ret != LDB_SUCCESS) {
+ DEBUG(1, ("Search for objectClass=* on dn %s"
+ "returned %s\n", req1->names[0].str,
+ ldb_strerror(ret)));
+ return WERR_OK;
+ }
+
+ if (res->count != 1) {
+ DEBUG(1, ("Search for objectClass=* on dn %s"
+ "returned less than 1 objects\n", req1->names[0].str));
+ return WERR_OK;
+ }
+
+ str = ldb_msg_find_attr_as_string(res->msgs[0], "dNSHostName", NULL);
+ if (str != NULL) {
+ names[1].result_name = talloc_strdup(names, str);
+ W_ERROR_HAVE_NO_MEMORY(names[1].result_name);
+ names[1].status = DRSUAPI_DS_NAME_STATUS_OK;
+ }
+
+ dn = ldb_msg_find_attr_as_dn(sam_ctx, mem_ctx, res->msgs[0], "serverReference");
+ if (dn != NULL) {
+ names[2].result_name = ldb_dn_alloc_linearized(names, dn);
+ W_ERROR_HAVE_NO_MEMORY(names[2].result_name);
+ names[2].status = DRSUAPI_DS_NAME_STATUS_OK;
+ }
+
+ talloc_free(dn);
+ talloc_free(res);
+
+ return WERR_OK;
+}
diff --git a/source4/dsdb/samdb/ldb_modules/acl.c b/source4/dsdb/samdb/ldb_modules/acl.c
new file mode 100644
index 0000000..b8ee21b
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/acl.c
@@ -0,0 +1,2892 @@
+/*
+ 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 reigns */
+ 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 reigns */
+ 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 reigns */
+ 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;
+ }
+
+ /*
+ * This call is made with
+ * IMPLICIT_OWNER_READ_CONTROL_AND_WRITE_DAC_RIGHTS
+ * and without reference to the dSHeuristics via
+ * dsdb_block_owner_implicit_rights(). This is
+ * probably a Windows bug but for now we match
+ * exactly.
+ */
+ ret = acl_check_access_on_attribute_implicit_owner(
+ module,
+ msg,
+ sd,
+ sid,
+ SEC_STD_WRITE_DAC,
+ attr,
+ objectclass,
+ IMPLICIT_OWNER_READ_CONTROL_AND_WRITE_DAC_RIGHTS);
+ 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;
+ }
+ }
+
+ if (flags != (SECINFO_OWNER | SECINFO_GROUP | SECINFO_DACL | SECINFO_SACL)) {
+ const struct ldb_message_element *el = samdb_find_attribute(ldb,
+ sd_msg,
+ "objectclass",
+ "computer");
+ if (el != NULL) {
+ return LDB_SUCCESS;
+ }
+ }
+
+ 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)
+{
+ krb5_error_code ret, princ_size;
+ krb5_context krb_ctx;
+ krb5_error_code kerr;
+ krb5_principal principal;
+ char *instanceName = NULL;
+ char *serviceType = NULL;
+ char *serviceName = NULL;
+ 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;
+ }
+
+ ret = smb_krb5_principal_get_comp_string(mem_ctx, krb_ctx,
+ principal, 1, &instanceName);
+ if (ret) {
+ goto fail;
+ }
+ ret = smb_krb5_principal_get_comp_string(mem_ctx, krb_ctx,
+ principal, 0, &serviceType);
+ if (ret) {
+ goto fail;
+ }
+ if (krb5_princ_size(krb_ctx, principal) == 3) {
+ ret = smb_krb5_principal_get_comp_string(mem_ctx, krb_ctx,
+ principal, 2, &serviceName);
+ if (ret) {
+ goto fail;
+ }
+ }
+
+ 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 = NULL;
+ 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;
+ const struct ldb_message *msg = NULL;
+ const struct ldb_message *search_res = NULL;
+
+ static const char *acl_attrs[] = {
+ "samAccountName",
+ "dnsHostName",
+ "userAccountControl",
+ NULL
+ };
+ static const char *netbios_attrs[] = {
+ "nETBIOSName",
+ NULL
+ };
+
+ if (req->operation == LDB_MODIFY) {
+ msg = req->op.mod.message;
+ } else if (req->operation == LDB_ADD) {
+ msg = req->op.add.message;
+ }
+
+ 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),
+ msg->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, 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) {
+ search_res = msg;
+ } else {
+ talloc_free(tmp_ctx);
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ if (req->operation == LDB_MODIFY) {
+ dns_host_name_val = ldb_msg_find_ldb_val(search_res, "dNSHostName");
+ }
+
+ ret = dsdb_msg_get_single_value(msg,
+ "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(search_res, "userAccountControl", 0);
+
+ if (req->operation == LDB_MODIFY) {
+ sam_account_name_val = ldb_msg_find_ldb_val(search_res, "sAMAccountName");
+ }
+
+ ret = dsdb_msg_get_single_value(msg,
+ "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
+ *
+ * Note - do we have the necessary attributes for this during an add operation?
+ * How should we test this?
+ */
+ if (userAccountControl & (UF_SERVER_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT)) {
+ ret = dsdb_module_find_ntdsguid_for_computer(module, tmp_ctx,
+ msg->dn, &ntds, req);
+ if (ret != LDB_SUCCESS) {
+ ldb_asprintf_errstring(ldb, "Failed to find NTDSDSA objectGuid for %s: %s",
+ ldb_dn_get_linearized(msg->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 (req->operation == LDB_MODIFY) {
+ msg = req->op.mod.message;
+ } else if (req->operation == LDB_ADD) {
+ msg = req->op.add.message;
+ }
+
+ 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),
+ msg->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
+ };
+
+ /*
+ * 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) {
+ 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;
+ }
+ }
+
+ if (req->operation == LDB_MODIFY) {
+ 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;
+}
+
+/* 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_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;
+ const struct dsdb_class *computer_objectclass = NULL;
+ const struct ldb_message_element *oc_el = NULL;
+ struct ldb_message_element sorted_oc_el;
+ struct ldb_control *as_system;
+ struct ldb_control *sd_ctrl = NULL;
+ struct ldb_message_element *el;
+ unsigned int instanceType = 0;
+ struct dsdb_control_calculated_default_sd *control_sd = NULL;
+ const struct dsdb_attribute *attr = NULL;
+ const char **must_contain = NULL;
+ const struct ldb_message *msg = req->op.add.message;
+ const struct dom_sid *domain_sid = NULL;
+ int i = 0;
+ bool attribute_authorization;
+ bool is_subclass;
+
+ 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;
+ }
+
+ if (dsdb_module_am_system(module) || as_system) {
+ return ldb_next_request(module, req);
+ }
+
+ ldb = ldb_module_get_ctx(module);
+ domain_sid = samdb_domain_sid(ldb);
+
+ parent = ldb_dn_get_parent(req, msg->dn);
+ if (parent == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ schema = dsdb_get_schema(ldb, req);
+ if (!schema) {
+ return ldb_operr(ldb);
+ }
+
+ /* Find the objectclass of the new account. */
+
+ oc_el = ldb_msg_find_element(msg, "objectclass");
+ if (oc_el == NULL) {
+ ldb_asprintf_errstring(ldb_module_get_ctx(module),
+ "acl: unable to find or validate structural objectClass on %s\n",
+ ldb_dn_get_linearized(msg->dn));
+ return ldb_module_done(req, NULL, NULL, LDB_ERR_OPERATIONS_ERROR);
+ }
+
+ schema = dsdb_get_schema(ldb, req);
+ if (schema == NULL) {
+ return ldb_operr(ldb);
+ }
+
+ ret = dsdb_sort_objectClass_attr(ldb, schema, oc_el, req, &sorted_oc_el);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ objectclass = dsdb_get_last_structural_class(schema, &sorted_oc_el);
+ if (objectclass == NULL) {
+ return ldb_operr(ldb);
+ }
+
+ el = ldb_msg_find_element(msg, "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(msg,
+ "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(msg->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
+ */
+ /* Note - do we need per-attribute checks? */
+ 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
+ */
+ } else {
+ 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(msg->dn));
+ return ret;
+ }
+ }
+
+ attribute_authorization = dsdb_attribute_authz_on_ldap_add(module,
+ req,
+ req);
+ if (!attribute_authorization) {
+ /* Skip the remaining checks */
+ goto success;
+ }
+
+ /* Check if we have computer objectclass. */
+ computer_objectclass = dsdb_class_by_lDAPDisplayName(schema, "computer");
+ if (computer_objectclass == NULL) {
+ return ldb_operr(ldb);
+ }
+
+ is_subclass = dsdb_is_subclass_of(schema, objectclass, computer_objectclass);
+ if (!is_subclass) {
+ /*
+ * This object is not a computer (or derived from computer), so
+ * skip the remaining checks.
+ */
+ goto success;
+ }
+
+ /*
+ * we have established we have CC right, now check per-attribute
+ * access based on the default SD
+ */
+
+ sd_ctrl = ldb_request_get_control(req,
+ DSDB_CONTROL_CALCULATED_DEFAULT_SD_OID);
+ if (sd_ctrl == NULL) {
+ goto success;
+ }
+
+ {
+ TALLOC_CTX *tmp_ctx = talloc_new(req);
+ control_sd = (struct dsdb_control_calculated_default_sd *) sd_ctrl->data;
+ DBG_DEBUG("Received cookie descriptor %s\n\n",
+ sddl_encode(tmp_ctx, control_sd->default_sd, domain_sid));
+ TALLOC_FREE(tmp_ctx);
+ /* Mark the "change" control as uncritical (done) */
+ sd_ctrl->critical = false;
+ }
+
+ /*
+ * At this point we do not yet have the object's SID, so we
+ * leave it empty. It is irrelevant, as it is used to expand
+ * Principal-Self, and rights granted to PS will have no effect
+ * in this case
+ */
+ /* check if we have WD, no need to perform other attribute checks if we do */
+ attr = dsdb_attribute_by_lDAPDisplayName(schema, "nTSecurityDescriptor");
+ if (attr == NULL) {
+ return ldb_operr(ldb);
+ }
+
+ if (control_sd->specified_sacl) {
+ const struct security_token *token = acl_user_token(module);
+ bool has_priv = security_token_has_privilege(token, SEC_PRIV_SECURITY);
+ if (!has_priv) {
+ return LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS;
+ }
+ }
+
+ ret = acl_check_access_on_attribute(module,
+ req,
+ control_sd->default_sd,
+ NULL,
+ SEC_STD_WRITE_DAC,
+ attr,
+ objectclass);
+ if (ret == LDB_SUCCESS) {
+ goto success;
+ }
+
+ if (control_sd->specified_sd) {
+ bool block_owner_rights = dsdb_block_owner_implicit_rights(module,
+ req,
+ req);
+ if (block_owner_rights) {
+ ldb_asprintf_errstring(ldb_module_get_ctx(module),
+ "Object %s has no SD modification rights",
+ ldb_dn_get_linearized(msg->dn));
+ dsdb_acl_debug(control_sd->default_sd,
+ acl_user_token(module),
+ msg->dn,
+ true,
+ 10);
+ ret = LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS;
+ return ret;
+ }
+ }
+
+ must_contain = dsdb_full_attribute_list(req, schema, &sorted_oc_el,
+ DSDB_SCHEMA_ALL_MUST);
+ for (i=0; i < msg->num_elements; i++) {
+ el = &msg->elements[i];
+
+ attr = dsdb_attribute_by_lDAPDisplayName(schema, el->name);
+ if (attr == NULL && ldb_attr_cmp("clearTextPassword", el->name) != 0) {
+ ldb_asprintf_errstring(ldb, "acl_add: attribute '%s' "
+ "on entry '%s' was not found in the schema!",
+ el->name,
+ ldb_dn_get_linearized(msg->dn));
+ ret = LDB_ERR_NO_SUCH_ATTRIBUTE;
+ return ret;
+ }
+
+ if (attr != NULL) {
+ bool found = str_list_check(must_contain, attr->lDAPDisplayName);
+ /* do not check the mandatory attributes */
+ if (found) {
+ continue;
+ }
+ }
+
+ if (ldb_attr_cmp("dBCSPwd", el->name) == 0 ||
+ ldb_attr_cmp("unicodePwd", el->name) == 0 ||
+ ldb_attr_cmp("userPassword", el->name) == 0 ||
+ ldb_attr_cmp("clearTextPassword", el->name) == 0) {
+ continue;
+ } else if (ldb_attr_cmp("member", el->name) == 0) {
+ ret = acl_check_self_membership(req,
+ module,
+ req,
+ control_sd->default_sd,
+ NULL,
+ attr,
+ objectclass);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ } else if (ldb_attr_cmp("servicePrincipalName", el->name) == 0) {
+ ret = acl_check_spn(req,
+ module,
+ req,
+ el,
+ control_sd->default_sd,
+ NULL,
+ attr,
+ objectclass,
+ NULL);
+ if (ret != LDB_SUCCESS) {
+ ldb_asprintf_errstring(ldb_module_get_ctx(module),
+ "Object %s cannot be created with spn",
+ ldb_dn_get_linearized(msg->dn));
+ dsdb_acl_debug(control_sd->default_sd,
+ acl_user_token(module),
+ msg->dn,
+ true,
+ 10);
+ return ret;
+ }
+ } else if (ldb_attr_cmp("dnsHostName", el->name) == 0) {
+ ret = acl_check_dns_host_name(req,
+ module,
+ req,
+ el,
+ control_sd->default_sd,
+ NULL,
+ attr,
+ objectclass,
+ NULL);
+ if (ret != LDB_SUCCESS) {
+ ldb_asprintf_errstring(ldb_module_get_ctx(module),
+ "Object %s cannot be created with dnsHostName",
+ ldb_dn_get_linearized(msg->dn));
+ dsdb_acl_debug(control_sd->default_sd,
+ acl_user_token(module),
+ msg->dn,
+ true,
+ 10);
+ return ret;
+ }
+ } else {
+ ret = acl_check_access_on_attribute(module,
+ req,
+ control_sd->default_sd,
+ NULL,
+ 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",
+ ldb_dn_get_linearized(msg->dn));
+ dsdb_acl_debug(control_sd->default_sd,
+ acl_user_token(module),
+ msg->dn,
+ true,
+ 10);
+ ret = LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS;
+ return ret;
+ }
+ }
+ }
+success:
+ return ldb_next_request(module, req);
+}
+
+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;
+ 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;
+ }
+ 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;
+
+ bool block_owner_rights;
+ enum implicit_owner_rights implicit_owner_rights;
+
+ 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;
+ }
+
+ block_owner_rights = !dsdb_module_am_administrator(module);
+
+ if (block_owner_rights) {
+ block_owner_rights = dsdb_block_owner_implicit_rights(module,
+ req,
+ req);
+ }
+ if (block_owner_rights) {
+ block_owner_rights = samdb_find_attribute(ldb,
+ acl_res->msgs[0],
+ "objectclass",
+ "computer");
+ }
+
+ implicit_owner_rights = block_owner_rights ?
+ IMPLICIT_OWNER_READ_CONTROL_RIGHTS :
+ IMPLICIT_OWNER_READ_CONTROL_AND_WRITE_DAC_RIGHTS;
+
+ ret = acl_check_access_on_attribute_implicit_owner(module,
+ tmp_ctx,
+ sd,
+ sid,
+ access_mask,
+ attr,
+ objectclass,
+ implicit_owner_rights);
+ 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 should 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 should 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..0b6280c
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/acl_read.c
@@ -0,0 +1,1302 @@
+/*
+ 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(const 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;
+ }
+
+ if (ldb_attr_always_visible(tree->u.present.attr)) {
+ /* No need to check this attribute. */
+ return 0;
+ }
+
+ break;
+
+ case LDB_OP_EQUALITY:
+ if (ldb_attr_always_visible(tree->u.equality.attr)) {
+ /* No need to check this attribute. */
+ return 0;
+ }
+
+ break;
+
+ default: /* single attribute in tree */
+ break;
+ }
+
+ 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_implicit_owner(ac->module, mem_ctx, sd, sid,
+ access_mask, attr, objectclass,
+ IMPLICIT_OWNER_READ_CONTROL_RIGHTS);
+ 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..352997e
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/acl_util.c
@@ -0,0 +1,376 @@
+/*
+ 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_implicit_owner(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,
+ enum implicit_owner_rights implicit_owner_rights)
+{
+ 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_implicit_owner(sd, token,
+ access_mask,
+ &access_granted,
+ root,
+ rp_sid,
+ implicit_owner_rights);
+ 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_attribute(struct ldb_module *module,
+ TALLOC_CTX *mem_ctx,
+ struct security_descriptor *sd,
+ struct dom_sid *rp_sid,
+ uint32_t access_mask,
+ const struct dsdb_attribute *attr,
+ const struct dsdb_class *objectclass)
+{
+ return acl_check_access_on_attribute_implicit_owner(module,
+ mem_ctx,
+ sd,
+ rp_sid,
+ access_mask,
+ attr,
+ objectclass,
+ IMPLICIT_OWNER_READ_CONTROL_RIGHTS);
+}
+
+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..d0cdaa3
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/anr.c
@@ -0,0 +1,440 @@
+/*
+ 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 occurrences 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:
+ if (ldb_attr_cmp(tree->u.equality.attr, ctx->attr) == 0) {
+ ctx->found_anr = true;
+ }
+ break;
+ case LDB_OP_GREATER:
+ case LDB_OP_LESS:
+ case LDB_OP_APPROX:
+ if (ldb_attr_cmp(tree->u.comparison.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..9b8b06b
--- /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[PRIMARY_USER_SID_INDEX];
+}
+/*
+ * @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[PRIMARY_USER_SID_INDEX];
+
+}
+
+/*
+ * @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 values 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\n");
+ 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 suppressed.
+ *
+ * {
+ * "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..6d7d30a
--- /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
+ * absence 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..4f57573
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/descriptor.c
@@ -0,0 +1,2069 @@
+/*
+ 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,
+ const 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 struct security_descriptor *get_new_descriptor_nonlinear(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;
+ 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);
+ }
+
+ 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 *final_sd = NULL;
+ enum ndr_err_code ndr_err;
+ DATA_BLOB *linear_sd = talloc(mem_ctx, DATA_BLOB);
+
+ if (!linear_sd) {
+ return NULL;
+ }
+
+ final_sd = get_new_descriptor_nonlinear(module,
+ dn,
+ mem_ctx,
+ objectclass,
+ parent,
+ object,
+ old_sd,
+ sd_flags);
+ if (final_sd == NULL) {
+ 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 bool can_write_owner(TALLOC_CTX *mem_ctx,
+ struct ldb_context *ldb,
+ struct ldb_dn *dn,
+ const struct security_token *security_token,
+ const struct dom_sid *owner_sid)
+{
+ const struct dom_sid *default_owner = NULL;
+
+ /* If the user possesses SE_RESTORE_PRIVILEGE, the write is allowed. */
+ bool ok = security_token_has_privilege(security_token, SEC_PRIV_RESTORE);
+ if (ok) {
+ return true;
+ }
+
+ /* The user can write their own SID to a security descriptor. */
+ ok = security_token_is_sid(security_token, owner_sid);
+ if (ok) {
+ return true;
+ }
+
+ /*
+ * The user can write the SID of the "default administrators group" that
+ * they are a member of.
+ */
+ default_owner = get_default_ag(mem_ctx, dn,
+ security_token, ldb);
+ if (default_owner != NULL) {
+ ok = security_token_is_sid(security_token, owner_sid);
+ }
+
+ return ok;
+}
+
+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 = NULL;
+ 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;
+ enum ndr_err_code ndr_err;
+ struct dsdb_control_calculated_default_sd *control_sd = NULL;
+ uint32_t sd_flags = dsdb_request_sd_flags(req, NULL);
+ struct security_descriptor *user_descriptor = 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;
+
+ control_sd = talloc(req, struct dsdb_control_calculated_default_sd);
+ if (control_sd == NULL) {
+ return ldb_operr(ldb);
+ }
+ control_sd->specified_sd = false;
+ control_sd->specified_sacl = false;
+ if (user_sd != NULL) {
+ user_descriptor = talloc(req, struct security_descriptor);
+ if (user_descriptor == NULL) {
+ return ldb_operr(ldb);
+ }
+ ndr_err = ndr_pull_struct_blob(user_sd, 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 ldb_operr(ldb);
+ }
+ /*
+ * calculate the permissions needed, since in acl we no longer have
+ * access to the original user descriptor
+ */
+ control_sd->specified_sd = true;
+ control_sd->specified_sacl = user_descriptor->sacl != NULL;
+
+ if (user_descriptor->owner_sid != NULL) {
+ /* Verify the owner of the security descriptor. */
+
+ const struct auth_session_info *session_info
+ = ldb_get_opaque(ldb, DSDB_SESSION_INFO);
+
+ bool ok = can_write_owner(req,
+ ldb,
+ dn,
+ session_info->security_token,
+ user_descriptor->owner_sid);
+ talloc_free(user_descriptor);
+ if (!ok) {
+ return dsdb_module_werror(module,
+ LDB_ERR_CONSTRAINT_VIOLATION,
+ WERR_INVALID_OWNER,
+ "invalid addition of owner SID");
+ }
+ }
+ }
+
+ sd = get_new_descriptor(module, dn, req,
+ objectclass, parent_sd,
+ user_sd, NULL, sd_flags);
+ if (sd == NULL) {
+ return ldb_operr(ldb);
+ }
+
+ control_sd->default_sd = get_new_descriptor_nonlinear(module,
+ dn,
+ req,
+ objectclass,
+ parent_sd,
+ NULL,
+ NULL,
+ sd_flags);
+ if (control_sd->default_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.");
+ }
+
+ *control_sd->default_sd->owner_sid = global_sid_NULL;
+ ret = ldb_request_add_control(add_req,
+ DSDB_CONTROL_CALCULATED_DEFAULT_SD_OID,
+ false, (void *)control_sd);
+ if (ret != LDB_SUCCESS) {
+ return ldb_module_operr(module);
+ }
+ 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 = NULL;
+ 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);
+ }
+
+ if (sd_flags & SECINFO_OWNER && user_sd != NULL) {
+ /* Verify the new owner of the security descriptor. */
+
+ struct security_descriptor *user_descriptor = NULL;
+ enum ndr_err_code ndr_err;
+ const struct auth_session_info *session_info;
+ bool ok;
+
+ user_descriptor = talloc(req, struct security_descriptor);
+
+ if (user_descriptor == NULL) {
+ return ldb_operr(ldb);
+ }
+ ndr_err = ndr_pull_struct_blob(user_sd, 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 ldb_operr(ldb);
+ }
+
+ session_info = ldb_get_opaque(ldb, DSDB_SESSION_INFO);
+
+ ok = can_write_owner(req,
+ ldb,
+ dn,
+ session_info->security_token,
+ user_descriptor->owner_sid);
+ talloc_free(user_descriptor);
+ if (!ok) {
+ return dsdb_module_werror(module,
+ LDB_ERR_CONSTRAINT_VIOLATION,
+ WERR_INVALID_OWNER,
+ "invalid modification of owner SID");
+ }
+ }
+
+ ldb_debug(ldb, LDB_DEBUG_TRACE,"descriptor_modify: %s\n", ldb_dn_get_linearized(dn));
+
+ ret = dsdb_module_search_dn(module, req, &current_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..9901a99
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/dirsync.c
@@ -0,0 +1,1381 @@
+/*
+ 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 useful to track the entries that need 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;
+ /* Adjustment 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 increment 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 interrogation
+ *
+ */
+ 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 be 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;
+ }
+ el = ldb_msg_find_element(msg, "objectGUID");
+ if ( el != NULL) {
+ guidfound = true;
+ }
+ /*
+ * We expect to find the GUID in the object
+ */
+ SMB_ASSERT(guidfound == true);
+ return ldb_module_send_entry(dsc->req, msg, controls);
+ }
+
+ newmsg = ldb_msg_new(dsc->req);
+ if (newmsg == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ 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 uptodateness 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 uptodateness 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 referral
+ */
+ 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 hierarchy 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 outputting flags is used to say more results.
+ * For the moment we didn't honour 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);
+ }
+
+ 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
+ * Windows does 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 any case 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..dee864b
--- /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 coming 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..1f04389
--- /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 algorithm 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 compatible 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..035a920
--- /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 searches 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..a949bfb
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/extended_dn_out.c
@@ -0,0 +1,672 @@
+/*
+ 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=Administrator,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=Administrator,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 == NULL || !p->normalise) && ac->inject) {
+ 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;
+ bool bl_requested = true;
+ 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);
+ }
+
+ if (attribute->linkID & 1 &&
+ attribute->bl_maybe_invisible &&
+ !have_reveal_control)
+ {
+ const char * const *attrs = ac->req->op.search.attrs;
+
+ if (attrs != NULL) {
+ bl_requested = ldb_attr_in_list(attrs,
+ attribute->lDAPDisplayName);
+ } else {
+ bl_requested = false;
+ }
+ }
+
+ 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;
+ uint32_t rmd_flags;
+
+ /* this is a fast method for detecting deleted
+ linked attributes, working on the unparsed
+ ldb_val */
+ rmd_flags = dsdb_dn_val_rmd_flags(plain_dn);
+ if (rmd_flags & DSDB_RMD_FLAG_DELETED && !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;
+ }
+ if (rmd_flags & DSDB_RMD_FLAG_HIDDEN_BL && !bl_requested) {
+ /*
+ * Hidden backlinks are not revealed unless
+ * requested.
+ *
+ * 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 != NULL && 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 && !have_reveal_control) {
+ /* 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 && !ldb_attr_in_list(req->op.search.attrs, "*")) {
+ if (! ldb_attr_in_list(req->op.search.attrs, "objectGUID")) {
+ ac->remove_guid = true;
+ }
+ if (! ldb_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..42970da
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/extended_dn_store.c
@@ -0,0 +1,830 @@
+/*
+ ldb database library
+
+ Copyright (C) Simo Sorce 2005-2008
+ Copyright (C) Andrew Bartlett <abartlet@samba.org> 2007-2009
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/*
+ * Name: ldb
+ *
+ * Component: ldb extended dn control module
+ *
+ * Description: this module builds a special dn for returned search
+ * results nad creates the special DN in the backend store for new
+ * values.
+ *
+ * This also has the curious result that we convert <SID=S-1-2-345>
+ * in an attribute value into a normal DN for the rest of the stack
+ * to process
+ *
+ * Authors: Simo Sorce
+ * Andrew Bartlett
+ */
+
+#include "includes.h"
+#include <ldb.h>
+#include <ldb_errors.h>
+#include <ldb_module.h>
+#include "librpc/gen_ndr/ndr_misc.h"
+#include "dsdb/samdb/samdb.h"
+#include "libcli/security/security.h"
+#include "dsdb/samdb/ldb_modules/util.h"
+#include <time.h>
+
+struct extended_dn_replace_list {
+ struct extended_dn_replace_list *next;
+ struct dsdb_dn *dsdb_dn;
+ TALLOC_CTX *mem_ctx;
+ struct ldb_val *replace_dn;
+ struct extended_dn_context *ac;
+ struct ldb_request *search_req;
+ bool fpo_enabled;
+ bool require_object;
+ bool got_entry;
+};
+
+
+struct extended_dn_context {
+ const struct dsdb_schema *schema;
+ struct ldb_module *module;
+ struct ldb_context *ldb;
+ struct ldb_request *req;
+ struct ldb_request *new_req;
+
+ struct extended_dn_replace_list *ops;
+ struct extended_dn_replace_list *cur;
+
+ /*
+ * Used by the FPO-enabled attribute validation.
+ */
+ struct dsdb_trust_routing_table *routing_table;
+};
+
+
+static struct extended_dn_context *extended_dn_context_init(struct ldb_module *module,
+ struct ldb_request *req)
+{
+ struct extended_dn_context *ac;
+ struct ldb_context *ldb = ldb_module_get_ctx(module);
+ ac = talloc_zero(req, struct extended_dn_context);
+ if (ac == NULL) {
+ ldb_oom(ldb);
+ return NULL;
+ }
+
+ ac->schema = dsdb_get_schema(ldb, ac);
+ ac->module = module;
+ ac->ldb = ldb;
+ ac->req = req;
+
+ return ac;
+}
+
+static int extended_replace_dn(struct extended_dn_replace_list *os,
+ struct ldb_dn *dn)
+{
+ struct dsdb_dn *dsdb_dn = NULL;
+ const char *str = NULL;
+
+ /*
+ * Rebuild with the string or binary 'extra part' the
+ * DN may have had as a prefix
+ */
+ dsdb_dn = dsdb_dn_construct(os, dn,
+ os->dsdb_dn->extra_part,
+ os->dsdb_dn->oid);
+ if (dsdb_dn == NULL) {
+ return ldb_module_operr(os->ac->module);
+ }
+
+ str = dsdb_dn_get_extended_linearized(os->mem_ctx,
+ dsdb_dn, 1);
+ if (str == NULL) {
+ return ldb_module_operr(os->ac->module);
+ }
+
+ /*
+ * Replace the DN with the extended version of the DN
+ * (ie, add SID and GUID)
+ */
+ *os->replace_dn = data_blob_string_const(str);
+ os->got_entry = true;
+ return LDB_SUCCESS;
+}
+
+static int extended_dn_handle_fpo_attr(struct extended_dn_replace_list *os)
+{
+ struct dom_sid target_sid = { 0, };
+ struct dom_sid target_domain = { 0, };
+ struct ldb_message *fmsg = NULL;
+ char *fsid = NULL;
+ const struct dom_sid *domain_sid = NULL;
+ struct ldb_dn *domain_dn = NULL;
+ const struct lsa_TrustDomainInfoInfoEx *tdo = NULL;
+ uint32_t trust_attributes = 0;
+ const char *no_attrs[] = { NULL, };
+ struct ldb_result *res = NULL;
+ NTSTATUS status;
+ bool match;
+ bool ok;
+ int ret;
+
+ /*
+ * DN doesn't exist yet
+ *
+ * Check if a foreign SID is specified,
+ * which would trigger the creation
+ * of a foreignSecurityPrincipal.
+ */
+ status = dsdb_get_extended_dn_sid(os->dsdb_dn->dn,
+ &target_sid,
+ "SID");
+ if (NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) {
+ /*
+ * No SID specified
+ */
+ return dsdb_module_werror(os->ac->module,
+ LDB_ERR_NO_SUCH_OBJECT,
+ WERR_NO_SUCH_USER,
+ "specified dn doesn't exist");
+ }
+ if (!NT_STATUS_IS_OK(status)) {
+ return ldb_module_operr(os->ac->module);
+ }
+ if (ldb_dn_get_extended_comp_num(os->dsdb_dn->dn) != 1) {
+ return dsdb_module_werror(os->ac->module,
+ LDB_ERR_NO_SUCH_OBJECT,
+ WERR_NO_SUCH_USER,
+ "specified extended component other than SID");
+ }
+ if (ldb_dn_get_comp_num(os->dsdb_dn->dn) != 0) {
+ return dsdb_module_werror(os->ac->module,
+ LDB_ERR_NO_SUCH_OBJECT,
+ WERR_NO_SUCH_USER,
+ "specified more the SID");
+ }
+
+ target_domain = target_sid;
+ sid_split_rid(&target_domain, NULL);
+
+ match = dom_sid_equal(&global_sid_Builtin, &target_domain);
+ if (match) {
+ /*
+ * Non existing BUILTIN sid
+ */
+ return dsdb_module_werror(os->ac->module,
+ LDB_ERR_NO_SUCH_OBJECT,
+ WERR_NO_SUCH_MEMBER,
+ "specified sid doesn't exist in BUILTIN");
+ }
+
+ domain_sid = samdb_domain_sid(os->ac->ldb);
+ if (domain_sid == NULL) {
+ return ldb_module_operr(os->ac->module);
+ }
+ match = dom_sid_equal(domain_sid, &target_domain);
+ if (match) {
+ /*
+ * Non existing SID in our domain.
+ */
+ return dsdb_module_werror(os->ac->module,
+ LDB_ERR_UNWILLING_TO_PERFORM,
+ WERR_DS_INVALID_GROUP_TYPE,
+ "specified sid doesn't exist in domain");
+ }
+
+ if (os->ac->routing_table == NULL) {
+ status = dsdb_trust_routing_table_load(os->ac->ldb, os->ac,
+ &os->ac->routing_table);
+ if (!NT_STATUS_IS_OK(status)) {
+ return ldb_module_operr(os->ac->module);
+ }
+ }
+
+ tdo = dsdb_trust_domain_by_sid(os->ac->routing_table,
+ &target_domain, NULL);
+ if (tdo != NULL) {
+ trust_attributes = tdo->trust_attributes;
+ }
+
+ if (trust_attributes & LSA_TRUST_ATTRIBUTE_WITHIN_FOREST) {
+ return dsdb_module_werror(os->ac->module,
+ LDB_ERR_UNWILLING_TO_PERFORM,
+ WERR_DS_INVALID_GROUP_TYPE,
+ "specified sid doesn't exist in forest");
+ }
+
+ fmsg = ldb_msg_new(os);
+ if (fmsg == NULL) {
+ return ldb_module_oom(os->ac->module);
+ }
+
+ fsid = dom_sid_string(fmsg, &target_sid);
+ if (fsid == NULL) {
+ return ldb_module_oom(os->ac->module);
+ }
+
+ domain_dn = ldb_get_default_basedn(os->ac->ldb);
+ if (domain_dn == NULL) {
+ return ldb_module_operr(os->ac->module);
+ }
+
+ fmsg->dn = ldb_dn_copy(fmsg, domain_dn);
+ if (fmsg->dn == NULL) {
+ return ldb_module_oom(os->ac->module);
+ }
+
+ ok = ldb_dn_add_child_fmt(fmsg->dn,
+ "CN=%s,CN=ForeignSecurityPrincipals",
+ fsid);
+ if (!ok) {
+ return ldb_module_oom(os->ac->module);
+ }
+
+ ret = ldb_msg_add_string(fmsg, "objectClass", "foreignSecurityPrincipal");
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ ret = dsdb_module_add(os->ac->module, fmsg,
+ DSDB_FLAG_AS_SYSTEM |
+ DSDB_FLAG_NEXT_MODULE,
+ os->ac->req);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ ret = dsdb_module_search_dn(os->ac->module, fmsg, &res,
+ fmsg->dn, no_attrs,
+ DSDB_FLAG_AS_SYSTEM |
+ DSDB_FLAG_NEXT_MODULE |
+ DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT,
+ os->ac->req);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ /*
+ * dsdb_module_search_dn() guarantees exactly one result message
+ * on success.
+ */
+ ret = extended_replace_dn(os, res->msgs[0]->dn);
+ TALLOC_FREE(fmsg);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ return LDB_SUCCESS;
+}
+
+/* An extra layer of indirection because LDB does not allow the original request to be altered */
+
+static int extended_final_callback(struct ldb_request *req, struct ldb_reply *ares)
+{
+ int ret = LDB_ERR_OPERATIONS_ERROR;
+ struct extended_dn_context *ac;
+ ac = talloc_get_type(req->context, struct extended_dn_context);
+
+ if (ares->error != LDB_SUCCESS) {
+ ret = ldb_module_done(ac->req, ares->controls,
+ ares->response, ares->error);
+ } else {
+ switch (ares->type) {
+ case LDB_REPLY_ENTRY:
+
+ ret = ldb_module_send_entry(ac->req, ares->message, ares->controls);
+ break;
+ case LDB_REPLY_REFERRAL:
+
+ ret = ldb_module_send_referral(ac->req, ares->referral);
+ break;
+ case LDB_REPLY_DONE:
+
+ ret = ldb_module_done(ac->req, ares->controls,
+ ares->response, ares->error);
+ break;
+ }
+ }
+ return ret;
+}
+
+static int extended_replace_callback(struct ldb_request *req, struct ldb_reply *ares)
+{
+ struct extended_dn_replace_list *os = talloc_get_type(req->context,
+ struct extended_dn_replace_list);
+
+ if (!ares) {
+ return ldb_module_done(os->ac->req, NULL, NULL,
+ LDB_ERR_OPERATIONS_ERROR);
+ }
+ if (ares->error == LDB_ERR_NO_SUCH_OBJECT) {
+ if (os->got_entry) {
+ /* This is in internal error... */
+ int ret = ldb_module_operr(os->ac->module);
+ return ldb_module_done(os->ac->req, NULL, NULL, ret);
+ }
+
+ if (os->require_object && os->fpo_enabled) {
+ int ret;
+
+ ret = extended_dn_handle_fpo_attr(os);
+ if (ret != LDB_SUCCESS) {
+ return ldb_module_done(os->ac->req, NULL, NULL,
+ ret);
+ }
+ /* os->got_entry is true at this point... */
+ }
+
+ if (!os->got_entry && os->require_object) {
+ /*
+ * It's an error if the target doesn't exist,
+ * unless it's a delete.
+ */
+ int ret = dsdb_module_werror(os->ac->module,
+ LDB_ERR_CONSTRAINT_VIOLATION,
+ WERR_DS_NAME_REFERENCE_INVALID,
+ "Referenced object not found");
+ return ldb_module_done(os->ac->req, NULL, NULL, ret);
+ }
+
+ /* Don't worry too much about dangling references */
+
+ ldb_reset_err_string(os->ac->ldb);
+ if (os->next) {
+ struct extended_dn_replace_list *next;
+
+ next = os->next;
+
+ talloc_free(os);
+
+ os = next;
+ return ldb_next_request(os->ac->module, next->search_req);
+ } else {
+ /* Otherwise, we are done - let's run the
+ * request now we have swapped the DNs for the
+ * full versions */
+ return ldb_next_request(os->ac->module, os->ac->new_req);
+ }
+ }
+ if (ares->error != LDB_SUCCESS) {
+ return ldb_module_done(os->ac->req, ares->controls,
+ ares->response, ares->error);
+ }
+
+ /* Only entries are interesting, and we only want the olddn */
+ switch (ares->type) {
+ case LDB_REPLY_ENTRY:
+ {
+ /* This *must* be the right DN, as this is a base
+ * search. We can't check, as it could be an extended
+ * DN, so a module below will resolve it */
+ int ret;
+
+ ret = extended_replace_dn(os, ares->message->dn);
+ if (ret != LDB_SUCCESS) {
+ return ldb_module_done(os->ac->req, NULL, NULL, ret);
+ }
+ /* os->got_entry is true at this point */
+ break;
+ }
+ case LDB_REPLY_REFERRAL:
+ /* ignore */
+ break;
+
+ case LDB_REPLY_DONE:
+
+ talloc_free(ares);
+
+ if (!os->got_entry && os->require_object && os->fpo_enabled) {
+ int ret;
+
+ ret = extended_dn_handle_fpo_attr(os);
+ if (ret != LDB_SUCCESS) {
+ return ldb_module_done(os->ac->req, NULL, NULL,
+ ret);
+ }
+ /* os->got_entry is true at this point... */
+ }
+
+ if (!os->got_entry && os->require_object) {
+ /*
+ * It's an error if the target doesn't exist,
+ * unless it's a delete.
+ */
+ int ret = dsdb_module_werror(os->ac->module,
+ LDB_ERR_CONSTRAINT_VIOLATION,
+ WERR_DS_NAME_REFERENCE_INVALID,
+ "Referenced object not found");
+ return ldb_module_done(os->ac->req, NULL, NULL, ret);
+ }
+
+ /* Run the next search */
+
+ if (os->next) {
+ struct extended_dn_replace_list *next;
+
+ next = os->next;
+
+ talloc_free(os);
+
+ os = next;
+ return ldb_next_request(os->ac->module, next->search_req);
+ } else {
+ /* Otherwise, we are done - let's run the
+ * request now we have swapped the DNs for the
+ * full versions */
+ return ldb_next_request(os->ac->module, os->ac->new_req);
+ }
+ }
+
+ talloc_free(ares);
+ return LDB_SUCCESS;
+}
+
+/* We have a 'normal' DN in the inbound request. We need to find out
+ * what the GUID and SID are on the DN it points to, so we can
+ * construct an extended DN for storage.
+ *
+ * This creates a list of DNs to look up, and the plain DN to replace
+ */
+
+static int extended_store_replace(struct extended_dn_context *ac,
+ TALLOC_CTX *callback_mem_ctx,
+ struct ldb_dn *self_dn,
+ struct ldb_val *plain_dn,
+ bool is_delete,
+ const struct dsdb_attribute *schema_attr)
+{
+ const char *oid = schema_attr->syntax->ldap_oid;
+ int ret;
+ struct extended_dn_replace_list *os;
+ static const char *attrs[] = {
+ "objectSid",
+ "objectGUID",
+ NULL
+ };
+ uint32_t ctrl_flags = 0;
+ bool is_untrusted = ldb_req_is_untrusted(ac->req);
+
+ os = talloc_zero(ac, struct extended_dn_replace_list);
+ if (!os) {
+ return ldb_oom(ac->ldb);
+ }
+
+ os->ac = ac;
+
+ os->mem_ctx = callback_mem_ctx;
+
+ os->dsdb_dn = dsdb_dn_parse(os, ac->ldb, plain_dn, oid);
+ if (!os->dsdb_dn || !ldb_dn_validate(os->dsdb_dn->dn)) {
+ talloc_free(os);
+ ldb_asprintf_errstring(ac->ldb,
+ "could not parse %.*s as a %s DN", (int)plain_dn->length, plain_dn->data,
+ oid);
+ return LDB_ERR_INVALID_DN_SYNTAX;
+ }
+
+ if (self_dn != NULL) {
+ ret = ldb_dn_compare(self_dn, os->dsdb_dn->dn);
+ if (ret == 0) {
+ /*
+ * If this is a reference to the object
+ * itself during an 'add', we won't
+ * be able to find the object.
+ */
+ talloc_free(os);
+ return LDB_SUCCESS;
+ }
+ }
+
+ if (is_delete && !ldb_dn_has_extended(os->dsdb_dn->dn)) {
+ /* NO need to figure this DN out, this element is
+ * going to be deleted anyway, and because it's not
+ * extended, we have enough information to do the
+ * delete */
+ talloc_free(os);
+ return LDB_SUCCESS;
+ }
+
+
+ os->replace_dn = plain_dn;
+
+ /* The search request here might happen to be for an
+ * 'extended' style DN, such as <GUID=abced...>. The next
+ * module in the stack will convert this into a normal DN for
+ * processing */
+ ret = ldb_build_search_req(&os->search_req,
+ ac->ldb, os, os->dsdb_dn->dn, LDB_SCOPE_BASE, NULL,
+ attrs, NULL, os, extended_replace_callback,
+ ac->req);
+ LDB_REQ_SET_LOCATION(os->search_req);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(os);
+ return ret;
+ }
+
+ /*
+ * By default we require the presence of the target.
+ */
+ os->require_object = true;
+
+ /*
+ * Handle FPO-enabled attributes, see
+ * [MS-ADTS] 3.1.1.5.2.3 Special Classes and Attributes:
+ *
+ * FPO-enabled attributes: member, msDS-MembersForAzRole,
+ * msDS-NeverRevealGroup, msDS-NonMembers, msDS-RevealOnDemandGroup,
+ * msDS-ServiceAccount.
+ *
+ * Note there's no msDS-ServiceAccount in any schema (only
+ * msDS-HostServiceAccount and that's not an FPO-enabled attribute
+ * at least not in W2008R2)
+ *
+ * msDS-NonMembers always generates NOT_SUPPORTED against W2008R2.
+ *
+ * See also [MS-SAMR] 3.1.1.8.9 member.
+ */
+ switch (schema_attr->attributeID_id) {
+ case DRSUAPI_ATTID_member:
+ case DRSUAPI_ATTID_msDS_MembersForAzRole:
+ case DRSUAPI_ATTID_msDS_NeverRevealGroup:
+ case DRSUAPI_ATTID_msDS_RevealOnDemandGroup:
+ os->fpo_enabled = true;
+ break;
+
+ case DRSUAPI_ATTID_msDS_HostServiceAccount:
+ /* This is NOT a FPO-enabled attribute */
+ break;
+
+ case DRSUAPI_ATTID_msDS_NonMembers:
+ return dsdb_module_werror(os->ac->module,
+ LDB_ERR_UNWILLING_TO_PERFORM,
+ WERR_NOT_SUPPORTED,
+ "msDS-NonMembers is not supported");
+ }
+
+ if (schema_attr->linkID == 0) {
+ /*
+ * None linked attributes allow references
+ * to deleted objects.
+ */
+ ctrl_flags |= DSDB_SEARCH_SHOW_RECYCLED;
+ }
+
+ if (is_delete) {
+ /*
+ * On delete want to be able to
+ * find a deleted object, but
+ * it's not a problem if they doesn't
+ * exist.
+ */
+ ctrl_flags |= DSDB_SEARCH_SHOW_RECYCLED;
+ os->require_object = false;
+ }
+
+ if (!is_untrusted) {
+ struct ldb_control *ctrl = NULL;
+
+ /*
+ * During provision or dbcheck we may not find
+ * an object.
+ */
+
+ ctrl = ldb_request_get_control(ac->req, LDB_CONTROL_RELAX_OID);
+ if (ctrl != NULL) {
+ os->require_object = false;
+ }
+ ctrl = ldb_request_get_control(ac->req, DSDB_CONTROL_DBCHECK);
+ if (ctrl != NULL) {
+ os->require_object = false;
+ }
+ }
+
+ ret = dsdb_request_add_controls(os->search_req,
+ DSDB_FLAG_AS_SYSTEM |
+ ctrl_flags |
+ DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(os);
+ return ret;
+ }
+
+ if (ac->ops) {
+ ac->cur->next = os;
+ } else {
+ ac->ops = os;
+ }
+ ac->cur = os;
+
+ return LDB_SUCCESS;
+}
+
+
+/* add */
+static int extended_dn_add(struct ldb_module *module, struct ldb_request *req)
+{
+ struct extended_dn_context *ac;
+ int ret;
+ unsigned int i, j;
+
+ if (ldb_dn_is_special(req->op.add.message->dn)) {
+ /* do not manipulate our control entries */
+ return ldb_next_request(module, req);
+ }
+
+ ac = extended_dn_context_init(module, req);
+ if (!ac) {
+ return ldb_operr(ldb_module_get_ctx(module));
+ }
+
+ if (!ac->schema) {
+ /* without schema, this doesn't make any sense */
+ talloc_free(ac);
+ return ldb_next_request(module, req);
+ }
+
+ for (i=0; i < req->op.add.message->num_elements; i++) {
+ const struct ldb_message_element *el = &req->op.add.message->elements[i];
+ const struct dsdb_attribute *schema_attr
+ = dsdb_attribute_by_lDAPDisplayName(ac->schema, el->name);
+ if (!schema_attr) {
+ continue;
+ }
+
+ /* We only setup an extended DN GUID on DN elements */
+ if (schema_attr->dn_format == DSDB_INVALID_DN) {
+ continue;
+ }
+
+ if (schema_attr->attributeID_id == DRSUAPI_ATTID_distinguishedName) {
+ /* distinguishedName values are ignored */
+ continue;
+ }
+
+ /* Before we setup a procedure to modify the incoming message, we must copy it */
+ if (!ac->new_req) {
+ struct ldb_message *msg = ldb_msg_copy(ac, req->op.add.message);
+ if (!msg) {
+ return ldb_oom(ldb_module_get_ctx(module));
+ }
+
+ ret = ldb_build_add_req(&ac->new_req, ac->ldb, ac, msg, req->controls, ac, extended_final_callback, req);
+ LDB_REQ_SET_LOCATION(ac->new_req);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+ /* Re-calculate el */
+ el = &ac->new_req->op.add.message->elements[i];
+ for (j = 0; j < el->num_values; j++) {
+ ret = extended_store_replace(ac, ac->new_req,
+ req->op.add.message->dn,
+ &el->values[j],
+ false, schema_attr);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ }
+ }
+
+ /* if no DNs were set continue */
+ if (ac->ops == NULL) {
+ talloc_free(ac);
+ return ldb_next_request(module, req);
+ }
+
+ /* start with the searches */
+ return ldb_next_request(module, ac->ops->search_req);
+}
+
+/* modify */
+static int extended_dn_modify(struct ldb_module *module, struct ldb_request *req)
+{
+ /* Look over list of modifications */
+ /* Find if any are for linked attributes */
+ /* Determine the effect of the modification */
+ /* Apply the modify to the linked entry */
+
+ unsigned int i, j;
+ struct extended_dn_context *ac;
+ struct ldb_control *fix_links_control = NULL;
+ struct ldb_control *fix_link_sid_ctrl = NULL;
+ int ret;
+
+ if (ldb_dn_is_special(req->op.mod.message->dn)) {
+ /* do not manipulate our control entries */
+ return ldb_next_request(module, req);
+ }
+
+ ac = extended_dn_context_init(module, req);
+ if (!ac) {
+ return ldb_operr(ldb_module_get_ctx(module));
+ }
+
+ if (!ac->schema) {
+ talloc_free(ac);
+ /* without schema, this doesn't make any sense */
+ return ldb_next_request(module, req);
+ }
+
+ fix_links_control = ldb_request_get_control(req,
+ DSDB_CONTROL_DBCHECK_FIX_DUPLICATE_LINKS);
+ if (fix_links_control != NULL) {
+ return ldb_next_request(module, req);
+ }
+
+ fix_link_sid_ctrl = ldb_request_get_control(ac->req,
+ DSDB_CONTROL_DBCHECK_FIX_LINK_DN_SID);
+ if (fix_link_sid_ctrl != NULL) {
+ return ldb_next_request(module, req);
+ }
+
+ for (i=0; i < req->op.mod.message->num_elements; i++) {
+ const struct ldb_message_element *el = &req->op.mod.message->elements[i];
+ const struct dsdb_attribute *schema_attr
+ = dsdb_attribute_by_lDAPDisplayName(ac->schema, el->name);
+ if (!schema_attr) {
+ continue;
+ }
+
+ /* We only setup an extended DN GUID on these particular DN objects */
+ if (schema_attr->dn_format == DSDB_INVALID_DN) {
+ continue;
+ }
+
+ if (schema_attr->attributeID_id == DRSUAPI_ATTID_distinguishedName) {
+ /* distinguishedName values are ignored */
+ continue;
+ }
+
+ /* Before we setup a procedure to modify the incoming message, we must copy it */
+ if (!ac->new_req) {
+ struct ldb_message *msg = ldb_msg_copy(ac, req->op.mod.message);
+ if (!msg) {
+ talloc_free(ac);
+ return ldb_oom(ac->ldb);
+ }
+
+ ret = ldb_build_mod_req(&ac->new_req, ac->ldb, ac, msg, req->controls, ac, extended_final_callback, req);
+ LDB_REQ_SET_LOCATION(ac->new_req);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(ac);
+ return ret;
+ }
+ }
+ /* Re-calculate el */
+ el = &ac->new_req->op.mod.message->elements[i];
+ /* For each value being added, we need to setup the lookups to fill in the extended DN */
+ for (j = 0; j < el->num_values; j++) {
+ /* If we are just going to delete this
+ * element, only do a lookup if
+ * extended_store_replace determines it's an
+ * input of an extended DN */
+ bool is_delete = (LDB_FLAG_MOD_TYPE(el->flags) == LDB_FLAG_MOD_DELETE);
+
+ ret = extended_store_replace(ac, ac->new_req,
+ NULL, /* self_dn to be ignored */
+ &el->values[j],
+ is_delete, schema_attr);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(ac);
+ return ret;
+ }
+ }
+ }
+
+ /* if DNs were set continue */
+ if (ac->ops == NULL) {
+ talloc_free(ac);
+ return ldb_next_request(module, req);
+ }
+
+ /* start with the searches */
+ return ldb_next_request(module, ac->ops->search_req);
+}
+
+static const struct ldb_module_ops ldb_extended_dn_store_module_ops = {
+ .name = "extended_dn_store",
+ .add = extended_dn_add,
+ .modify = extended_dn_modify,
+};
+
+int ldb_extended_dn_store_module_init(const char *version)
+{
+ LDB_MODULE_CHECK_VERSION(version);
+ return ldb_register_module(&ldb_extended_dn_store_module_ops);
+}
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..1b05e89
--- /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 are 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 their
+ * 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..d1232c7
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/linked_attributes.c
@@ -0,0 +1,1587 @@
+/*
+ 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) {
+ /*
+ * Our only caller doesn’t want to know about errors finding a
+ * forward link for which we have a backlink — in particular,
+ * during the tombstoning of an object, the forward links have
+ * already been removed when this routine is called by
+ * dsdb_module_rename() inside replmd_delete_internals().
+ */
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+ }
+
+ 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);
+ }
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+ 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 modifies 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..479b3a6
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/netlogon.c
@@ -0,0 +1,516 @@
+/*
+ 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;
+ const char *user_encoded = NULL;
+
+ user_encoded = ldb_binary_encode_string(mem_ctx, user);
+ if (user_encoded == NULL) {
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ /* 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))",
+ user_encoded,
+ 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..bdd6b90
--- /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 explanation 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 manager 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..da4217a
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/objectclass_attrs.c
@@ -0,0 +1,752 @@
+/*
+ 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);
+ }
+
+ if (attr->linkID & 1) {
+ /*
+ * We need to allow backlinks on all objects
+ * even if the schema doesn't allow it.
+ */
+ continue;
+ }
+
+ /* 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..1317b58
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/operational.c
@@ -0,0 +1,1920 @@
+/*
+ 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 "dsdb/samdb/samdb.h"
+#include "dsdb/samdb/ldb_modules/util.h"
+
+#include "auth/auth.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 auth_SidAttr **groupSIDs,
+ uint32_t *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:"LDB_OID_COMPARATOR_AND":=%u)"
+ "(groupType:"LDB_OID_COMPARATOR_OR":=%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:"LDB_OID_COMPARATOR_AND":=%u))",
+ GROUP_TYPE_SECURITY_ENABLED);
+ break;
+
+ /* for RevMembGetAccountGroups, exclude built-in groups */
+ case ACCOUNT_GROUPS:
+ filter = talloc_asprintf(mem_ctx,
+ "(&(objectClass=group)"
+ "(!(groupType:"LDB_OID_COMPARATOR_AND":=%u))"
+ "(groupType:"LDB_OID_COMPARATOR_AND":=%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 returned 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);
+ uint32_t i;
+ int ret;
+ struct auth_SidAttr *groupSIDs = NULL;
+ uint32_t 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].sid);
+ 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 heuristics 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 INT64_MAX;
+ }
+
+ pwdLastSet = ldb_msg_find_attr_as_int64(msg, "pwdLastSet", 0);
+ if (pwdLastSet == 0) {
+ return 0;
+ }
+
+ if (pwdLastSet <= -1) {
+ /*
+ * This can't really happen...
+ */
+ return INT64_MAX;
+ }
+
+ if (pwdLastSet >= INT64_MAX) {
+ /*
+ * Somethings wrong with the clock...
+ */
+ return INT64_MAX;
+ }
+
+ /*
+ * Note that maxPwdAge is a stored as negative value.
+ *
+ * Possible values are in the range of:
+ *
+ * maxPwdAge: -864000000001
+ * to
+ * maxPwdAge: -9223372036854775808 (INT64_MIN)
+ *
+ */
+ maxPwdAge = get_user_max_pwd_age(module, msg, parent, domain_dn);
+ if (maxPwdAge >= -864000000000) {
+ /*
+ * This is not really possible...
+ */
+ return INT64_MAX;
+ }
+
+ if (maxPwdAge == INT64_MIN) {
+ return INT64_MAX;
+ }
+
+ /*
+ * Note we already caught maxPwdAge == INT64_MIN
+ * and pwdLastSet >= INT64_MAX above.
+ *
+ * Remember maxPwdAge is a negative number,
+ * so it results in the following.
+ *
+ * 0x7FFFFFFFFFFFFFFEULL + INT64_MAX
+ * =
+ * 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 >= INT64_MAX) {
+ return INT64_MAX;
+ }
+
+ 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 auth_SidAttr *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, "");
+ if (sid_filter == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ 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, &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 auth_SidAttr *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 auth_SidAttr *groupSIDs = NULL;
+ uint32_t 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 auth_SidAttr *user_sid = NULL;
+
+ /* lookup the best PSO object, based on the user's SID */
+ user_sid = samdb_result_dom_sid_attrs(
+ tmp_ctx, user_msg, "objectSid",
+ SE_GROUP_DEFAULT_FLAGS);
+
+ 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 ad hoc 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 ldb_parse_tree *tree;
+ 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;
+}
+
+struct operational_present_ctx {
+ const char *attr;
+ bool found_operational;
+};
+
+/*
+ callback to determine if an operational attribute (needing
+ replacement) is in use at all
+ */
+static int operational_present(struct ldb_parse_tree *tree, void *private_context)
+{
+ struct operational_present_ctx *ctx = private_context;
+ switch (tree->operation) {
+ case LDB_OP_EQUALITY:
+ if (ldb_attr_cmp(tree->u.equality.attr, ctx->attr) == 0) {
+ ctx->found_operational = true;
+ }
+ break;
+ case LDB_OP_GREATER:
+ case LDB_OP_LESS:
+ case LDB_OP_APPROX:
+ if (ldb_attr_cmp(tree->u.comparison.attr, ctx->attr) == 0) {
+ ctx->found_operational = true;
+ }
+ break;
+ case LDB_OP_SUBSTRING:
+ if (ldb_attr_cmp(tree->u.substring.attr, ctx->attr) == 0) {
+ ctx->found_operational = true;
+ }
+ break;
+ case LDB_OP_PRESENT:
+ if (ldb_attr_cmp(tree->u.present.attr, ctx->attr) == 0) {
+ ctx->found_operational = true;
+ }
+ break;
+ case LDB_OP_EXTENDED:
+ if (tree->u.extended.attr &&
+ ldb_attr_cmp(tree->u.extended.attr, ctx->attr) == 0) {
+ ctx->found_operational = true;
+ }
+ break;
+ default:
+ break;
+ }
+ return LDB_SUCCESS;
+}
+
+
+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;
+ struct operational_present_ctx ctx;
+ 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;
+
+ ctx.found_operational = false;
+
+ /*
+ * find any attributes in the parse tree that are searchable,
+ * but are stored using a different name in the backend, so we
+ * only duplicate the memory when needed
+ */
+ for (i=0;i<ARRAY_SIZE(parse_tree_sub);i++) {
+ ctx.attr = parse_tree_sub[i].attr;
+
+ ldb_parse_tree_walk(req->op.search.tree,
+ operational_present,
+ &ctx);
+ if (ctx.found_operational) {
+ break;
+ }
+ }
+
+ if (ctx.found_operational) {
+
+ ac->tree = ldb_parse_tree_copy_shallow(ac,
+ req->op.search.tree);
+
+ if (ac->tree == NULL) {
+ return ldb_operr(ldb);
+ }
+
+ /* 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(ac->tree,
+ parse_tree_sub[i].attr,
+ parse_tree_sub[i].replace);
+ }
+ } else {
+ /* Avoid allocating a copy if we do not need to */
+ ac->tree = req->op.search.tree;
+ }
+
+ 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,
+ ac->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..83729b0
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/paged_results.c
@@ -0,0 +1,888 @@
+/*
+ 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 };
+ void *ref = 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);
+ if (req_extended_dn == NULL) {
+ return ldb_module_oom(module);
+ }
+ req_extended_dn->controls = req->controls;
+ ext_ctrl_data = talloc_zero(req,
+ struct ldb_extended_dn_control);
+ if (ext_ctrl_data == NULL) {
+ return ldb_module_oom(module);
+ }
+ 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;
+ }
+
+ /*
+ * LDB does not have a function to take a full copy of
+ * this, but at least take a shallow copy
+ */
+ ac->store->expr = ldb_parse_tree_copy_shallow(ac->store,
+ req->op.search.tree);
+
+ if (ac->store->expr == NULL) {
+ return ldb_operr(ldb);
+ }
+
+ /*
+ * As the above is only a shallow copy, take a
+ * reference to ensure the values are kept around
+ */
+ ref = talloc_reference(ac->store, req->op.search.tree);
+ if (ref == NULL) {
+ return ldb_module_oom(module);
+ }
+ ac->store->expr_str = ldb_filter_from_tree(ac->store,
+ req->op.search.tree);
+ if (ac->store->expr_str == NULL) {
+ return ldb_module_oom(module);
+ }
+ if (req->op.search.attrs != NULL) {
+ ac->store->attrs = paged_copy_attrs(ac->store,
+ req->op.search.attrs);
+ if (ac->store->attrs == NULL) {
+ return ldb_module_oom(module);
+ }
+ }
+
+ /* 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..bd636c7
--- /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 multiple 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 && search_ret != LDB_ERR_NO_SUCH_OBJECT) {
+ return search_ret;
+ }
+
+ /* now delete the object in the other partitions, if required
+ */
+ 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 these 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(ret2),
+ 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;
+ }
+ }
+
+ ret2 = 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..0c218e7
--- /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 already 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..cf35624
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/partition_metadata.c
@@ -0,0 +1,596 @@
+/*
+ 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;
+ 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");
+ }
+
+ 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(NULL, (char *)tdb_data.dptr, tdb_data.dsize);
+ SAFE_FREE(tdb_data.dptr);
+ if (value_str == NULL) {
+ return ldb_module_oom(module);
+ }
+
+ *value = smb_strtoull(value_str, NULL, 10, &error, SMB_STR_STANDARD);
+ talloc_free(value_str);
+ if (error != 0) {
+ return ldb_module_error(module, LDB_ERR_OPERATIONS_ERROR,
+ "partition_metadata: converision failed");
+ }
+
+ 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..0a7a78c
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/password_hash.c
@@ -0,0 +1,5220 @@
+/*
+ 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 continues 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)) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "ndr_pull_struct_blob_all");
+ }
+
+ if (scb->sub.num_packages < 2) {
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ return ldb_module_oom(module);
+ }
+
+ if (strcmp(scb->sub.packages[i].name, "Packages") == 0) {
+ if (scpp) {
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ 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.
+ */
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ 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)) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "ndr_pull_struct_blob Packages");
+ }
+
+ if (p->names == NULL) {
+ talloc_free(scb);
+ 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)) {
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ 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)) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "ndr_pull_struct_blob PrimaryKerberos");
+ }
+
+ if (k->version != 3) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "PrimaryKerberos version != 3");
+ }
+
+ if (k->ctr.ctr3.salt.string == NULL) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "PrimaryKerberos salt == NULL");
+ }
+
+ if (strlen(k->ctr.ctr3.salt.string) == 0) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "PrimaryKerberos strlen(salt) == 0");
+ }
+
+ if (k->ctr.ctr3.num_keys != 2) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "PrimaryKerberos num_keys != 2");
+ }
+
+ if (k->ctr.ctr3.num_old_keys > k->ctr.ctr3.num_keys) {
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "PrimaryKerberos key[1] != DES_CBC_CRC");
+ }
+
+ if (k->ctr.ctr3.keys[0].value_len != 8) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "PrimaryKerberos key[0] value_len != 8");
+ }
+ if (k->ctr.ctr3.keys[1].value_len != 8) {
+ talloc_free(scb);
+ 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;
+ }
+
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ 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)) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "ndr_pull_struct_blob PrimaryKerberosNeverKeys");
+ }
+
+ if (k->version != 4) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "KerberosNerverKeys version != 4");
+ }
+
+ if (k->ctr.ctr4.salt.string == NULL) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "KerberosNewerKeys salt == NULL");
+ }
+
+ if (strlen(k->ctr.ctr4.salt.string) == 0) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "KerberosNewerKeys strlen(salt) == 0");
+ }
+
+ if (k->ctr.ctr4.num_keys != 4) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "KerberosNewerKeys num_keys != 4");
+ }
+
+ if (k->ctr.ctr4.num_old_keys > k->ctr.ctr4.num_keys) {
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "KerberosNewerKeys key[1] != AES128");
+ }
+ if (k->ctr.ctr4.keys[2].keytype != ENCTYPE_DES_CBC_MD5) {
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "KerberosNewerKeys key[3] != DES_CBC_CRC");
+ }
+
+ if (k->ctr.ctr4.keys[0].value_len != 32) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "KerberosNewerKeys key[0] value_len != 32");
+ }
+ if (k->ctr.ctr4.keys[1].value_len != 16) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "KerberosNewerKeys key[1] value_len != 16");
+ }
+ if (k->ctr.ctr4.keys[2].value_len != 8) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "KerberosNewerKeys key[2] value_len != 8");
+ }
+ if (k->ctr.ctr4.keys[3].value_len != 8) {
+ talloc_free(scb);
+ 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) {
+ talloc_free(scb);
+ 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)) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "ndr_pull_struct_blob PrimaryCLEARTEXT");
+ }
+
+ if ((ct->cleartext.length % 2) != 0) {
+ talloc_free(scb);
+ 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)) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "ndr_pull_struct_blob_all");
+ }
+
+ if (sce->values[0].length != blob.length) {
+ talloc_free(scb);
+ return ldb_error(ldb, LDB_ERR_CONSTRAINT_VIOLATION,
+ "supplementalCredentialsBlob length differ");
+ }
+
+ if (!mem_equal_const_time(sce->values[0].data, blob.data, blob.length)) {
+ talloc_free(scb);
+ 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);
+ smb_krb5_free_data_contents(io->smb_krb5_context->krb5_context,
+ &salt_data);
+ if (salt.data == NULL) {
+ return ldb_oom(ldb);
+ }
+ io->g.salt = salt.data;
+ salt.length = strlen(io->g.salt);
+
+ /*
+ * 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 reproducible 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
+
+ /* Generate 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 algorithm 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);
+ if (hash_value->scheme == NULL) {
+ TALLOC_FREE(frame);
+ return ldb_oom(ldb);
+ }
+ 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);
+ if (cmd == NULL) {
+ TALLOC_FREE(frame);
+ return ldb_oom(ldb);
+ }
+ } else {
+ cmd = talloc_asprintf(frame, "$%d$%s", algorithm, salt);
+ if (cmd == NULL) {
+ TALLOC_FREE(frame);
+ return ldb_oom(ldb);
+ }
+ }
+
+ /*
+ * 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;
+ }
+ }
+ TALLOC_FREE(frame);
+ 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;
+
+ 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 supplementalCredentials value
+ */
+ return LDB_SUCCESS;
+ }
+
+ /* if there's an old supplementalCredentials 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;
+
+ 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,
+ &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,
+ &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 around 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,
+ &current);
+ 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);
+ struct dsdb_encrypted_connection_state *opaque_connection_state =
+ ldb_get_opaque(ldb,DSDB_OPAQUE_ENCRYPTED_CONNECTION_STATE_NAME);
+
+ *werror = WERR_INVALID_PARAMETER;
+
+ if (!io->ac->update_password) {
+ return LDB_SUCCESS;
+ }
+
+ /*
+ * Prevent update password on an insecure connection.
+ * The opaque is added in the ldap backend init.
+ */
+ if (opaque_connection_state != NULL &&
+ !opaque_connection_state->using_encrypted_connection) {
+ ret = LDB_ERR_UNWILLING_TO_PERFORM;
+ *werror = WERR_GEN_FAILURE;
+ ldb_asprintf_errstring(ldb,
+ "%08X: SvcErr: DSID-031A126C, "
+ "problem 5003 (WILL_NOT_PERFORM), "
+ "data 0\n"
+ "Password modification over LDAP "
+ "must be over an encrypted connection",
+ W_ERROR_V(*werror));
+ return ret;
+ }
+
+ /*
+ * First check the old password is correct, for password
+ * changes when this hasn't already been checked by a
+ * trustworthy 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 to 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 won't 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,
+ NULL /* client_audit_info */,
+ NULL /* server_audit_info */);
+
+ }
+ 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;
+
+ *io = (struct setup_password_fields_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,
+ &quoted_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);
+ if (io->n.nt_hash == NULL) {
+ return ldb_oom(ldb);
+ }
+ 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);
+ if (io->og.nt_hash == NULL) {
+ return ldb_oom(ldb);
+ }
+ 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
+ * precedence 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 won't 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\n",
+ 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 look 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\n", 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 tombstone 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..7aec006
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/repl_meta_data.c
@@ -0,0 +1,8764 @@
+/*
+ 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;
+ bool bl_maybe_invisible;
+ bool bl_invisible;
+};
+
+/*
+ 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;
+}
+
+static int replmd_backlink_invisible(struct ldb_module *module,
+ struct ldb_message *msg,
+ struct la_backlink *bl)
+{
+ struct ldb_context *ldb = ldb_module_get_ctx(module);
+ const struct dsdb_schema *schema = NULL;
+ TALLOC_CTX *frame = NULL;
+ struct ldb_message_element *oc_element = NULL;
+ const char **allowed_attrs = NULL;
+ bool bl_allowed;
+
+ if (!bl->active || !bl->bl_maybe_invisible || bl->bl_invisible) {
+ return LDB_SUCCESS;
+ }
+
+ schema = dsdb_get_schema(ldb, NULL);
+ if (schema == NULL) {
+ return ldb_module_operr(module);
+ }
+
+ oc_element = ldb_msg_find_element(msg, "objectClass");
+ if (oc_element == NULL) {
+ return ldb_module_operr(module);
+ }
+
+ frame = talloc_stackframe();
+
+ allowed_attrs = dsdb_full_attribute_list(frame,
+ schema,
+ oc_element,
+ DSDB_SCHEMA_ALL);
+ if (allowed_attrs == NULL) {
+ TALLOC_FREE(frame);
+ return ldb_module_oom(module);
+ }
+
+ bl_allowed = str_list_check(allowed_attrs, bl->attr_name);
+ if (!bl_allowed) {
+ bl->bl_maybe_invisible = false;
+ bl->bl_invisible = true;
+ }
+
+ TALLOC_FREE(frame);
+ return LDB_SUCCESS;
+}
+
+/*
+ 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;
+ struct ldb_message *old_msg = NULL;
+ const char * const empty_attrs[] = { NULL };
+ const char * const oc_attrs[] = { "objectClass", NULL };
+ const char * const *attrs = NULL;
+ uint32_t rmd_flags = 0;
+ int ret;
+ struct ldb_context *ldb = ldb_module_get_ctx(module);
+ struct ldb_message *msg;
+ TALLOC_CTX *frame = talloc_stackframe();
+ char *dn_string;
+
+ if (bl->active && bl->bl_maybe_invisible) {
+ attrs = oc_attrs;
+ } else {
+ attrs = empty_attrs;
+ }
+
+ /*
+ - find DN of target
+ - find DN of source
+ - construct ldb_message
+ - either an add or a delete
+ */
+ ret = dsdb_module_obj_by_guid(module,
+ frame,
+ &old_msg,
+ &bl->target_guid,
+ attrs,
+ 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;
+ }
+ target_dn = old_msg->dn;
+
+ ret = replmd_backlink_invisible(module, old_msg, bl);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(frame);
+ return ret;
+ }
+
+ if (bl->active && bl->bl_invisible) {
+ rmd_flags |= DSDB_RMD_FLAG_HIDDEN_BL;
+ }
+
+ 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);
+ }
+
+ if (rmd_flags != 0) {
+ const char *flags_string = NULL;
+ struct ldb_val flagsv;
+
+ flags_string = talloc_asprintf(frame, "%u", rmd_flags);
+ if (flags_string == NULL) {
+ talloc_free(frame);
+ return ldb_module_oom(module);
+ }
+
+ flagsv = data_blob_string_const(flags_string);
+
+ ret = ldb_dn_set_extended_component(source_dn, "RMD_FLAGS", &flagsv);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(frame);
+ return ret;
+ }
+ }
+
+ /* construct a ldb_message for adding/deleting the backlink */
+ msg->dn = target_dn;
+ dn_string = ldb_dn_get_extended_linearized(frame, source_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;
+ bl->bl_maybe_invisible = target_attr->bl_maybe_invisible;
+ bl->bl_invisible = false;
+
+ 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;
+ bl.bl_maybe_invisible = target_attr->bl_maybe_invisible;
+ bl.bl_invisible = false;
+
+ 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
+ * periodically 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 simpler)
+ * 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) {
+ talloc_free(tmp_ctx);
+ 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);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+ /* 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);
+ }
+ ret = ldb_msg_add_empty(old_msg, el->name, 0, &new_el);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ new_el->num_values = el->num_values;
+ new_el->values = talloc_steal(msg->elements, el->values);
+
+ /* TODO: this relies 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 completed, 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\n",
+ 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\n",
+ 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\n",
+ 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 attribute, 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, NULL);
+ 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 modification 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 modification 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\n",
+ 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'\n",
+ 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\n",
+ 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 inheritance 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 triggered '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 triggered '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 triggered '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 up-to-date 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 because 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 because 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..5c16396
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/resolve_oids.c
@@ -0,0 +1,710 @@
+/*
+ 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:
+ attr = tree->u.equality.attr;
+ valp = &tree->u.equality.value;
+ break;
+ case LDB_OP_GREATER:
+ case LDB_OP_LESS:
+ case LDB_OP_APPROX:
+ attr = tree->u.comparison.attr;
+ valp = &tree->u.comparison.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:
+ attrp = &tree->u.equality.attr;
+ valp = &tree->u.equality.value;
+ break;
+ case LDB_OP_GREATER:
+ case LDB_OP_LESS:
+ case LDB_OP_APPROX:
+ attrp = &tree->u.comparison.attr;
+ valp = &tree->u.comparison.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..aa5f873
--- /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 locally.
+ * 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 locally.
+ * 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..d80d2af
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/rootdse.c
@@ -0,0 +1,1802 @@
+/*
+ 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 *ext_dn;
+ 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);
+
+ ext_dn = talloc_get_type(edn_control->data, struct ldb_extended_dn_control);
+ if (ext_dn) {
+ edn_type = ext_dn->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 meaningful 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 *ext_dn;
+ int edn_type = 0;
+ ext_dn = talloc_get_type(edn_control->data, struct ldb_extended_dn_control);
+ if (ext_dn != NULL) {
+ edn_type = ext_dn->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
+ *
+ * We can't call samdb_ntds_settings_dn() in the rootdse, as
+ * that routine used the rootdse result!
+ */
+ 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..e5e5ce7
--- /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
+ * explanation */
+ 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..37213a5
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/samba_dsdb.c
@@ -0,0 +1,611 @@
+/*
+ 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;
+
+ ret = ldb_msg_add_empty(features_msg, SAMBA_COMPATIBLE_FEATURES_ATTR,
+ LDB_FLAG_MOD_DELETE, &features_el);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ 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..d79138a
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/samldb.c
@@ -0,0 +1,5736 @@
+/*
+ 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 0xBFFFFFFF,
+ * 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 very 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 };
+ const char * const group_type_attrs[] = { "groupType", NULL };
+ unsigned group_type;
+
+ 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,
+ group_type_attrs, 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;
+
+ /* The new primary group must not be domain-local. */
+ group_type = ldb_msg_find_attr_as_uint(group_res->msgs[0], "groupType", 0);
+ if (group_type & GROUP_TYPE_RESOURCE_GROUP) {
+ return dsdb_module_werror(ac->module,
+ LDB_ERR_UNWILLING_TO_PERFORM,
+ WERR_MEMBER_NOT_IN_GROUP,
+ "may not set resource group as primary group!");
+ }
+
+ 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..17dd7c4
--- /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;
+ }
+
+ /*
+ * normally 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 separately */
+
+ 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..178a991
--- /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 therefore 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..fecaa3e
--- /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(&regex, rs, 0);
+ assert_int_equal(0, ret);
+
+ ret = regexec(&regex, line, 0, NULL, 0);
+ assert_int_equal(0, ret);
+
+ regfree(&regex);
+ 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(&regex, rs, 0);
+ assert_int_equal(0, ret);
+
+ ret = regexec(&regex, line, 0, NULL, 0);
+ assert_int_equal(0, ret);
+
+ regfree(&regex);
+ 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(&regex, rs, 0);
+ assert_int_equal(0, ret);
+
+ ret = regexec(&regex, line, 0, NULL, 0);
+ assert_int_equal(0, ret);
+
+ regfree(&regex);
+ 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(&regex, rs, 0);
+ assert_int_equal(0, ret);
+
+ ret = regexec(&regex, line, 0, NULL, 0);
+ assert_int_equal(0, ret);
+
+ regfree(&regex);
+ 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(&regex, rs, 0);
+ assert_int_equal(0, ret);
+
+ ret = regexec(&regex, line, 0, NULL, 0);
+ assert_int_equal(0, ret);
+
+ regfree(&regex);
+ 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(&regex, rs, 0);
+ assert_int_equal(0, ret);
+
+ ret = regexec(&regex, line, 0, NULL, 0);
+ assert_int_equal(0, ret);
+
+ regfree(&regex);
+ TALLOC_FREE(ctx);
+
+}
+
+/*
+ * minimal unit test of commit_failure_hr, that ensures
+ * that all the expected content 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(&regex, rs, 0);
+ assert_int_equal(0, ret);
+
+ ret = regexec(&regex, line, 0, NULL, 0);
+ assert_int_equal(0, ret);
+
+ regfree(&regex);
+ 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(&regex, rs, 0);
+ assert_int_equal(0, ret);
+
+ ret = regexec(&regex, line, 0, NULL, 0);
+ assert_int_equal(0, ret);
+
+ regfree(&regex);
+ 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(&regex, rs, 0);
+ assert_int_equal(0, ret);
+
+ ret = regexec(&regex, line, 0, NULL, 0);
+ assert_int_equal(0, ret);
+
+ regfree(&regex);
+ 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..44f7b77
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/tests/test_audit_util.c
@@ -0,0 +1,1261 @@
+/*
+ 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();
+ assert_false(json_is_invalid(&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 multi-valued attribute
+ */
+ 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(&regex, rs, 0);
+ assert_int_equal(0, ret);
+
+ ret = regexec(&regex, line, 0, NULL, 0);
+ assert_int_equal(0, ret);
+
+ regfree(&regex);
+ 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..2d37d53
--- /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 that 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..0342da3
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/util.c
@@ -0,0 +1,1921 @@
+/*
+ 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 an object given a GUID. This searches across all partitions
+ */
+int dsdb_module_obj_by_guid(struct ldb_module *module,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message **_msg,
+ const struct GUID *guid,
+ const char * const *attrs,
+ 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,
+ 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;
+ }
+
+ *_msg = talloc_steal(mem_ctx, res->msgs[0]);
+
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+}
+
+/*
+ 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_message *msg = NULL;
+ static const char * const attrs[] = { NULL };
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ int ret;
+
+ ret = dsdb_module_obj_by_guid(module,
+ tmp_ctx,
+ &msg,
+ guid,
+ attrs,
+ parent);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ *dn = talloc_steal(mem_ctx, msg->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)
+{
+ static const char * const 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 * const 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=Configuration 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;
+ static const char * const 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) {
+ static const char * const 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 * const 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!\n");
+ 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=Administrator,cn=users,dc=samba,dc=example,dc=com becomes
+ CN=Administrator,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 determine 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..63726c0
--- /dev/null
+++ b/source4/dsdb/samdb/ldb_modules/util.h
@@ -0,0 +1,43 @@
+/*
+ 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 "librpc/gen_ndr/security.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(&current->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'
+)
diff --git a/source4/dsdb/samdb/samdb.c b/source4/dsdb/samdb/samdb.c
new file mode 100644
index 0000000..42375a8
--- /dev/null
+++ b/source4/dsdb/samdb/samdb.c
@@ -0,0 +1,344 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ interface functions for the sam database
+
+ Copyright (C) Andrew Tridgell 2004
+ Copyright (C) Volker Lendecke 2004
+ Copyright (C) Andrew Bartlett <abartlet@samba.org> 2006
+
+ 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 "librpc/gen_ndr/ndr_netlogon.h"
+#include "librpc/gen_ndr/ndr_security.h"
+#include "lib/events/events.h"
+#include "lib/ldb-samba/ldb_wrap.h"
+#include <ldb.h>
+#include <ldb_errors.h>
+#include "libcli/security/security.h"
+#include "libcli/security/claims-conversions.h"
+#include "libcli/auth/libcli_auth.h"
+#include "libcli/ldap/ldap_ndr.h"
+#include "system/time.h"
+#include "system/filesys.h"
+#include "ldb_wrap.h"
+#include "../lib/util/util_ldb.h"
+#include "dsdb/samdb/samdb.h"
+#include "../libds/common/flags.h"
+#include "param/param.h"
+#include "lib/events/events.h"
+#include "auth/credentials/credentials.h"
+#include "param/secrets.h"
+#include "auth/auth.h"
+#include "lib/tsocket/tsocket.h"
+#include "lib/param/loadparm.h"
+
+/*
+ connect to the SAM database specified by URL
+ return an opaque context pointer on success, or NULL on failure
+ */
+int samdb_connect_url(TALLOC_CTX *mem_ctx,
+ struct tevent_context *ev_ctx,
+ struct loadparm_context *lp_ctx,
+ struct auth_session_info *session_info,
+ unsigned int flags,
+ const char *url,
+ const struct tsocket_address *remote_address,
+ struct ldb_context **ldb_ret,
+ char **errstring)
+{
+ struct ldb_context *ldb = NULL;
+ int ret;
+ *ldb_ret = NULL;
+ *errstring = NULL;
+
+ /* We create sam.ldb in provision, and never anywhere else */
+ flags |= LDB_FLG_DONT_CREATE_DB;
+
+ if (remote_address == NULL) {
+ ldb = ldb_wrap_find(url, ev_ctx, lp_ctx,
+ session_info, NULL, flags);
+ if (ldb != NULL) {
+ *ldb_ret = talloc_reference(mem_ctx, ldb);
+ if (*ldb_ret == NULL) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ return LDB_SUCCESS;
+ }
+ }
+
+ ldb = samba_ldb_init(mem_ctx, ev_ctx, lp_ctx, session_info, NULL);
+
+ if (ldb == NULL) {
+ *errstring = talloc_asprintf(mem_ctx,
+ "Failed to set up Samba ldb "
+ "wrappers with samba_ldb_init() "
+ "to connect to %s",
+ url);
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ dsdb_set_global_schema(ldb);
+
+ ret = samba_ldb_connect(ldb, lp_ctx, url, flags);
+ if (ret != LDB_SUCCESS) {
+ *errstring = talloc_asprintf(mem_ctx,
+ "Failed to connect to %s: %s",
+ url,
+ ldb_errstring(ldb));
+ talloc_free(ldb);
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ /*
+ * If a remote_address was specified, then set it on the DB
+ * and do not add to the wrap list (as we need to keep the LDB
+ * pointer unique for the address).
+ *
+ * We use this for audit logging and for the "netlogon" attribute
+ */
+ if (remote_address != NULL) {
+ ldb_set_opaque(ldb, "remoteAddress",
+ discard_const(remote_address));
+ *ldb_ret = ldb;
+ return LDB_SUCCESS;
+ }
+
+ if (!ldb_wrap_add(url, ev_ctx, lp_ctx, session_info, NULL, flags, ldb)) {
+ *errstring = talloc_asprintf(mem_ctx,
+ "Failed to add cached DB reference"
+ " to %s",
+ url);
+ talloc_free(ldb);
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ *ldb_ret = ldb;
+ return LDB_SUCCESS;
+}
+
+
+/*
+ connect to the SAM database
+ return an opaque context pointer on success, or NULL on failure
+ */
+struct ldb_context *samdb_connect(TALLOC_CTX *mem_ctx,
+ struct tevent_context *ev_ctx,
+ struct loadparm_context *lp_ctx,
+ struct auth_session_info *session_info,
+ const struct tsocket_address *remote_address,
+ unsigned int flags)
+{
+ char *errstring;
+ struct ldb_context *ldb;
+ int ret = samdb_connect_url(mem_ctx,
+ ev_ctx,
+ lp_ctx,
+ session_info,
+ flags,
+ "sam.ldb",
+ remote_address,
+ &ldb,
+ &errstring);
+ if (ret == LDB_SUCCESS) {
+ return ldb;
+ }
+ return NULL;
+}
+
+/****************************************************************************
+ Create the SID list for this user.
+****************************************************************************/
+NTSTATUS security_token_create(TALLOC_CTX *mem_ctx,
+ struct loadparm_context *lp_ctx,
+ uint32_t num_sids,
+ const struct auth_SidAttr *sids,
+ uint32_t num_device_sids,
+ const struct auth_SidAttr *device_sids,
+ struct auth_claims auth_claims,
+ uint32_t session_info_flags,
+ struct security_token **token)
+{
+ struct security_token *ptoken;
+ uint32_t i;
+ NTSTATUS status;
+ enum claims_evaluation_control evaluate_claims;
+ bool sids_are_valid = false;
+ bool device_sids_are_valid = false;
+ bool authentication_was_compounded = session_info_flags & AUTH_SESSION_INFO_FORCE_COMPOUNDED_AUTHENTICATION;
+
+ /*
+ * Some special-case callers can't supply the lp_ctx, but do
+ * not interact with claims or conditional ACEs
+ */
+ if (lp_ctx == NULL) {
+ evaluate_claims = CLAIMS_EVALUATION_INVALID_STATE;
+ } else {
+ enum acl_claims_evaluation claims_evaultion_setting
+ = lpcfg_acl_claims_evaluation(lp_ctx);
+
+ /*
+ * We are well inside the AD DC, so we do not need to check
+ * the server role etc
+ */
+ switch (claims_evaultion_setting) {
+ case ACL_CLAIMS_EVALUATION_AD_DC_ONLY:
+ evaluate_claims = CLAIMS_EVALUATION_ALWAYS;
+ break;
+ default:
+ evaluate_claims = CLAIMS_EVALUATION_NEVER;
+ }
+ }
+
+ ptoken = security_token_initialise(mem_ctx, evaluate_claims);
+ NT_STATUS_HAVE_NO_MEMORY(ptoken);
+
+ if (num_sids > UINT32_MAX - 6) {
+ talloc_free(ptoken);
+ return NT_STATUS_INVALID_PARAMETER;
+ }
+ ptoken->sids = talloc_array(ptoken, struct dom_sid, num_sids + 6 /* over-allocate */);
+ if (ptoken->sids == NULL) {
+ talloc_free(ptoken);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ ptoken->num_sids = 0;
+
+ for (i = 0; i < num_sids; i++) {
+ uint32_t check_sid_idx;
+ for (check_sid_idx = 0;
+ check_sid_idx < ptoken->num_sids;
+ check_sid_idx++) {
+ if (dom_sid_equal(&ptoken->sids[check_sid_idx], &sids[i].sid)) {
+ break;
+ }
+ }
+
+ if (check_sid_idx == ptoken->num_sids) {
+ const struct dom_sid *sid = &sids[i].sid;
+
+ sids_are_valid = sids_are_valid || dom_sid_equal(
+ sid, &global_sid_Claims_Valid);
+ authentication_was_compounded = authentication_was_compounded || dom_sid_equal(
+ sid, &global_sid_Compounded_Authentication);
+
+ ptoken->sids = talloc_realloc(ptoken, ptoken->sids, struct dom_sid, ptoken->num_sids + 1);
+ if (ptoken->sids == NULL) {
+ talloc_free(ptoken);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ ptoken->sids[ptoken->num_sids] = *sid;
+ ptoken->num_sids++;
+ }
+ }
+
+ if (authentication_was_compounded && num_device_sids) {
+ ptoken->device_sids = talloc_array(ptoken, struct dom_sid, num_device_sids);
+ if (ptoken->device_sids == NULL) {
+ talloc_free(ptoken);
+ return NT_STATUS_NO_MEMORY;
+ }
+ for (i = 0; i < num_device_sids; i++) {
+ uint32_t check_sid_idx;
+ for (check_sid_idx = 0;
+ check_sid_idx < ptoken->num_device_sids;
+ check_sid_idx++) {
+ if (dom_sid_equal(&ptoken->device_sids[check_sid_idx], &device_sids[i].sid)) {
+ break;
+ }
+ }
+
+ if (check_sid_idx == ptoken->num_device_sids) {
+ const struct dom_sid *device_sid = &device_sids[i].sid;
+
+ device_sids_are_valid = device_sids_are_valid || dom_sid_equal(
+ device_sid, &global_sid_Claims_Valid);
+
+ ptoken->device_sids = talloc_realloc(ptoken,
+ ptoken->device_sids,
+ struct dom_sid,
+ ptoken->num_device_sids + 1);
+ if (ptoken->device_sids == NULL) {
+ talloc_free(ptoken);
+ return NT_STATUS_NO_MEMORY;
+ }
+
+ ptoken->device_sids[ptoken->num_device_sids] = *device_sid;
+ ptoken->num_device_sids++;
+ }
+ }
+ }
+
+ /* The caller may have requested simple privileges, for example if there isn't a local DB */
+ if (session_info_flags & AUTH_SESSION_INFO_SIMPLE_PRIVILEGES) {
+ /* Shortcuts to prevent recursion and avoid lookups */
+ if (ptoken->sids == NULL) {
+ ptoken->privilege_mask = 0;
+ } else if (security_token_is_system(ptoken)) {
+ ptoken->privilege_mask = ~0;
+ } else if (security_token_is_anonymous(ptoken)) {
+ ptoken->privilege_mask = 0;
+ } else if (security_token_has_builtin_administrators(ptoken)) {
+ ptoken->privilege_mask = ~0;
+ } else {
+ /* All other 'users' get a empty priv set so far */
+ ptoken->privilege_mask = 0;
+ }
+ } else {
+ /* setup the privilege mask for this token */
+ status = samdb_privilege_setup(lp_ctx, ptoken);
+ if (!NT_STATUS_IS_OK(status)) {
+ talloc_free(ptoken);
+ DEBUG(1,("Unable to access privileges database\n"));
+ return status;
+ }
+ }
+
+ /*
+ * TODO: we might want to regard ‘session_info_flags’ for the device
+ * SIDs as well as for the client SIDs.
+ */
+
+ if (sids_are_valid) {
+ status = claims_data_security_claims(ptoken,
+ auth_claims.user_claims,
+ &ptoken->user_claims,
+ &ptoken->num_user_claims);
+ if (!NT_STATUS_IS_OK(status)) {
+ talloc_free(ptoken);
+ return status;
+ }
+ }
+
+ if (device_sids_are_valid && authentication_was_compounded) {
+ status = claims_data_security_claims(ptoken,
+ auth_claims.device_claims,
+ &ptoken->device_claims,
+ &ptoken->num_device_claims);
+ if (!NT_STATUS_IS_OK(status)) {
+ talloc_free(ptoken);
+ return status;
+ }
+ }
+
+ security_token_debug(0, 10, ptoken);
+
+ *token = ptoken;
+
+ return NT_STATUS_OK;
+}
diff --git a/source4/dsdb/samdb/samdb.h b/source4/dsdb/samdb/samdb.h
new file mode 100644
index 0000000..ba7cd1b
--- /dev/null
+++ b/source4/dsdb/samdb/samdb.h
@@ -0,0 +1,410 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ interface functions for the sam database
+
+ 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/>.
+*/
+
+#ifndef __SAMDB_H__
+#define __SAMDB_H__
+
+struct auth_SidAttr;
+struct auth_session_info;
+struct dsdb_control_current_partition;
+struct dsdb_extended_replicated_object;
+struct dsdb_extended_replicated_objects;
+struct loadparm_context;
+struct tevent_context;
+struct tsocket_address;
+struct dsdb_trust_routing_table;
+
+enum dsdb_password_checked {
+ DSDB_PASSWORD_NOT_CHECKED = 0, /* unused */
+ DSDB_PASSWORD_RESET,
+ DSDB_PASSWORD_CHECKED_AND_CORRECT
+};
+
+#include "librpc/gen_ndr/security.h"
+#include <ldb.h>
+#include "lib/ldb-samba/ldif_handlers.h"
+#include "librpc/gen_ndr/samr.h"
+#include "librpc/gen_ndr/drsuapi.h"
+#include "librpc/gen_ndr/drsblobs.h"
+#include "dsdb/schema/schema.h"
+#include "auth/session.h"
+#include "dsdb/samdb/samdb_proto.h"
+#include "dsdb/common/dsdb_dn.h"
+#include "dsdb/common/util_links.h"
+#include "dsdb/common/proto.h"
+#include "../libds/common/flags.h"
+
+#define DSDB_CONTROL_CURRENT_PARTITION_OID "1.3.6.1.4.1.7165.4.3.2"
+struct dsdb_control_current_partition {
+ /*
+ * this is the version of the dsdb_control_current_partition
+ * version 0: initial implementation
+ * version 1: got rid of backend and module fields
+ */
+#define DSDB_CONTROL_CURRENT_PARTITION_VERSION 1
+ uint32_t version;
+ struct ldb_dn *dn;
+};
+
+
+/*
+ flags in dsdb_repl_flags to control replication logic
+ */
+#define DSDB_REPL_FLAG_PRIORITISE_INCOMING 1
+#define DSDB_REPL_FLAG_PARTIAL_REPLICA 2
+#define DSDB_REPL_FLAG_ADD_NCNAME 4
+#define DSDB_REPL_FLAG_EXPECT_NO_SECRETS 8
+#define DSDB_REPL_FLAG_OBJECT_SUBSET 16
+#define DSDB_REPL_FLAG_TARGETS_UPTODATE 32
+
+#define DSDB_CONTROL_REPLICATED_UPDATE_OID "1.3.6.1.4.1.7165.4.3.3"
+
+#define DSDB_CONTROL_DN_STORAGE_FORMAT_OID "1.3.6.1.4.1.7165.4.3.4"
+/* DSDB_CONTROL_DN_STORAGE_FORMAT_OID has NULL data and behaves very
+ * much like LDB_CONTROL_EXTENDED_DN_OID when the DB stores an
+ * extended DN, and otherwise returns normal DNs */
+
+#define DSDB_CONTROL_PASSWORD_CHANGE_STATUS_OID "1.3.6.1.4.1.7165.4.3.8"
+
+struct dsdb_user_pwd_settings {
+ uint32_t pwdProperties;
+ uint32_t pwdHistoryLength;
+ int64_t maxPwdAge;
+ int64_t minPwdAge;
+ uint32_t minPwdLength;
+ bool store_cleartext;
+ const char *netbios_domain;
+ const char *dns_domain;
+ const char *realm;
+};
+
+struct dsdb_control_password_change_status {
+ struct dsdb_user_pwd_settings domain_data;
+ enum samPwdChangeReason reject_reason;
+};
+
+#define DSDB_CONTROL_PASSWORD_HASH_VALUES_OID "1.3.6.1.4.1.7165.4.3.9"
+
+#define DSDB_CONTROL_PASSWORD_CHANGE_OLD_PW_CHECKED_OID "1.3.6.1.4.1.7165.4.3.10"
+struct dsdb_control_password_change {
+ enum dsdb_password_checked old_password_checked;
+};
+
+/**
+ DSDB_CONTROL_APPLY_LINKS is internal to Samba4 - a token passed between repl_meta_data and linked_attributes modules
+*/
+#define DSDB_CONTROL_APPLY_LINKS "1.3.6.1.4.1.7165.4.3.11"
+
+/*
+ * this should only be used for importing users from Samba3
+ */
+#define DSDB_CONTROL_BYPASS_PASSWORD_HASH_OID "1.3.6.1.4.1.7165.4.3.12"
+
+/**
+ OID used to allow the replacement of replPropertyMetaData.
+ It is used when the current replmetadata needs to be edited.
+*/
+#define DSDB_CONTROL_CHANGEREPLMETADATA_OID "1.3.6.1.4.1.7165.4.3.14"
+
+/* passed when we want to get the behaviour of the non-global catalog port */
+#define DSDB_CONTROL_NO_GLOBAL_CATALOG "1.3.6.1.4.1.7165.4.3.17"
+
+/* passed when we want special behaviour for partial replicas */
+#define DSDB_CONTROL_PARTIAL_REPLICA "1.3.6.1.4.1.7165.4.3.18"
+
+/* passed when we want special behaviour for dbcheck */
+#define DSDB_CONTROL_DBCHECK "1.3.6.1.4.1.7165.4.3.19"
+
+/* passed when dbcheck wants to modify a read only replica (very special case) */
+#define DSDB_CONTROL_DBCHECK_MODIFY_RO_REPLICA "1.3.6.1.4.1.7165.4.3.19.1"
+
+/* passed by dbcheck to fix duplicate linked attributes (bug #13095) */
+#define DSDB_CONTROL_DBCHECK_FIX_DUPLICATE_LINKS "1.3.6.1.4.1.7165.4.3.19.2"
+
+/* passed by dbcheck to fix the DN string of a one-way-link (bug #13495) */
+#define DSDB_CONTROL_DBCHECK_FIX_LINK_DN_NAME "1.3.6.1.4.1.7165.4.3.19.3"
+
+/* passed by dbcheck to fix the DN SID of a one-way-link (bug #13418) */
+#define DSDB_CONTROL_DBCHECK_FIX_LINK_DN_SID "1.3.6.1.4.1.7165.4.3.19.4"
+
+/* passed when importing plain text password on upgrades */
+#define DSDB_CONTROL_PASSWORD_BYPASS_LAST_SET_OID "1.3.6.1.4.1.7165.4.3.20"
+
+/*
+ * passed from the descriptor module in order to
+ * store the recalculated nTSecurityDescriptor without
+ * modifying the replPropertyMetaData.
+ */
+#define DSDB_CONTROL_SEC_DESC_PROPAGATION_OID "1.3.6.1.4.1.7165.4.3.21"
+
+/*
+ * passed when creating a interdomain trust account through LSA
+ * to relax constraints in the samldb ldb module.
+ */
+#define DSDB_CONTROL_PERMIT_INTERDOMAIN_TRUST_UAC_OID "1.3.6.1.4.1.7165.4.3.23"
+
+/*
+ * Internal control to mark requests as being part of Tombstone restoring
+ * procedure - it requires slightly special behavior like:
+ * - a bit different security checks
+ * - restoring certain attributes to their default values, etc
+ */
+#define DSDB_CONTROL_RESTORE_TOMBSTONE_OID "1.3.6.1.4.1.7165.4.3.24"
+
+/**
+ OID used to allow the replacement of replPropertyMetaData.
+ It is used when the current replmetadata needs only to be re-sorted, but not edited.
+*/
+#define DSDB_CONTROL_CHANGEREPLMETADATA_RESORT_OID "1.3.6.1.4.1.7165.4.3.25"
+
+/*
+ * pass the default state of pwdLastSet between the "samldb" and "password_hash"
+ * modules.
+ */
+#define DSDB_CONTROL_PASSWORD_DEFAULT_LAST_SET_OID "1.3.6.1.4.1.7165.4.3.26"
+
+/*
+ * pass the userAccountControl changes between the "samldb" and "password_hash"
+ * modules.
+ */
+#define DSDB_CONTROL_PASSWORD_USER_ACCOUNT_CONTROL_OID "1.3.6.1.4.1.7165.4.3.27"
+struct dsdb_control_password_user_account_control {
+ uint32_t req_flags; /* the flags given by the client request */
+ uint32_t old_flags; /* the old flags stored (0 on add) */
+ uint32_t new_flags; /* the new flags stored */
+};
+
+/*
+ * Ignores strict checking when adding objects to samldb.
+ * This is used when provisioning, as checking all objects when added
+ * was slow due to an unindexed search.
+ */
+#define DSDB_CONTROL_SKIP_DUPLICATES_CHECK_OID "1.3.6.1.4.1.7165.4.3.28"
+
+/* passed when we want to thoroughly delete linked attributes */
+#define DSDB_CONTROL_REPLMD_VANISH_LINKS "1.3.6.1.4.1.7165.4.3.29"
+
+/*
+ * lockoutTime is a replicated attribute, but must be modified before
+ * connectivity occurs to allow password lockouts.
+ */
+#define DSDB_CONTROL_FORCE_RODC_LOCAL_CHANGE "1.3.6.1.4.1.7165.4.3.31"
+
+#define DSDB_CONTROL_INVALID_NOT_IMPLEMENTED "1.3.6.1.4.1.7165.4.3.32"
+
+/*
+ * Used to pass "user password change" vs "password reset" from the ACL to the
+ * password_hash module, ensuring both modules treat the request identical.
+ */
+#define DSDB_CONTROL_PASSWORD_ACL_VALIDATION_OID "1.3.6.1.4.1.7165.4.3.33"
+struct dsdb_control_password_acl_validation {
+ bool pwd_reset;
+};
+
+/*
+ * Used to pass the current transaction identifier from the audit_log
+ * module to group membership auditing module
+ */
+#define DSDB_CONTROL_TRANSACTION_IDENTIFIER_OID "1.3.6.1.4.1.7165.4.3.34"
+struct dsdb_control_transaction_identifier {
+ struct GUID transaction_guid;
+};
+
+/*
+ * passed when we want to allow validated writes to dNSHostName and
+ * servicePrincipalName.
+ */
+#define DSDB_CONTROL_FORCE_ALLOW_VALIDATED_DNS_HOSTNAME_SPN_WRITE_OID "1.3.6.1.4.1.7165.4.3.35"
+
+/*
+ * Used by descriptor module to pass a special SD to acl module,
+ * one without the user-provided descriptor taken into account
+ */
+
+#define DSDB_CONTROL_CALCULATED_DEFAULT_SD_OID "1.3.6.1.4.1.7165.4.3.36"
+struct dsdb_control_calculated_default_sd {
+ struct security_descriptor *default_sd;
+ bool specified_sd:1;
+ bool specified_sacl:1;
+};
+
+#define DSDB_CONTROL_ACL_READ_OID "1.3.6.1.4.1.7165.4.3.37"
+
+#define DSDB_EXTENDED_REPLICATED_OBJECTS_OID "1.3.6.1.4.1.7165.4.4.1"
+struct dsdb_extended_replicated_object {
+ struct ldb_message *msg;
+ struct GUID object_guid;
+ struct GUID *parent_guid;
+ const char *when_changed;
+ struct replPropertyMetaDataBlob *meta_data;
+
+ /* Only used for internal processing in repl_meta_data */
+ struct ldb_dn *last_known_parent;
+ struct ldb_dn *local_parent_dn;
+};
+
+/*
+ * the schema_dn is passed as struct ldb_dn in
+ * req->op.extended.data
+ */
+#define DSDB_EXTENDED_SCHEMA_UPDATE_NOW_OID "1.3.6.1.4.1.7165.4.4.2"
+
+struct dsdb_extended_replicated_objects {
+ /*
+ * this is the version of the dsdb_extended_replicated_objects
+ * version 0: initial implementation
+ */
+#define DSDB_EXTENDED_REPLICATED_OBJECTS_VERSION 3
+ uint32_t version;
+
+ /* DSDB_REPL_FLAG_* flags */
+ uint32_t dsdb_repl_flags;
+
+ struct ldb_dn *partition_dn;
+
+ const struct repsFromTo1 *source_dsa;
+ const struct drsuapi_DsReplicaCursor2CtrEx *uptodateness_vector;
+
+ uint32_t num_objects;
+ struct dsdb_extended_replicated_object *objects;
+
+ uint32_t linked_attributes_count;
+ struct drsuapi_DsReplicaLinkedAttribute *linked_attributes;
+
+ WERROR error;
+
+ bool originating_updates;
+};
+
+/* In ldb.h: LDB_EXTENDED_SEQUENCE_NUMBER 1.3.6.1.4.1.7165.4.4.3 */
+
+#define DSDB_EXTENDED_CREATE_PARTITION_OID "1.3.6.1.4.1.7165.4.4.4"
+struct dsdb_create_partition_exop {
+ struct ldb_dn *new_dn;
+};
+
+/* this takes a struct dsdb_fsmo_extended_op */
+#define DSDB_EXTENDED_ALLOCATE_RID_POOL "1.3.6.1.4.1.7165.4.4.5"
+
+struct dsdb_fsmo_extended_op {
+ uint64_t fsmo_info;
+ struct GUID destination_dsa_guid;
+};
+
+#define DSDB_EXTENDED_SCHEMA_UPGRADE_IN_PROGRESS_OID "1.3.6.1.4.1.7165.4.4.6"
+
+/*
+ * passed from the descriptor module in order to
+ * store the recalculated nTSecurityDescriptor without
+ * modifying the replPropertyMetaData.
+ */
+#define DSDB_EXTENDED_SEC_DESC_PROPAGATION_OID "1.3.6.1.4.1.7165.4.4.7"
+struct dsdb_extended_sec_desc_propagation_op {
+ struct ldb_dn *nc_root;
+ struct GUID guid;
+ bool include_self;
+ struct GUID parent_guid;
+};
+
+/* this takes no data */
+#define DSDB_EXTENDED_CREATE_OWN_RID_SET "1.3.6.1.4.1.7165.4.4.8"
+
+/* this takes a struct dsdb_extended_allocate_rid */
+#define DSDB_EXTENDED_ALLOCATE_RID "1.3.6.1.4.1.7165.4.4.9"
+
+struct dsdb_extended_allocate_rid {
+ uint32_t rid;
+};
+
+#define DSDB_EXTENDED_SCHEMA_LOAD "1.3.6.1.4.1.7165.4.4.10"
+
+#define DSDB_OPENLDAP_DEREFERENCE_CONTROL "1.3.6.1.4.1.4203.666.5.16"
+
+struct dsdb_openldap_dereference {
+ const char *source_attribute;
+ const char **dereference_attribute;
+};
+
+struct dsdb_openldap_dereference_control {
+ struct dsdb_openldap_dereference **dereference;
+};
+
+struct dsdb_openldap_dereference_result {
+ const char *source_attribute;
+ const char *dereferenced_dn;
+ int num_attributes;
+ struct ldb_message_element *attributes;
+};
+
+struct dsdb_openldap_dereference_result_control {
+ struct dsdb_openldap_dereference_result **attributes;
+};
+
+struct samldb_msds_intid_persistant {
+ uint32_t msds_intid;
+};
+
+#define SAMLDB_MSDS_INTID_OPAQUE "SAMLDB_MSDS_INTID_OPAQUE"
+
+#define DSDB_PARTITION_DN "@PARTITION"
+#define DSDB_PARTITION_ATTR "partition"
+
+#define DSDB_EXTENDED_DN_STORE_FORMAT_OPAQUE_NAME "dsdb_extended_dn_store_format"
+struct dsdb_extended_dn_store_format {
+ bool store_extended_dn_in_ldb;
+};
+
+#define DSDB_OPAQUE_PARTITION_MODULE_MSG_OPAQUE_NAME "DSDB_OPAQUE_PARTITION_MODULE_MSG"
+
+#define DSDB_FULL_JOIN_REPLICATION_COMPLETED_OPAQUE_NAME "DSDB_FULL_JOIN_REPLICATION_COMPLETED"
+
+#define DSDB_OPAQUE_ENCRYPTED_CONNECTION_STATE_NAME "DSDB_OPAQUE_ENCRYPTED_CONNECTION_STATE_MSG"
+struct dsdb_encrypted_connection_state {
+ bool using_encrypted_connection;
+};
+
+#define DSDB_SAMDB_MINIMUM_ALLOWED_RID 1000
+
+#define DSDB_METADATA_SCHEMA_SEQ_NUM "SCHEMA_SEQ_NUM"
+
+/*
+ * must be in LDB_FLAG_INTERNAL_MASK
+ * see also the values in lib/ldb/include/ldb_module.h
+ */
+#define DSDB_FLAG_INTERNAL_FORCE_META_DATA 0x10000
+
+#define SAMBA_COMPATIBLE_FEATURES_ATTR "compatibleFeatures"
+#define SAMBA_REQUIRED_FEATURES_ATTR "requiredFeatures"
+#define SAMBA_FEATURES_SUPPORTED_FLAG "@SAMBA_FEATURES_SUPPORTED"
+
+#define SAMBA_SORTED_LINKS_FEATURE "sortedLinks"
+#define SAMBA_ENCRYPTED_SECRETS_FEATURE "encryptedSecrets"
+/*
+ * lmdb level one feature is an experimental release with basic support
+ * for lmdb database files, instead of tdb.
+ * - Keys are limited to 511 bytes long so GUID indexes are required
+ * - Currently only the:
+ * partition data files
+ * are in lmdb format.
+ */
+#define SAMBA_LMDB_LEVEL_ONE_FEATURE "lmdbLevelOne"
+
+#endif /* __SAMDB_H__ */
diff --git a/source4/dsdb/samdb/samdb_privilege.c b/source4/dsdb/samdb/samdb_privilege.c
new file mode 100644
index 0000000..c50243c
--- /dev/null
+++ b/source4/dsdb/samdb/samdb_privilege.c
@@ -0,0 +1,134 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ manipulate privilege records in samdb
+
+ 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/>.
+*/
+
+#include "includes.h"
+#include "libcli/ldap/ldap_ndr.h"
+#include "dsdb/samdb/samdb.h"
+#include "auth/auth.h"
+#include "libcli/security/security.h"
+#include "../lib/util/util_ldb.h"
+#include "param/param.h"
+#include "ldb_wrap.h"
+
+/* connect to the privilege database */
+struct ldb_context *privilege_connect(TALLOC_CTX *mem_ctx,
+ struct loadparm_context *lp_ctx)
+{
+ return ldb_wrap_connect(mem_ctx, NULL, lp_ctx, "privilege.ldb",
+ NULL, NULL, 0);
+}
+
+/*
+ add privilege bits for one sid to a security_token
+*/
+static NTSTATUS samdb_privilege_setup_sid(struct ldb_context *pdb, TALLOC_CTX *mem_ctx,
+ struct security_token *token,
+ const struct dom_sid *sid)
+{
+ const char * const attrs[] = { "privilege", NULL };
+ struct ldb_message **res = NULL;
+ struct ldb_message_element *el;
+ unsigned int i;
+ int ret;
+ char *sidstr;
+
+ sidstr = ldap_encode_ndr_dom_sid(mem_ctx, sid);
+ NT_STATUS_HAVE_NO_MEMORY(sidstr);
+
+ ret = gendb_search(pdb, mem_ctx, NULL, &res, attrs, "objectSid=%s", sidstr);
+ talloc_free(sidstr);
+ if (ret != 1) {
+ /* not an error to not match */
+ return NT_STATUS_OK;
+ }
+
+ el = ldb_msg_find_element(res[0], "privilege");
+ if (el == NULL) {
+ return NT_STATUS_OK;
+ }
+
+ for (i=0;i<el->num_values;i++) {
+ const char *priv_str = (const char *)el->values[i].data;
+ enum sec_privilege privilege = sec_privilege_id(priv_str);
+ if (privilege == SEC_PRIV_INVALID) {
+ uint32_t right_bit = sec_right_bit(priv_str);
+ security_token_set_right_bit(token, right_bit);
+ if (right_bit == 0) {
+ DEBUG(1,("Unknown privilege '%s' in samdb\n",
+ priv_str));
+ }
+ continue;
+ }
+ security_token_set_privilege(token, privilege);
+ }
+
+ return NT_STATUS_OK;
+}
+
+/*
+ setup the privilege mask for this security token based on our
+ local SAM
+*/
+NTSTATUS samdb_privilege_setup(struct loadparm_context *lp_ctx, struct security_token *token)
+{
+ struct ldb_context *pdb;
+ TALLOC_CTX *mem_ctx;
+ unsigned int i;
+ NTSTATUS status;
+
+ /* Shortcuts to prevent recursion and avoid lookups */
+ if (token->sids == NULL) {
+ token->privilege_mask = 0;
+ return NT_STATUS_OK;
+ }
+
+ if (security_token_is_system(token)) {
+ token->privilege_mask = ~0;
+ return NT_STATUS_OK;
+ }
+
+ if (security_token_is_anonymous(token)) {
+ token->privilege_mask = 0;
+ return NT_STATUS_OK;
+ }
+
+ mem_ctx = talloc_new(token);
+ pdb = privilege_connect(mem_ctx, lp_ctx);
+ if (pdb == NULL) {
+ talloc_free(mem_ctx);
+ return NT_STATUS_INTERNAL_DB_CORRUPTION;
+ }
+
+ token->privilege_mask = 0;
+
+ for (i=0;i<token->num_sids;i++) {
+ status = samdb_privilege_setup_sid(pdb, mem_ctx,
+ token, &token->sids[i]);
+ if (!NT_STATUS_IS_OK(status)) {
+ talloc_free(mem_ctx);
+ return status;
+ }
+ }
+
+ talloc_free(mem_ctx);
+
+ return NT_STATUS_OK;
+}
diff --git a/source4/dsdb/schema/dsdb_dn.c b/source4/dsdb/schema/dsdb_dn.c
new file mode 100644
index 0000000..06565a9
--- /dev/null
+++ b/source4/dsdb/schema/dsdb_dn.c
@@ -0,0 +1,102 @@
+/*
+ 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/>.
+*/
+
+#include "includes.h"
+#include "dsdb/samdb/samdb.h"
+#include <ldb_module.h>
+#include "librpc/ndr/libndr.h"
+#include "libcli/security/dom_sid.h"
+
+/*
+ convert a dsdb_dn to a linked attribute data blob
+*/
+WERROR dsdb_dn_la_to_blob(struct ldb_context *sam_ctx,
+ const struct dsdb_attribute *schema_attrib,
+ const struct dsdb_schema *schema,
+ TALLOC_CTX *mem_ctx,
+ struct dsdb_dn *dsdb_dn, DATA_BLOB **blob)
+{
+ struct ldb_val v;
+ WERROR werr;
+ struct ldb_message_element val_el;
+ struct drsuapi_DsReplicaAttribute drs;
+ struct dsdb_syntax_ctx syntax_ctx;
+
+ /* use default syntax conversion context */
+ dsdb_syntax_ctx_init(&syntax_ctx, sam_ctx, schema);
+
+ /* we need a message_element with just one value in it */
+ v = data_blob_string_const(dsdb_dn_get_extended_linearized(mem_ctx, dsdb_dn, 1));
+
+ val_el.name = schema_attrib->lDAPDisplayName;
+ val_el.values = &v;
+ val_el.num_values = 1;
+
+ werr = schema_attrib->syntax->ldb_to_drsuapi(&syntax_ctx, schema_attrib, &val_el, mem_ctx, &drs);
+ W_ERROR_NOT_OK_RETURN(werr);
+
+ if (drs.value_ctr.num_values != 1) {
+ DEBUG(1,(__location__ ": Failed to build DRS blob for linked attribute %s\n",
+ schema_attrib->lDAPDisplayName));
+ return WERR_DS_DRA_INTERNAL_ERROR;
+ }
+
+ *blob = drs.value_ctr.values[0].blob;
+ return WERR_OK;
+}
+
+/*
+ convert a data blob to a dsdb_dn
+ */
+WERROR dsdb_dn_la_from_blob(struct ldb_context *sam_ctx,
+ const struct dsdb_attribute *schema_attrib,
+ const struct dsdb_schema *schema,
+ TALLOC_CTX *mem_ctx,
+ DATA_BLOB *blob,
+ struct dsdb_dn **dsdb_dn)
+{
+ WERROR werr;
+ struct ldb_message_element new_el;
+ struct drsuapi_DsReplicaAttribute drs;
+ struct drsuapi_DsAttributeValue val;
+ struct dsdb_syntax_ctx syntax_ctx;
+
+ /* use default syntax conversion context */
+ dsdb_syntax_ctx_init(&syntax_ctx, sam_ctx, schema);
+
+ drs.value_ctr.num_values = 1;
+ drs.value_ctr.values = &val;
+ val.blob = blob;
+
+ werr = schema_attrib->syntax->drsuapi_to_ldb(&syntax_ctx, schema_attrib, &drs, mem_ctx, &new_el);
+ W_ERROR_NOT_OK_RETURN(werr);
+
+ if (new_el.num_values != 1) {
+ return WERR_INTERNAL_ERROR;
+ }
+
+ *dsdb_dn = dsdb_dn_parse(mem_ctx, sam_ctx, &new_el.values[0], schema_attrib->syntax->ldap_oid);
+ if (!*dsdb_dn) {
+ return WERR_INTERNAL_ERROR;
+ }
+
+ return WERR_OK;
+}
diff --git a/source4/dsdb/schema/prefixmap.h b/source4/dsdb/schema/prefixmap.h
new file mode 100644
index 0000000..339a221
--- /dev/null
+++ b/source4/dsdb/schema/prefixmap.h
@@ -0,0 +1,54 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ DRS::prefixMap data structures
+
+ Copyright (C) Kamen Mazdrashki <kamen.mazdrashki@postpath.com> 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/>.
+*/
+
+#ifndef _DSDB_PREFIXMAP_H
+#define _DSDB_PREFIXMAP_H
+
+/**
+ * ATTRTYP ranges
+ * Ref: MS-ADTS, 3.1.1.2.6 ATTRTYP
+ */
+enum dsdb_attid_type {
+ DSDB_ATTID_TYPE_PFM = 1, /* attid in [0x00000000..0x7FFFFFFF] */
+ DSDB_ATTID_TYPE_INTID = 2, /* attid in [0x80000000..0xBFFFFFFF] */
+ DSDB_ATTID_TYPE_RESERVED = 3, /* attid in [0xC0000000..0xFFFEFFFF] */
+ DSDB_ATTID_TYPE_INTERNAL = 4, /* attid in [0xFFFF0000..0xFFFFFFFF] */
+};
+
+/**
+ * oid-prefix in prefixmap
+ */
+struct dsdb_schema_prefixmap_oid {
+ uint32_t id;
+ DATA_BLOB bin_oid; /* partial binary-oid prefix */
+};
+
+/**
+ * DSDB prefixMap internal presentation
+ */
+struct dsdb_schema_prefixmap {
+ uint32_t length;
+ struct dsdb_schema_prefixmap_oid *prefixes;
+};
+
+
+
+#endif /* _DSDB_PREFIXMAP_H */
diff --git a/source4/dsdb/schema/schema.h b/source4/dsdb/schema/schema.h
new file mode 100644
index 0000000..700f404
--- /dev/null
+++ b/source4/dsdb/schema/schema.h
@@ -0,0 +1,351 @@
+/*
+ Unix SMB/CIFS Implementation.
+ DSDB schema header
+
+ Copyright (C) Stefan Metzmacher <metze@samba.org> 2006
+
+ 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/>.
+
+*/
+
+#ifndef _DSDB_SCHEMA_H
+#define _DSDB_SCHEMA_H
+
+#include "prefixmap.h"
+
+enum dsdb_dn_format {
+ DSDB_NORMAL_DN,
+ DSDB_BINARY_DN,
+ DSDB_STRING_DN,
+ DSDB_INVALID_DN
+};
+
+
+struct dsdb_attribute;
+struct dsdb_class;
+struct dsdb_schema;
+struct dsdb_dn;
+
+struct dsdb_syntax_ctx {
+ struct ldb_context *ldb;
+ const struct dsdb_schema *schema;
+
+ /* set when converting objects under Schema NC */
+ bool is_schema_nc;
+
+ /* remote prefixMap to be used for drsuapi_to_ldb conversions */
+ const struct dsdb_schema_prefixmap *pfm_remote;
+};
+
+
+struct dsdb_syntax {
+ const char *name;
+ const char *ldap_oid;
+ uint32_t oMSyntax;
+ struct ldb_val oMObjectClass;
+ const char *attributeSyntax_oid;
+ const char *equality;
+ const char *substring;
+ const char *comment;
+ const char *ldb_syntax;
+
+ WERROR (*drsuapi_to_ldb)(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out);
+ WERROR (*ldb_to_drsuapi)(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out);
+ WERROR (*validate_ldb)(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in);
+ bool auto_normalise;
+ bool userParameters; /* Indicates the syntax userParameters should be forced to */
+};
+
+struct dsdb_attribute {
+ struct dsdb_attribute *prev, *next;
+
+ const char *cn;
+ const char *lDAPDisplayName;
+ const char *attributeID_oid;
+ uint32_t attributeID_id;
+ struct GUID schemaIDGUID;
+ uint32_t mAPIID;
+ uint32_t msDS_IntId;
+
+ struct GUID attributeSecurityGUID;
+ struct GUID objectGUID;
+
+ uint32_t searchFlags;
+ uint32_t systemFlags;
+ bool isMemberOfPartialAttributeSet;
+ uint32_t linkID;
+
+ const char *attributeSyntax_oid;
+ uint32_t attributeSyntax_id;
+ uint32_t oMSyntax;
+ struct ldb_val oMObjectClass;
+
+ bool isSingleValued;
+ uint32_t *rangeLower;
+ uint32_t *rangeUpper;
+ bool extendedCharsAllowed;
+
+ uint32_t schemaFlagsEx;
+ struct ldb_val msDs_Schema_Extensions;
+
+ bool showInAdvancedViewOnly;
+ const char *adminDisplayName;
+ const char *adminDescription;
+ const char *classDisplayName;
+ bool isEphemeral;
+ bool isDefunct;
+ bool systemOnly;
+
+ bool one_way_link;
+ bool bl_maybe_invisible;
+ enum dsdb_dn_format dn_format;
+
+ /* internal stuff */
+ const struct dsdb_syntax *syntax;
+ const struct ldb_schema_attribute *ldb_schema_attribute;
+};
+
+struct dsdb_class {
+ struct dsdb_class *prev, *next;
+
+ const char *cn;
+ const char *lDAPDisplayName;
+ const char *governsID_oid;
+ uint32_t governsID_id;
+ struct GUID schemaIDGUID;
+ struct GUID objectGUID;
+
+ uint32_t objectClassCategory;
+ const char *rDNAttID;
+ const char *defaultObjectCategory;
+
+ const char *subClassOf;
+
+ const char **systemAuxiliaryClass;
+ const char **systemPossSuperiors;
+ const char **systemMustContain;
+ const char **systemMayContain;
+
+ const char **auxiliaryClass;
+ const char **possSuperiors;
+ const char **mustContain;
+ const char **mayContain;
+ const char **possibleInferiors;
+ const char **systemPossibleInferiors;
+
+ const char *defaultSecurityDescriptor;
+
+ uint32_t schemaFlagsEx;
+ uint32_t systemFlags;
+ struct ldb_val msDs_Schema_Extensions;
+
+ bool showInAdvancedViewOnly;
+ const char *adminDisplayName;
+ const char *adminDescription;
+ const char *classDisplayName;
+ bool defaultHidingValue;
+ bool isDefunct;
+ bool systemOnly;
+
+ uint32_t subClassOf_id;
+ uint32_t *systemAuxiliaryClass_ids;
+ uint32_t *auxiliaryClass_ids;
+ uint32_t *systemMayContain_ids;
+ uint32_t *systemMustContain_ids;
+ uint32_t *possSuperiors_ids;
+ uint32_t *mustContain_ids;
+ uint32_t *mayContain_ids;
+ uint32_t *systemPossSuperiors_ids;
+
+ /* An ordered index showing how this subClass fits into the
+ * subClass tree. that is, an objectclass that is not
+ * subClassOf anything is 0 (just in case), and top is 1, and
+ * subClasses of top are 2, subclasses of those classes are
+ * 3 */
+ uint32_t subClass_order;
+
+ struct {
+ const char **supclasses;
+ const char **subclasses;
+ const char **subclasses_direct;
+ const char **posssuperiors;
+ } tmp;
+};
+
+enum schema_set_enum {
+ SCHEMA_MEMORY_ONLY = 0,
+ SCHEMA_WRITE = 1,
+ SCHEMA_COMPARE = 2,
+};
+
+/**
+ * data stored in schemaInfo attribute
+ */
+struct dsdb_schema_info {
+ uint32_t revision;
+ struct GUID invocation_id;
+};
+
+
+struct dsdb_schema {
+ struct dsdb_schema_prefixmap *prefixmap;
+
+ /*
+ * the last element of the prefix mapping table isn't a oid,
+ * it starts with 0xFF and has 21 bytes and is maybe a schema
+ * version number
+ *
+ * this is the content of the schemaInfo attribute of the
+ * Schema-Partition head object.
+ */
+ struct dsdb_schema_info *schema_info;
+
+ struct dsdb_attribute *attributes;
+ struct dsdb_class *classes;
+
+ struct dsdb_attribute **attributes_to_remove;
+ uint32_t attributes_to_remove_size;
+ struct dsdb_class **classes_to_remove;
+ uint32_t classes_to_remove_size;
+
+ /* lists of classes sorted by various attributes, for faster
+ access */
+ uint32_t num_classes;
+ struct dsdb_class **classes_by_lDAPDisplayName;
+ struct dsdb_class **classes_by_governsID_id;
+ struct dsdb_class **classes_by_governsID_oid;
+ struct dsdb_class **classes_by_cn;
+
+ /* lists of attributes sorted by various fields */
+ uint32_t num_attributes;
+ struct dsdb_attribute **attributes_by_lDAPDisplayName;
+ struct dsdb_attribute **attributes_by_attributeID_id;
+ struct dsdb_attribute **attributes_by_attributeID_oid;
+ struct dsdb_attribute **attributes_by_linkID;
+ struct dsdb_attribute **attributes_by_cn;
+ uint32_t num_int_id_attr;
+ struct dsdb_attribute **attributes_by_msDS_IntId;
+
+ struct {
+ bool we_are_master;
+ bool update_allowed;
+ struct ldb_dn *master_dn;
+ } fsmo;
+
+ /* Was this schema loaded from ldb (if so, then we will reload it when we detect a change in ldb) */
+ bool refresh_in_progress;
+ time_t ts_last_change;
+ /* This 'opaque' is stored in the metadata and is used to check if the currently
+ * loaded schema needs a reload because another process has signaled that it has been
+ * requested to reload the schema (either due through DRS or via the schemaUpdateNow).
+ */
+ uint64_t metadata_usn;
+
+ /* Should the syntax handlers in this case handle all incoming OIDs automatically, assigning them as an OID if no text name is known? */
+ bool relax_OID_conversions;
+
+ /*
+ * we're currently trying to construct a working_schema
+ * in order to replicate the schema partition.
+ *
+ * We use this in order to avoid temporary failure DEBUG messages
+ */
+ bool resolving_in_progress;
+};
+
+#define DSDB_SCHEMA_COMMON_ATTRS \
+ "objectClass", \
+ "cn", \
+ "lDAPDisplayName", \
+ "schemaIDGUID", \
+ "objectGUID", \
+ "systemFlags", \
+ "schemaFlagsEx", \
+ "msDs-Schema-Extensions", \
+ "showInAdvancedViewOnly", \
+ "adminDisplayName", \
+ "adminDescription", \
+ "isDefunct", \
+ "systemOnly"
+
+#define DSDB_SCHEMA_ATTR_ATTRS \
+ "attributeID", \
+ "msDS-IntId", \
+ "mAPIID", \
+ "attributeSecurityGUID", \
+ "searchFlags", \
+ "isMemberOfPartialAttributeSet", \
+ "linkID", \
+ "attributeSyntax", \
+ "oMSyntax", \
+ "oMObjectClass", \
+ "isSingleValued", \
+ "rangeLower", \
+ "rangeUpper", \
+ "extendedCharsAllowed", \
+ "classDisplayName", \
+ "isEphemeral"
+
+#define DSDB_SCHEMA_CLASS_ATTRS \
+ "governsID", \
+ "objectClassCategory", \
+ "rDNAttID", \
+ "defaultObjectCategory", \
+ "subClassOf", \
+ "systemAuxiliaryClass", \
+ "auxiliaryClass", \
+ "systemMustContain", \
+ "systemMayContain", \
+ "mustContain", \
+ "mayContain", \
+ "systemPossSuperiors", \
+ "possSuperiors", \
+ "defaultSecurityDescriptor", \
+ "classDisplayName", \
+ "defaultHidingValue"
+
+enum dsdb_attr_list_query {
+ DSDB_SCHEMA_ALL_MAY,
+ DSDB_SCHEMA_ALL_MUST,
+ DSDB_SCHEMA_SYS_MAY,
+ DSDB_SCHEMA_SYS_MUST,
+ DSDB_SCHEMA_MAY,
+ DSDB_SCHEMA_MUST,
+ DSDB_SCHEMA_ALL
+};
+
+enum dsdb_schema_convert_target {
+ TARGET_OPENLDAP,
+ TARGET_FEDORA_DS,
+ TARGET_AD_SCHEMA_SUBENTRY
+};
+
+struct ldb_module;
+
+typedef struct dsdb_schema *(*dsdb_schema_refresh_fn)(struct ldb_module *module,
+ struct tevent_context *ev,
+ struct dsdb_schema *schema, bool is_global_schema);
+#include "dsdb/schema/proto.h"
+
+#endif /* _DSDB_SCHEMA_H */
diff --git a/source4/dsdb/schema/schema_convert_to_ol.c b/source4/dsdb/schema/schema_convert_to_ol.c
new file mode 100644
index 0000000..013787e
--- /dev/null
+++ b/source4/dsdb/schema/schema_convert_to_ol.c
@@ -0,0 +1,377 @@
+/*
+ schema conversion routines
+
+ Copyright (C) Andrew Bartlett 2006-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 "dsdb/samdb/samdb.h"
+#include "system/locale.h"
+
+#undef strcasecmp
+
+#define SEPARATOR "\n "
+
+struct attr_map {
+ char *old_attr;
+ char *new_attr;
+};
+
+struct oid_map {
+ char *old_oid;
+ char *new_oid;
+};
+
+static char *print_schema_recursive(char *append_to_string, struct dsdb_schema *schema, const char *print_class,
+ enum dsdb_schema_convert_target target,
+ const char **attrs_skip, const struct attr_map *attr_map, const struct oid_map *oid_map)
+{
+ char *out = append_to_string;
+ const struct dsdb_class *objectclass;
+ objectclass = dsdb_class_by_lDAPDisplayName(schema, print_class);
+ if (!objectclass) {
+ DEBUG(0, ("Cannot find class %s in schema\n", print_class));
+ return NULL;
+ }
+
+ /* We have been asked to skip some attributes/objectClasses */
+ if (attrs_skip == NULL || !str_list_check_ci(attrs_skip, objectclass->lDAPDisplayName)) {
+ TALLOC_CTX *mem_ctx = talloc_new(append_to_string);
+ const char *name = objectclass->lDAPDisplayName;
+ const char *oid = objectclass->governsID_oid;
+ const char *subClassOf = objectclass->subClassOf;
+ int objectClassCategory = objectclass->objectClassCategory;
+ const char **must;
+ const char **may;
+ char *schema_entry = NULL;
+ struct ldb_val objectclass_name_as_ldb_val = data_blob_string_const(objectclass->lDAPDisplayName);
+ struct ldb_message_element objectclass_name_as_el = {
+ .name = "objectClass",
+ .num_values = 1,
+ .values = &objectclass_name_as_ldb_val
+ };
+ unsigned int j;
+ unsigned int attr_idx;
+
+ if (!mem_ctx) {
+ DEBUG(0, ("Failed to create new talloc context\n"));
+ return NULL;
+ }
+
+ /* We might have been asked to remap this oid, due to a conflict */
+ for (j=0; oid_map && oid_map[j].old_oid; j++) {
+ if (strcasecmp(oid, oid_map[j].old_oid) == 0) {
+ oid = oid_map[j].new_oid;
+ break;
+ }
+ }
+
+ /* We might have been asked to remap this name, due to a conflict */
+ for (j=0; name && attr_map && attr_map[j].old_attr; j++) {
+ if (strcasecmp(name, attr_map[j].old_attr) == 0) {
+ name = attr_map[j].new_attr;
+ break;
+ }
+ }
+
+ /* We might have been asked to remap this subClassOf, due to a conflict */
+ for (j=0; subClassOf && attr_map && attr_map[j].old_attr; j++) {
+ if (strcasecmp(subClassOf, attr_map[j].old_attr) == 0) {
+ subClassOf = attr_map[j].new_attr;
+ break;
+ }
+ }
+
+ may = dsdb_full_attribute_list(mem_ctx, schema, &objectclass_name_as_el, DSDB_SCHEMA_ALL_MAY);
+
+ for (j=0; may && may[j]; j++) {
+ /* We might have been asked to remap this name, due to a conflict */
+ for (attr_idx=0; attr_map && attr_map[attr_idx].old_attr; attr_idx++) {
+ if (strcasecmp(may[j], attr_map[attr_idx].old_attr) == 0) {
+ may[j] = attr_map[attr_idx].new_attr;
+ break;
+ }
+ }
+ }
+
+ must = dsdb_full_attribute_list(mem_ctx, schema, &objectclass_name_as_el, DSDB_SCHEMA_ALL_MUST);
+
+ for (j=0; must && must[j]; j++) {
+ /* We might have been asked to remap this name, due to a conflict */
+ for (attr_idx=0; attr_map && attr_map[attr_idx].old_attr; attr_idx++) {
+ if (strcasecmp(must[j], attr_map[attr_idx].old_attr) == 0) {
+ must[j] = attr_map[attr_idx].new_attr;
+ break;
+ }
+ }
+ }
+
+ schema_entry = schema_class_description(mem_ctx, target,
+ SEPARATOR,
+ oid,
+ name,
+ NULL,
+ subClassOf,
+ objectClassCategory,
+ must,
+ may,
+ NULL);
+ if (schema_entry == NULL) {
+ talloc_free(mem_ctx);
+ DEBUG(0, ("failed to generate schema description for %s\n", name));
+ return NULL;
+ }
+
+ switch (target) {
+ case TARGET_OPENLDAP:
+ out = talloc_asprintf_append(out, "objectclass %s\n\n", schema_entry);
+ break;
+ case TARGET_FEDORA_DS:
+ out = talloc_asprintf_append(out, "objectClasses: %s\n", schema_entry);
+ break;
+ default:
+ talloc_free(mem_ctx);
+ DEBUG(0,(__location__ " Wrong type of target %u!\n", (unsigned)target));
+ return NULL;
+ }
+ talloc_free(mem_ctx);
+ }
+
+
+ for (objectclass=schema->classes; objectclass; objectclass = objectclass->next) {
+ if (ldb_attr_cmp(objectclass->subClassOf, print_class) == 0
+ && ldb_attr_cmp(objectclass->lDAPDisplayName, print_class) != 0) {
+ out = print_schema_recursive(out, schema, objectclass->lDAPDisplayName,
+ target, attrs_skip, attr_map, oid_map);
+ }
+ }
+ return out;
+}
+
+/* Routine to linearise our internal schema into the format that
+ OpenLDAP and Fedora DS use for their backend.
+
+ The 'mappings' are of a format like:
+
+#Standard OpenLDAP attributes
+labeledURI
+#The memberOf plugin provides this attribute
+memberOf
+#These conflict with OpenLDAP builtins
+attributeTypes:samba4AttributeTypes
+2.5.21.5:1.3.6.1.4.1.7165.4.255.7
+
+*/
+
+
+char *dsdb_convert_schema_to_openldap(struct ldb_context *ldb, char *target_str, const char *mappings)
+{
+ /* Read list of attributes to skip, OIDs to map */
+ TALLOC_CTX *mem_ctx = talloc_new(ldb);
+ char *line;
+ char *out;
+ const char **attrs_skip = NULL;
+ unsigned int num_skip = 0;
+ struct oid_map *oid_map = NULL;
+ unsigned int num_oid_maps = 0;
+ struct attr_map *attr_map = NULL;
+ unsigned int num_attr_maps = 0;
+ struct dsdb_attribute *attribute;
+ struct dsdb_schema *schema;
+ enum dsdb_schema_convert_target target;
+
+ char *next_line = talloc_strdup(mem_ctx, mappings);
+
+ if (!target_str || strcasecmp(target_str, "openldap") == 0) {
+ target = TARGET_OPENLDAP;
+ } else if (strcasecmp(target_str, "fedora-ds") == 0) {
+ target = TARGET_FEDORA_DS;
+ } else {
+ talloc_free(mem_ctx);
+ DEBUG(0, ("Invalid target type for schema conversion %s\n", target_str));
+ return NULL;
+ }
+
+ /* The mappings are line-separated, and specify details such as OIDs to skip etc */
+ while (1) {
+ line = next_line;
+ next_line = strchr(line, '\n');
+ if (!next_line) {
+ break;
+ }
+ next_line[0] = '\0';
+ next_line++;
+
+ /* Blank Line */
+ if (line[0] == '\0') {
+ continue;
+ }
+ /* Comment */
+ if (line[0] == '#') {
+ continue;
+ }
+
+ if (isdigit(line[0])) {
+ char *p = strchr(line, ':');
+ if (!p) {
+ DEBUG(0, ("schema mapping file line has OID but no OID to map to: %s\n", line));
+ return NULL;
+ }
+ p[0] = '\0';
+ p++;
+ oid_map = talloc_realloc(mem_ctx, oid_map, struct oid_map, num_oid_maps + 2);
+ trim_string(line, " ", " ");
+ oid_map[num_oid_maps].old_oid = talloc_strdup(oid_map, line);
+ trim_string(p, " ", " ");
+ oid_map[num_oid_maps].new_oid = p;
+ num_oid_maps++;
+ oid_map[num_oid_maps].old_oid = NULL;
+ } else {
+ char *p = strchr(line, ':');
+ if (p) {
+ /* remap attribute/objectClass */
+ p[0] = '\0';
+ p++;
+ attr_map = talloc_realloc(mem_ctx, attr_map, struct attr_map, num_attr_maps + 2);
+ trim_string(line, " ", " ");
+ attr_map[num_attr_maps].old_attr = talloc_strdup(attr_map, line);
+ trim_string(p, " ", " ");
+ attr_map[num_attr_maps].new_attr = p;
+ num_attr_maps++;
+ attr_map[num_attr_maps].old_attr = NULL;
+ } else {
+ /* skip attribute/objectClass */
+ attrs_skip = talloc_realloc(mem_ctx, attrs_skip, const char *, num_skip + 2);
+ trim_string(line, " ", " ");
+ attrs_skip[num_skip] = talloc_strdup(attrs_skip, line);
+ num_skip++;
+ attrs_skip[num_skip] = NULL;
+ }
+ }
+ }
+
+ schema = dsdb_get_schema(ldb, mem_ctx);
+ if (!schema) {
+ talloc_free(mem_ctx);
+ DEBUG(0, ("No schema on ldb to convert!\n"));
+ return NULL;
+ }
+
+ switch (target) {
+ case TARGET_OPENLDAP:
+ out = talloc_strdup(mem_ctx, "");
+ break;
+ case TARGET_FEDORA_DS:
+ out = talloc_strdup(mem_ctx, "dn: cn=schema\n");
+ break;
+ default:
+ talloc_free(mem_ctx);
+ DEBUG(0,(__location__ " Wrong type of target %u!\n", (unsigned)target));
+ return NULL;
+ }
+
+ for (attribute=schema->attributes; attribute; attribute = attribute->next) {
+ const char *name = attribute->lDAPDisplayName;
+ const char *oid = attribute->attributeID_oid;
+ const char *syntax = attribute->attributeSyntax_oid;
+ const char *equality = NULL, *substring = NULL;
+ bool single_value = attribute->isSingleValued;
+
+ char *schema_entry = NULL;
+ unsigned int j;
+
+ /* We have been asked to skip some attributes/objectClasses */
+ if (attrs_skip && str_list_check_ci(attrs_skip, name)) {
+ continue;
+ }
+
+ /* We might have been asked to remap this oid, due to a conflict */
+ for (j=0; oid && oid_map && oid_map[j].old_oid; j++) {
+ if (strcasecmp(oid, oid_map[j].old_oid) == 0) {
+ oid = oid_map[j].new_oid;
+ break;
+ }
+ }
+
+ if (attribute->syntax) {
+ /* We might have been asked to remap this oid,
+ * due to a conflict, or lack of
+ * implementation */
+ syntax = attribute->syntax->ldap_oid;
+ /* We might have been asked to remap this oid, due to a conflict */
+ for (j=0; syntax && oid_map && oid_map[j].old_oid; j++) {
+ if (strcasecmp(syntax, oid_map[j].old_oid) == 0) {
+ syntax = oid_map[j].new_oid;
+ break;
+ }
+ }
+
+ equality = attribute->syntax->equality;
+ substring = attribute->syntax->substring;
+ }
+
+ /* We might have been asked to remap this name, due to a conflict */
+ for (j=0; name && attr_map && attr_map[j].old_attr; j++) {
+ if (strcasecmp(name, attr_map[j].old_attr) == 0) {
+ name = attr_map[j].new_attr;
+ break;
+ }
+ }
+
+ schema_entry = schema_attribute_description(mem_ctx,
+ target,
+ SEPARATOR,
+ oid,
+ name,
+ equality,
+ substring,
+ syntax,
+ single_value,
+ false,
+ NULL, NULL,
+ NULL, NULL,
+ false, false);
+
+ if (schema_entry == NULL) {
+ talloc_free(mem_ctx);
+ DEBUG(0, ("failed to generate attribute description for %s\n", name));
+ return NULL;
+ }
+
+ switch (target) {
+ case TARGET_OPENLDAP:
+ out = talloc_asprintf_append(out, "attributetype %s\n\n", schema_entry);
+ break;
+ case TARGET_FEDORA_DS:
+ out = talloc_asprintf_append(out, "attributeTypes: %s\n", schema_entry);
+ break;
+ default:
+ talloc_free(mem_ctx);
+ DEBUG(0,(__location__ " Wrong type of target %u!\n", (unsigned)target));
+ return NULL;
+ }
+ }
+
+ out = print_schema_recursive(out, schema, "top", target, attrs_skip, attr_map, oid_map);
+
+ talloc_steal(ldb, out);
+ talloc_free(mem_ctx);
+
+ return out;
+}
+
diff --git a/source4/dsdb/schema/schema_description.c b/source4/dsdb/schema/schema_description.c
new file mode 100644
index 0000000..5fc7015
--- /dev/null
+++ b/source4/dsdb/schema/schema_description.c
@@ -0,0 +1,397 @@
+/*
+ Unix SMB/CIFS Implementation.
+ Print schema info into string format
+
+ Copyright (C) Andrew Bartlett 2006-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 "dsdb/samdb/samdb.h"
+#include "librpc/ndr/libndr.h"
+
+#undef strcasecmp
+
+char *schema_attribute_description(TALLOC_CTX *mem_ctx,
+ enum dsdb_schema_convert_target target,
+ const char *separator,
+ const char *oid,
+ const char *name,
+ const char *equality,
+ const char *substring,
+ const char *syntax,
+ bool single_value, bool operational,
+ uint32_t *range_lower,
+ uint32_t *range_upper,
+ const char *property_guid,
+ const char *property_set_guid,
+ bool indexed, bool system_only)
+{
+ char *schema_entry = talloc_asprintf(mem_ctx,
+ "(%s%s%s", separator, oid, separator);
+
+ talloc_asprintf_addbuf(
+ &schema_entry, "NAME '%s'%s", name, separator);
+
+ if (equality) {
+ talloc_asprintf_addbuf(
+ &schema_entry, "EQUALITY %s%s", equality, separator);
+ }
+ if (substring) {
+ talloc_asprintf_addbuf(
+ &schema_entry, "SUBSTR %s%s", substring, separator);
+ }
+
+ if (syntax) {
+ talloc_asprintf_addbuf(
+ &schema_entry, "SYNTAX %s%s", syntax, separator);
+ }
+
+ if (single_value) {
+ talloc_asprintf_addbuf(
+ &schema_entry, "SINGLE-VALUE%s", separator);
+ }
+
+ if (operational) {
+ talloc_asprintf_addbuf(
+ &schema_entry, "NO-USER-MODIFICATION%s", separator);
+ }
+
+ if (range_lower) {
+ talloc_asprintf_addbuf(
+ &schema_entry,
+ "RANGE-LOWER '%u'%s",
+ *range_lower,
+ separator);
+ }
+
+ if (range_upper) {
+ talloc_asprintf_addbuf(
+ &schema_entry,
+ "RANGE-UPPER '%u'%s",
+ *range_upper,
+ separator);
+ }
+
+ if (property_guid) {
+ talloc_asprintf_addbuf(
+ &schema_entry,
+ "PROPERTY-GUID '%s'%s",
+ property_guid,
+ separator);
+ }
+
+ if (property_set_guid) {
+ talloc_asprintf_addbuf(
+ &schema_entry,
+ "PROPERTY-SET-GUID '%s'%s",
+ property_set_guid,
+ separator);
+ }
+
+ if (indexed) {
+ talloc_asprintf_addbuf(
+ &schema_entry, "INDEXED%s", separator);
+ }
+
+ if (system_only) {
+ talloc_asprintf_addbuf(
+ &schema_entry, "SYSTEM-ONLY%s", separator);
+ }
+
+ talloc_asprintf_addbuf(&schema_entry, ")");
+
+ return schema_entry;
+}
+
+char *schema_attribute_to_description(TALLOC_CTX *mem_ctx, const struct dsdb_attribute *attribute)
+{
+ char *schema_description;
+ const char *syntax = attribute->syntax->ldap_oid;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ if (!tmp_ctx) {
+ return NULL;
+ }
+
+ schema_description
+ = schema_attribute_description(mem_ctx,
+ TARGET_AD_SCHEMA_SUBENTRY,
+ " ",
+ attribute->attributeID_oid,
+ attribute->lDAPDisplayName,
+ NULL, NULL, talloc_asprintf(tmp_ctx, "'%s'", syntax),
+ attribute->isSingleValued,
+ attribute->systemOnly,/* TODO: is this correct? */
+ NULL, NULL, NULL, NULL,
+ false, false);
+ talloc_free(tmp_ctx);
+ return schema_description;
+}
+
+char *schema_attribute_to_extendedInfo(TALLOC_CTX *mem_ctx, const struct dsdb_attribute *attribute)
+{
+ char *schema_description;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ if (!tmp_ctx) {
+ return NULL;
+ }
+
+ schema_description
+ = schema_attribute_description(mem_ctx,
+ TARGET_AD_SCHEMA_SUBENTRY,
+ " ",
+ attribute->attributeID_oid,
+ attribute->lDAPDisplayName,
+ NULL, NULL, NULL,
+ false, false,
+ attribute->rangeLower,
+ attribute->rangeUpper,
+ GUID_hexstring(tmp_ctx, &attribute->schemaIDGUID),
+ GUID_hexstring(tmp_ctx, &attribute->attributeSecurityGUID),
+ /*
+ * We actually ignore the indexed
+ * flag for confidential
+ * attributes, but we'll include
+ * it for the purposes of
+ * description.
+ */
+ (attribute->searchFlags & SEARCH_FLAG_ATTINDEX),
+ attribute->systemOnly);
+ talloc_free(tmp_ctx);
+ return schema_description;
+}
+
+#define APPEND_ATTRS(attributes) \
+ do { \
+ unsigned int k; \
+ for (k=0; attributes && attributes[k]; k++) { \
+ const char *attr_name = attributes[k]; \
+ \
+ talloc_asprintf_addbuf(&schema_entry, \
+ "%s ", \
+ attr_name); \
+ if (attributes[k+1]) { \
+ if (target == TARGET_OPENLDAP && ((k+1)%5 == 0)) { \
+ talloc_asprintf_addbuf(&schema_entry, \
+ "$%s ", separator); \
+ } else { \
+ talloc_asprintf_addbuf(&schema_entry, \
+ "$ "); \
+ } \
+ } \
+ } \
+ } while (0)
+
+
+/* Print a schema class or dITContentRule as a string.
+ *
+ * To print a scheam class, specify objectClassCategory but not auxillary_classes
+ * To print a dITContentRule, specify auxillary_classes but set objectClassCategory == -1
+ *
+ */
+
+char *schema_class_description(TALLOC_CTX *mem_ctx,
+ enum dsdb_schema_convert_target target,
+ const char *separator,
+ const char *oid,
+ const char *name,
+ const char **auxillary_classes,
+ const char *subClassOf,
+ int objectClassCategory,
+ const char **must,
+ const char **may,
+ const char *schemaHexGUID)
+{
+ char *schema_entry = talloc_asprintf(mem_ctx,
+ "(%s%s%s", separator, oid, separator);
+
+ talloc_asprintf_addbuf(&schema_entry, "NAME '%s'%s", name, separator);
+
+ if (auxillary_classes) {
+ talloc_asprintf_addbuf(&schema_entry, "AUX ( ");
+
+ APPEND_ATTRS(auxillary_classes);
+
+ talloc_asprintf_addbuf(&schema_entry, ")%s", separator);
+ }
+
+ if (subClassOf && strcasecmp(subClassOf, name) != 0) {
+ talloc_asprintf_addbuf(
+ &schema_entry, "SUP %s%s", subClassOf, separator);
+ }
+
+ switch (objectClassCategory) {
+ case -1:
+ break;
+ /* Dummy case for when used for printing ditContentRules */
+ case 0:
+ /*
+ * NOTE: this is an type 88 class
+ * e.g. 2.5.6.6 NAME 'person'
+ * but w2k3 gives STRUCTURAL here!
+ */
+ talloc_asprintf_addbuf(
+ &schema_entry, "STRUCTURAL%s", separator);
+ break;
+ case 1:
+ talloc_asprintf_addbuf(
+ &schema_entry, "STRUCTURAL%s", separator);
+ break;
+ case 2:
+ talloc_asprintf_addbuf(
+ &schema_entry, "ABSTRACT%s", separator);
+ break;
+ case 3:
+ talloc_asprintf_addbuf(
+ &schema_entry, "AUXILIARY%s", separator);
+ break;
+ }
+
+ if (must) {
+ talloc_asprintf_addbuf(
+ &schema_entry,
+ "MUST (%s",
+ target == TARGET_AD_SCHEMA_SUBENTRY ? "" : " ");
+
+ APPEND_ATTRS(must);
+
+ talloc_asprintf_addbuf(
+ &schema_entry, ")%s", separator);
+ }
+
+ if (may) {
+ talloc_asprintf_addbuf(
+ &schema_entry,
+ "MAY (%s",
+ target == TARGET_AD_SCHEMA_SUBENTRY ? "" : " ");
+
+ APPEND_ATTRS(may);
+
+ talloc_asprintf_addbuf(
+ &schema_entry, ")%s", separator);
+ }
+
+ if (schemaHexGUID) {
+ talloc_asprintf_addbuf(
+ &schema_entry,
+ "CLASS-GUID '%s'%s",
+ schemaHexGUID,
+ separator);
+ }
+
+ talloc_asprintf_addbuf(&schema_entry, ")");
+
+ return schema_entry;
+}
+
+char *schema_class_to_description(TALLOC_CTX *mem_ctx, const struct dsdb_class *sclass)
+{
+ char *schema_description;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ if (!tmp_ctx) {
+ return NULL;
+ }
+
+ schema_description
+ = schema_class_description(mem_ctx,
+ TARGET_AD_SCHEMA_SUBENTRY,
+ " ",
+ sclass->governsID_oid,
+ sclass->lDAPDisplayName,
+ NULL,
+ sclass->subClassOf,
+ sclass->objectClassCategory,
+ dsdb_attribute_list(tmp_ctx,
+ sclass, DSDB_SCHEMA_ALL_MUST),
+ dsdb_attribute_list(tmp_ctx,
+ sclass, DSDB_SCHEMA_ALL_MAY),
+ NULL);
+ talloc_free(tmp_ctx);
+ return schema_description;
+}
+
+char *schema_class_to_dITContentRule(TALLOC_CTX *mem_ctx, const struct dsdb_class *sclass,
+ const struct dsdb_schema *schema)
+{
+ unsigned int i;
+ char *schema_description;
+ const char **aux_class_list = NULL;
+ const char **attrs;
+ const char **must_attr_list = NULL;
+ const char **may_attr_list = NULL;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ const struct dsdb_class *aux_class;
+ if (!tmp_ctx) {
+ return NULL;
+ }
+
+ aux_class_list = merge_attr_list(tmp_ctx, aux_class_list, sclass->systemAuxiliaryClass);
+ aux_class_list = merge_attr_list(tmp_ctx, aux_class_list, sclass->auxiliaryClass);
+
+ for (i=0; aux_class_list && aux_class_list[i]; i++) {
+ aux_class = dsdb_class_by_lDAPDisplayName(schema, aux_class_list[i]);
+
+ attrs = dsdb_attribute_list(mem_ctx, aux_class, DSDB_SCHEMA_ALL_MUST);
+ must_attr_list = merge_attr_list(mem_ctx, must_attr_list, attrs);
+
+ attrs = dsdb_attribute_list(mem_ctx, aux_class, DSDB_SCHEMA_ALL_MAY);
+ may_attr_list = merge_attr_list(mem_ctx, may_attr_list, attrs);
+ }
+
+ schema_description
+ = schema_class_description(mem_ctx,
+ TARGET_AD_SCHEMA_SUBENTRY,
+ " ",
+ sclass->governsID_oid,
+ sclass->lDAPDisplayName,
+ (const char **)aux_class_list,
+ NULL, /* Must not specify a
+ * SUP (subclass) in
+ * ditContentRules
+ * per MS-ADTS
+ * 3.1.1.3.1.1.1 */
+ -1, must_attr_list, may_attr_list,
+ NULL);
+ talloc_free(tmp_ctx);
+ return schema_description;
+}
+
+char *schema_class_to_extendedInfo(TALLOC_CTX *mem_ctx, const struct dsdb_class *sclass)
+{
+ char *schema_description = NULL;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ if (!tmp_ctx) {
+ return NULL;
+ }
+
+ schema_description
+ = schema_class_description(mem_ctx,
+ TARGET_AD_SCHEMA_SUBENTRY,
+ " ",
+ sclass->governsID_oid,
+ sclass->lDAPDisplayName,
+ NULL,
+ NULL, /* Must not specify a
+ * SUP (subclass) in
+ * ditContentRules
+ * per MS-ADTS
+ * 3.1.1.3.1.1.1 */
+ -1, NULL, NULL,
+ GUID_hexstring(tmp_ctx, &sclass->schemaIDGUID));
+ talloc_free(tmp_ctx);
+ return schema_description;
+}
+
+
diff --git a/source4/dsdb/schema/schema_filtered.c b/source4/dsdb/schema/schema_filtered.c
new file mode 100644
index 0000000..d341040
--- /dev/null
+++ b/source4/dsdb/schema/schema_filtered.c
@@ -0,0 +1,101 @@
+/*
+ Unix SMB/CIFS Implementation.
+ API for determining af an attribute belongs to the filtered set.
+
+ Copyright (C) Nadezhda Ivanova <nivanova@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/samdb/samdb.h"
+#include "dsdb/common/util.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.h"
+#include "param/param.h"
+
+static const char * const never_in_filtered_attrs[] = {
+ "accountExpires",
+ "codePage",
+ "creationTime",
+ "dNSHostName",
+ "displayName",
+ "domainReplica",
+ "fSMORoleOwner",
+ "flatName",
+ "isCriticalSystemObject",
+ "lockOutObservationWindow",
+ "lockoutDuration",
+ "lockoutTime",
+ "logonHours",
+ "maxPwdAge",
+ "minPwdAge",
+ "minPwdLength",
+ "msDS-AdditionalDnsHostName",
+ "msDS-AdditionalSamAccountName",
+ "msDS-AllowedToDelegateTo",
+ "msDS-AuthenticatedAtDC",
+ "msDS-ExecuteScriptPassword",
+ "msDS-KrbTgtLink",
+ "msDS-SPNSuffixes",
+ "msDS-SupportedEncryptionTypes",
+ "msDS-TrustForestTrustInfo",
+ "nETBIOSName",
+ "nTMixedDomain",
+ "notFiltlockoutThreshold",
+ "operatingSystem",
+ "operatingSystemServicePack",
+ "operatingSystemVersion",
+ "pwdHistoryLength",
+ "pwdLastSet",
+ "pwdProperties",
+ "rid",
+ "sIDHistory",
+ "securityIdentifier",
+ "servicePrincipalName",
+ "trustAttributes",
+ "trustDirection",
+ "trustParent",
+ "trustPartner",
+ "trustPosixOffset",
+ "trustType",
+ DSDB_SECRET_ATTRIBUTES
+};
+
+/* returns true if the attribute can be in a filtered replica */
+
+bool dsdb_attribute_is_attr_in_filtered_replica(struct dsdb_attribute *attribute)
+{
+ int i, size = sizeof(never_in_filtered_attrs)/sizeof(char *);
+ if (attribute->systemOnly ||
+ attribute->schemaFlagsEx & SCHEMA_FLAG_ATTR_IS_CRITICAL) {
+ return false;
+ }
+ if (attribute->systemFlags & (DS_FLAG_ATTR_NOT_REPLICATED |
+ DS_FLAG_ATTR_REQ_PARTIAL_SET_MEMBER |
+ DS_FLAG_ATTR_IS_CONSTRUCTED)) {
+ return false;
+ }
+
+ for (i=0; i < size; i++) {
+ if (strcmp(attribute->lDAPDisplayName, never_in_filtered_attrs[i]) == 0) {
+ return false;
+ }
+ }
+
+ if (attribute->searchFlags & SEARCH_FLAG_RODC_ATTRIBUTE) {
+ return false;
+ }
+ return true;
+}
diff --git a/source4/dsdb/schema/schema_inferiors.c b/source4/dsdb/schema/schema_inferiors.c
new file mode 100644
index 0000000..56f5733
--- /dev/null
+++ b/source4/dsdb/schema/schema_inferiors.c
@@ -0,0 +1,347 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ implement possibleInferiors calculation
+
+ 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/>.
+
+*/
+/*
+ This module is a C implementation of the logic in the
+ dsdb/samdb/ldb_modules/tests/possibleInferiors.py code
+
+ To understand the C code, please see the python code first
+ */
+
+#include "includes.h"
+#include "dsdb/samdb/samdb.h"
+
+
+/*
+ create the SUPCLASSES() list
+ */
+static const char **schema_supclasses(const struct dsdb_schema *schema,
+ struct dsdb_class *schema_class)
+{
+ const char **list;
+
+ if (schema_class->tmp.supclasses) {
+ return schema_class->tmp.supclasses;
+ }
+
+ list = const_str_list(str_list_make_empty(schema_class));
+ if (list == NULL) {
+ DEBUG(0,(__location__ " out of memory\n"));
+ return NULL;
+ }
+
+ /* Cope with 'top SUP top', i.e. top is subClassOf top */
+ if (schema_class->subClassOf &&
+ strcmp(schema_class->lDAPDisplayName, schema_class->subClassOf) == 0) {
+ schema_class->tmp.supclasses = list;
+ return list;
+ }
+
+ if (schema_class->subClassOf) {
+ const struct dsdb_class *schema_class2 = dsdb_class_by_lDAPDisplayName(schema, schema_class->subClassOf);
+ const char **list2;
+ list = str_list_add_const(list, schema_class->subClassOf);
+
+ list2 = schema_supclasses(schema, discard_const_p(struct dsdb_class, schema_class2));
+ list = str_list_append_const(list, list2);
+ }
+
+ schema_class->tmp.supclasses = str_list_unique(list);
+
+ return schema_class->tmp.supclasses;
+}
+
+/*
+ this one is used internally
+ matches SUBCLASSES() python function
+ */
+static const char **schema_subclasses(const struct dsdb_schema *schema,
+ TALLOC_CTX *mem_ctx,
+ const char **oclist)
+{
+ const char **list = const_str_list(str_list_make_empty(mem_ctx));
+ unsigned int i;
+
+ for (i=0; oclist && oclist[i]; i++) {
+ const struct dsdb_class *schema_class = dsdb_class_by_lDAPDisplayName(schema, oclist[i]);
+ if (!schema_class) {
+ DEBUG(0, ("ERROR: Unable to locate subClass: '%s'\n", oclist[i]));
+ continue;
+ }
+ list = str_list_append_const(list, schema_class->tmp.subclasses);
+ }
+ return list;
+}
+
+
+/*
+ equivalent of the POSSSUPERIORS() python function
+ */
+static const char **schema_posssuperiors(const struct dsdb_schema *schema,
+ struct dsdb_class *schema_class)
+{
+ if (schema_class->tmp.posssuperiors == NULL) {
+ const char **list2 = const_str_list(str_list_make_empty(schema_class));
+ const char **list3;
+ unsigned int i;
+
+ list2 = str_list_append_const(list2, schema_class->systemPossSuperiors);
+ list2 = str_list_append_const(list2, schema_class->possSuperiors);
+ list3 = schema_supclasses(schema, schema_class);
+ for (i=0; list3 && list3[i]; i++) {
+ const struct dsdb_class *class2 = dsdb_class_by_lDAPDisplayName(schema, list3[i]);
+ if (!class2) {
+ DEBUG(0, ("ERROR: Unable to locate supClass: '%s'\n", list3[i]));
+ continue;
+ }
+ list2 = str_list_append_const(list2, schema_posssuperiors(schema,
+ discard_const_p(struct dsdb_class, class2)));
+ }
+ list2 = str_list_append_const(list2, schema_subclasses(schema, list2, list2));
+
+ schema_class->tmp.posssuperiors = str_list_unique(list2);
+ }
+
+ return schema_class->tmp.posssuperiors;
+}
+
+static const char **schema_subclasses_recurse(const struct dsdb_schema *schema,
+ struct dsdb_class *schema_class)
+{
+ const char **list = str_list_copy_const(schema_class, schema_class->tmp.subclasses_direct);
+ unsigned int i;
+ for (i=0;list && list[i]; i++) {
+ const struct dsdb_class *schema_class2 = dsdb_class_by_lDAPDisplayName(schema, list[i]);
+ if (schema_class != schema_class2) {
+ list = str_list_append_const(list, schema_subclasses_recurse(schema,
+ discard_const_p(struct dsdb_class, schema_class2)));
+ }
+ }
+ return list;
+}
+
+/* Walk down the subClass tree, setting a higher index as we go down
+ * each level. top is 1, subclasses of top are 2, etc */
+static void schema_subclasses_order_recurse(const struct dsdb_schema *schema,
+ struct dsdb_class *schema_class,
+ const int order)
+{
+ const char **list = schema_class->tmp.subclasses_direct;
+ unsigned int i;
+ schema_class->subClass_order = order;
+ for (i=0;list && list[i]; i++) {
+ const struct dsdb_class *schema_class2 = dsdb_class_by_lDAPDisplayName(schema, list[i]);
+ schema_subclasses_order_recurse(schema, discard_const_p(struct dsdb_class, schema_class2), order+1);
+ }
+ return;
+}
+
+static int schema_create_subclasses(const struct dsdb_schema *schema)
+{
+ struct dsdb_class *schema_class, *top;
+
+ for (schema_class=schema->classes; schema_class; schema_class=schema_class->next) {
+ struct dsdb_class *schema_class2 = discard_const_p(struct dsdb_class,
+ dsdb_class_by_lDAPDisplayName(schema, schema_class->subClassOf));
+ if (schema_class2 == NULL) {
+ DEBUG(0,("ERROR: no subClassOf '%s' for '%s'\n",
+ schema_class->subClassOf,
+ schema_class->lDAPDisplayName));
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ if (schema_class2 && schema_class != schema_class2) {
+ if (schema_class2->tmp.subclasses_direct == NULL) {
+ schema_class2->tmp.subclasses_direct = const_str_list(str_list_make_empty(schema_class2));
+ if (!schema_class2->tmp.subclasses_direct) {
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+ }
+ schema_class2->tmp.subclasses_direct = str_list_add_const(schema_class2->tmp.subclasses_direct,
+ schema_class->lDAPDisplayName);
+ }
+ }
+
+ for (schema_class=schema->classes; schema_class; schema_class=schema_class->next) {
+ schema_class->tmp.subclasses = str_list_unique(schema_subclasses_recurse(schema, schema_class));
+
+ /* Initialize the subClass order, to ensure we can't have uninitialized sort on the subClass hierarchy */
+ schema_class->subClass_order = 0;
+ }
+
+ top = discard_const_p(struct dsdb_class, dsdb_class_by_lDAPDisplayName(schema, "top"));
+ if (!top) {
+ DEBUG(0,("ERROR: no 'top' class in loaded schema\n"));
+ return LDB_ERR_OPERATIONS_ERROR;
+ }
+
+ schema_subclasses_order_recurse(schema, top, 1);
+ return LDB_SUCCESS;
+}
+
+static void schema_fill_possible_inferiors(const struct dsdb_schema *schema,
+ struct dsdb_class *schema_class)
+{
+ struct dsdb_class *c2;
+ const char **poss_inf = NULL;
+ const char **sys_poss_inf = NULL;
+
+ for (c2 = schema->classes; c2; c2 = c2->next) {
+ const char **superiors = schema_posssuperiors(schema, c2);
+ if (c2->objectClassCategory != 2 &&
+ c2->objectClassCategory != 3 &&
+ str_list_check(superiors, schema_class->lDAPDisplayName))
+ {
+ if (c2->systemOnly == false) {
+ if (poss_inf == NULL) {
+ poss_inf = const_str_list(str_list_make_empty(schema_class));
+ }
+ poss_inf = str_list_add_const(poss_inf,
+ c2->lDAPDisplayName);
+ }
+ if (sys_poss_inf == NULL) {
+ sys_poss_inf = const_str_list(str_list_make_empty(schema_class));
+ }
+ sys_poss_inf = str_list_add_const(sys_poss_inf,
+ c2->lDAPDisplayName);
+ }
+ }
+ schema_class->systemPossibleInferiors = str_list_unique(sys_poss_inf);
+ schema_class->possibleInferiors = str_list_unique(poss_inf);
+}
+
+/*
+ fill in a string class name from a governs_ID
+ */
+static void schema_fill_from_class_one(const struct dsdb_schema *schema,
+ const struct dsdb_class *c,
+ const char **s,
+ const uint32_t id)
+{
+ if (*s == NULL && id != 0) {
+ const struct dsdb_class *c2 =
+ dsdb_class_by_governsID_id(schema, id);
+ if (c2) {
+ *s = c2->lDAPDisplayName;
+ }
+ }
+}
+
+/*
+ fill in a list of string class names from a governs_ID list
+ */
+static void schema_fill_from_class_list(const struct dsdb_schema *schema,
+ const struct dsdb_class *c,
+ const char ***s,
+ const uint32_t *ids)
+{
+ if (*s == NULL && ids != NULL) {
+ unsigned int i;
+ for (i=0;ids[i];i++) ;
+ *s = talloc_array(c, const char *, i+1);
+ for (i=0;ids[i];i++) {
+ const struct dsdb_class *c2 =
+ dsdb_class_by_governsID_id(schema, ids[i]);
+ if (c2) {
+ (*s)[i] = c2->lDAPDisplayName;
+ } else {
+ (*s)[i] = NULL;
+ }
+ }
+ (*s)[i] = NULL;
+ }
+}
+
+/*
+ fill in a list of string attribute names from a attributeID list
+ */
+static void schema_fill_from_attribute_list(const struct dsdb_schema *schema,
+ const struct dsdb_class *c,
+ const char ***s,
+ const uint32_t *ids)
+{
+ if (*s == NULL && ids != NULL) {
+ unsigned int i;
+ for (i=0;ids[i];i++) ;
+ *s = talloc_array(c, const char *, i+1);
+ for (i=0;ids[i];i++) {
+ const struct dsdb_attribute *a =
+ dsdb_attribute_by_attributeID_id(schema, ids[i]);
+ if (a) {
+ (*s)[i] = a->lDAPDisplayName;
+ } else {
+ (*s)[i] = NULL;
+ }
+ }
+ (*s)[i] = NULL;
+ }
+}
+
+/*
+ if the schema came from DRS then some attributes will be setup as IDs
+ */
+static void schema_fill_from_ids(const struct dsdb_schema *schema)
+{
+ struct dsdb_class *c;
+ for (c=schema->classes; c; c=c->next) {
+ schema_fill_from_class_one(schema, c, &c->subClassOf, c->subClassOf_id);
+ schema_fill_from_attribute_list(schema, c, &c->systemMayContain, c->systemMayContain_ids);
+ schema_fill_from_attribute_list(schema, c, &c->systemMustContain, c->systemMustContain_ids);
+ schema_fill_from_attribute_list(schema, c, &c->mustContain, c->mustContain_ids);
+ schema_fill_from_attribute_list(schema, c, &c->mayContain, c->mayContain_ids);
+ schema_fill_from_class_list(schema, c, &c->possSuperiors, c->possSuperiors_ids);
+ schema_fill_from_class_list(schema, c, &c->systemPossSuperiors, c->systemPossSuperiors_ids);
+ schema_fill_from_class_list(schema, c, &c->systemAuxiliaryClass, c->systemAuxiliaryClass_ids);
+ schema_fill_from_class_list(schema, c, &c->auxiliaryClass, c->auxiliaryClass_ids);
+ }
+}
+
+int schema_fill_constructed(const struct dsdb_schema *schema)
+{
+ int ret;
+ struct dsdb_class *schema_class;
+
+ /* make sure we start with a clean cache */
+ for (schema_class=schema->classes; schema_class; schema_class=schema_class->next) {
+ ZERO_STRUCT(schema_class->tmp);
+ }
+
+ schema_fill_from_ids(schema);
+
+ ret = schema_create_subclasses(schema);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ for (schema_class=schema->classes; schema_class; schema_class=schema_class->next) {
+ schema_fill_possible_inferiors(schema, schema_class);
+ }
+
+ /* free up our internal cache elements */
+ for (schema_class=schema->classes; schema_class; schema_class=schema_class->next) {
+ TALLOC_FREE(schema_class->tmp.supclasses);
+ TALLOC_FREE(schema_class->tmp.subclasses_direct);
+ TALLOC_FREE(schema_class->tmp.subclasses);
+ TALLOC_FREE(schema_class->tmp.posssuperiors);
+ }
+
+ return LDB_SUCCESS;
+}
diff --git a/source4/dsdb/schema/schema_info_attr.c b/source4/dsdb/schema/schema_info_attr.c
new file mode 100644
index 0000000..04b2964
--- /dev/null
+++ b/source4/dsdb/schema/schema_info_attr.c
@@ -0,0 +1,235 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ SCHEMA::schemaInfo implementation
+
+ Copyright (C) Kamen Mazdrashki <kamenim@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"
+
+
+/**
+ * Creates and initializes new dsdb_schema_info value.
+ * Initial schemaInfo values is with:
+ * revision = 0
+ * invocationId = GUID_ZERO
+ */
+WERROR dsdb_schema_info_new(TALLOC_CTX *mem_ctx, struct dsdb_schema_info **_schema_info)
+{
+ struct dsdb_schema_info *schema_info;
+
+ schema_info = talloc_zero(mem_ctx, struct dsdb_schema_info);
+ W_ERROR_HAVE_NO_MEMORY(schema_info);
+
+ *_schema_info = schema_info;
+
+ return WERR_OK;
+}
+
+/**
+ * Creates and initializes new dsdb_schema_info blob value.
+ * Initial schemaInfo values is with:
+ * revision = 0
+ * invocationId = GUID_ZERO
+ */
+WERROR dsdb_schema_info_blob_new(TALLOC_CTX *mem_ctx, DATA_BLOB *_schema_info_blob)
+{
+ DATA_BLOB blob;
+
+ blob = data_blob_talloc_zero(mem_ctx, 21);
+ W_ERROR_HAVE_NO_MEMORY(blob.data);
+
+ /* Set the schemaInfo marker to 0xFF */
+ blob.data[0] = 0xFF;
+
+ *_schema_info_blob = blob;
+
+ return WERR_OK;
+}
+
+
+/**
+ * Verify the 'blob' is a valid schemaInfo blob
+ */
+bool dsdb_schema_info_blob_is_valid(const DATA_BLOB *blob)
+{
+ if (!blob || !blob->data) {
+ return false;
+ }
+
+ /* schemaInfo blob must be 21 bytes long */
+ if (blob->length != 21) {
+ return false;
+ }
+
+ /* schemaInfo blob should start with 0xFF */
+ if (blob->data[0] != 0xFF) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Parse schemaInfo structure from a data_blob
+ * (DATA_BLOB or ldb_val).
+ * Suitable for parsing blobs that come from
+ * DRS interface or from LDB database
+ */
+WERROR dsdb_schema_info_from_blob(const DATA_BLOB *blob,
+ TALLOC_CTX *mem_ctx, struct dsdb_schema_info **_schema_info)
+{
+ TALLOC_CTX *temp_ctx;
+ enum ndr_err_code ndr_err;
+ struct dsdb_schema_info *schema_info;
+ struct schemaInfoBlob schema_info_blob;
+
+ /* verify schemaInfo blob is valid */
+ if (!dsdb_schema_info_blob_is_valid(blob)) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ temp_ctx = talloc_new(mem_ctx);
+ W_ERROR_HAVE_NO_MEMORY(temp_ctx);
+
+ ndr_err = ndr_pull_struct_blob_all(blob, temp_ctx,
+ &schema_info_blob,
+ (ndr_pull_flags_fn_t)ndr_pull_schemaInfoBlob);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ NTSTATUS nt_status = ndr_map_error2ntstatus(ndr_err);
+ talloc_free(temp_ctx);
+ return ntstatus_to_werror(nt_status);
+ }
+
+ schema_info = talloc(mem_ctx, struct dsdb_schema_info);
+ if (!schema_info) {
+ talloc_free(temp_ctx);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ /* note that we accept revision numbers of zero now - w2k8r2
+ sends a revision of zero on initial vampire */
+ schema_info->revision = schema_info_blob.revision;
+ schema_info->invocation_id = schema_info_blob.invocation_id;
+ *_schema_info = schema_info;
+
+ talloc_free(temp_ctx);
+ return WERR_OK;
+}
+
+/**
+ * Creates a blob from schemaInfo structure
+ * Suitable for packing schemaInfo into a blob
+ * which is to be used in DRS interface of LDB database
+ */
+WERROR dsdb_blob_from_schema_info(const struct dsdb_schema_info *schema_info,
+ TALLOC_CTX *mem_ctx, DATA_BLOB *blob)
+{
+ enum ndr_err_code ndr_err;
+ struct schemaInfoBlob schema_info_blob;
+
+ schema_info_blob.marker = 0xFF;
+ schema_info_blob.revision = schema_info->revision;
+ schema_info_blob.invocation_id = schema_info->invocation_id;
+
+ ndr_err = ndr_push_struct_blob(blob, mem_ctx,
+ &schema_info_blob,
+ (ndr_push_flags_fn_t)ndr_push_schemaInfoBlob);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ NTSTATUS nt_status = ndr_map_error2ntstatus(ndr_err);
+ return ntstatus_to_werror(nt_status);
+ }
+
+ return WERR_OK;
+}
+
+/**
+ * Compares schemaInfo signatures in dsdb_schema and prefixMap.
+ * NOTE: At present function compares schemaInfo values
+ * as string without taking into account schemaVersion field
+ *
+ * @return WERR_OK if schemaInfos are equal
+ * WERR_DS_DRA_SCHEMA_MISMATCH if schemaInfos are different
+ */
+WERROR dsdb_schema_info_cmp(const struct dsdb_schema *schema,
+ const struct drsuapi_DsReplicaOIDMapping_Ctr *ctr)
+{
+ TALLOC_CTX *frame = NULL;
+ DATA_BLOB blob = data_blob_null;
+ struct dsdb_schema_info *schema_info = NULL;
+ const struct drsuapi_DsReplicaOIDMapping *mapping = NULL;
+ WERROR werr;
+
+ /* we should have at least schemaInfo element */
+ if (ctr->num_mappings < 1) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ /* verify schemaInfo element is valid */
+ mapping = &ctr->mappings[ctr->num_mappings - 1];
+ if (mapping->id_prefix != 0) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ blob = data_blob_const(mapping->oid.binary_oid, mapping->oid.length);
+ if (!dsdb_schema_info_blob_is_valid(&blob)) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ frame = talloc_stackframe();
+ werr = dsdb_schema_info_from_blob(&blob, frame, &schema_info);
+ if (!W_ERROR_IS_OK(werr)) {
+ TALLOC_FREE(frame);
+ return werr;
+ }
+
+ /*
+ * shouldn't really be possible is dsdb_schema_info_from_blob
+ * succeeded, this check is just to satisfy static checker
+ */
+ if (schema_info == NULL) {
+ TALLOC_FREE(frame);
+ return WERR_INVALID_PARAMETER;
+ }
+
+ if (schema->schema_info->revision > schema_info->revision) {
+ /*
+ * It's ok if our schema is newer than the remote one
+ */
+ werr = WERR_OK;
+ } else if (schema->schema_info->revision < schema_info->revision) {
+ werr = WERR_DS_DRA_SCHEMA_MISMATCH;
+ } else if (!GUID_equal(&schema->schema_info->invocation_id,
+ &schema_info->invocation_id))
+ {
+ werr = WERR_DS_DRA_SCHEMA_CONFLICT;
+ } else {
+ werr = WERR_OK;
+ }
+
+ TALLOC_FREE(frame);
+ return werr;
+}
+
+
diff --git a/source4/dsdb/schema/schema_init.c b/source4/dsdb/schema/schema_init.c
new file mode 100644
index 0000000..c8197b8
--- /dev/null
+++ b/source4/dsdb/schema/schema_init.c
@@ -0,0 +1,1038 @@
+/*
+ Unix SMB/CIFS Implementation.
+ DSDB schema header
+
+ Copyright (C) Stefan Metzmacher <metze@samba.org> 2006-2007
+ Copyright (C) Andrew Bartlett <abartlet@samba.org> 2006-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 "dsdb/samdb/samdb.h"
+#include "dsdb/common/util.h"
+#include <ldb_errors.h>
+#include "../lib/util/dlinklist.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 <ldb_module.h>
+#include "../lib/util/asn1.h"
+
+#undef strcasecmp
+
+struct dsdb_schema *dsdb_new_schema(TALLOC_CTX *mem_ctx)
+{
+ struct dsdb_schema *schema = talloc_zero(mem_ctx, struct dsdb_schema);
+ if (!schema) {
+ return NULL;
+ }
+
+ return schema;
+}
+
+struct dsdb_schema *dsdb_schema_copy_shallow(TALLOC_CTX *mem_ctx,
+ struct ldb_context *ldb,
+ const struct dsdb_schema *schema)
+{
+ int ret;
+ struct dsdb_class *cls;
+ struct dsdb_attribute *attr;
+ struct dsdb_schema *schema_copy;
+
+ schema_copy = dsdb_new_schema(mem_ctx);
+ if (!schema_copy) {
+ return NULL;
+ }
+
+ /* copy prexiMap & schemaInfo */
+ schema_copy->prefixmap = dsdb_schema_pfm_copy_shallow(schema_copy,
+ schema->prefixmap);
+ if (!schema_copy->prefixmap) {
+ goto failed;
+ }
+
+ schema_copy->schema_info = talloc(schema_copy, struct dsdb_schema_info);
+ if (!schema_copy->schema_info) {
+ goto failed;
+ }
+ *schema_copy->schema_info = *schema->schema_info;
+
+ /* copy classes and attributes*/
+ for (cls = schema->classes; cls; cls = cls->next) {
+ struct dsdb_class *class_copy = talloc_memdup(schema_copy,
+ cls, sizeof(*cls));
+ if (!class_copy) {
+ goto failed;
+ }
+ DLIST_ADD(schema_copy->classes, class_copy);
+ }
+ schema_copy->num_classes = schema->num_classes;
+
+ for (attr = schema->attributes; attr; attr = attr->next) {
+ struct dsdb_attribute *a_copy = talloc_memdup(schema_copy,
+ attr, sizeof(*attr));
+ if (!a_copy) {
+ goto failed;
+ }
+ DLIST_ADD(schema_copy->attributes, a_copy);
+ }
+ schema_copy->num_attributes = schema->num_attributes;
+
+ /* rebuild indexes */
+ ret = dsdb_setup_sorted_accessors(ldb, schema_copy);
+ if (ret != LDB_SUCCESS) {
+ goto failed;
+ }
+
+ /* leave reload_seq_number = 0 so it will be refresh ASAP */
+
+ return schema_copy;
+
+failed:
+ talloc_free(schema_copy);
+ return NULL;
+}
+
+
+WERROR dsdb_load_prefixmap_from_drsuapi(struct dsdb_schema *schema,
+ const struct drsuapi_DsReplicaOIDMapping_Ctr *ctr)
+{
+ WERROR werr;
+ struct dsdb_schema_info *schema_info = NULL;
+ struct dsdb_schema_prefixmap *pfm = NULL;
+
+ werr = dsdb_schema_pfm_from_drsuapi_pfm(ctr, true, schema, &pfm, &schema_info);
+ W_ERROR_NOT_OK_RETURN(werr);
+
+ /* set loaded prefixMap */
+ talloc_free(schema->prefixmap);
+ schema->prefixmap = pfm;
+
+ talloc_free(schema->schema_info);
+ schema->schema_info = schema_info;
+
+ return WERR_OK;
+}
+
+static WERROR _dsdb_prefixmap_from_ldb_val(const struct ldb_val *pfm_ldb_val,
+ TALLOC_CTX *mem_ctx,
+ struct dsdb_schema_prefixmap **_pfm)
+{
+ WERROR werr;
+ enum ndr_err_code ndr_err;
+ struct prefixMapBlob pfm_blob;
+
+ TALLOC_CTX *temp_ctx = talloc_new(mem_ctx);
+ W_ERROR_HAVE_NO_MEMORY(temp_ctx);
+
+ ndr_err = ndr_pull_struct_blob(pfm_ldb_val, temp_ctx,
+ &pfm_blob,
+ (ndr_pull_flags_fn_t)ndr_pull_prefixMapBlob);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ NTSTATUS nt_status = ndr_map_error2ntstatus(ndr_err);
+ DEBUG(0,("_dsdb_prefixmap_from_ldb_val: Failed to parse prefixmap of length %u: %s\n",
+ (unsigned int)pfm_ldb_val->length, ndr_map_error2string(ndr_err)));
+ talloc_free(temp_ctx);
+ return ntstatus_to_werror(nt_status);
+ }
+
+ if (pfm_blob.version != PREFIX_MAP_VERSION_DSDB) {
+ DEBUG(0,("_dsdb_prefixmap_from_ldb_val: pfm_blob->version %u incorrect\n", (unsigned int)pfm_blob.version));
+ talloc_free(temp_ctx);
+ return WERR_VERSION_PARSE_ERROR;
+ }
+
+ /* call the drsuapi version */
+ werr = dsdb_schema_pfm_from_drsuapi_pfm(&pfm_blob.ctr.dsdb, false, mem_ctx, _pfm, NULL);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0, (__location__ " dsdb_schema_pfm_from_drsuapi_pfm failed: %s\n", win_errstr(werr)));
+ talloc_free(temp_ctx);
+ return werr;
+ }
+
+ talloc_free(temp_ctx);
+
+ return werr;
+}
+
+WERROR dsdb_load_oid_mappings_ldb(struct dsdb_schema *schema,
+ const struct ldb_val *prefixMap,
+ const struct ldb_val *schemaInfo)
+{
+ WERROR werr;
+ struct dsdb_schema_info *schema_info = NULL;
+ struct dsdb_schema_prefixmap *pfm = NULL;
+ TALLOC_CTX *mem_ctx;
+
+ /* verify schemaInfo blob is valid one */
+ if (!dsdb_schema_info_blob_is_valid(schemaInfo)) {
+ DEBUG(0,(__location__": dsdb_schema_info_blob_is_valid() failed.\n"));
+ return WERR_INVALID_PARAMETER;
+ }
+
+ mem_ctx = talloc_new(schema);
+ W_ERROR_HAVE_NO_MEMORY(mem_ctx);
+
+ /* fetch prefixMap */
+ werr = _dsdb_prefixmap_from_ldb_val(prefixMap,
+ mem_ctx, &pfm);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0, (__location__ " _dsdb_prefixmap_from_ldb_val failed: %s\n", win_errstr(werr)));
+ talloc_free(mem_ctx);
+ return werr;
+ }
+
+ /* decode schema_info */
+ werr = dsdb_schema_info_from_blob(schemaInfo, mem_ctx, &schema_info);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0, (__location__ " dsdb_schema_info_from_blob failed: %s\n", win_errstr(werr)));
+ talloc_free(mem_ctx);
+ return werr;
+ }
+
+ /* store prefixMap and schema_info into cached Schema */
+ talloc_free(schema->prefixmap);
+ schema->prefixmap = talloc_steal(schema, pfm);
+
+ talloc_free(schema->schema_info);
+ schema->schema_info = talloc_steal(schema, schema_info);
+
+ /* clean up locally allocated mem */
+ talloc_free(mem_ctx);
+
+ return WERR_OK;
+}
+
+WERROR dsdb_get_oid_mappings_drsuapi(const struct dsdb_schema *schema,
+ bool include_schema_info,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaOIDMapping_Ctr **_ctr)
+{
+ return dsdb_drsuapi_pfm_from_schema_pfm(schema->prefixmap,
+ include_schema_info ? schema->schema_info : NULL,
+ mem_ctx, _ctr);
+}
+
+WERROR dsdb_get_drsuapi_prefixmap_as_blob(const struct drsuapi_DsReplicaOIDMapping_Ctr *ctr,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_val *prefixMap)
+{
+ struct prefixMapBlob pfm;
+ enum ndr_err_code ndr_err;
+ pfm.version = PREFIX_MAP_VERSION_DSDB;
+ pfm.reserved = 0;
+ pfm.ctr.dsdb = *ctr;
+
+ ndr_err = ndr_push_struct_blob(prefixMap, mem_ctx, &pfm,
+ (ndr_push_flags_fn_t)ndr_push_prefixMapBlob);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ NTSTATUS nt_status = ndr_map_error2ntstatus(ndr_err);
+ return ntstatus_to_werror(nt_status);
+ }
+ return WERR_OK;
+}
+
+WERROR dsdb_get_oid_mappings_ldb(const struct dsdb_schema *schema,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_val *prefixMap,
+ struct ldb_val *schemaInfo)
+{
+ WERROR status;
+ struct drsuapi_DsReplicaOIDMapping_Ctr *ctr;
+
+ status = dsdb_get_oid_mappings_drsuapi(schema, false, mem_ctx, &ctr);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ status = dsdb_get_drsuapi_prefixmap_as_blob(ctr, mem_ctx, prefixMap);
+ talloc_free(ctr);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ status = dsdb_blob_from_schema_info(schema->schema_info, mem_ctx, schemaInfo);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ return WERR_OK;
+}
+
+
+/*
+ * this function is called from within a ldb transaction from the schema_fsmo module
+ */
+WERROR dsdb_create_prefix_mapping(struct ldb_context *ldb, struct dsdb_schema *schema, const char *full_oid)
+{
+ WERROR status;
+ uint32_t attid;
+ TALLOC_CTX *mem_ctx;
+ struct dsdb_schema_prefixmap *pfm;
+ struct dsdb_schema_prefixmap *orig_pfm = NULL;
+
+ mem_ctx = talloc_new(ldb);
+ W_ERROR_HAVE_NO_MEMORY(mem_ctx);
+
+ /* Read prefixes from disk*/
+ status = dsdb_read_prefixes_from_ldb(ldb, mem_ctx, &pfm);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(0,("dsdb_create_prefix_mapping: dsdb_read_prefixes_from_ldb: %s\n",
+ win_errstr(status)));
+ talloc_free(mem_ctx);
+ return status;
+ }
+
+ /* Check if there is a prefix for the oid in the prefixes array*/
+ status = dsdb_schema_pfm_find_oid(pfm, full_oid, NULL);
+ if (W_ERROR_IS_OK(status)) {
+ /* prefix found*/
+ talloc_free(mem_ctx);
+ return status;
+ } else if (!W_ERROR_EQUAL(status, WERR_NOT_FOUND)) {
+ /* error */
+ DEBUG(0,("dsdb_create_prefix_mapping: dsdb_find_prefix_for_oid: %s\n",
+ win_errstr(status)));
+ talloc_free(mem_ctx);
+ return status;
+ }
+
+ /* Create the new mapping for the prefix of full_oid */
+ status = dsdb_schema_pfm_make_attid(pfm, full_oid, &attid);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(0,("dsdb_create_prefix_mapping: dsdb_schema_pfm_make_attid: %s\n",
+ win_errstr(status)));
+ talloc_free(mem_ctx);
+ return status;
+ }
+
+ /*
+ * We temporary replcate schema->prefixmap.
+ */
+ orig_pfm = schema->prefixmap;
+ schema->prefixmap = pfm;
+
+ /* Update prefixMap in ldb*/
+ status = dsdb_write_prefixes_from_schema_to_ldb(mem_ctx, ldb, schema);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(0,("dsdb_create_prefix_mapping: dsdb_write_prefixes_to_ldb: %s\n",
+ win_errstr(status)));
+ talloc_free(mem_ctx);
+ return status;
+ }
+
+ DEBUG(2,(__location__ " Added prefixMap %s - now have %u prefixes\n",
+ full_oid, schema->prefixmap->length));
+
+ /*
+ * We restore the original prefix map.
+ *
+ * The next schema reload should get an updated prefix map!
+ */
+ schema->prefixmap = orig_pfm;
+
+ talloc_free(mem_ctx);
+ return status;
+}
+
+
+WERROR dsdb_write_prefixes_from_schema_to_ldb(TALLOC_CTX *mem_ctx, struct ldb_context *ldb,
+ const struct dsdb_schema *schema)
+{
+ WERROR status;
+ int ldb_ret;
+ struct ldb_message *msg;
+ struct ldb_dn *schema_dn;
+ struct prefixMapBlob pfm_blob;
+ struct ldb_val ndr_blob;
+ enum ndr_err_code ndr_err;
+ TALLOC_CTX *temp_ctx;
+ struct drsuapi_DsReplicaOIDMapping_Ctr *ctr;
+
+ schema_dn = ldb_get_schema_basedn(ldb);
+ if (!schema_dn) {
+ DEBUG(0,("dsdb_write_prefixes_from_schema_to_ldb: no schema dn present\n"));
+ return WERR_FOOBAR;
+ }
+
+ temp_ctx = talloc_new(mem_ctx);
+ W_ERROR_HAVE_NO_MEMORY(temp_ctx);
+
+ /* convert schema_prefixMap to prefixMap blob */
+ status = dsdb_get_oid_mappings_drsuapi(schema, false, temp_ctx, &ctr);
+ if (!W_ERROR_IS_OK(status)) {
+ talloc_free(temp_ctx);
+ return status;
+ }
+
+ pfm_blob.version = PREFIX_MAP_VERSION_DSDB;
+ pfm_blob.ctr.dsdb = *ctr;
+
+ ndr_err = ndr_push_struct_blob(&ndr_blob, temp_ctx,
+ &pfm_blob,
+ (ndr_push_flags_fn_t)ndr_push_prefixMapBlob);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ talloc_free(temp_ctx);
+ return WERR_FOOBAR;
+ }
+
+ /* write serialized prefixMap into LDB */
+ msg = ldb_msg_new(temp_ctx);
+ if (!msg) {
+ talloc_free(temp_ctx);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ msg->dn = schema_dn;
+ ldb_ret = ldb_msg_add_value(msg, "prefixMap", &ndr_blob, NULL);
+ if (ldb_ret != 0) {
+ talloc_free(temp_ctx);
+ DEBUG(0,("dsdb_write_prefixes_from_schema_to_ldb: ldb_msg_add_value failed\n"));
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ ldb_ret = dsdb_replace(ldb, msg, DSDB_FLAG_AS_SYSTEM);
+
+ talloc_free(temp_ctx);
+
+ if (ldb_ret != 0) {
+ DEBUG(0,("dsdb_write_prefixes_from_schema_to_ldb: dsdb_replace failed\n"));
+ return WERR_FOOBAR;
+ }
+
+ return WERR_OK;
+}
+
+WERROR dsdb_read_prefixes_from_ldb(struct ldb_context *ldb, TALLOC_CTX *mem_ctx, struct dsdb_schema_prefixmap **_pfm)
+{
+ WERROR werr;
+ int ldb_ret;
+ const struct ldb_val *prefix_val;
+ struct ldb_dn *schema_dn;
+ struct ldb_result *schema_res = NULL;
+ static const char *schema_attrs[] = {
+ "prefixMap",
+ NULL
+ };
+
+ schema_dn = ldb_get_schema_basedn(ldb);
+ if (!schema_dn) {
+ DEBUG(0,("dsdb_read_prefixes_from_ldb: no schema dn present\n"));
+ return WERR_FOOBAR;
+ }
+
+ ldb_ret = ldb_search(ldb, mem_ctx, &schema_res, schema_dn, LDB_SCOPE_BASE, schema_attrs, NULL);
+ if (ldb_ret == LDB_ERR_NO_SUCH_OBJECT) {
+ DEBUG(0,("dsdb_read_prefixes_from_ldb: no prefix map present\n"));
+ talloc_free(schema_res);
+ return WERR_FOOBAR;
+ } else if (ldb_ret != LDB_SUCCESS) {
+ DEBUG(0,("dsdb_read_prefixes_from_ldb: failed to search the schema head\n"));
+ talloc_free(schema_res);
+ return WERR_FOOBAR;
+ }
+
+ prefix_val = ldb_msg_find_ldb_val(schema_res->msgs[0], "prefixMap");
+ if (!prefix_val) {
+ DEBUG(0,("dsdb_read_prefixes_from_ldb: no prefixMap attribute found\n"));
+ talloc_free(schema_res);
+ return WERR_FOOBAR;
+ }
+
+ werr = _dsdb_prefixmap_from_ldb_val(prefix_val,
+ mem_ctx,
+ _pfm);
+ talloc_free(schema_res);
+ W_ERROR_NOT_OK_RETURN(werr);
+
+ return WERR_OK;
+}
+
+/*
+ this will be replaced with something that looks at the right part of
+ the schema once we know where unique indexing information is hidden
+ */
+static bool dsdb_schema_unique_attribute(const char *attr)
+{
+ const char *attrs[] = { "objectGUID", NULL };
+ unsigned int i;
+ for (i=0;attrs[i];i++) {
+ if (ldb_attr_cmp(attr, attrs[i]) == 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+
+/*
+ setup the ldb_schema_attribute field for a dsdb_attribute
+ */
+static int dsdb_schema_setup_ldb_schema_attribute(struct ldb_context *ldb,
+ struct dsdb_attribute *attr)
+{
+ const char *syntax = attr->syntax->ldb_syntax;
+ const struct ldb_schema_syntax *s;
+ struct ldb_schema_attribute *a;
+
+ if (!syntax) {
+ syntax = attr->syntax->ldap_oid;
+ }
+
+ s = ldb_samba_syntax_by_lDAPDisplayName(ldb, attr->lDAPDisplayName);
+ if (s == NULL) {
+ s = ldb_samba_syntax_by_name(ldb, syntax);
+ }
+ if (s == NULL) {
+ s = ldb_standard_syntax_by_name(ldb, syntax);
+ }
+
+ if (s == NULL) {
+ return ldb_operr(ldb);
+ }
+
+ attr->ldb_schema_attribute = a = talloc(attr, struct ldb_schema_attribute);
+ if (attr->ldb_schema_attribute == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ a->name = attr->lDAPDisplayName;
+ a->flags = 0;
+ a->syntax = s;
+
+ if (dsdb_schema_unique_attribute(a->name)) {
+ a->flags |= LDB_ATTR_FLAG_UNIQUE_INDEX;
+ }
+ if (attr->isSingleValued) {
+ a->flags |= LDB_ATTR_FLAG_SINGLE_VALUE;
+ }
+
+ /*
+ * 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 (attr->searchFlags & SEARCH_FLAG_ATTINDEX &&
+ !(attr->searchFlags & SEARCH_FLAG_CONFIDENTIAL))
+ {
+ a->flags |= LDB_ATTR_FLAG_INDEXED;
+ }
+
+
+ return LDB_SUCCESS;
+}
+
+
+#define GET_STRING_LDB(msg, attr, mem_ctx, p, elem, strict) do { \
+ const struct ldb_val *get_string_val = ldb_msg_find_ldb_val(msg, attr); \
+ if (get_string_val == NULL) { \
+ if (strict) { \
+ d_printf("%s: %s == NULL in %s\n", __location__, attr, ldb_dn_get_linearized(msg->dn)); \
+ return WERR_INVALID_PARAMETER; \
+ } else { \
+ (p)->elem = NULL; \
+ } \
+ } else { \
+ (p)->elem = talloc_strndup(mem_ctx, \
+ (const char *)get_string_val->data, \
+ get_string_val->length); \
+ if (!(p)->elem) { \
+ d_printf("%s: talloc_strndup failed for %s\n", __location__, attr); \
+ return WERR_NOT_ENOUGH_MEMORY; \
+ } \
+ } \
+} while (0)
+
+#define GET_STRING_LIST_LDB(msg, attr, mem_ctx, p, elem) do { \
+ int get_string_list_counter; \
+ struct ldb_message_element *get_string_list_el = ldb_msg_find_element(msg, attr); \
+ /* We may get empty attributes over the replication channel */ \
+ if (get_string_list_el == NULL || get_string_list_el->num_values == 0) { \
+ (p)->elem = NULL; \
+ break; \
+ } \
+ (p)->elem = talloc_array(mem_ctx, const char *, get_string_list_el->num_values + 1); \
+ for (get_string_list_counter=0; \
+ get_string_list_counter < get_string_list_el->num_values; \
+ get_string_list_counter++) { \
+ (p)->elem[get_string_list_counter] = talloc_strndup((p)->elem, \
+ (const char *)get_string_list_el->values[get_string_list_counter].data, \
+ get_string_list_el->values[get_string_list_counter].length); \
+ if (!(p)->elem[get_string_list_counter]) { \
+ d_printf("%s: talloc_strndup failed for %s\n", __location__, attr); \
+ return WERR_NOT_ENOUGH_MEMORY; \
+ } \
+ (p)->elem[get_string_list_counter+1] = NULL; \
+ } \
+ talloc_steal(mem_ctx, (p)->elem); \
+} while (0)
+
+#define GET_BOOL_LDB(msg, attr, p, elem, strict) do { \
+ const char *str; \
+ str = ldb_msg_find_attr_as_string(msg, attr, NULL);\
+ if (str == NULL) { \
+ if (strict) { \
+ d_printf("%s: %s == NULL\n", __location__, attr); \
+ return WERR_INVALID_PARAMETER; \
+ } else { \
+ (p)->elem = false; \
+ } \
+ } else if (strcasecmp("TRUE", str) == 0) { \
+ (p)->elem = true; \
+ } else if (strcasecmp("FALSE", str) == 0) { \
+ (p)->elem = false; \
+ } else { \
+ d_printf("%s: %s == %s\n", __location__, attr, str); \
+ return WERR_INVALID_PARAMETER; \
+ } \
+} while (0)
+
+#define GET_UINT32_LDB(msg, attr, p, elem) do { \
+ (p)->elem = ldb_msg_find_attr_as_uint(msg, attr, 0);\
+} while (0)
+
+#define GET_UINT32_PTR_LDB(msg, attr, mem_ctx, p, elem) do { \
+ uint64_t _v = ldb_msg_find_attr_as_uint64(msg, attr, UINT64_MAX);\
+ if (_v == UINT64_MAX) { \
+ (p)->elem = NULL; \
+ } else if (_v > UINT32_MAX) { \
+ d_printf("%s: %s == 0x%llX\n", __location__, \
+ attr, (unsigned long long)_v); \
+ return WERR_INVALID_PARAMETER; \
+ } else { \
+ (p)->elem = talloc(mem_ctx, uint32_t); \
+ if (!(p)->elem) { \
+ d_printf("%s: talloc failed for %s\n", __location__, attr); \
+ return WERR_NOT_ENOUGH_MEMORY; \
+ } \
+ *(p)->elem = (uint32_t)_v; \
+ } \
+} while (0)
+
+#define GET_GUID_LDB(msg, attr, p, elem) do { \
+ (p)->elem = samdb_result_guid(msg, attr);\
+} while (0)
+
+#define GET_BLOB_LDB(msg, attr, mem_ctx, p, elem) do { \
+ const struct ldb_val *_val;\
+ _val = ldb_msg_find_ldb_val(msg, attr);\
+ if (_val) {\
+ (p)->elem = *_val;\
+ talloc_steal(mem_ctx, (p)->elem.data);\
+ } else {\
+ ZERO_STRUCT((p)->elem);\
+ }\
+} while (0)
+
+/** Create an dsdb_attribute out of ldb message, attr must be already talloced
+ *
+ * If supplied the attribute will be checked against the prefixmap to
+ * ensure it can be mapped. However we can't have this attribute
+ * const as dsdb_schema_pfm_attid_from_oid calls
+ * dsdb_schema_pfm_make_attid_impl() which may modify prefixmap in
+ * other situations.
+ */
+
+WERROR dsdb_attribute_from_ldb(struct dsdb_schema_prefixmap *prefixmap,
+ struct ldb_message *msg,
+ struct dsdb_attribute *attr)
+{
+ WERROR status;
+ if (attr == NULL) {
+ DEBUG(0, ("%s: attr is null, it's expected not to be so\n", __location__));
+ return WERR_INVALID_PARAMETER;
+ }
+
+ GET_STRING_LDB(msg, "cn", attr, attr, cn, false);
+
+ /*
+ * This allows for the fact that the CN attribute is not
+ * replicated over DRS, it is only replicated under the alias
+ * 'name'.
+ */
+ if (attr->cn == NULL) {
+ GET_STRING_LDB(msg, "name", attr, attr, cn, true);
+ }
+
+ GET_STRING_LDB(msg, "lDAPDisplayName", attr, attr, lDAPDisplayName, true);
+ GET_STRING_LDB(msg, "attributeID", attr, attr, attributeID_oid, true);
+ if (!prefixmap || prefixmap->length == 0) {
+ /* set an invalid value */
+ attr->attributeID_id = DRSUAPI_ATTID_INVALID;
+ } else {
+ status = dsdb_schema_pfm_attid_from_oid(prefixmap,
+ attr->attributeID_oid,
+ &attr->attributeID_id);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(0,("%s: '%s': unable to map attributeID %s: %s\n",
+ __location__, attr->lDAPDisplayName, attr->attributeID_oid,
+ win_errstr(status)));
+ return status;
+ }
+ }
+ /* fetch msDS-IntId to be used in resolving ATTRTYP values */
+ GET_UINT32_LDB(msg, "msDS-IntId", attr, msDS_IntId);
+
+ GET_GUID_LDB(msg, "schemaIDGUID", attr, schemaIDGUID);
+ GET_UINT32_LDB(msg, "mAPIID", attr, mAPIID);
+
+ GET_GUID_LDB(msg, "attributeSecurityGUID", attr, attributeSecurityGUID);
+
+ GET_GUID_LDB(msg, "objectGUID", attr, objectGUID);
+
+ GET_UINT32_LDB(msg, "searchFlags", attr, searchFlags);
+ GET_UINT32_LDB(msg, "systemFlags", attr, systemFlags);
+ GET_BOOL_LDB(msg, "isMemberOfPartialAttributeSet", attr, isMemberOfPartialAttributeSet, false);
+ GET_UINT32_LDB(msg, "linkID", attr, linkID);
+
+ GET_STRING_LDB(msg, "attributeSyntax", attr, attr, attributeSyntax_oid, true);
+ if (!prefixmap || prefixmap->length == 0) {
+ /* set an invalid value */
+ attr->attributeSyntax_id = DRSUAPI_ATTID_INVALID;
+ } else {
+ status = dsdb_schema_pfm_attid_from_oid(prefixmap,
+ attr->attributeSyntax_oid,
+ &attr->attributeSyntax_id);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(0,("%s: '%s': unable to map attributeSyntax_ %s: %s\n",
+ __location__, attr->lDAPDisplayName, attr->attributeSyntax_oid,
+ win_errstr(status)));
+ return status;
+ }
+ }
+ GET_UINT32_LDB(msg, "oMSyntax", attr, oMSyntax);
+ GET_BLOB_LDB(msg, "oMObjectClass", attr, attr, oMObjectClass);
+
+ GET_BOOL_LDB(msg, "isSingleValued", attr, isSingleValued, true);
+ GET_UINT32_PTR_LDB(msg, "rangeLower", attr, attr, rangeLower);
+ GET_UINT32_PTR_LDB(msg, "rangeUpper", attr, attr, rangeUpper);
+ GET_BOOL_LDB(msg, "extendedCharsAllowed", attr, extendedCharsAllowed, false);
+
+ GET_UINT32_LDB(msg, "schemaFlagsEx", attr, schemaFlagsEx);
+ GET_BLOB_LDB(msg, "msDs-Schema-Extensions", attr, attr, msDs_Schema_Extensions);
+
+ GET_BOOL_LDB(msg, "showInAdvancedViewOnly", attr, showInAdvancedViewOnly, false);
+ GET_STRING_LDB(msg, "adminDisplayName", attr, attr, adminDisplayName, false);
+ GET_STRING_LDB(msg, "adminDescription", attr, attr, adminDescription, false);
+ GET_STRING_LDB(msg, "classDisplayName", attr, attr, classDisplayName, false);
+ GET_BOOL_LDB(msg, "isEphemeral", attr, isEphemeral, false);
+ GET_BOOL_LDB(msg, "isDefunct", attr, isDefunct, false);
+ GET_BOOL_LDB(msg, "systemOnly", attr, systemOnly, false);
+
+ return WERR_OK;
+}
+
+WERROR dsdb_set_attribute_from_ldb_dups(struct ldb_context *ldb,
+ struct dsdb_schema *schema,
+ struct ldb_message *msg,
+ bool checkdups)
+{
+ WERROR status;
+ struct dsdb_attribute *attr = talloc_zero(schema, struct dsdb_attribute);
+ if (!attr) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ status = dsdb_attribute_from_ldb(schema->prefixmap, msg, attr);
+ if (!W_ERROR_IS_OK(status)) {
+ return status;
+ }
+
+ attr->syntax = dsdb_syntax_for_attribute(attr);
+ if (!attr->syntax) {
+ DEBUG(0,(__location__ ": Unknown schema syntax for %s\n",
+ attr->lDAPDisplayName));
+ return WERR_DS_ATT_SCHEMA_REQ_SYNTAX;
+ }
+
+ if (dsdb_schema_setup_ldb_schema_attribute(ldb, attr) != LDB_SUCCESS) {
+ DEBUG(0,(__location__ ": Unknown schema syntax for %s - ldb_syntax: %s, ldap_oid: %s\n",
+ attr->lDAPDisplayName,
+ attr->syntax->ldb_syntax,
+ attr->syntax->ldap_oid));
+ return WERR_DS_ATT_SCHEMA_REQ_SYNTAX;
+ }
+
+ if (checkdups) {
+ const struct dsdb_attribute *a2;
+ struct dsdb_attribute **a;
+ uint32_t i;
+
+ a2 = dsdb_attribute_by_attributeID_id(schema,
+ attr->attributeID_id);
+ if (a2 == NULL) {
+ goto done;
+ }
+
+ i = schema->attributes_to_remove_size;
+ a = talloc_realloc(schema, schema->attributes_to_remove,
+ struct dsdb_attribute *, i + 1);
+ if (a == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ /* Mark the old attribute as to be removed */
+ a[i] = discard_const_p(struct dsdb_attribute, a2);
+ schema->attributes_to_remove = a;
+ schema->attributes_to_remove_size++;
+ }
+
+done:
+ DLIST_ADD(schema->attributes, attr);
+ return WERR_OK;
+}
+
+WERROR dsdb_set_attribute_from_ldb(struct ldb_context *ldb,
+ struct dsdb_schema *schema,
+ struct ldb_message *msg)
+{
+ return dsdb_set_attribute_from_ldb_dups(ldb, schema, msg, false);
+}
+
+WERROR dsdb_set_class_from_ldb_dups(struct dsdb_schema *schema,
+ struct ldb_message *msg,
+ bool checkdups)
+{
+ WERROR status;
+ struct dsdb_class *obj = talloc_zero(schema, struct dsdb_class);
+ if (!obj) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ GET_STRING_LDB(msg, "cn", obj, obj, cn, false);
+
+ /*
+ * This allows for the fact that the CN attribute is not
+ * replicated over DRS, it is only replicated under the alias
+ * 'name'.
+ */
+ if (obj->cn == NULL) {
+ GET_STRING_LDB(msg, "name", obj, obj, cn, true);
+ }
+
+ GET_STRING_LDB(msg, "lDAPDisplayName", obj, obj, lDAPDisplayName, true);
+ GET_STRING_LDB(msg, "governsID", obj, obj, governsID_oid, true);
+ if (!schema->prefixmap || schema->prefixmap->length == 0) {
+ /* set an invalid value */
+ obj->governsID_id = DRSUAPI_ATTID_INVALID;
+ } else {
+ status = dsdb_schema_pfm_attid_from_oid(schema->prefixmap,
+ obj->governsID_oid,
+ &obj->governsID_id);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(0,("%s: '%s': unable to map governsID %s: %s\n",
+ __location__, obj->lDAPDisplayName, obj->governsID_oid,
+ win_errstr(status)));
+ return status;
+ }
+ }
+ GET_GUID_LDB(msg, "schemaIDGUID", obj, schemaIDGUID);
+ GET_GUID_LDB(msg, "objectGUID", obj, objectGUID);
+
+ GET_UINT32_LDB(msg, "objectClassCategory", obj, objectClassCategory);
+ GET_STRING_LDB(msg, "rDNAttID", obj, obj, rDNAttID, false);
+ GET_STRING_LDB(msg, "defaultObjectCategory", obj, obj, defaultObjectCategory, true);
+
+ GET_STRING_LDB(msg, "subClassOf", obj, obj, subClassOf, true);
+
+ GET_STRING_LIST_LDB(msg, "systemAuxiliaryClass", obj, obj, systemAuxiliaryClass);
+ GET_STRING_LIST_LDB(msg, "auxiliaryClass", obj, obj, auxiliaryClass);
+
+ GET_STRING_LIST_LDB(msg, "systemMustContain", obj, obj, systemMustContain);
+ GET_STRING_LIST_LDB(msg, "systemMayContain", obj, obj, systemMayContain);
+ GET_STRING_LIST_LDB(msg, "mustContain", obj, obj, mustContain);
+ GET_STRING_LIST_LDB(msg, "mayContain", obj, obj, mayContain);
+
+ GET_STRING_LIST_LDB(msg, "systemPossSuperiors", obj, obj, systemPossSuperiors);
+ GET_STRING_LIST_LDB(msg, "possSuperiors", obj, obj, possSuperiors);
+
+ GET_STRING_LDB(msg, "defaultSecurityDescriptor", obj, obj, defaultSecurityDescriptor, false);
+
+ GET_UINT32_LDB(msg, "schemaFlagsEx", obj, schemaFlagsEx);
+ GET_UINT32_LDB(msg, "systemFlags", obj, systemFlags);
+ GET_BLOB_LDB(msg, "msDs-Schema-Extensions", obj, obj, msDs_Schema_Extensions);
+
+ GET_BOOL_LDB(msg, "showInAdvancedViewOnly", obj, showInAdvancedViewOnly, false);
+ GET_STRING_LDB(msg, "adminDisplayName", obj, obj, adminDisplayName, false);
+ GET_STRING_LDB(msg, "adminDescription", obj, obj, adminDescription, false);
+ GET_STRING_LDB(msg, "classDisplayName", obj, obj, classDisplayName, false);
+ GET_BOOL_LDB(msg, "defaultHidingValue", obj, defaultHidingValue, false);
+ GET_BOOL_LDB(msg, "isDefunct", obj, isDefunct, false);
+ GET_BOOL_LDB(msg, "systemOnly", obj, systemOnly, false);
+
+ if (checkdups) {
+ const struct dsdb_class *c2;
+ struct dsdb_class **c;
+ uint32_t i;
+
+ c2 = dsdb_class_by_governsID_id(schema, obj->governsID_id);
+ if (c2 == NULL) {
+ goto done;
+ }
+
+ i = schema->classes_to_remove_size;
+ c = talloc_realloc(schema, schema->classes_to_remove,
+ struct dsdb_class *, i + 1);
+ if (c == NULL) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ /* Mark the old class to be removed */
+ c[i] = discard_const_p(struct dsdb_class, c2);
+ schema->classes_to_remove = c;
+ schema->classes_to_remove_size++;
+ }
+
+done:
+ DLIST_ADD(schema->classes, obj);
+ return WERR_OK;
+}
+
+WERROR dsdb_set_class_from_ldb(struct dsdb_schema *schema,
+ struct ldb_message *msg)
+{
+ return dsdb_set_class_from_ldb_dups(schema, msg, false);
+}
+
+#define dsdb_oom(error_string, mem_ctx) *error_string = talloc_asprintf(mem_ctx, "dsdb out of memory at %s:%d\n", __FILE__, __LINE__)
+
+/*
+ Fill a DSDB schema from the ldb results provided. This is called
+ directly when a schema must be created with a pre-initialised prefixMap
+*/
+
+int dsdb_load_ldb_results_into_schema(TALLOC_CTX *mem_ctx, struct ldb_context *ldb,
+ struct dsdb_schema *schema,
+ struct ldb_result *attrs_class_res,
+ char **error_string)
+{
+ unsigned int i;
+ WERROR status;
+
+ schema->ts_last_change = 0;
+ for (i=0; i < attrs_class_res->count; i++) {
+ const char *prefixMap = NULL;
+ /*
+ * attrs_class_res also includes the schema object;
+ * we only want to process classes & attributes
+ */
+ prefixMap = ldb_msg_find_attr_as_string(
+ attrs_class_res->msgs[i],
+ "prefixMap", NULL);
+ if (prefixMap != NULL) {
+ continue;
+ }
+
+ status = dsdb_schema_set_el_from_ldb_msg(ldb, schema,
+ attrs_class_res->msgs[i]);
+ if (!W_ERROR_IS_OK(status)) {
+ *error_string = talloc_asprintf(mem_ctx,
+ "dsdb_load_ldb_results_into_schema: failed to load attribute or class definition: %s:%s",
+ ldb_dn_get_linearized(attrs_class_res->msgs[i]->dn),
+ win_errstr(status));
+ DEBUG(0,(__location__ ": %s\n", *error_string));
+ return LDB_ERR_CONSTRAINT_VIOLATION;
+ }
+ }
+
+ return LDB_SUCCESS;
+}
+
+/*
+ Create a DSDB schema from the ldb results provided. This is called
+ directly when the schema is provisioned from an on-disk LDIF file, or
+ from dsdb_schema_from_schema_dn in schema_fsmo
+*/
+
+int dsdb_schema_from_ldb_results(TALLOC_CTX *mem_ctx, struct ldb_context *ldb,
+ struct ldb_message *schema_msg,
+ struct ldb_result *attrs_class_res,
+ struct dsdb_schema **schema_out,
+ char **error_string)
+{
+ WERROR status;
+ const struct ldb_val *prefix_val;
+ const struct ldb_val *info_val;
+ struct ldb_val info_val_default;
+ struct dsdb_schema *schema;
+ void *lp_opaque = ldb_get_opaque(ldb, "loadparm");
+ int ret;
+
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ if (!tmp_ctx) {
+ dsdb_oom(error_string, mem_ctx);
+ return ldb_operr(ldb);
+ }
+
+ schema = dsdb_new_schema(tmp_ctx);
+ if (!schema) {
+ dsdb_oom(error_string, mem_ctx);
+ talloc_free(tmp_ctx);
+ return ldb_operr(ldb);
+ }
+
+ if (lp_opaque) {
+ struct loadparm_context *lp_ctx = talloc_get_type_abort(lp_opaque, struct loadparm_context);
+ schema->fsmo.update_allowed = lpcfg_parm_bool(lp_ctx, NULL,
+ "dsdb", "schema update allowed",
+ false);
+ }
+
+ prefix_val = ldb_msg_find_ldb_val(schema_msg, "prefixMap");
+ if (!prefix_val) {
+ *error_string = talloc_asprintf(mem_ctx,
+ "schema_fsmo_init: no prefixMap attribute found");
+ DEBUG(0,(__location__ ": %s\n", *error_string));
+ talloc_free(tmp_ctx);
+ return LDB_ERR_CONSTRAINT_VIOLATION;
+ }
+ info_val = ldb_msg_find_ldb_val(schema_msg, "schemaInfo");
+ if (!info_val) {
+ status = dsdb_schema_info_blob_new(mem_ctx, &info_val_default);
+ if (!W_ERROR_IS_OK(status)) {
+ *error_string = talloc_asprintf(mem_ctx,
+ "schema_fsmo_init: dsdb_schema_info_blob_new() failed - %s",
+ win_errstr(status));
+ DEBUG(0,(__location__ ": %s\n", *error_string));
+ talloc_free(tmp_ctx);
+ return ldb_operr(ldb);
+ }
+ info_val = &info_val_default;
+ }
+
+ status = dsdb_load_oid_mappings_ldb(schema, prefix_val, info_val);
+ if (!W_ERROR_IS_OK(status)) {
+ *error_string = talloc_asprintf(mem_ctx,
+ "schema_fsmo_init: failed to load oid mappings: %s",
+ win_errstr(status));
+ DEBUG(0,(__location__ ": %s\n", *error_string));
+ talloc_free(tmp_ctx);
+ return LDB_ERR_CONSTRAINT_VIOLATION;
+ }
+
+ ret = dsdb_load_ldb_results_into_schema(mem_ctx, ldb, schema, attrs_class_res, error_string);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return ret;
+ }
+
+ schema->fsmo.master_dn = ldb_msg_find_attr_as_dn(ldb, schema, schema_msg, "fSMORoleOwner");
+ if (ldb_dn_compare(samdb_ntds_settings_dn(ldb, tmp_ctx), schema->fsmo.master_dn) == 0) {
+ schema->fsmo.we_are_master = true;
+ } else {
+ schema->fsmo.we_are_master = false;
+ }
+
+ DEBUG(5, ("schema_fsmo_init: we are master[%s] updates allowed[%s]\n",
+ (schema->fsmo.we_are_master?"yes":"no"),
+ (schema->fsmo.update_allowed?"yes":"no")));
+
+ *schema_out = talloc_steal(mem_ctx, schema);
+ talloc_free(tmp_ctx);
+ return LDB_SUCCESS;
+}
diff --git a/source4/dsdb/schema/schema_prefixmap.c b/source4/dsdb/schema/schema_prefixmap.c
new file mode 100644
index 0000000..3a6a130
--- /dev/null
+++ b/source4/dsdb/schema/schema_prefixmap.c
@@ -0,0 +1,710 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ DRS::prefixMap implementation
+
+ Copyright (C) Kamen Mazdrashki <kamen.mazdrashki@postpath.com> 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 "dsdb/samdb/samdb.h"
+#include "librpc/gen_ndr/ndr_drsuapi.h"
+#include "librpc/gen_ndr/ndr_drsblobs.h"
+#include "../lib/util/asn1.h"
+#include "lib/util/smb_strtox.h"
+
+
+/**
+ * Determine range type for supplied ATTID
+ */
+enum dsdb_attid_type dsdb_pfm_get_attid_type(uint32_t attid)
+{
+ if (attid <= 0x7FFFFFFF) {
+ return DSDB_ATTID_TYPE_PFM;
+ }
+ else if (attid <= 0xBFFFFFFF) {
+ return DSDB_ATTID_TYPE_INTID;
+ }
+ else if (attid <= 0xFFFEFFFF) {
+ return DSDB_ATTID_TYPE_RESERVED;
+ }
+ else {
+ return DSDB_ATTID_TYPE_INTERNAL;
+ }
+}
+
+/**
+ * Allocates schema_prefixMap object in supplied memory context
+ */
+static struct dsdb_schema_prefixmap *_dsdb_schema_prefixmap_talloc(TALLOC_CTX *mem_ctx,
+ uint32_t length)
+{
+ struct dsdb_schema_prefixmap *pfm;
+
+ pfm = talloc_zero(mem_ctx, struct dsdb_schema_prefixmap);
+ if (!pfm) {
+ return NULL;
+ }
+
+ pfm->length = length;
+ pfm->prefixes = talloc_zero_array(pfm, struct dsdb_schema_prefixmap_oid,
+ pfm->length);
+ if (!pfm->prefixes) {
+ talloc_free(pfm);
+ return NULL;
+ }
+
+ return pfm;
+}
+
+/**
+ * Initial prefixMap creation according to:
+ * [MS-DRSR] section 5.12.2
+ */
+WERROR dsdb_schema_pfm_new(TALLOC_CTX *mem_ctx, struct dsdb_schema_prefixmap **_pfm)
+{
+ uint32_t i;
+ struct dsdb_schema_prefixmap *pfm;
+ const struct {
+ uint32_t id;
+ const char *oid_prefix;
+ } pfm_init_data[] = {
+ {.id=0x00000000, .oid_prefix="2.5.4"},
+ {.id=0x00000001, .oid_prefix="2.5.6"},
+ {.id=0x00000002, .oid_prefix="1.2.840.113556.1.2"},
+ {.id=0x00000003, .oid_prefix="1.2.840.113556.1.3"},
+ {.id=0x00000004, .oid_prefix="2.16.840.1.101.2.2.1"},
+ {.id=0x00000005, .oid_prefix="2.16.840.1.101.2.2.3"},
+ {.id=0x00000006, .oid_prefix="2.16.840.1.101.2.1.5"},
+ {.id=0x00000007, .oid_prefix="2.16.840.1.101.2.1.4"},
+ {.id=0x00000008, .oid_prefix="2.5.5"},
+ {.id=0x00000009, .oid_prefix="1.2.840.113556.1.4"},
+ {.id=0x0000000A, .oid_prefix="1.2.840.113556.1.5"},
+ {.id=0x00000013, .oid_prefix="0.9.2342.19200300.100"},
+ {.id=0x00000014, .oid_prefix="2.16.840.1.113730.3"},
+ {.id=0x00000015, .oid_prefix="0.9.2342.19200300.100.1"},
+ {.id=0x00000016, .oid_prefix="2.16.840.1.113730.3.1"},
+ {.id=0x00000017, .oid_prefix="1.2.840.113556.1.5.7000"},
+ {.id=0x00000018, .oid_prefix="2.5.21"},
+ {.id=0x00000019, .oid_prefix="2.5.18"},
+ {.id=0x0000001A, .oid_prefix="2.5.20"},
+ };
+
+ /* allocate mem for prefix map */
+ pfm = _dsdb_schema_prefixmap_talloc(mem_ctx, ARRAY_SIZE(pfm_init_data));
+ W_ERROR_HAVE_NO_MEMORY(pfm);
+
+ /* build prefixes */
+ for (i = 0; i < pfm->length; i++) {
+ if (!ber_write_partial_OID_String(pfm, &pfm->prefixes[i].bin_oid, pfm_init_data[i].oid_prefix)) {
+ talloc_free(pfm);
+ return WERR_INTERNAL_ERROR;
+ }
+ pfm->prefixes[i].id = pfm_init_data[i].id;
+ }
+
+ *_pfm = pfm;
+
+ return WERR_OK;
+}
+
+
+struct dsdb_schema_prefixmap *dsdb_schema_pfm_copy_shallow(TALLOC_CTX *mem_ctx,
+ const struct dsdb_schema_prefixmap *pfm)
+{
+ uint32_t i;
+ struct dsdb_schema_prefixmap *pfm_copy;
+
+ pfm_copy = _dsdb_schema_prefixmap_talloc(mem_ctx, pfm->length);
+ if (!pfm_copy) {
+ return NULL;
+ }
+ for (i = 0; i < pfm_copy->length; i++) {
+ pfm_copy->prefixes[i] = pfm->prefixes[i];
+ }
+
+ return pfm_copy;
+}
+
+/**
+ * Adds oid to prefix map.
+ * On success returns ID for newly added index
+ * or ID of existing entry that matches oid
+ * Reference: [MS-DRSR] section 5.12.2
+ *
+ * \param pfm prefixMap
+ * \param bin_oid OID prefix to be added to prefixMap
+ * \param pfm_id Location where to store prefixMap entry ID
+ */
+WERROR dsdb_schema_pfm_add_entry(struct dsdb_schema_prefixmap *pfm,
+ DATA_BLOB bin_oid,
+ const uint32_t *remote_id,
+ uint32_t *_idx)
+{
+ uint32_t i;
+ struct dsdb_schema_prefixmap_oid * pfm_entry;
+ struct dsdb_schema_prefixmap_oid * prefixes_new;
+
+ /* dup memory for bin-oid prefix to be added */
+ bin_oid = data_blob_dup_talloc(pfm, bin_oid);
+ W_ERROR_HAVE_NO_MEMORY(bin_oid.data);
+
+ /* make room for new entry */
+ prefixes_new = talloc_realloc(pfm, pfm->prefixes, struct dsdb_schema_prefixmap_oid, pfm->length + 1);
+ if (!prefixes_new) {
+ talloc_free(bin_oid.data);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ pfm->prefixes = prefixes_new;
+
+ /* make new unique ID in prefixMap */
+ pfm_entry = &pfm->prefixes[pfm->length];
+ pfm_entry->id = 0;
+ for (i = 0; i < pfm->length; i++) {
+ if (pfm_entry->id < pfm->prefixes[i].id) {
+ pfm_entry->id = pfm->prefixes[i].id;
+ }
+
+ if (remote_id == NULL) {
+ continue;
+ }
+
+ if (pfm->prefixes[i].id == *remote_id) {
+ /*
+ * We can't use the remote id.
+ * it's already in use.
+ */
+ remote_id = NULL;
+ }
+ }
+
+ /* add new bin-oid prefix */
+ if (remote_id != NULL) {
+ pfm_entry->id = *remote_id;
+ } else {
+ pfm_entry->id++;
+ }
+ pfm_entry->bin_oid = bin_oid;
+
+ if (_idx != NULL) {
+ *_idx = pfm->length;
+ }
+ pfm->length++;
+
+ return WERR_OK;
+}
+
+
+/**
+ * Make partial binary OID for supplied OID.
+ * Reference: [MS-DRSR] section 5.12.2
+ */
+static WERROR _dsdb_pfm_make_binary_oid(const char *full_oid, TALLOC_CTX *mem_ctx,
+ DATA_BLOB *_bin_oid, uint32_t *_last_subid)
+{
+ uint32_t last_subid;
+ const char *oid_subid;
+ int error = 0;
+
+ /* make last sub-identifier value */
+ oid_subid = strrchr(full_oid, '.');
+ if (!oid_subid) {
+ return WERR_INVALID_PARAMETER;
+ }
+ oid_subid++;
+ last_subid = smb_strtoul(oid_subid, NULL, 10, &error, SMB_STR_STANDARD);
+ if (error != 0) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ /* encode oid in BER format */
+ if (!ber_write_OID_String(mem_ctx, _bin_oid, full_oid)) {
+ DEBUG(0,("ber_write_OID_String() failed for %s\n", full_oid));
+ return WERR_INTERNAL_ERROR;
+ }
+
+ /* get the prefix of the OID */
+ if (last_subid < 128) {
+ _bin_oid->length -= 1;
+ } else {
+ _bin_oid->length -= 2;
+ }
+
+ /* return last_value if requested */
+ if (_last_subid) {
+ *_last_subid = last_subid;
+ }
+
+ return WERR_OK;
+}
+
+/**
+ * Lookup partial-binary-oid in prefixMap
+ */
+WERROR dsdb_schema_pfm_find_binary_oid(const struct dsdb_schema_prefixmap *pfm,
+ DATA_BLOB bin_oid,
+ uint32_t *_idx)
+{
+ uint32_t i;
+
+ for (i = 0; i < pfm->length; i++) {
+ if (pfm->prefixes[i].bin_oid.length != bin_oid.length) {
+ continue;
+ }
+
+ if (memcmp(pfm->prefixes[i].bin_oid.data, bin_oid.data, bin_oid.length) == 0) {
+ if (_idx) {
+ *_idx = i;
+ }
+ return WERR_OK;
+ }
+ }
+
+ return WERR_NOT_FOUND;
+}
+
+/**
+ * Lookup full-oid in prefixMap
+ * Note: this may be slow.
+ */
+WERROR dsdb_schema_pfm_find_oid(const struct dsdb_schema_prefixmap *pfm,
+ const char *full_oid,
+ uint32_t *_idx)
+{
+ WERROR werr;
+ DATA_BLOB bin_oid;
+
+ ZERO_STRUCT(bin_oid);
+
+ /* make partial-binary-oid to look for */
+ werr = _dsdb_pfm_make_binary_oid(full_oid, NULL, &bin_oid, NULL);
+ W_ERROR_NOT_OK_RETURN(werr);
+
+ /* lookup the partial-oid */
+ werr = dsdb_schema_pfm_find_binary_oid(pfm, bin_oid, _idx);
+
+ data_blob_free(&bin_oid);
+
+ return werr;
+}
+
+/**
+ * Make ATTID for given OID
+ * If OID is not in prefixMap, new prefix
+ * may be added depending on 'can_change_pfm' flag
+ * Reference: [MS-DRSR] section 5.12.2
+ */
+static WERROR dsdb_schema_pfm_make_attid_impl(struct dsdb_schema_prefixmap *pfm,
+ const char *oid,
+ bool can_change_pfm,
+ uint32_t *attid)
+{
+ WERROR werr;
+ uint32_t idx;
+ uint32_t lo_word, hi_word;
+ uint32_t last_subid;
+ DATA_BLOB bin_oid;
+
+ if (!pfm) {
+ return WERR_INVALID_PARAMETER;
+ }
+ if (!oid) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ werr = _dsdb_pfm_make_binary_oid(oid, pfm, &bin_oid, &last_subid);
+ W_ERROR_NOT_OK_RETURN(werr);
+
+ /* search the prefix in the prefix table, if none found, add
+ * one entry for new prefix.
+ */
+ werr = dsdb_schema_pfm_find_binary_oid(pfm, bin_oid, &idx);
+ if (W_ERROR_IS_OK(werr)) {
+ /* free memory allocated for bin_oid */
+ data_blob_free(&bin_oid);
+ } else {
+ /* return error in read-only mode */
+ if (!can_change_pfm) {
+ DEBUG(0, ("Unable to convert %s to an attid, and can_change_pfm=false!\n", oid));
+ return werr;
+ }
+
+ /* entry does not exists, add it */
+ werr = dsdb_schema_pfm_add_entry(pfm, bin_oid, NULL, &idx);
+ W_ERROR_NOT_OK_RETURN(werr);
+ }
+
+ /* compose the attid */
+ lo_word = last_subid % 16384; /* actually get lower 14 bits: lo_word & 0x3FFF */
+ if (last_subid >= 16384) {
+ /* mark it so that it is known to not be the whole lastValue
+ * This will raise 16-th bit*/
+ lo_word += 32768;
+ }
+ hi_word = pfm->prefixes[idx].id;
+
+ /* make ATTID:
+ * HIWORD is prefixMap id
+ * LOWORD is truncated binary-oid */
+ *attid = (hi_word * 65536) + lo_word;
+
+ return WERR_OK;
+}
+
+/**
+ * Make ATTID for given OID
+ * Reference: [MS-DRSR] section 5.12.2
+ *
+ * Note: This function may change prefixMap if prefix
+ * for supplied 'oid' doesn't exists yet.
+ * It is recommended to be used mostly when caller
+ * want to add new prefixes.
+ * Otherwise dsdb_schema_pfm_attid_from_oid() should be used.
+ */
+WERROR dsdb_schema_pfm_make_attid(struct dsdb_schema_prefixmap *pfm,
+ const char *oid,
+ uint32_t *attid)
+{
+ return dsdb_schema_pfm_make_attid_impl(pfm, oid, true, attid);
+}
+
+/**
+ * Make ATTID for given OID
+ * Reference: [MS-DRSR] section 5.12.2
+ */
+WERROR dsdb_schema_pfm_attid_from_oid(struct dsdb_schema_prefixmap *pfm,
+ const char *oid,
+ uint32_t *attid)
+{
+ return dsdb_schema_pfm_make_attid_impl(pfm, oid, false, attid);
+}
+
+/**
+ * Make OID for given ATTID.
+ * Reference: [MS-DRSR] section 5.12.2
+ */
+WERROR dsdb_schema_pfm_oid_from_attid(const struct dsdb_schema_prefixmap *pfm,
+ uint32_t attid,
+ TALLOC_CTX *mem_ctx, const char **_oid)
+{
+ uint32_t i;
+ uint32_t hi_word, lo_word;
+ DATA_BLOB bin_oid = {NULL, 0};
+ char *oid;
+ struct dsdb_schema_prefixmap_oid *pfm_entry;
+ WERROR werr = WERR_OK;
+
+ /* sanity check for attid requested */
+ if (dsdb_pfm_get_attid_type(attid) != DSDB_ATTID_TYPE_PFM) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ /* crack attid value */
+ hi_word = attid >> 16;
+ lo_word = attid & 0xFFFF;
+
+ /* locate corRespoNding prefixMap entry */
+ pfm_entry = NULL;
+ for (i = 0; i < pfm->length; i++) {
+ if (hi_word == pfm->prefixes[i].id) {
+ pfm_entry = &pfm->prefixes[i];
+ break;
+ }
+ }
+
+ if (!pfm_entry) {
+ DEBUG(1,("Failed to find prefixMap entry for ATTID = 0x%08X (%d)\n",
+ attid, attid));
+ return WERR_DS_NO_ATTRIBUTE_OR_VALUE;
+ }
+
+ /* copy oid prefix making enough room */
+ bin_oid.length = pfm_entry->bin_oid.length + 2;
+ bin_oid.data = talloc_array(mem_ctx, uint8_t, bin_oid.length);
+ W_ERROR_HAVE_NO_MEMORY(bin_oid.data);
+ memcpy(bin_oid.data, pfm_entry->bin_oid.data, pfm_entry->bin_oid.length);
+
+ if (lo_word < 128) {
+ bin_oid.length = bin_oid.length - 1;
+ bin_oid.data[bin_oid.length-1] = lo_word;
+ }
+ else {
+ if (lo_word >= 32768) {
+ lo_word -= 32768;
+ }
+ bin_oid.data[bin_oid.length-2] = (0x80 | ((lo_word>>7) & 0x7f));
+ bin_oid.data[bin_oid.length-1] = lo_word & 0x7f;
+ }
+
+ if (!ber_read_OID_String(mem_ctx, bin_oid, &oid)) {
+ DEBUG(0,("ber_read_OID_String() failed for %s\n",
+ hex_encode_talloc(bin_oid.data, bin_oid.data, bin_oid.length)));
+ werr = WERR_INTERNAL_ERROR;
+ }
+
+ /* free locally allocated memory */
+ talloc_free(bin_oid.data);
+
+ *_oid = oid;
+
+ return werr;
+}
+
+
+/**
+ * Verifies drsuapi mappings.
+ */
+static WERROR _dsdb_drsuapi_pfm_verify(const struct drsuapi_DsReplicaOIDMapping_Ctr *ctr,
+ bool have_schema_info)
+{
+ uint32_t i;
+ uint32_t num_mappings;
+ struct drsuapi_DsReplicaOIDMapping *mapping;
+
+ /* check input params */
+ if (!ctr) {
+ return WERR_INVALID_PARAMETER;
+ }
+ if (!ctr->mappings) {
+ return WERR_INVALID_PARAMETER;
+ }
+ num_mappings = ctr->num_mappings;
+
+ if (have_schema_info) {
+ DATA_BLOB blob;
+
+ if (ctr->num_mappings < 2) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ /* check last entry for being special */
+ mapping = &ctr->mappings[ctr->num_mappings - 1];
+ if (mapping->id_prefix != 0) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ /* verify schemaInfo blob is valid one */
+ blob = data_blob_const(mapping->oid.binary_oid, mapping->oid.length);
+ if (!dsdb_schema_info_blob_is_valid(&blob)) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ /* get number of read mappings in the map */
+ num_mappings--;
+ }
+
+ /* now, verify rest of entries for being at least not null */
+ for (i = 0; i < num_mappings; i++) {
+ mapping = &ctr->mappings[i];
+ if (!mapping->oid.length) {
+ return WERR_INVALID_PARAMETER;
+ }
+ if (!mapping->oid.binary_oid) {
+ return WERR_INVALID_PARAMETER;
+ }
+ /* check it is not the special entry */
+ if (*mapping->oid.binary_oid == 0xFF) {
+ return WERR_INVALID_PARAMETER;
+ }
+ }
+
+ return WERR_OK;
+}
+
+/**
+ * Convert drsuapi_ prefix map to prefixMap internal presentation.
+ *
+ * \param ctr Pointer to drsuapi_DsReplicaOIDMapping_Ctr which represents drsuapi_ prefixMap
+ * \param have_schema_info if drsuapi_prefixMap have schem_info in it or not
+ * \param mem_ctx TALLOC_CTX to make allocations in
+ * \param _pfm Out pointer to hold newly created prefixMap
+ * \param _schema_info Out param to store schema_info to. If NULL, schema_info is not decoded
+ */
+WERROR dsdb_schema_pfm_from_drsuapi_pfm(const struct drsuapi_DsReplicaOIDMapping_Ctr *ctr,
+ bool have_schema_info,
+ TALLOC_CTX *mem_ctx,
+ struct dsdb_schema_prefixmap **_pfm,
+ struct dsdb_schema_info **_schema_info)
+{
+ WERROR werr;
+ uint32_t i;
+ DATA_BLOB blob;
+ uint32_t num_mappings;
+ struct dsdb_schema_prefixmap *pfm;
+
+ if (!_pfm) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ /*
+ * error out if schema_info is requested
+ * but it is not in the drsuapi_prefixMap
+ */
+ if (_schema_info && !have_schema_info) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ /* verify drsuapi_pefixMap */
+ werr =_dsdb_drsuapi_pfm_verify(ctr, have_schema_info);
+ W_ERROR_NOT_OK_RETURN(werr);
+
+ /* allocate mem for prefix map */
+ num_mappings = ctr->num_mappings;
+ if (have_schema_info) {
+ num_mappings--;
+ }
+ pfm = _dsdb_schema_prefixmap_talloc(mem_ctx, num_mappings);
+ W_ERROR_HAVE_NO_MEMORY(pfm);
+
+ /* copy entries from drsuapi_prefixMap */
+ for (i = 0; i < pfm->length; i++) {
+ blob = data_blob_talloc(pfm,
+ ctr->mappings[i].oid.binary_oid,
+ ctr->mappings[i].oid.length);
+ if (!blob.data) {
+ talloc_free(pfm);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ pfm->prefixes[i].id = ctr->mappings[i].id_prefix;
+ pfm->prefixes[i].bin_oid = blob;
+ }
+
+ /* fetch schema_info if requested */
+ if (_schema_info) {
+ /* by this time, i should have this value,
+ * but set it here for clarity */
+ i = ctr->num_mappings - 1;
+
+ blob = data_blob_const(ctr->mappings[i].oid.binary_oid,
+ ctr->mappings[i].oid.length);
+ werr = dsdb_schema_info_from_blob(&blob, mem_ctx, _schema_info);
+ if (!W_ERROR_IS_OK(werr)) {
+ talloc_free(pfm);
+ return werr;
+ }
+ }
+
+ /* schema_prefixMap created successfully */
+ *_pfm = pfm;
+
+ return WERR_OK;
+}
+
+/**
+ * Convert drsuapi_ prefix map to prefixMap internal presentation.
+ *
+ * \param pfm Schema prefixMap to be converted
+ * \param schema_info schema_info string - if NULL, we don't need it
+ * \param mem_ctx TALLOC_CTX to make allocations in
+ * \param _ctr Out pointer to drsuapi_DsReplicaOIDMapping_Ctr prefix map structure
+ */
+WERROR dsdb_drsuapi_pfm_from_schema_pfm(const struct dsdb_schema_prefixmap *pfm,
+ const struct dsdb_schema_info *schema_info,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaOIDMapping_Ctr **_ctr)
+{
+ uint32_t i;
+ DATA_BLOB blob;
+ struct drsuapi_DsReplicaOIDMapping_Ctr *ctr;
+
+ if (!_ctr) {
+ return WERR_INVALID_PARAMETER;
+ }
+ if (!pfm) {
+ return WERR_INVALID_PARAMETER;
+ }
+ if (pfm->length == 0) {
+ return WERR_INVALID_PARAMETER;
+ }
+
+ /* allocate memory for the structure */
+ ctr = talloc_zero(mem_ctx, struct drsuapi_DsReplicaOIDMapping_Ctr);
+ W_ERROR_HAVE_NO_MEMORY(ctr);
+
+ ctr->num_mappings = (schema_info ? pfm->length + 1 : pfm->length);
+ ctr->mappings = talloc_array(ctr, struct drsuapi_DsReplicaOIDMapping, ctr->num_mappings);
+ if (!ctr->mappings) {
+ talloc_free(ctr);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ /* copy entries from schema_prefixMap */
+ for (i = 0; i < pfm->length; i++) {
+ blob = data_blob_dup_talloc(ctr, pfm->prefixes[i].bin_oid);
+ if (!blob.data) {
+ talloc_free(ctr);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ ctr->mappings[i].id_prefix = pfm->prefixes[i].id;
+ ctr->mappings[i].oid.length = blob.length;
+ ctr->mappings[i].oid.binary_oid = blob.data;
+ }
+
+ /* make schema_info entry if needed */
+ if (schema_info) {
+ WERROR werr;
+
+ /* by this time, i should have this value,
+ * but set it here for clarity */
+ i = ctr->num_mappings - 1;
+
+ werr = dsdb_blob_from_schema_info(schema_info, ctr, &blob);
+ if (!W_ERROR_IS_OK(werr)) {
+ talloc_free(ctr);
+ return werr;
+ }
+
+ ctr->mappings[i].id_prefix = 0;
+ ctr->mappings[i].oid.length = blob.length;
+ ctr->mappings[i].oid.binary_oid = blob.data;
+ }
+
+ /* drsuapi_prefixMap constructed successfully */
+ *_ctr = ctr;
+
+ return WERR_OK;
+}
+
+/**
+ * Verifies schema prefixMap and drsuapi prefixMap are same.
+ * Note that we just need to verify pfm contains prefixes
+ * from ctr, not that those prefixes has same id_prefix.
+ */
+WERROR dsdb_schema_pfm_contains_drsuapi_pfm(const struct dsdb_schema_prefixmap *pfm,
+ const struct drsuapi_DsReplicaOIDMapping_Ctr *ctr)
+{
+ WERROR werr;
+ uint32_t i;
+ uint32_t idx;
+ DATA_BLOB bin_oid;
+
+ /* verify drsuapi_pefixMap */
+ werr = _dsdb_drsuapi_pfm_verify(ctr, true);
+ W_ERROR_NOT_OK_RETURN(werr);
+
+ /* check pfm contains every entry from ctr, except the last one */
+ for (i = 0; i < ctr->num_mappings - 1; i++) {
+ bin_oid.length = ctr->mappings[i].oid.length;
+ bin_oid.data = ctr->mappings[i].oid.binary_oid;
+
+ werr = dsdb_schema_pfm_find_binary_oid(pfm, bin_oid, &idx);
+ if (!W_ERROR_IS_OK(werr)) {
+ return WERR_DS_DRA_SCHEMA_MISMATCH;
+ }
+ }
+
+ return WERR_OK;
+}
diff --git a/source4/dsdb/schema/schema_query.c b/source4/dsdb/schema/schema_query.c
new file mode 100644
index 0000000..0721e99
--- /dev/null
+++ b/source4/dsdb/schema/schema_query.c
@@ -0,0 +1,620 @@
+/*
+ Unix SMB/CIFS Implementation.
+ DSDB schema header
+
+ Copyright (C) Stefan Metzmacher <metze@samba.org> 2006-2007
+ Copyright (C) Andrew Bartlett <abartlet@samba.org> 2006-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 "dsdb/samdb/samdb.h"
+#include <ldb_module.h>
+#include "lib/util/binsearch.h"
+#include "lib/util/tsort.h"
+#include "util/dlinklist.h"
+
+#undef strcasecmp
+#undef strncasecmp
+
+static const char **dsdb_full_attribute_list_internal(TALLOC_CTX *mem_ctx,
+ const struct dsdb_schema *schema,
+ const char **class_list,
+ enum dsdb_attr_list_query query);
+
+static int uint32_cmp(uint32_t c1, uint32_t c2)
+{
+ if (c1 == c2) return 0;
+ return c1 > c2 ? 1 : -1;
+}
+
+static int strcasecmp_with_ldb_val(const struct ldb_val *target, const char *str)
+{
+ int ret = strncasecmp((const char *)target->data, str, target->length);
+ if (ret == 0) {
+ size_t len = strlen(str);
+ if (target->length > len) {
+ if (target->data[len] == 0) {
+ return 0;
+ }
+ return 1;
+ }
+ return (target->length - len);
+ }
+ return ret;
+}
+
+const struct dsdb_attribute *dsdb_attribute_by_attributeID_id(const struct dsdb_schema *schema,
+ uint32_t id)
+{
+ struct dsdb_attribute *c;
+
+ /*
+ * 0xFFFFFFFF is used as value when no mapping table is available,
+ * so don't try to match with it
+ */
+ if (id == 0xFFFFFFFF) return NULL;
+
+ /* check for msDS-IntId type attribute */
+ if (dsdb_pfm_get_attid_type(id) == DSDB_ATTID_TYPE_INTID) {
+ BINARY_ARRAY_SEARCH_P(schema->attributes_by_msDS_IntId,
+ schema->num_int_id_attr, msDS_IntId, id, uint32_cmp, c);
+ return c;
+ }
+
+ BINARY_ARRAY_SEARCH_P(schema->attributes_by_attributeID_id,
+ schema->num_attributes, attributeID_id, id, uint32_cmp, c);
+ return c;
+}
+
+const struct dsdb_attribute *dsdb_attribute_by_attributeID_oid(const struct dsdb_schema *schema,
+ const char *oid)
+{
+ struct dsdb_attribute *c;
+
+ if (!oid) return NULL;
+
+ BINARY_ARRAY_SEARCH_P(schema->attributes_by_attributeID_oid,
+ schema->num_attributes, attributeID_oid, oid, strcasecmp, c);
+ return c;
+}
+
+const struct dsdb_attribute *dsdb_attribute_by_lDAPDisplayName(const struct dsdb_schema *schema,
+ const char *name)
+{
+ struct dsdb_attribute *c;
+
+ if (!name) return NULL;
+
+ BINARY_ARRAY_SEARCH_P(schema->attributes_by_lDAPDisplayName,
+ schema->num_attributes, lDAPDisplayName, name, strcasecmp, c);
+ return c;
+}
+
+const struct dsdb_attribute *dsdb_attribute_by_lDAPDisplayName_ldb_val(const struct dsdb_schema *schema,
+ const struct ldb_val *name)
+{
+ struct dsdb_attribute *a;
+
+ if (!name) return NULL;
+
+ BINARY_ARRAY_SEARCH_P(schema->attributes_by_lDAPDisplayName,
+ schema->num_attributes, lDAPDisplayName, name, strcasecmp_with_ldb_val, a);
+ return a;
+}
+
+const struct dsdb_attribute *dsdb_attribute_by_linkID(const struct dsdb_schema *schema,
+ int linkID)
+{
+ struct dsdb_attribute *c;
+
+ BINARY_ARRAY_SEARCH_P(schema->attributes_by_linkID,
+ schema->num_attributes, linkID, linkID, uint32_cmp, c);
+ return c;
+}
+
+const struct dsdb_attribute *dsdb_attribute_by_cn_ldb_val(const struct dsdb_schema *schema,
+ const struct ldb_val *cn)
+{
+ struct dsdb_attribute *c;
+
+ BINARY_ARRAY_SEARCH_P(schema->attributes_by_cn,
+ schema->num_attributes, cn, cn, strcasecmp_with_ldb_val, c);
+ return c;
+}
+
+const struct dsdb_class *dsdb_class_by_governsID_id(const struct dsdb_schema *schema,
+ uint32_t id)
+{
+ struct dsdb_class *c;
+
+ /*
+ * 0xFFFFFFFF is used as value when no mapping table is available,
+ * so don't try to match with it
+ */
+ if (id == 0xFFFFFFFF) return NULL;
+
+ BINARY_ARRAY_SEARCH_P(schema->classes_by_governsID_id,
+ schema->num_classes, governsID_id, id, uint32_cmp, c);
+ return c;
+}
+
+const struct dsdb_class *dsdb_class_by_governsID_oid(const struct dsdb_schema *schema,
+ const char *oid)
+{
+ struct dsdb_class *c;
+ if (!oid) return NULL;
+ BINARY_ARRAY_SEARCH_P(schema->classes_by_governsID_oid,
+ schema->num_classes, governsID_oid, oid, strcasecmp, c);
+ return c;
+}
+
+const struct dsdb_class *dsdb_class_by_lDAPDisplayName(const struct dsdb_schema *schema,
+ const char *name)
+{
+ struct dsdb_class *c;
+ if (!name) return NULL;
+ BINARY_ARRAY_SEARCH_P(schema->classes_by_lDAPDisplayName,
+ schema->num_classes, lDAPDisplayName, name, strcasecmp, c);
+ return c;
+}
+
+const struct dsdb_class *dsdb_class_by_lDAPDisplayName_ldb_val(const struct dsdb_schema *schema,
+ const struct ldb_val *name)
+{
+ struct dsdb_class *c;
+ if (!name) return NULL;
+ BINARY_ARRAY_SEARCH_P(schema->classes_by_lDAPDisplayName,
+ schema->num_classes, lDAPDisplayName, name, strcasecmp_with_ldb_val, c);
+ return c;
+}
+
+const struct dsdb_class *dsdb_class_by_cn_ldb_val(const struct dsdb_schema *schema,
+ const struct ldb_val *cn)
+{
+ struct dsdb_class *c;
+ if (!cn) return NULL;
+ BINARY_ARRAY_SEARCH_P(schema->classes_by_cn,
+ schema->num_classes, cn, cn, strcasecmp_with_ldb_val, c);
+ return c;
+}
+
+const char *dsdb_lDAPDisplayName_by_id(const struct dsdb_schema *schema,
+ uint32_t id)
+{
+ const struct dsdb_attribute *a;
+ const struct dsdb_class *c;
+
+ a = dsdb_attribute_by_attributeID_id(schema, id);
+ if (a) {
+ return a->lDAPDisplayName;
+ }
+
+ c = dsdb_class_by_governsID_id(schema, id);
+ if (c) {
+ return c->lDAPDisplayName;
+ }
+
+ return NULL;
+}
+
+/**
+ Return a list of linked attributes, in lDAPDisplayName format.
+
+ This may be used to determine if a modification would require
+ backlinks to be updated, for example
+*/
+
+WERROR dsdb_linked_attribute_lDAPDisplayName_list(const struct dsdb_schema *schema, TALLOC_CTX *mem_ctx, const char ***attr_list_ret)
+{
+ const char **attr_list = NULL;
+ struct dsdb_attribute *cur;
+ unsigned int i = 0;
+ for (cur = schema->attributes; cur; cur = cur->next) {
+ if (cur->linkID == 0) continue;
+
+ attr_list = talloc_realloc(mem_ctx, attr_list, const char *, i+2);
+ if (!attr_list) {
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+ attr_list[i] = cur->lDAPDisplayName;
+ i++;
+ }
+ if (attr_list != NULL && attr_list[i] != NULL) {
+ attr_list[i] = NULL;
+ }
+ *attr_list_ret = attr_list;
+ return WERR_OK;
+}
+
+const char **merge_attr_list(TALLOC_CTX *mem_ctx,
+ const char **attrs, const char * const*new_attrs)
+{
+ const char **ret_attrs;
+ unsigned int i;
+ size_t new_len, new_attr_len, orig_len = str_list_length(attrs);
+ if (new_attrs == NULL || new_attrs[0] == NULL) {
+ return attrs;
+ }
+ new_attr_len = str_list_length(new_attrs);
+
+ ret_attrs = talloc_realloc(mem_ctx,
+ attrs, const char *, orig_len + new_attr_len + 1);
+ if (ret_attrs) {
+ for (i = 0; i < new_attr_len; i++) {
+ ret_attrs[orig_len + i] = new_attrs[i];
+ }
+ new_len = orig_len + new_attr_len;
+
+ ret_attrs[new_len] = NULL;
+ }
+
+ return ret_attrs;
+}
+
+/*
+ Return a merged list of the attributes of exactly one class (not
+ considering subclasses, auxiliary classes etc)
+*/
+
+const char **dsdb_attribute_list(TALLOC_CTX *mem_ctx, const struct dsdb_class *sclass, enum dsdb_attr_list_query query)
+{
+ const char **attr_list = NULL;
+ switch (query) {
+ case DSDB_SCHEMA_ALL_MAY:
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass->mayContain);
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass->systemMayContain);
+ break;
+
+ case DSDB_SCHEMA_ALL_MUST:
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass->mustContain);
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass->systemMustContain);
+ break;
+
+ case DSDB_SCHEMA_SYS_MAY:
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass->systemMayContain);
+ break;
+
+ case DSDB_SCHEMA_SYS_MUST:
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass->systemMustContain);
+ break;
+
+ case DSDB_SCHEMA_MAY:
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass->mayContain);
+ break;
+
+ case DSDB_SCHEMA_MUST:
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass->mustContain);
+ break;
+
+ case DSDB_SCHEMA_ALL:
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass->mayContain);
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass->systemMayContain);
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass->mustContain);
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass->systemMustContain);
+ break;
+ }
+ return attr_list;
+}
+
+static const char **attribute_list_from_class(TALLOC_CTX *mem_ctx,
+ const struct dsdb_schema *schema,
+ const struct dsdb_class *sclass,
+ enum dsdb_attr_list_query query)
+{
+ const char **this_class_list;
+ const char **system_recursive_list;
+ const char **recursive_list;
+ const char **attr_list;
+
+ this_class_list = dsdb_attribute_list(mem_ctx, sclass, query);
+
+ recursive_list = dsdb_full_attribute_list_internal(mem_ctx, schema,
+ sclass->systemAuxiliaryClass,
+ query);
+
+ system_recursive_list = dsdb_full_attribute_list_internal(mem_ctx, schema,
+ sclass->auxiliaryClass,
+ query);
+
+ attr_list = this_class_list;
+ attr_list = merge_attr_list(mem_ctx, attr_list, recursive_list);
+ attr_list = merge_attr_list(mem_ctx, attr_list, system_recursive_list);
+ return attr_list;
+}
+
+/* Return a full attribute list for a given class list
+
+ Via attribute_list_from_class() this calls itself when recursing on auxiliary classes
+ */
+static const char **dsdb_full_attribute_list_internal(TALLOC_CTX *mem_ctx,
+ const struct dsdb_schema *schema,
+ const char **class_list,
+ enum dsdb_attr_list_query query)
+{
+ unsigned int i;
+ const char **attr_list = NULL;
+
+ for (i=0; class_list && class_list[i]; i++) {
+ const char **sclass_list
+ = attribute_list_from_class(mem_ctx, schema,
+ dsdb_class_by_lDAPDisplayName(schema, class_list[i]),
+ query);
+
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass_list);
+ }
+ return attr_list;
+}
+
+/* Return a full attribute list for a given class list (as a ldb_message_element)
+
+ Using the ldb_message_element ensures we do length-limited
+ comparisons, rather than casting the possibly-unterminated string
+
+ Via attribute_list_from_class() this calls
+ dsdb_full_attribute_list_internal() when recursing on auxiliary classes
+ */
+static const char **dsdb_full_attribute_list_internal_el(TALLOC_CTX *mem_ctx,
+ const struct dsdb_schema *schema,
+ const struct ldb_message_element *el,
+ enum dsdb_attr_list_query query)
+{
+ unsigned int i;
+ const char **attr_list = NULL;
+
+ for (i=0; i < el->num_values; i++) {
+ const char **sclass_list
+ = attribute_list_from_class(mem_ctx, schema,
+ dsdb_class_by_lDAPDisplayName_ldb_val(schema, &el->values[i]),
+ query);
+
+ attr_list = merge_attr_list(mem_ctx, attr_list, sclass_list);
+ }
+ return attr_list;
+}
+
+static int qsort_string(const char **s1, const char **s2)
+{
+ return strcasecmp(*s1, *s2);
+}
+
+/* Helper function to remove duplicates from the attribute list to be returned */
+static const char **dedup_attr_list(const char **attr_list)
+{
+ size_t new_len = str_list_length(attr_list);
+ /* Remove duplicates */
+ if (new_len > 1) {
+ size_t i;
+ TYPESAFE_QSORT(attr_list, new_len, qsort_string);
+
+ for (i=1; i < new_len; i++) {
+ const char **val1 = &attr_list[i-1];
+ const char **val2 = &attr_list[i];
+ if (ldb_attr_cmp(*val1, *val2) == 0) {
+ memmove(val1, val2, (new_len - i) * sizeof( *attr_list));
+ attr_list[new_len-1] = NULL;
+ new_len--;
+ i--;
+ }
+ }
+ }
+ return attr_list;
+}
+
+/* Return a full attribute list for a given class list (as a ldb_message_element)
+
+ Using the ldb_message_element ensures we do length-limited
+ comparisons, rather than casting the possibly-unterminated string
+
+ The result contains only unique values
+ */
+const char **dsdb_full_attribute_list(TALLOC_CTX *mem_ctx,
+ const struct dsdb_schema *schema,
+ const struct ldb_message_element *class_list,
+ enum dsdb_attr_list_query query)
+{
+ const char **attr_list = dsdb_full_attribute_list_internal_el(mem_ctx, schema, class_list, query);
+ return dedup_attr_list(attr_list);
+}
+
+/* Return the schemaIDGUID of a class */
+
+const struct GUID *class_schemaid_guid_by_lDAPDisplayName(const struct dsdb_schema *schema,
+ const char *name)
+{
+ const struct dsdb_class *object_class = dsdb_class_by_lDAPDisplayName(schema, name);
+ if (!object_class)
+ return NULL;
+
+ return &object_class->schemaIDGUID;
+}
+
+const struct GUID *attribute_schemaid_guid_by_lDAPDisplayName(const struct dsdb_schema *schema,
+ const char *name)
+{
+ const struct dsdb_attribute *attr = dsdb_attribute_by_lDAPDisplayName(schema, name);
+ if (!attr)
+ return NULL;
+
+ return &attr->schemaIDGUID;
+}
+
+/*
+ * Sort a "objectClass" attribute (LDB message element "objectclass_element")
+ * into correct order and validate that all object classes specified actually
+ * exist in the schema.
+ * The output is written in an existing LDB message element
+ * "out_objectclass_element" where the values will be allocated on "mem_ctx".
+ */
+int dsdb_sort_objectClass_attr(struct ldb_context *ldb,
+ const struct dsdb_schema *schema,
+ const struct ldb_message_element *objectclass_element,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out_objectclass_element)
+{
+ unsigned int i, lowest;
+ struct class_list {
+ struct class_list *prev, *next;
+ const struct dsdb_class *objectclass;
+ } *unsorted = NULL, *sorted = NULL, *current = NULL,
+ *poss_parent = NULL, *new_parent = NULL,
+ *current_lowest = NULL, *current_lowest_struct = NULL;
+ struct ldb_message_element *el;
+ TALLOC_CTX *tmp_mem_ctx;
+
+ tmp_mem_ctx = talloc_new(mem_ctx);
+ if (tmp_mem_ctx == NULL) {
+ return ldb_oom(ldb);
+ }
+
+ /*
+ * DESIGN:
+ *
+ * We work on 4 different 'bins' (implemented here as linked lists):
+ *
+ * * sorted: the eventual list, in the order we wish to push
+ * into the database. This is the only ordered list.
+ *
+ * * parent_class: The current parent class 'bin' we are
+ * trying to find subclasses for
+ *
+ * * subclass: The subclasses we have found so far
+ *
+ * * unsorted: The remaining objectClasses
+ *
+ * The process is a matter of filtering objectClasses up from
+ * unsorted into sorted. Order is irrelevant in the later 3 'bins'.
+ *
+ * We start with 'top' (found and promoted to parent_class
+ * initially). Then we find (in unsorted) all the direct
+ * subclasses of 'top'. parent_classes is concatenated onto
+ * the end of 'sorted', and subclass becomes the list in
+ * parent_class.
+ *
+ * We then repeat, until we find no more subclasses. Any left
+ * over classes are added to the end.
+ *
+ */
+
+ /*
+ * Firstly, dump all the "objectClass" values into the unsorted bin,
+ * except for 'top', which is special
+ */
+ for (i=0; i < objectclass_element->num_values; i++) {
+ current = talloc(tmp_mem_ctx, struct class_list);
+ if (!current) {
+ talloc_free(tmp_mem_ctx);
+ return ldb_oom(ldb);
+ }
+ current->objectclass = dsdb_class_by_lDAPDisplayName_ldb_val(schema, &objectclass_element->values[i]);
+ if (!current->objectclass) {
+ ldb_asprintf_errstring(ldb, "objectclass %.*s is not a valid objectClass in schema",
+ (int)objectclass_element->values[i].length, (const char *)objectclass_element->values[i].data);
+ /* This looks weird, but windows apparently returns this for invalid objectClass values */
+ talloc_free(tmp_mem_ctx);
+ return LDB_ERR_NO_SUCH_ATTRIBUTE;
+ } else if (current->objectclass->isDefunct) {
+ ldb_asprintf_errstring(ldb, "objectclass %.*s marked as isDefunct objectClass in schema - not valid for new objects",
+ (int)objectclass_element->values[i].length, (const char *)objectclass_element->values[i].data);
+ /* This looks weird, but windows apparently returns this for invalid objectClass values */
+ talloc_free(tmp_mem_ctx);
+ return LDB_ERR_NO_SUCH_ATTRIBUTE;
+ }
+
+ /* Don't add top to list, we will do that later */
+ if (ldb_attr_cmp("top", current->objectclass->lDAPDisplayName) != 0) {
+ DLIST_ADD_END(unsorted, current);
+ }
+ }
+
+
+ /* Add top here, to prevent duplicates */
+ current = talloc(tmp_mem_ctx, struct class_list);
+ current->objectclass = dsdb_class_by_lDAPDisplayName(schema, "top");
+ DLIST_ADD_END(sorted, current);
+
+ /* For each object: find parent chain */
+ for (current = unsorted; current != NULL; current = current->next) {
+ for (poss_parent = unsorted; poss_parent; poss_parent = poss_parent->next) {
+ if (ldb_attr_cmp(poss_parent->objectclass->lDAPDisplayName, current->objectclass->subClassOf) == 0) {
+ break;
+ }
+ }
+ /* If we didn't get to the end of the list, we need to add this parent */
+ if (poss_parent || (ldb_attr_cmp("top", current->objectclass->subClassOf) == 0)) {
+ continue;
+ }
+
+ new_parent = talloc(tmp_mem_ctx, struct class_list);
+ new_parent->objectclass = dsdb_class_by_lDAPDisplayName(schema, current->objectclass->subClassOf);
+ DLIST_ADD_END(unsorted, new_parent);
+ }
+
+ /* For each object: order by hierarchy */
+ while (unsorted != NULL) {
+ lowest = UINT_MAX;
+ current_lowest = current_lowest_struct = NULL;
+ for (current = unsorted; current != NULL; current = current->next) {
+ if (current->objectclass->subClass_order <= lowest) {
+ /*
+ * According to MS-ADTS 3.1.1.1.4 structural
+ * and 88 object classes are always listed after
+ * the other class types in a subclass hierarchy
+ */
+ if (current->objectclass->objectClassCategory > 1) {
+ current_lowest = current;
+ } else {
+ current_lowest_struct = current;
+ }
+ lowest = current->objectclass->subClass_order;
+ }
+ }
+ if (current_lowest == NULL) {
+ current_lowest = current_lowest_struct;
+ }
+
+ if (current_lowest != NULL) {
+ DLIST_REMOVE(unsorted,current_lowest);
+ DLIST_ADD_END(sorted,current_lowest);
+ }
+ }
+
+ /* Now rebuild the sorted "objectClass" message element */
+ el = out_objectclass_element;
+
+ el->flags = objectclass_element->flags;
+ el->name = talloc_strdup(mem_ctx, objectclass_element->name);
+ if (el->name == NULL) {
+ talloc_free(tmp_mem_ctx);
+ return ldb_oom(ldb);
+ }
+ el->num_values = 0;
+ el->values = NULL;
+ for (current = sorted; current != NULL; current = current->next) {
+ el->values = talloc_realloc(mem_ctx, el->values,
+ struct ldb_val, el->num_values + 1);
+ if (el->values == NULL) {
+ talloc_free(tmp_mem_ctx);
+ return ldb_oom(ldb);
+ }
+ el->values[el->num_values] = data_blob_string_const(current->objectclass->lDAPDisplayName);
+
+ ++(el->num_values);
+ }
+
+ talloc_free(tmp_mem_ctx);
+ return LDB_SUCCESS;
+}
diff --git a/source4/dsdb/schema/schema_set.c b/source4/dsdb/schema/schema_set.c
new file mode 100644
index 0000000..398091c
--- /dev/null
+++ b/source4/dsdb/schema/schema_set.c
@@ -0,0 +1,1191 @@
+/*
+ Unix SMB/CIFS implementation.
+ DSDB schema header
+
+ Copyright (C) Stefan Metzmacher <metze@samba.org> 2006-2007
+ Copyright (C) Andrew Bartlett <abartlet@samba.org> 2006-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 "lib/util/dlinklist.h"
+#include "dsdb/samdb/samdb.h"
+#include <ldb_module.h>
+#include "param/param.h"
+#include "librpc/ndr/libndr.h"
+#include "librpc/gen_ndr/ndr_misc.h"
+#include "lib/util/tsort.h"
+
+#undef strcasecmp
+
+/* change this when we change something in our schema code that
+ * requires a re-index of the database
+ */
+#define SAMDB_INDEXING_VERSION "3"
+
+/*
+ override the name to attribute handler function
+ */
+const struct ldb_schema_attribute *dsdb_attribute_handler_override(struct ldb_context *ldb,
+ void *private_data,
+ const char *name)
+{
+ struct dsdb_schema *schema = talloc_get_type_abort(private_data, struct dsdb_schema);
+ const struct dsdb_attribute *a = dsdb_attribute_by_lDAPDisplayName(schema, name);
+ if (a == NULL) {
+ /* this will fall back to ldb internal handling */
+ return NULL;
+ }
+ return a->ldb_schema_attribute;
+}
+
+/*
+ * Set the attribute handlers onto the LDB, and potentially write the
+ * @INDEXLIST, @IDXONE and @ATTRIBUTES records. The @ATTRIBUTES records
+ * are required so we can operate on a schema-less database (say the
+ * backend during emergency fixes) and during the schema load.
+ */
+int dsdb_schema_set_indices_and_attributes(struct ldb_context *ldb,
+ struct dsdb_schema *schema,
+ enum schema_set_enum mode)
+{
+ int ret = LDB_SUCCESS;
+ struct ldb_result *res;
+ struct ldb_result *res_idx;
+ struct dsdb_attribute *attr;
+ struct ldb_message *mod_msg;
+ TALLOC_CTX *mem_ctx;
+ struct ldb_message *msg;
+ struct ldb_message *msg_idx;
+
+ struct loadparm_context *lp_ctx =
+ talloc_get_type(ldb_get_opaque(ldb, "loadparm"),
+ struct loadparm_context);
+ bool guid_indexing = true;
+ bool declare_ordered_integer_in_attributes = true;
+ uint32_t pack_format_override;
+ if (lp_ctx != NULL) {
+ /*
+ * GUID indexing is wanted by Samba by default. This allows
+ * an override in a specific case for downgrades.
+ */
+ guid_indexing = lpcfg_parm_bool(lp_ctx,
+ NULL,
+ "dsdb",
+ "guid index",
+ true);
+ /*
+ * If the pack format has been overridden to a previous
+ * version, then act like ORDERED_INTEGER doesn't exist,
+ * because it's a new type and we don't want to deal with
+ * possible issues with databases containing version 1 pack
+ * format and ordered types.
+ *
+ * This approach means that the @ATTRIBUTES will be
+ * incorrect for integers. Many other @ATTRIBUTES
+ * values are gross simplifications, but the presence
+ * of the ORDERED_INTEGER keyword prevents the old
+ * Samba from starting and then forcing a reindex.
+ *
+ * It is too difficult to override the actual index
+ * formatter, but this doesn't matter in practice.
+ */
+ pack_format_override =
+ (intptr_t)ldb_get_opaque(ldb, "pack_format_override");
+ if (pack_format_override == LDB_PACKING_FORMAT ||
+ pack_format_override == LDB_PACKING_FORMAT_NODN) {
+ declare_ordered_integer_in_attributes = false;
+ }
+ }
+ /* setup our own attribute name to schema handler */
+ ldb_schema_attribute_set_override_handler(ldb, dsdb_attribute_handler_override, schema);
+ ldb_schema_set_override_indexlist(ldb, true);
+ if (guid_indexing) {
+ ldb_schema_set_override_GUID_index(ldb, "objectGUID", "GUID");
+ }
+
+ if (mode == SCHEMA_MEMORY_ONLY) {
+ return ret;
+ }
+
+ mem_ctx = talloc_new(ldb);
+ if (!mem_ctx) {
+ return ldb_oom(ldb);
+ }
+
+ msg = ldb_msg_new(mem_ctx);
+ if (!msg) {
+ ldb_oom(ldb);
+ goto op_error;
+ }
+ msg_idx = ldb_msg_new(mem_ctx);
+ if (!msg_idx) {
+ ldb_oom(ldb);
+ goto op_error;
+ }
+ msg->dn = ldb_dn_new(msg, ldb, "@ATTRIBUTES");
+ if (!msg->dn) {
+ ldb_oom(ldb);
+ goto op_error;
+ }
+ msg_idx->dn = ldb_dn_new(msg_idx, ldb, "@INDEXLIST");
+ if (!msg_idx->dn) {
+ ldb_oom(ldb);
+ goto op_error;
+ }
+
+ ret = ldb_msg_add_string(msg_idx, "@IDXONE", "1");
+ if (ret != LDB_SUCCESS) {
+ goto op_error;
+ }
+
+ if (guid_indexing) {
+ ret = ldb_msg_add_string(msg_idx, "@IDXGUID", "objectGUID");
+ if (ret != LDB_SUCCESS) {
+ goto op_error;
+ }
+
+ ret = ldb_msg_add_string(msg_idx, "@IDX_DN_GUID", "GUID");
+ if (ret != LDB_SUCCESS) {
+ goto op_error;
+ }
+ }
+
+ ret = ldb_msg_add_string(msg_idx, "@SAMDB_INDEXING_VERSION", SAMDB_INDEXING_VERSION);
+ if (ret != LDB_SUCCESS) {
+ goto op_error;
+ }
+
+ ret = ldb_msg_add_string(msg_idx, SAMBA_FEATURES_SUPPORTED_FLAG, "1");
+ if (ret != LDB_SUCCESS) {
+ goto op_error;
+ }
+
+ for (attr = schema->attributes; attr; attr = attr->next) {
+ const char *syntax = attr->syntax->ldb_syntax;
+
+ if (!syntax) {
+ syntax = attr->syntax->ldap_oid;
+ }
+
+ /*
+ * Write out a rough approximation of the schema
+ * as an @ATTRIBUTES value, for bootstrapping.
+ * Only write ORDERED_INTEGER if we're using GUID indexes,
+ */
+ if (strcmp(syntax, LDB_SYNTAX_INTEGER) == 0) {
+ ret = ldb_msg_add_string(msg, attr->lDAPDisplayName, "INTEGER");
+ } else if (strcmp(syntax, LDB_SYNTAX_ORDERED_INTEGER) == 0) {
+ if (declare_ordered_integer_in_attributes &&
+ guid_indexing) {
+ /*
+ * The normal production case
+ */
+ ret = ldb_msg_add_string(msg,
+ attr->lDAPDisplayName,
+ "ORDERED_INTEGER");
+ } else {
+ /*
+ * For this mode, we are going back to
+ * before GUID indexing so we write it out
+ * as INTEGER
+ *
+ * Down in LDB, the special handler
+ * (index_format_fn) that made
+ * ORDERED_INTEGER and INTEGER
+ * different has been disabled.
+ */
+ ret = ldb_msg_add_string(msg,
+ attr->lDAPDisplayName,
+ "INTEGER");
+ }
+ } else if (strcmp(syntax, LDB_SYNTAX_DIRECTORY_STRING) == 0) {
+ ret = ldb_msg_add_string(msg, attr->lDAPDisplayName,
+ "CASE_INSENSITIVE");
+ }
+ if (ret != LDB_SUCCESS) {
+ break;
+ }
+
+ /*
+ * 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 (attr->searchFlags & SEARCH_FLAG_ATTINDEX &&
+ !(attr->searchFlags & SEARCH_FLAG_CONFIDENTIAL))
+ {
+ /*
+ * When preparing to downgrade Samba, we need to write
+ * out an LDB without the new key word ORDERED_INTEGER.
+ */
+ if (strcmp(syntax, LDB_SYNTAX_ORDERED_INTEGER) == 0
+ && !declare_ordered_integer_in_attributes) {
+ /*
+ * Ugly, but do nothing, the best
+ * thing is to omit the reference
+ * entirely, the next transaction will
+ * spot this and rewrite everything.
+ *
+ * This way nothing will look at the
+ * index for this attribute until
+ * Samba starts and this is all
+ * rewritten.
+ */
+ } else {
+ ret = ldb_msg_add_string(msg_idx, "@IDXATTR", attr->lDAPDisplayName);
+ if (ret != LDB_SUCCESS) {
+ break;
+ }
+ }
+ }
+ }
+
+ if (ret != LDB_SUCCESS) {
+ talloc_free(mem_ctx);
+ return ret;
+ }
+
+ /*
+ * Try to avoid churning the attributes too much,
+ * we only want to do this if they have changed
+ */
+ ret = ldb_search(ldb, mem_ctx, &res, msg->dn, LDB_SCOPE_BASE, NULL,
+ NULL);
+ if (ret == LDB_ERR_NO_SUCH_OBJECT) {
+ if (mode == SCHEMA_COMPARE) {
+ /* We are probably not in a transaction */
+ goto wrong_mode;
+ }
+ ret = ldb_add(ldb, msg);
+ } else if (ret != LDB_SUCCESS) {
+ } else if (res->count != 1) {
+ if (mode == SCHEMA_COMPARE) {
+ /* We are probably not in a transaction */
+ goto wrong_mode;
+ }
+ ret = ldb_add(ldb, msg);
+ } else {
+ /* Annoyingly added to our search results */
+ ldb_msg_remove_attr(res->msgs[0], "distinguishedName");
+
+ ret = ldb_msg_difference(ldb, mem_ctx,
+ res->msgs[0], msg, &mod_msg);
+ if (ret != LDB_SUCCESS) {
+ goto op_error;
+ }
+ if (mod_msg->num_elements > 0) {
+ /*
+ * Do the replace with the difference, as we
+ * are under the read lock and we wish to do a
+ * delete of any removed/renamed attributes
+ */
+ if (mode == SCHEMA_COMPARE) {
+ /* We are probably not in a transaction */
+ goto wrong_mode;
+ }
+ ret = dsdb_modify(ldb, mod_msg, 0);
+ }
+ talloc_free(mod_msg);
+ }
+
+ if (ret == LDB_ERR_OPERATIONS_ERROR || ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS || ret == LDB_ERR_INVALID_DN_SYNTAX) {
+ /* We might be on a read-only DB or LDAP */
+ ret = LDB_SUCCESS;
+ }
+ if (ret != LDB_SUCCESS) {
+ DBG_ERR("Failed to set schema into @ATTRIBUTES: %s\n",
+ ldb_errstring(ldb));
+ talloc_free(mem_ctx);
+ return ret;
+ }
+
+ /* Now write out the indexes, as found in the schema (if they have changed) */
+
+ ret = ldb_search(ldb, mem_ctx, &res_idx, msg_idx->dn, LDB_SCOPE_BASE,
+ NULL, NULL);
+ if (ret == LDB_ERR_NO_SUCH_OBJECT) {
+ if (mode == SCHEMA_COMPARE) {
+ /* We are probably not in a transaction */
+ goto wrong_mode;
+ }
+ ret = ldb_add(ldb, msg_idx);
+ } else if (ret != LDB_SUCCESS) {
+ } else if (res_idx->count != 1) {
+ if (mode == SCHEMA_COMPARE) {
+ /* We are probably not in a transaction */
+ goto wrong_mode;
+ }
+ ret = ldb_add(ldb, msg_idx);
+ } else {
+ /* Annoyingly added to our search results */
+ ldb_msg_remove_attr(res_idx->msgs[0], "distinguishedName");
+
+ ret = ldb_msg_difference(ldb, mem_ctx,
+ res_idx->msgs[0], msg_idx, &mod_msg);
+ if (ret != LDB_SUCCESS) {
+ goto op_error;
+ }
+
+ /*
+ * We don't want to re-index just because we didn't
+ * see this flag
+ *
+ * DO NOT backport this logic earlier than 4.7, it
+ * isn't needed and would be dangerous before 4.6,
+ * where we add logic to samba_dsdb to manage
+ * @SAMBA_FEATURES_SUPPORTED and need to know if the
+ * DB has been re-opened by an earlier version.
+ *
+ */
+
+ if (mod_msg->num_elements == 1
+ && ldb_attr_cmp(mod_msg->elements[0].name,
+ SAMBA_FEATURES_SUPPORTED_FLAG) == 0) {
+ /*
+ * Ignore only adding
+ * @SAMBA_FEATURES_SUPPORTED
+ */
+ } else if (mod_msg->num_elements > 0) {
+
+ /*
+ * Do the replace with the difference, as we
+ * are under the read lock and we wish to do a
+ * delete of any removed/renamed attributes
+ */
+ if (mode == SCHEMA_COMPARE) {
+ /* We are probably not in a transaction */
+ goto wrong_mode;
+ }
+ ret = dsdb_modify(ldb, mod_msg, 0);
+ }
+ talloc_free(mod_msg);
+ }
+ if (ret == LDB_ERR_OPERATIONS_ERROR || ret == LDB_ERR_INSUFFICIENT_ACCESS_RIGHTS || ret == LDB_ERR_INVALID_DN_SYNTAX) {
+ /* We might be on a read-only DB */
+ ret = LDB_SUCCESS;
+ }
+
+ if (ret != LDB_SUCCESS) {
+ DBG_ERR("Failed to set schema into @INDEXLIST: %s\n",
+ ldb_errstring(ldb));
+ }
+
+ talloc_free(mem_ctx);
+ return ret;
+
+op_error:
+ talloc_free(mem_ctx);
+ return ldb_operr(ldb);
+
+wrong_mode:
+ talloc_free(mem_ctx);
+ return LDB_ERR_BUSY;
+}
+
+
+/*
+ create extra attribute shortcuts
+ */
+static void dsdb_setup_attribute_shortcuts(struct ldb_context *ldb, struct dsdb_schema *schema)
+{
+ struct dsdb_attribute *attribute;
+ const struct dsdb_class *top_class = NULL;
+ TALLOC_CTX *frame = talloc_stackframe();
+ const char **top_allowed_attrs = NULL;
+
+ top_class = dsdb_class_by_lDAPDisplayName(schema, "top");
+ if (top_class != NULL) {
+ top_allowed_attrs = dsdb_attribute_list(frame,
+ top_class,
+ DSDB_SCHEMA_ALL);
+ }
+
+ /* setup fast access to one_way_link and DN format */
+ for (attribute=schema->attributes; attribute; attribute=attribute->next) {
+ attribute->dn_format = dsdb_dn_oid_to_format(attribute->syntax->ldap_oid);
+
+ attribute->bl_maybe_invisible = false;
+
+ if (attribute->dn_format == DSDB_INVALID_DN) {
+ attribute->one_way_link = false;
+ continue;
+ }
+
+ /* these are not considered to be one way links for
+ the purpose of DN link fixups */
+ if (ldb_attr_cmp("distinguishedName", attribute->lDAPDisplayName) == 0 ||
+ ldb_attr_cmp("objectCategory", attribute->lDAPDisplayName) == 0) {
+ attribute->one_way_link = false;
+ continue;
+ }
+
+ if (attribute->linkID == 0) {
+ attribute->one_way_link = true;
+ continue;
+ }
+
+ if (attribute->linkID & 1) {
+ const struct dsdb_attribute *fw_attr = NULL;
+ bool in_top = false;
+
+ if (top_allowed_attrs != NULL) {
+ in_top = str_list_check(top_allowed_attrs,
+ attribute->lDAPDisplayName);
+ }
+
+ if (in_top) {
+ continue;
+ }
+
+ attribute->bl_maybe_invisible = true;
+
+ fw_attr = dsdb_attribute_by_linkID(schema,
+ attribute->linkID - 1);
+ if (fw_attr != NULL) {
+ struct dsdb_attribute *_fw_attr =
+ discard_const_p(struct dsdb_attribute,
+ fw_attr);
+ _fw_attr->bl_maybe_invisible = true;
+ }
+
+ continue;
+ }
+
+ /* handle attributes with a linkID but no backlink */
+ if ((attribute->linkID & 1) == 0 &&
+ dsdb_attribute_by_linkID(schema, attribute->linkID + 1) == NULL) {
+ attribute->one_way_link = true;
+ continue;
+ }
+ attribute->one_way_link = false;
+ }
+
+ TALLOC_FREE(frame);
+}
+
+static int uint32_cmp(uint32_t c1, uint32_t c2)
+{
+ if (c1 == c2) return 0;
+ return c1 > c2 ? 1 : -1;
+}
+
+static int dsdb_compare_class_by_lDAPDisplayName(struct dsdb_class **c1, struct dsdb_class **c2)
+{
+ return strcasecmp((*c1)->lDAPDisplayName, (*c2)->lDAPDisplayName);
+}
+static int dsdb_compare_class_by_governsID_id(struct dsdb_class **c1, struct dsdb_class **c2)
+{
+ return uint32_cmp((*c1)->governsID_id, (*c2)->governsID_id);
+}
+static int dsdb_compare_class_by_governsID_oid(struct dsdb_class **c1, struct dsdb_class **c2)
+{
+ return strcasecmp((*c1)->governsID_oid, (*c2)->governsID_oid);
+}
+static int dsdb_compare_class_by_cn(struct dsdb_class **c1, struct dsdb_class **c2)
+{
+ return strcasecmp((*c1)->cn, (*c2)->cn);
+}
+
+static int dsdb_compare_attribute_by_lDAPDisplayName(struct dsdb_attribute **a1, struct dsdb_attribute **a2)
+{
+ return strcasecmp((*a1)->lDAPDisplayName, (*a2)->lDAPDisplayName);
+}
+static int dsdb_compare_attribute_by_attributeID_id(struct dsdb_attribute **a1, struct dsdb_attribute **a2)
+{
+ return uint32_cmp((*a1)->attributeID_id, (*a2)->attributeID_id);
+}
+static int dsdb_compare_attribute_by_msDS_IntId(struct dsdb_attribute **a1, struct dsdb_attribute **a2)
+{
+ return uint32_cmp((*a1)->msDS_IntId, (*a2)->msDS_IntId);
+}
+static int dsdb_compare_attribute_by_attributeID_oid(struct dsdb_attribute **a1, struct dsdb_attribute **a2)
+{
+ return strcasecmp((*a1)->attributeID_oid, (*a2)->attributeID_oid);
+}
+static int dsdb_compare_attribute_by_linkID(struct dsdb_attribute **a1, struct dsdb_attribute **a2)
+{
+ return uint32_cmp((*a1)->linkID, (*a2)->linkID);
+}
+static int dsdb_compare_attribute_by_cn(struct dsdb_attribute **a1, struct dsdb_attribute **a2)
+{
+ return strcasecmp((*a1)->cn, (*a2)->cn);
+}
+
+/**
+ * Clean up Classes and Attributes accessor arrays
+ */
+static void dsdb_sorted_accessors_free(struct dsdb_schema *schema)
+{
+ /* free classes accessors */
+ TALLOC_FREE(schema->classes_by_lDAPDisplayName);
+ TALLOC_FREE(schema->classes_by_governsID_id);
+ TALLOC_FREE(schema->classes_by_governsID_oid);
+ TALLOC_FREE(schema->classes_by_cn);
+ /* free attribute accessors */
+ TALLOC_FREE(schema->attributes_by_lDAPDisplayName);
+ TALLOC_FREE(schema->attributes_by_attributeID_id);
+ TALLOC_FREE(schema->attributes_by_msDS_IntId);
+ TALLOC_FREE(schema->attributes_by_attributeID_oid);
+ TALLOC_FREE(schema->attributes_by_linkID);
+ TALLOC_FREE(schema->attributes_by_cn);
+}
+
+/*
+ create the sorted accessor arrays for the schema
+ */
+int dsdb_setup_sorted_accessors(struct ldb_context *ldb,
+ struct dsdb_schema *schema)
+{
+ struct dsdb_class *cur;
+ struct dsdb_attribute *a;
+ unsigned int i;
+ unsigned int num_int_id;
+ int ret;
+
+ for (i=0; i < schema->classes_to_remove_size; i++) {
+ DLIST_REMOVE(schema->classes, schema->classes_to_remove[i]);
+ TALLOC_FREE(schema->classes_to_remove[i]);
+ }
+ for (i=0; i < schema->attributes_to_remove_size; i++) {
+ DLIST_REMOVE(schema->attributes, schema->attributes_to_remove[i]);
+ TALLOC_FREE(schema->attributes_to_remove[i]);
+ }
+
+ TALLOC_FREE(schema->classes_to_remove);
+ schema->classes_to_remove_size = 0;
+ TALLOC_FREE(schema->attributes_to_remove);
+ schema->attributes_to_remove_size = 0;
+
+ /* free all caches */
+ dsdb_sorted_accessors_free(schema);
+
+ /* count the classes */
+ for (i=0, cur=schema->classes; cur; i++, cur=cur->next) /* noop */ ;
+ schema->num_classes = i;
+
+ /* setup classes_by_* */
+ schema->classes_by_lDAPDisplayName = talloc_array(schema, struct dsdb_class *, i);
+ schema->classes_by_governsID_id = talloc_array(schema, struct dsdb_class *, i);
+ schema->classes_by_governsID_oid = talloc_array(schema, struct dsdb_class *, i);
+ schema->classes_by_cn = talloc_array(schema, struct dsdb_class *, i);
+ if (schema->classes_by_lDAPDisplayName == NULL ||
+ schema->classes_by_governsID_id == NULL ||
+ schema->classes_by_governsID_oid == NULL ||
+ schema->classes_by_cn == NULL) {
+ goto failed;
+ }
+
+ for (i=0, cur=schema->classes; cur; i++, cur=cur->next) {
+ schema->classes_by_lDAPDisplayName[i] = cur;
+ schema->classes_by_governsID_id[i] = cur;
+ schema->classes_by_governsID_oid[i] = cur;
+ schema->classes_by_cn[i] = cur;
+ }
+
+ /* sort the arrays */
+ TYPESAFE_QSORT(schema->classes_by_lDAPDisplayName, schema->num_classes, dsdb_compare_class_by_lDAPDisplayName);
+ TYPESAFE_QSORT(schema->classes_by_governsID_id, schema->num_classes, dsdb_compare_class_by_governsID_id);
+ TYPESAFE_QSORT(schema->classes_by_governsID_oid, schema->num_classes, dsdb_compare_class_by_governsID_oid);
+ TYPESAFE_QSORT(schema->classes_by_cn, schema->num_classes, dsdb_compare_class_by_cn);
+
+ /* now build the attribute accessor arrays */
+
+ /* count the attributes
+ * and attributes with msDS-IntId set */
+ num_int_id = 0;
+ for (i=0, a=schema->attributes; a; i++, a=a->next) {
+ if (a->msDS_IntId != 0) {
+ num_int_id++;
+ }
+ }
+ schema->num_attributes = i;
+ schema->num_int_id_attr = num_int_id;
+
+ /* setup attributes_by_* */
+ schema->attributes_by_lDAPDisplayName = talloc_array(schema, struct dsdb_attribute *, i);
+ schema->attributes_by_attributeID_id = talloc_array(schema, struct dsdb_attribute *, i);
+ schema->attributes_by_msDS_IntId = talloc_array(schema,
+ struct dsdb_attribute *, num_int_id);
+ schema->attributes_by_attributeID_oid = talloc_array(schema, struct dsdb_attribute *, i);
+ schema->attributes_by_linkID = talloc_array(schema, struct dsdb_attribute *, i);
+ schema->attributes_by_cn = talloc_array(schema, struct dsdb_attribute *, i);
+ if (schema->attributes_by_lDAPDisplayName == NULL ||
+ schema->attributes_by_attributeID_id == NULL ||
+ schema->attributes_by_msDS_IntId == NULL ||
+ schema->attributes_by_attributeID_oid == NULL ||
+ schema->attributes_by_linkID == NULL ||
+ schema->attributes_by_cn == NULL) {
+ goto failed;
+ }
+
+ num_int_id = 0;
+ for (i=0, a=schema->attributes; a; i++, a=a->next) {
+ schema->attributes_by_lDAPDisplayName[i] = a;
+ schema->attributes_by_attributeID_id[i] = a;
+ schema->attributes_by_attributeID_oid[i] = a;
+ schema->attributes_by_linkID[i] = a;
+ schema->attributes_by_cn[i] = a;
+ /* append attr-by-msDS-IntId values */
+ if (a->msDS_IntId != 0) {
+ schema->attributes_by_msDS_IntId[num_int_id] = a;
+ num_int_id++;
+ }
+ }
+ SMB_ASSERT(num_int_id == schema->num_int_id_attr);
+
+ /* sort the arrays */
+ TYPESAFE_QSORT(schema->attributes_by_lDAPDisplayName, schema->num_attributes, dsdb_compare_attribute_by_lDAPDisplayName);
+ TYPESAFE_QSORT(schema->attributes_by_attributeID_id, schema->num_attributes, dsdb_compare_attribute_by_attributeID_id);
+ TYPESAFE_QSORT(schema->attributes_by_msDS_IntId, schema->num_int_id_attr, dsdb_compare_attribute_by_msDS_IntId);
+ TYPESAFE_QSORT(schema->attributes_by_attributeID_oid, schema->num_attributes, dsdb_compare_attribute_by_attributeID_oid);
+ TYPESAFE_QSORT(schema->attributes_by_linkID, schema->num_attributes, dsdb_compare_attribute_by_linkID);
+ TYPESAFE_QSORT(schema->attributes_by_cn, schema->num_attributes, dsdb_compare_attribute_by_cn);
+
+ dsdb_setup_attribute_shortcuts(ldb, schema);
+
+ ret = schema_fill_constructed(schema);
+ if (ret != LDB_SUCCESS) {
+ dsdb_sorted_accessors_free(schema);
+ return ret;
+ }
+
+ return LDB_SUCCESS;
+
+failed:
+ dsdb_sorted_accessors_free(schema);
+ return ldb_oom(ldb);
+}
+
+/**
+ * Attach the schema to an opaque pointer on the ldb,
+ * so ldb modules can find it
+ */
+int dsdb_set_schema_refresh_function(struct ldb_context *ldb,
+ dsdb_schema_refresh_fn refresh_fn,
+ struct ldb_module *module)
+{
+ int ret = ldb_set_opaque(ldb, "dsdb_schema_refresh_fn", refresh_fn);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ ret = ldb_set_opaque(ldb, "dsdb_schema_refresh_fn_private_data", module);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+ return LDB_SUCCESS;
+}
+
+/**
+ * Attach the schema to an opaque pointer on the ldb,
+ * so ldb modules can find it
+ */
+int dsdb_set_schema(struct ldb_context *ldb,
+ struct dsdb_schema *schema,
+ enum schema_set_enum write_indices_and_attributes)
+{
+ struct dsdb_schema *old_schema;
+ int ret;
+
+ ret = dsdb_setup_sorted_accessors(ldb, schema);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ old_schema = ldb_get_opaque(ldb, "dsdb_schema");
+
+ ret = ldb_set_opaque(ldb, "dsdb_use_global_schema", NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ ret = ldb_set_opaque(ldb, "dsdb_schema", schema);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ talloc_steal(ldb, schema);
+
+ /* Set the new attributes based on the new schema */
+ ret = dsdb_schema_set_indices_and_attributes(ldb, schema,
+ write_indices_and_attributes);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ /*
+ * Remove the reference to the schema we just overwrote - if there was
+ * none, NULL is harmless here.
+ */
+ if (old_schema != schema) {
+ talloc_unlink(ldb, old_schema);
+ }
+
+ return ret;
+}
+
+/**
+ * Global variable to hold one copy of the schema, used to avoid memory bloat
+ */
+static struct dsdb_schema *global_schema;
+
+/**
+ * Make this ldb use a specified schema, already fully calculated and belonging to another ldb
+ *
+ * The write_indices_and_attributes controls writing of the @ records
+ * because we cannot write to a database that does not yet exist on
+ * disk.
+ */
+int dsdb_reference_schema(struct ldb_context *ldb, struct dsdb_schema *schema,
+ enum schema_set_enum write_indices_and_attributes)
+{
+ int ret;
+ void *ptr;
+ void *schema_parent = NULL;
+ bool is_already_parent;
+ struct dsdb_schema *old_schema;
+ old_schema = ldb_get_opaque(ldb, "dsdb_schema");
+ ret = ldb_set_opaque(ldb, "dsdb_schema", schema);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ /* Remove the reference to the schema we just overwrote - if there was
+ * none, NULL is harmless here */
+ talloc_unlink(ldb, old_schema);
+
+ /* Reference schema on ldb if it wasn't done already */
+ schema_parent = talloc_parent(schema);
+ is_already_parent = (schema_parent == ldb);
+ if (!is_already_parent) {
+ ptr = talloc_reference(ldb, schema);
+ if (ptr == NULL) {
+ return ldb_oom(ldb);
+ }
+ }
+
+ /* Make this ldb use local schema preferably */
+ ret = ldb_set_opaque(ldb, "dsdb_use_global_schema", NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ ret = ldb_set_opaque(ldb, "dsdb_refresh_fn", NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ ret = ldb_set_opaque(ldb, "dsdb_refresh_fn_private_data", NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ ret = dsdb_schema_set_indices_and_attributes(ldb, schema, write_indices_and_attributes);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ return LDB_SUCCESS;
+}
+
+/**
+ * Make this ldb use the 'global' schema, setup to avoid having multiple copies in this process
+ */
+int dsdb_set_global_schema(struct ldb_context *ldb)
+{
+ int ret;
+ void *use_global_schema = (void *)1;
+ void *ptr;
+ struct dsdb_schema *old_schema = ldb_get_opaque(ldb, "dsdb_schema");
+
+ ret = ldb_set_opaque(ldb, "dsdb_use_global_schema", use_global_schema);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ if (global_schema == NULL) {
+ return LDB_SUCCESS;
+ }
+
+ /* Remove any pointer to a previous schema */
+ ret = ldb_set_opaque(ldb, "dsdb_schema", NULL);
+ if (ret != LDB_SUCCESS) {
+ return ret;
+ }
+
+ /* Remove the reference to the schema we just overwrote - if there was
+ * none, NULL is harmless here */
+ talloc_unlink(ldb, old_schema);
+
+ /* Set the new attributes based on the new schema */
+ /* Don't write indices and attributes, it's expensive */
+ ret = dsdb_schema_set_indices_and_attributes(ldb, global_schema, SCHEMA_MEMORY_ONLY);
+ if (ret == LDB_SUCCESS) {
+ void *schema_parent = talloc_parent(global_schema);
+ bool is_already_parent =
+ (schema_parent == ldb);
+ if (!is_already_parent) {
+ ptr = talloc_reference(ldb, global_schema);
+ if (ptr == NULL) {
+ return ldb_oom(ldb);
+ }
+ ret = ldb_set_opaque(ldb, "dsdb_schema", global_schema);
+ }
+ }
+
+ return ret;
+}
+
+bool dsdb_uses_global_schema(struct ldb_context *ldb)
+{
+ return (ldb_get_opaque(ldb, "dsdb_use_global_schema") != NULL);
+}
+
+/**
+ * Find the schema object for this ldb
+ *
+ * If reference_ctx is not NULL, then talloc_reference onto that context
+ */
+
+struct dsdb_schema *dsdb_get_schema(struct ldb_context *ldb, TALLOC_CTX *reference_ctx)
+{
+ const void *p;
+ struct dsdb_schema *schema_out = NULL;
+ struct dsdb_schema *schema_in = NULL;
+ dsdb_schema_refresh_fn refresh_fn;
+ struct ldb_module *loaded_from_module;
+ bool use_global_schema;
+ TALLOC_CTX *tmp_ctx = talloc_new(reference_ctx);
+ if (tmp_ctx == NULL) {
+ return NULL;
+ }
+
+ /* see if we have a cached copy */
+ use_global_schema = dsdb_uses_global_schema(ldb);
+ if (use_global_schema) {
+ schema_in = global_schema;
+ } else {
+ p = ldb_get_opaque(ldb, "dsdb_schema");
+ if (p != NULL) {
+ schema_in = talloc_get_type_abort(p, struct dsdb_schema);
+ }
+ }
+
+ refresh_fn = ldb_get_opaque(ldb, "dsdb_schema_refresh_fn");
+ if (refresh_fn) {
+ loaded_from_module = ldb_get_opaque(ldb, "dsdb_schema_refresh_fn_private_data");
+
+ SMB_ASSERT(loaded_from_module && (ldb_module_get_ctx(loaded_from_module) == ldb));
+ }
+
+ if (refresh_fn) {
+ /* We need to guard against recursive calls here */
+ if (ldb_set_opaque(ldb, "dsdb_schema_refresh_fn", NULL) != LDB_SUCCESS) {
+ ldb_debug_set(ldb, LDB_DEBUG_FATAL,
+ "dsdb_get_schema: clearing dsdb_schema_refresh_fn failed");
+ } else {
+ schema_out = refresh_fn(loaded_from_module,
+ ldb_get_event_context(ldb),
+ schema_in,
+ use_global_schema);
+ }
+ if (ldb_set_opaque(ldb, "dsdb_schema_refresh_fn", refresh_fn) != LDB_SUCCESS) {
+ ldb_debug_set(ldb, LDB_DEBUG_FATAL,
+ "dsdb_get_schema: re-setting dsdb_schema_refresh_fn failed");
+ }
+ if (!schema_out) {
+ schema_out = schema_in;
+ ldb_debug_set(ldb, LDB_DEBUG_FATAL,
+ "dsdb_get_schema: refresh_fn() failed");
+ }
+ } else {
+ schema_out = schema_in;
+ }
+
+ /* This removes the extra reference above */
+ talloc_free(tmp_ctx);
+
+ /*
+ * If ref ctx exists and doesn't already reference schema, then add
+ * a reference. Otherwise, just return schema.
+ *
+ * We must use talloc_parent(), which is not quite free (there
+ * is no direct parent pointer in talloc, only one on the
+ * first child within a linked list), but is much cheaper than
+ * talloc_is_parent() which walks the whole tree up to the top
+ * looking for a potential grand-grand(etc)-parent.
+ */
+ if (reference_ctx == NULL) {
+ return schema_out;
+ } else {
+ void *schema_parent = talloc_parent(schema_out);
+ bool is_already_parent =
+ schema_parent == reference_ctx;
+ if (is_already_parent) {
+ return schema_out;
+ } else {
+ return talloc_reference(reference_ctx,
+ schema_out);
+ }
+ }
+}
+
+/**
+ * Make the schema found on this ldb the 'global' schema
+ */
+
+void dsdb_make_schema_global(struct ldb_context *ldb, struct dsdb_schema *schema)
+{
+ if (!schema) {
+ return;
+ }
+
+ if (global_schema) {
+ talloc_unlink(NULL, global_schema);
+ }
+
+ /* we want the schema to be around permanently */
+ talloc_reparent(ldb, NULL, schema);
+ global_schema = schema;
+
+ /* This calls the talloc_reference() of the global schema back onto the ldb */
+ dsdb_set_global_schema(ldb);
+}
+
+/**
+ * When loading the schema from LDIF files, we don't get the extended DNs.
+ *
+ * We need to set these up, so that from the moment we start the provision,
+ * the defaultObjectCategory links are set up correctly.
+ */
+int dsdb_schema_fill_extended_dn(struct ldb_context *ldb, struct dsdb_schema *schema)
+{
+ struct dsdb_class *cur;
+ const struct dsdb_class *target_class;
+ for (cur = schema->classes; cur; cur = cur->next) {
+ const struct ldb_val *rdn;
+ struct ldb_val guid;
+ NTSTATUS status;
+ int ret;
+ struct ldb_dn *dn = ldb_dn_new(NULL, ldb, cur->defaultObjectCategory);
+
+ if (!dn) {
+ return LDB_ERR_INVALID_DN_SYNTAX;
+ }
+ rdn = ldb_dn_get_component_val(dn, 0);
+ if (!rdn) {
+ talloc_free(dn);
+ return LDB_ERR_INVALID_DN_SYNTAX;
+ }
+ target_class = dsdb_class_by_cn_ldb_val(schema, rdn);
+ if (!target_class) {
+ talloc_free(dn);
+ return LDB_ERR_CONSTRAINT_VIOLATION;
+ }
+
+ status = GUID_to_ndr_blob(&target_class->objectGUID, dn, &guid);
+ if (!NT_STATUS_IS_OK(status)) {
+ talloc_free(dn);
+ return ldb_operr(ldb);
+ }
+ ret = ldb_dn_set_extended_component(dn, "GUID", &guid);
+ if (ret != LDB_SUCCESS) {
+ ret = ldb_error(ldb, ret, "Could not set GUID");
+ talloc_free(dn);
+ return ret;
+ }
+
+ cur->defaultObjectCategory = ldb_dn_get_extended_linearized(cur, dn, 1);
+ talloc_free(dn);
+ }
+ return LDB_SUCCESS;
+}
+
+/**
+ * @brief Add a new element to the schema and checks if it's a duplicate
+ *
+ * This function will add a new element to the schema and checks for existing
+ * duplicates.
+ *
+ * @param[in] ldb A pointer to an LDB context
+ *
+ * @param[in] schema A pointer to the dsdb_schema where the element
+ * will be added.
+ *
+ * @param[in] msg The ldb_message object representing the element
+ * to add.
+ *
+ * @param[in] checkdups A boolean to indicate if checks for duplicates
+ * should be done.
+ *
+ * @return A WERROR code
+ */
+WERROR dsdb_schema_set_el_from_ldb_msg_dups(struct ldb_context *ldb, struct dsdb_schema *schema,
+ struct ldb_message *msg, bool checkdups)
+{
+ const char* tstring;
+ time_t ts;
+ tstring = ldb_msg_find_attr_as_string(msg, "whenChanged", NULL);
+ /* keep a trace of the ts of the most recently changed object */
+ if (tstring) {
+ ts = ldb_string_to_time(tstring);
+ if (ts > schema->ts_last_change) {
+ schema->ts_last_change = ts;
+ }
+ }
+ if (samdb_find_attribute(ldb, msg,
+ "objectclass", "attributeSchema") != NULL) {
+
+ return dsdb_set_attribute_from_ldb_dups(ldb, schema, msg, checkdups);
+ } else if (samdb_find_attribute(ldb, msg,
+ "objectclass", "classSchema") != NULL) {
+ return dsdb_set_class_from_ldb_dups(schema, msg, checkdups);
+ }
+ /* Don't fail on things not classes or attributes */
+ return WERR_OK;
+}
+
+WERROR dsdb_schema_set_el_from_ldb_msg(struct ldb_context *ldb,
+ struct dsdb_schema *schema,
+ struct ldb_message *msg)
+{
+ return dsdb_schema_set_el_from_ldb_msg_dups(ldb, schema, msg, false);
+}
+
+/**
+ * Rather than read a schema from the LDB itself, read it from an ldif
+ * file. This allows schema to be loaded and used while adding the
+ * schema itself to the directory.
+ *
+ * Should be called with a transaction (or failing that, have no concurrent
+ * access while called).
+ */
+
+WERROR dsdb_set_schema_from_ldif(struct ldb_context *ldb,
+ const char *pf, const char *df,
+ const char *dn)
+{
+ struct ldb_ldif *ldif;
+ struct ldb_message *msg;
+ TALLOC_CTX *mem_ctx;
+ WERROR status;
+ int ret;
+ struct dsdb_schema *schema;
+ const struct ldb_val *prefix_val;
+ const struct ldb_val *info_val;
+ struct ldb_val info_val_default;
+
+
+ mem_ctx = talloc_new(ldb);
+ if (!mem_ctx) {
+ goto nomem;
+ }
+
+ schema = dsdb_new_schema(mem_ctx);
+ if (!schema) {
+ goto nomem;
+ }
+ schema->fsmo.we_are_master = true;
+ schema->fsmo.update_allowed = true;
+ schema->fsmo.master_dn = ldb_dn_new(schema, ldb, "@PROVISION_SCHEMA_MASTER");
+ if (!schema->fsmo.master_dn) {
+ goto nomem;
+ }
+
+ /*
+ * load the prefixMap attribute from pf
+ */
+ ldif = ldb_ldif_read_string(ldb, &pf);
+ if (!ldif) {
+ status = WERR_INVALID_PARAMETER;
+ goto failed;
+ }
+ talloc_steal(mem_ctx, ldif);
+
+ ret = ldb_msg_normalize(ldb, mem_ctx, ldif->msg, &msg);
+ if (ret != LDB_SUCCESS) {
+ goto nomem;
+ }
+ talloc_free(ldif);
+
+ prefix_val = ldb_msg_find_ldb_val(msg, "prefixMap");
+ if (!prefix_val) {
+ status = WERR_INVALID_PARAMETER;
+ goto failed;
+ }
+
+ info_val = ldb_msg_find_ldb_val(msg, "schemaInfo");
+ if (!info_val) {
+ status = dsdb_schema_info_blob_new(mem_ctx, &info_val_default);
+ W_ERROR_NOT_OK_GOTO(status, failed);
+ info_val = &info_val_default;
+ }
+
+ status = dsdb_load_oid_mappings_ldb(schema, prefix_val, info_val);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(0,("ERROR: dsdb_load_oid_mappings_ldb() failed with %s\n", win_errstr(status)));
+ goto failed;
+ }
+
+ schema->ts_last_change = 0;
+ /* load the attribute and class definitions out of df */
+ while ((ldif = ldb_ldif_read_string(ldb, &df))) {
+ talloc_steal(mem_ctx, ldif);
+
+ ret = ldb_msg_normalize(ldb, ldif, ldif->msg, &msg);
+ if (ret != LDB_SUCCESS) {
+ goto nomem;
+ }
+
+ status = dsdb_schema_set_el_from_ldb_msg(ldb, schema, msg);
+ talloc_free(ldif);
+ if (!W_ERROR_IS_OK(status)) {
+ goto failed;
+ }
+ }
+
+ /*
+ * TODO We may need a transaction here, otherwise this causes races.
+ *
+ * To do so may require an ldb_in_transaction function. In the
+ * meantime, assume that this is always called with a transaction or in
+ * isolation.
+ */
+ ret = dsdb_set_schema(ldb, schema, SCHEMA_WRITE);
+ if (ret != LDB_SUCCESS) {
+ status = WERR_FOOBAR;
+ DEBUG(0,("ERROR: dsdb_set_schema() failed with %s / %s\n",
+ ldb_strerror(ret), ldb_errstring(ldb)));
+ goto failed;
+ }
+
+ ret = dsdb_schema_fill_extended_dn(ldb, schema);
+ if (ret != LDB_SUCCESS) {
+ status = WERR_FOOBAR;
+ goto failed;
+ }
+
+ goto done;
+
+nomem:
+ status = WERR_NOT_ENOUGH_MEMORY;
+failed:
+done:
+ talloc_free(mem_ctx);
+ return status;
+}
diff --git a/source4/dsdb/schema/schema_syntax.c b/source4/dsdb/schema/schema_syntax.c
new file mode 100644
index 0000000..3bcf80d
--- /dev/null
+++ b/source4/dsdb/schema/schema_syntax.c
@@ -0,0 +1,2811 @@
+/*
+ Unix SMB/CIFS Implementation.
+ DSDB schema syntaxes
+
+ Copyright (C) Stefan Metzmacher <metze@samba.org> 2006
+ Copyright (C) Simo Sorce 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 "dsdb/samdb/samdb.h"
+#include "librpc/gen_ndr/ndr_drsuapi.h"
+#include "librpc/gen_ndr/ndr_security.h"
+#include "librpc/gen_ndr/ndr_misc.h"
+#include <ldb.h>
+#include <ldb_errors.h>
+#include "system/time.h"
+#include "../lib/util/charset/charset.h"
+#include "librpc/ndr/libndr.h"
+#include "../lib/util/asn1.h"
+
+#undef strcasecmp
+
+/**
+ * Initialize dsdb_syntax_ctx with default values
+ * for common cases.
+ */
+void dsdb_syntax_ctx_init(struct dsdb_syntax_ctx *ctx,
+ struct ldb_context *ldb,
+ const struct dsdb_schema *schema)
+{
+ ctx->ldb = ldb;
+ ctx->schema = schema;
+
+ /*
+ * 'true' will keep current behavior,
+ * i.e. attributeID_id will be returned by default
+ */
+ ctx->is_schema_nc = true;
+
+ ctx->pfm_remote = NULL;
+}
+
+
+/**
+ * Returns ATTID for DRS attribute.
+ *
+ * ATTID depends on whether we are replicating
+ * Schema NC or msDs-IntId is set for schemaAttribute
+ * for the attribute.
+ */
+uint32_t dsdb_attribute_get_attid(const struct dsdb_attribute *attr,
+ bool for_schema_nc)
+{
+ if (!for_schema_nc && attr->msDS_IntId) {
+ return attr->msDS_IntId;
+ }
+
+ return attr->attributeID_id;
+}
+
+/**
+ * Map an ATTID from remote DC to a local ATTID
+ * using remote prefixMap
+ */
+static bool dsdb_syntax_attid_from_remote_attid(const struct dsdb_syntax_ctx *ctx,
+ TALLOC_CTX *mem_ctx,
+ uint32_t id_remote,
+ uint32_t *id_local)
+{
+ WERROR werr;
+ const char *oid;
+
+ /*
+ * map remote ATTID to local directly in case
+ * of no remote prefixMap (during provision for instance)
+ */
+ if (!ctx->pfm_remote) {
+ *id_local = id_remote;
+ return true;
+ }
+
+ werr = dsdb_schema_pfm_oid_from_attid(ctx->pfm_remote, id_remote, mem_ctx, &oid);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0,("ATTID->OID failed (%s) for: 0x%08X\n", win_errstr(werr), id_remote));
+ return false;
+ }
+
+ werr = dsdb_schema_pfm_attid_from_oid(ctx->schema->prefixmap, oid, id_local);
+ if (!W_ERROR_IS_OK(werr)) {
+ DEBUG(0,("OID->ATTID failed (%s) for: %s\n", win_errstr(werr), oid));
+ return false;
+ }
+
+ return true;
+}
+
+static WERROR dsdb_syntax_FOOBAR_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ char *str;
+
+ if (in->value_ctr.values[i].blob == NULL) {
+ return WERR_FOOBAR;
+ }
+
+ str = talloc_asprintf(out->values, "%s: not implemented",
+ attr->syntax->name);
+ W_ERROR_HAVE_NO_MEMORY(str);
+
+ out->values[i] = data_blob_string_const(str);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_FOOBAR_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ return WERR_FOOBAR;
+}
+
+static WERROR dsdb_syntax_FOOBAR_validate_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ return WERR_FOOBAR;
+}
+
+static WERROR dsdb_syntax_BOOL_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ uint32_t v;
+ char *str;
+
+ if (in->value_ctr.values[i].blob == NULL) {
+ return WERR_FOOBAR;
+ }
+
+ if (in->value_ctr.values[i].blob->length != 4) {
+ return WERR_FOOBAR;
+ }
+
+ v = IVAL(in->value_ctr.values[i].blob->data, 0);
+
+ if (v != 0) {
+ str = talloc_strdup(out->values, "TRUE");
+ W_ERROR_HAVE_NO_MEMORY(str);
+ } else {
+ str = talloc_strdup(out->values, "FALSE");
+ W_ERROR_HAVE_NO_MEMORY(str);
+ }
+
+ out->values[i] = data_blob_string_const(str);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_BOOL_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ out->attid = dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values = in->num_values;
+ out->value_ctr.values = talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ out->value_ctr.values[i].blob = &blobs[i];
+
+ blobs[i] = data_blob_talloc(blobs, NULL, 4);
+ W_ERROR_HAVE_NO_MEMORY(blobs[i].data);
+
+ if (in->values[i].length >= 4 &&
+ strncmp("TRUE", (const char *)in->values[i].data, in->values[i].length) == 0) {
+ SIVAL(blobs[i].data, 0, 0x00000001);
+ } else if (in->values[i].length >= 5 &&
+ strncmp("FALSE", (const char *)in->values[i].data, in->values[i].length) == 0) {
+ SIVAL(blobs[i].data, 0, 0x00000000);
+ } else {
+ return WERR_FOOBAR;
+ }
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_BOOL_validate_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ unsigned int i;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ for (i=0; i < in->num_values; i++) {
+ if (in->values[i].length == 0) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ if (in->values[i].length >= 4 &&
+ strncmp("TRUE",
+ (const char *)in->values[i].data,
+ in->values[i].length) == 0) {
+ continue;
+ }
+ if (in->values[i].length >= 5 &&
+ strncmp("FALSE",
+ (const char *)in->values[i].data,
+ in->values[i].length) == 0) {
+ continue;
+ }
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_INT32_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ int32_t v;
+ char *str;
+
+ if (in->value_ctr.values[i].blob == NULL) {
+ return WERR_FOOBAR;
+ }
+
+ if (in->value_ctr.values[i].blob->length != 4) {
+ return WERR_FOOBAR;
+ }
+
+ v = IVALS(in->value_ctr.values[i].blob->data, 0);
+
+ str = talloc_asprintf(out->values, "%d", v);
+ W_ERROR_HAVE_NO_MEMORY(str);
+
+ out->values[i] = data_blob_string_const(str);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_INT32_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ out->attid = dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values = in->num_values;
+ out->value_ctr.values = talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ int32_t v;
+
+ out->value_ctr.values[i].blob = &blobs[i];
+
+ blobs[i] = data_blob_talloc(blobs, NULL, 4);
+ W_ERROR_HAVE_NO_MEMORY(blobs[i].data);
+
+ /* We've to use "strtoll" here to have the intended overflows.
+ * Otherwise we may get "LONG_MAX" and the conversion is wrong. */
+ v = (int32_t) strtoll((char *)in->values[i].data, NULL, 0);
+
+ SIVALS(blobs[i].data, 0, v);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_INT32_validate_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ unsigned int i;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ for (i=0; i < in->num_values; i++) {
+ long v;
+ char buf[sizeof("-2147483648")];
+ char *end = NULL;
+
+ ZERO_STRUCT(buf);
+ if (in->values[i].length >= sizeof(buf)) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ memcpy(buf, in->values[i].data, in->values[i].length);
+ errno = 0;
+ v = strtol(buf, &end, 10);
+ if (errno != 0) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ if (end && end[0] != '\0') {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ if (attr->rangeLower) {
+ if ((int32_t)v < (int32_t)*attr->rangeLower) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ }
+
+ if (attr->rangeUpper) {
+ if ((int32_t)v > (int32_t)*attr->rangeUpper) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ }
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_INT64_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ int64_t v;
+ char *str;
+
+ if (in->value_ctr.values[i].blob == NULL) {
+ return WERR_FOOBAR;
+ }
+
+ if (in->value_ctr.values[i].blob->length != 8) {
+ return WERR_FOOBAR;
+ }
+
+ v = BVALS(in->value_ctr.values[i].blob->data, 0);
+
+ str = talloc_asprintf(out->values, "%lld", (long long int)v);
+ W_ERROR_HAVE_NO_MEMORY(str);
+
+ out->values[i] = data_blob_string_const(str);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_INT64_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ out->attid = dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values = in->num_values;
+ out->value_ctr.values = talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ int64_t v;
+
+ out->value_ctr.values[i].blob = &blobs[i];
+
+ blobs[i] = data_blob_talloc(blobs, NULL, 8);
+ W_ERROR_HAVE_NO_MEMORY(blobs[i].data);
+
+ v = strtoll((const char *)in->values[i].data, NULL, 10);
+
+ SBVALS(blobs[i].data, 0, v);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_INT64_validate_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ unsigned int i;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ for (i=0; i < in->num_values; i++) {
+ long long v;
+ char buf[sizeof("-9223372036854775808")];
+ char *end = NULL;
+
+ ZERO_STRUCT(buf);
+ if (in->values[i].length >= sizeof(buf)) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ memcpy(buf, in->values[i].data, in->values[i].length);
+
+ errno = 0;
+ v = strtoll(buf, &end, 10);
+ if (errno != 0) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ if (end && end[0] != '\0') {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ if (attr->rangeLower) {
+ if ((int64_t)v < (int64_t)*attr->rangeLower) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ }
+
+ if (attr->rangeUpper) {
+ if ((int64_t)v > (int64_t)*attr->rangeUpper) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ }
+ }
+
+ return WERR_OK;
+}
+static WERROR dsdb_syntax_NTTIME_UTC_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ NTTIME v;
+ time_t t;
+ char *str;
+
+ if (in->value_ctr.values[i].blob == NULL) {
+ return WERR_FOOBAR;
+ }
+
+ if (in->value_ctr.values[i].blob->length != 8) {
+ return WERR_FOOBAR;
+ }
+
+ v = BVAL(in->value_ctr.values[i].blob->data, 0);
+ if (v == 0) {
+ /* special case for 1601 zero timestamp */
+ out->values[i] = data_blob_string_const("16010101000000.0Z");
+ continue;
+ }
+ v *= 10000000;
+ t = nt_time_to_unix(v);
+
+ /*
+ * NOTE: On a w2k3 server you can set a GeneralizedTime string
+ * via LDAP, but you get back an UTCTime string,
+ * but via DRSUAPI you get back the NTTIME_1sec value
+ * that represents the GeneralizedTime value!
+ *
+ * So if we store the UTCTime string in our ldb
+ * we'll loose information!
+ */
+ str = ldb_timestring_utc(out->values, t);
+ W_ERROR_HAVE_NO_MEMORY(str);
+ out->values[i] = data_blob_string_const(str);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_NTTIME_UTC_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ out->attid = dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values = in->num_values;
+ out->value_ctr.values = talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ NTTIME v;
+ time_t t;
+
+ out->value_ctr.values[i].blob = &blobs[i];
+
+ blobs[i] = data_blob_talloc(blobs, NULL, 8);
+ W_ERROR_HAVE_NO_MEMORY(blobs[i].data);
+
+ if (ldb_val_string_cmp(&in->values[i], "16010101000000.0Z") == 0) {
+ SBVALS(blobs[i].data, 0, 0);
+ continue;
+ }
+
+ t = ldb_string_utc_to_time((const char *)in->values[i].data);
+ unix_to_nt_time(&v, t);
+ v /= 10000000;
+
+ SBVAL(blobs[i].data, 0, v);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_NTTIME_UTC_validate_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ unsigned int i;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ for (i=0; i < in->num_values; i++) {
+ time_t t;
+ char buf[sizeof("090826075717Z")];
+
+ ZERO_STRUCT(buf);
+ if (in->values[i].length >= sizeof(buf)) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ memcpy(buf, in->values[i].data, in->values[i].length);
+
+ t = ldb_string_utc_to_time(buf);
+ if (t == 0) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ if (attr->rangeLower) {
+ if ((int32_t)t < (int32_t)*attr->rangeLower) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ }
+
+ if (attr->rangeUpper) {
+ if ((int32_t)t > (int32_t)*attr->rangeUpper) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ }
+
+ /*
+ * TODO: verify the comment in the
+ * dsdb_syntax_NTTIME_UTC_drsuapi_to_ldb() function!
+ */
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_NTTIME_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ NTTIME v;
+ time_t t;
+ char *str;
+
+ if (in->value_ctr.values[i].blob == NULL) {
+ return WERR_FOOBAR;
+ }
+
+ if (in->value_ctr.values[i].blob->length != 8) {
+ return WERR_FOOBAR;
+ }
+
+ v = BVAL(in->value_ctr.values[i].blob->data, 0);
+ if (v == 0) {
+ /* special case for 1601 zero timestamp */
+ out->values[i] = data_blob_string_const("16010101000000.0Z");
+ continue;
+ }
+ v *= 10000000;
+ t = nt_time_to_unix(v);
+
+ str = ldb_timestring(out->values, t);
+ W_ERROR_HAVE_NO_MEMORY(str);
+
+ out->values[i] = data_blob_string_const(str);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_NTTIME_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ out->attid = dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values = in->num_values;
+ out->value_ctr.values = talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ NTTIME v;
+ time_t t;
+ int ret;
+
+ out->value_ctr.values[i].blob = &blobs[i];
+
+ blobs[i] = data_blob_talloc(blobs, NULL, 8);
+ W_ERROR_HAVE_NO_MEMORY(blobs[i].data);
+
+ if (ldb_val_string_cmp(&in->values[i], "16010101000000.0Z") == 0) {
+ SBVALS(blobs[i].data, 0, 0);
+ continue;
+ }
+
+ ret = ldb_val_to_time(&in->values[i], &t);
+ if (ret != LDB_SUCCESS) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ unix_to_nt_time(&v, t);
+ v /= 10000000;
+
+ SBVAL(blobs[i].data, 0, v);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_NTTIME_validate_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ unsigned int i;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ for (i=0; i < in->num_values; i++) {
+ time_t t;
+ int ret;
+
+ ret = ldb_val_to_time(&in->values[i], &t);
+ if (ret != LDB_SUCCESS) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ if (attr->rangeLower) {
+ if ((int32_t)t < (int32_t)*attr->rangeLower) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ }
+
+ if (attr->rangeUpper) {
+ if ((int32_t)t > (int32_t)*attr->rangeUpper) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ }
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_DATA_BLOB_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ if (in->value_ctr.values[i].blob == NULL) {
+ return WERR_FOOBAR;
+ }
+
+ if (in->value_ctr.values[i].blob->length == 0) {
+ return WERR_FOOBAR;
+ }
+
+ out->values[i] = data_blob_dup_talloc(out->values,
+ *in->value_ctr.values[i].blob);
+ W_ERROR_HAVE_NO_MEMORY(out->values[i].data);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_DATA_BLOB_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ out->attid = dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values = in->num_values;
+ out->value_ctr.values = talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ out->value_ctr.values[i].blob = &blobs[i];
+
+ blobs[i] = data_blob_dup_talloc(blobs, in->values[i]);
+ W_ERROR_HAVE_NO_MEMORY(blobs[i].data);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_DATA_BLOB_validate_one_val(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_val *val)
+{
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ if (attr->rangeLower) {
+ if ((uint32_t)val->length < (uint32_t)*attr->rangeLower) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ }
+
+ if (attr->rangeUpper) {
+ if ((uint32_t)val->length > (uint32_t)*attr->rangeUpper) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_DATA_BLOB_validate_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ unsigned int i;
+ WERROR status;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ for (i=0; i < in->num_values; i++) {
+ if (in->values[i].length == 0) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ status = dsdb_syntax_DATA_BLOB_validate_one_val(ctx,
+ attr,
+ &in->values[i]);
+ if (!W_ERROR_IS_OK(status)) {
+ return status;
+ }
+ }
+
+ return WERR_OK;
+}
+
+static WERROR _dsdb_syntax_auto_OID_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ uint32_t v;
+ const struct dsdb_class *c;
+ const struct dsdb_attribute *a;
+ const char *str = NULL;
+
+ if (in->value_ctr.values[i].blob == NULL) {
+ return WERR_FOOBAR;
+ }
+
+ if (in->value_ctr.values[i].blob->length != 4) {
+ return WERR_FOOBAR;
+ }
+
+ v = IVAL(in->value_ctr.values[i].blob->data, 0);
+
+ if ((c = dsdb_class_by_governsID_id(ctx->schema, v))) {
+ str = talloc_strdup(out->values, c->lDAPDisplayName);
+ } else if ((a = dsdb_attribute_by_attributeID_id(ctx->schema, v))) {
+ str = talloc_strdup(out->values, a->lDAPDisplayName);
+ } else {
+ WERROR werr;
+ SMB_ASSERT(ctx->pfm_remote);
+ werr = dsdb_schema_pfm_oid_from_attid(ctx->pfm_remote, v,
+ out->values, &str);
+ W_ERROR_NOT_OK_RETURN(werr);
+ }
+ W_ERROR_HAVE_NO_MEMORY(str);
+
+ /* the values need to be reversed */
+ out->values[out->num_values - (i + 1)] = data_blob_string_const(str);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR _dsdb_syntax_OID_obj_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ uint32_t v, vo;
+ const struct dsdb_class *c;
+ const char *str;
+
+ if (in->value_ctr.values[i].blob == NULL) {
+ return WERR_FOOBAR;
+ }
+
+ if (in->value_ctr.values[i].blob->length != 4) {
+ return WERR_FOOBAR;
+ }
+
+ v = IVAL(in->value_ctr.values[i].blob->data, 0);
+ vo = v;
+
+ /* convert remote ATTID to local ATTID */
+ if (!dsdb_syntax_attid_from_remote_attid(ctx, mem_ctx, v, &v)) {
+ DEBUG(1,(__location__ ": Failed to map remote ATTID to local ATTID!\n"));
+ return WERR_FOOBAR;
+ }
+
+ c = dsdb_class_by_governsID_id(ctx->schema, v);
+ if (!c) {
+ int dbg_level = ctx->schema->resolving_in_progress ? 10 : 0;
+ DEBUG(dbg_level,(__location__ ": %s unknown local governsID 0x%08X remote 0x%08X%s\n",
+ attr->lDAPDisplayName, v, vo,
+ ctx->schema->resolving_in_progress ? "resolving in progress" : ""));
+ return WERR_DS_OBJ_CLASS_NOT_DEFINED;
+ }
+
+ str = talloc_strdup(out->values, c->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(str);
+
+ /* the values need to be reversed */
+ out->values[out->num_values - (i + 1)] = data_blob_string_const(str);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR _dsdb_syntax_OID_attr_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ uint32_t v, vo;
+ const struct dsdb_attribute *a;
+ const char *str;
+
+ if (in->value_ctr.values[i].blob == NULL) {
+ DEBUG(0, ("Attribute has no value\n"));
+ return WERR_FOOBAR;
+ }
+
+ if (in->value_ctr.values[i].blob->length != 4) {
+ DEBUG(0, ("Attribute has a value with 0 length\n"));
+ return WERR_FOOBAR;
+ }
+
+ v = IVAL(in->value_ctr.values[i].blob->data, 0);
+ vo = v;
+
+ /* convert remote ATTID to local ATTID */
+ if (!dsdb_syntax_attid_from_remote_attid(ctx, mem_ctx, v, &v)) {
+ DEBUG(1,(__location__ ": Failed to map remote ATTID to local ATTID!\n"));
+ return WERR_FOOBAR;
+ }
+
+ a = dsdb_attribute_by_attributeID_id(ctx->schema, v);
+ if (!a) {
+ int dbg_level = ctx->schema->resolving_in_progress ? 10 : 0;
+ DEBUG(dbg_level,(__location__ ": %s unknown local attributeID_id 0x%08X remote 0x%08X%s\n",
+ attr->lDAPDisplayName, v, vo,
+ ctx->schema->resolving_in_progress ? "resolving in progress" : ""));
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ str = talloc_strdup(out->values, a->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(str);
+
+ /* the values need to be reversed */
+ out->values[out->num_values - (i + 1)] = data_blob_string_const(str);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR _dsdb_syntax_OID_oid_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+ const struct dsdb_schema_prefixmap *prefixmap;
+
+ if (ctx->pfm_remote != NULL) {
+ prefixmap = ctx->pfm_remote;
+ } else {
+ prefixmap = ctx->schema->prefixmap;
+ }
+ SMB_ASSERT(prefixmap);
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ uint32_t attid;
+ WERROR status;
+ const char *oid;
+
+ if (in->value_ctr.values[i].blob == NULL) {
+ return WERR_FOOBAR;
+ }
+
+ if (in->value_ctr.values[i].blob->length != 4) {
+ return WERR_FOOBAR;
+ }
+
+ attid = IVAL(in->value_ctr.values[i].blob->data, 0);
+
+ status = dsdb_schema_pfm_oid_from_attid(prefixmap, attid,
+ out->values, &oid);
+ if (!W_ERROR_IS_OK(status)) {
+ DEBUG(0,(__location__ ": Error: Unknown ATTID 0x%08X\n",
+ attid));
+ return status;
+ }
+
+ out->values[i] = data_blob_string_const(oid);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR _dsdb_syntax_auto_OID_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ out->attid= dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values= in->num_values;
+ out->value_ctr.values= talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ const struct dsdb_class *obj_class;
+ const struct dsdb_attribute *obj_attr;
+ struct ldb_val *v;
+
+ out->value_ctr.values[i].blob= &blobs[i];
+
+ blobs[i] = data_blob_talloc(blobs, NULL, 4);
+ W_ERROR_HAVE_NO_MEMORY(blobs[i].data);
+
+ /* in DRS windows puts the classes in the opposite
+ order to the order used in ldap */
+ v = &in->values[(in->num_values-1)-i];
+
+ if ((obj_class = dsdb_class_by_lDAPDisplayName_ldb_val(ctx->schema, v))) {
+ SIVAL(blobs[i].data, 0, obj_class->governsID_id);
+ } else if ((obj_attr = dsdb_attribute_by_lDAPDisplayName_ldb_val(ctx->schema, v))) {
+ SIVAL(blobs[i].data, 0, obj_attr->attributeID_id);
+ } else {
+ uint32_t attid;
+ WERROR werr;
+ werr = dsdb_schema_pfm_attid_from_oid(ctx->schema->prefixmap,
+ (const char *)v->data,
+ &attid);
+ W_ERROR_NOT_OK_RETURN(werr);
+ SIVAL(blobs[i].data, 0, attid);
+ }
+
+ }
+
+
+ return WERR_OK;
+}
+
+static WERROR _dsdb_syntax_OID_obj_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ out->attid= dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values= in->num_values;
+ out->value_ctr.values= talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ const struct dsdb_class *obj_class;
+
+ out->value_ctr.values[i].blob= &blobs[i];
+
+ blobs[i] = data_blob_talloc(blobs, NULL, 4);
+ W_ERROR_HAVE_NO_MEMORY(blobs[i].data);
+
+ /* in DRS windows puts the classes in the opposite
+ order to the order used in ldap */
+ obj_class = dsdb_class_by_lDAPDisplayName(ctx->schema,
+ (const char *)in->values[(in->num_values-1)-i].data);
+ if (!obj_class) {
+ return WERR_FOOBAR;
+ }
+ SIVAL(blobs[i].data, 0, obj_class->governsID_id);
+ }
+
+
+ return WERR_OK;
+}
+
+static WERROR _dsdb_syntax_OID_attr_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ out->attid= dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values= in->num_values;
+ out->value_ctr.values= talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ const struct dsdb_attribute *obj_attr;
+
+ out->value_ctr.values[i].blob= &blobs[i];
+
+ blobs[i] = data_blob_talloc(blobs, NULL, 4);
+ W_ERROR_HAVE_NO_MEMORY(blobs[i].data);
+
+ obj_attr = dsdb_attribute_by_lDAPDisplayName(ctx->schema, (const char *)in->values[i].data);
+ if (!obj_attr) {
+ DEBUG(0, ("Unable to find attribute %s in the schema\n", (const char *)in->values[i].data));
+ return WERR_FOOBAR;
+ }
+ SIVAL(blobs[i].data, 0, obj_attr->attributeID_id);
+ }
+
+
+ return WERR_OK;
+}
+
+static WERROR _dsdb_syntax_OID_oid_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ out->attid= dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values= in->num_values;
+ out->value_ctr.values= talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ uint32_t attid;
+ WERROR status;
+
+ out->value_ctr.values[i].blob= &blobs[i];
+
+ blobs[i] = data_blob_talloc(blobs, NULL, 4);
+ W_ERROR_HAVE_NO_MEMORY(blobs[i].data);
+
+ status = dsdb_schema_pfm_attid_from_oid(ctx->schema->prefixmap,
+ (const char *)in->values[i].data,
+ &attid);
+ W_ERROR_NOT_OK_RETURN(status);
+
+ SIVAL(blobs[i].data, 0, attid);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_OID_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ WERROR werr;
+
+ switch (attr->attributeID_id) {
+ case DRSUAPI_ATTID_objectClass:
+ case DRSUAPI_ATTID_subClassOf:
+ case DRSUAPI_ATTID_auxiliaryClass:
+ case DRSUAPI_ATTID_systemAuxiliaryClass:
+ case DRSUAPI_ATTID_systemPossSuperiors:
+ case DRSUAPI_ATTID_possSuperiors:
+ werr = _dsdb_syntax_OID_obj_drsuapi_to_ldb(ctx, attr, in, mem_ctx, out);
+ break;
+ case DRSUAPI_ATTID_systemMustContain:
+ case DRSUAPI_ATTID_systemMayContain:
+ case DRSUAPI_ATTID_mustContain:
+ case DRSUAPI_ATTID_rDNAttId:
+ case DRSUAPI_ATTID_transportAddressAttribute:
+ case DRSUAPI_ATTID_mayContain:
+ werr = _dsdb_syntax_OID_attr_drsuapi_to_ldb(ctx, attr, in, mem_ctx, out);
+ break;
+ case DRSUAPI_ATTID_governsID:
+ case DRSUAPI_ATTID_attributeID:
+ case DRSUAPI_ATTID_attributeSyntax:
+ werr = _dsdb_syntax_OID_oid_drsuapi_to_ldb(ctx, attr, in, mem_ctx, out);
+ break;
+ default:
+ DEBUG(0,(__location__ ": Unknown handling for attributeID_id for %s\n",
+ attr->lDAPDisplayName));
+ return _dsdb_syntax_auto_OID_drsuapi_to_ldb(ctx, attr, in, mem_ctx, out);
+ }
+
+ /* When we are doing the vampire of a schema, we don't want
+ * the inability to reference an OID to get in the way.
+ * Otherwise, we won't get the new schema with which to
+ * understand this */
+ if (!W_ERROR_IS_OK(werr) && ctx->schema->relax_OID_conversions) {
+ return _dsdb_syntax_OID_oid_drsuapi_to_ldb(ctx, attr, in, mem_ctx, out);
+ }
+ return werr;
+}
+
+static WERROR dsdb_syntax_OID_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ switch (attr->attributeID_id) {
+ case DRSUAPI_ATTID_objectClass:
+ case DRSUAPI_ATTID_subClassOf:
+ case DRSUAPI_ATTID_auxiliaryClass:
+ case DRSUAPI_ATTID_systemAuxiliaryClass:
+ case DRSUAPI_ATTID_systemPossSuperiors:
+ case DRSUAPI_ATTID_possSuperiors:
+ return _dsdb_syntax_OID_obj_ldb_to_drsuapi(ctx, attr, in, mem_ctx, out);
+ case DRSUAPI_ATTID_systemMustContain:
+ case DRSUAPI_ATTID_systemMayContain:
+ case DRSUAPI_ATTID_mustContain:
+ case DRSUAPI_ATTID_rDNAttId:
+ case DRSUAPI_ATTID_transportAddressAttribute:
+ case DRSUAPI_ATTID_mayContain:
+ return _dsdb_syntax_OID_attr_ldb_to_drsuapi(ctx, attr, in, mem_ctx, out);
+ case DRSUAPI_ATTID_governsID:
+ case DRSUAPI_ATTID_attributeID:
+ case DRSUAPI_ATTID_attributeSyntax:
+ return _dsdb_syntax_OID_oid_ldb_to_drsuapi(ctx, attr, in, mem_ctx, out);
+ }
+
+ DEBUG(0,(__location__ ": Unknown handling for attributeID_id for %s\n",
+ attr->lDAPDisplayName));
+
+ return _dsdb_syntax_auto_OID_ldb_to_drsuapi(ctx, attr, in, mem_ctx, out);
+}
+
+static WERROR _dsdb_syntax_OID_validate_numericoid(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ unsigned int i;
+ TALLOC_CTX *tmp_ctx;
+
+ tmp_ctx = talloc_new(ctx->ldb);
+ W_ERROR_HAVE_NO_MEMORY(tmp_ctx);
+
+ for (i=0; i < in->num_values; i++) {
+ DATA_BLOB blob;
+ char *oid_out;
+ const char *oid = (const char*)in->values[i].data;
+
+ if (in->values[i].length == 0) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ if (!ber_write_OID_String(tmp_ctx, &blob, oid)) {
+ DEBUG(0,("ber_write_OID_String() failed for %s\n", oid));
+ talloc_free(tmp_ctx);
+ return WERR_INVALID_PARAMETER;
+ }
+
+ if (!ber_read_OID_String(tmp_ctx, blob, &oid_out)) {
+ DEBUG(0,("ber_read_OID_String() failed for %s\n",
+ hex_encode_talloc(tmp_ctx, blob.data, blob.length)));
+ talloc_free(tmp_ctx);
+ return WERR_INVALID_PARAMETER;
+ }
+
+ if (strcmp(oid, oid_out) != 0) {
+ talloc_free(tmp_ctx);
+ return WERR_INVALID_PARAMETER;
+ }
+ }
+
+ talloc_free(tmp_ctx);
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_OID_validate_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ WERROR status;
+ struct drsuapi_DsReplicaAttribute drs_tmp;
+ struct ldb_message_element ldb_tmp;
+ TALLOC_CTX *tmp_ctx;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ switch (attr->attributeID_id) {
+ case DRSUAPI_ATTID_governsID:
+ case DRSUAPI_ATTID_attributeID:
+ case DRSUAPI_ATTID_attributeSyntax:
+ return _dsdb_syntax_OID_validate_numericoid(ctx, attr, in);
+ }
+
+ /*
+ * TODO: optimize and verify this code
+ */
+
+ tmp_ctx = talloc_new(ctx->ldb);
+ W_ERROR_HAVE_NO_MEMORY(tmp_ctx);
+
+ status = dsdb_syntax_OID_ldb_to_drsuapi(ctx,
+ attr,
+ in,
+ tmp_ctx,
+ &drs_tmp);
+ if (!W_ERROR_IS_OK(status)) {
+ talloc_free(tmp_ctx);
+ return status;
+ }
+
+ status = dsdb_syntax_OID_drsuapi_to_ldb(ctx,
+ attr,
+ &drs_tmp,
+ tmp_ctx,
+ &ldb_tmp);
+ if (!W_ERROR_IS_OK(status)) {
+ talloc_free(tmp_ctx);
+ return status;
+ }
+
+ talloc_free(tmp_ctx);
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_UNICODE_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ size_t converted_size = 0;
+ char *str;
+
+ if (in->value_ctr.values[i].blob == NULL) {
+ return WERR_FOOBAR;
+ }
+
+ if (in->value_ctr.values[i].blob->length == 0) {
+ return WERR_FOOBAR;
+ }
+
+ if (!convert_string_talloc(out->values,
+ CH_UTF16, CH_UNIX,
+ in->value_ctr.values[i].blob->data,
+ in->value_ctr.values[i].blob->length,
+ &str, &converted_size)) {
+ return WERR_FOOBAR;
+ }
+
+ out->values[i] = data_blob_const(str, converted_size);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_UNICODE_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ out->attid = dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values = in->num_values;
+ out->value_ctr.values = talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ out->value_ctr.values[i].blob = &blobs[i];
+
+ if (!convert_string_talloc(blobs,
+ CH_UNIX, CH_UTF16,
+ in->values[i].data, in->values[i].length,
+ &blobs[i].data, &blobs[i].length)) {
+ return WERR_FOOBAR;
+ }
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_UNICODE_validate_one_val(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_val *val)
+{
+ void *dst = NULL;
+ size_t size;
+ bool ok;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ ok = convert_string_talloc(ctx->ldb,
+ CH_UNIX, CH_UTF16,
+ val->data,
+ val->length,
+ &dst,
+ &size);
+ TALLOC_FREE(dst);
+ if (!ok) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ if (attr->rangeLower) {
+ if ((size/2) < *attr->rangeLower) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ }
+
+ if (attr->rangeUpper) {
+ if ((size/2) > *attr->rangeUpper) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_UNICODE_validate_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ WERROR status;
+ unsigned int i;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ for (i=0; i < in->num_values; i++) {
+ if (in->values[i].length == 0) {
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ status = dsdb_syntax_UNICODE_validate_one_val(ctx,
+ attr,
+ &in->values[i]);
+ if (!W_ERROR_IS_OK(status)) {
+ return status;
+ }
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_one_DN_drsuapi_to_ldb(TALLOC_CTX *mem_ctx, struct ldb_context *ldb,
+ const struct dsdb_syntax *syntax,
+ const DATA_BLOB *in, DATA_BLOB *out)
+{
+ struct drsuapi_DsReplicaObjectIdentifier3 id3;
+ enum ndr_err_code ndr_err;
+ DATA_BLOB guid_blob;
+ struct ldb_dn *dn;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ int ret;
+ NTSTATUS status;
+
+ if (!tmp_ctx) {
+ W_ERROR_HAVE_NO_MEMORY(tmp_ctx);
+ }
+
+ if (in == NULL) {
+ talloc_free(tmp_ctx);
+ return WERR_FOOBAR;
+ }
+
+ if (in->length == 0) {
+ talloc_free(tmp_ctx);
+ return WERR_FOOBAR;
+ }
+
+
+ /* windows sometimes sends an extra two pad bytes here */
+ ndr_err = ndr_pull_struct_blob(in,
+ tmp_ctx, &id3,
+ (ndr_pull_flags_fn_t)ndr_pull_drsuapi_DsReplicaObjectIdentifier3);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ status = ndr_map_error2ntstatus(ndr_err);
+ talloc_free(tmp_ctx);
+ return ntstatus_to_werror(status);
+ }
+
+ dn = ldb_dn_new(tmp_ctx, ldb, id3.dn);
+ if (!dn) {
+ talloc_free(tmp_ctx);
+ /* If this fails, it must be out of memory, as it does not do much parsing */
+ W_ERROR_HAVE_NO_MEMORY(dn);
+ }
+
+ if (!GUID_all_zero(&id3.guid)) {
+ status = GUID_to_ndr_blob(&id3.guid, tmp_ctx, &guid_blob);
+ if (!NT_STATUS_IS_OK(status)) {
+ talloc_free(tmp_ctx);
+ return ntstatus_to_werror(status);
+ }
+
+ ret = ldb_dn_set_extended_component(dn, "GUID", &guid_blob);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return WERR_FOOBAR;
+ }
+ talloc_free(guid_blob.data);
+ }
+
+ if (id3.__ndr_size_sid) {
+ DATA_BLOB sid_blob;
+ ndr_err = ndr_push_struct_blob(&sid_blob, tmp_ctx, &id3.sid,
+ (ndr_push_flags_fn_t)ndr_push_dom_sid);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ status = ndr_map_error2ntstatus(ndr_err);
+ talloc_free(tmp_ctx);
+ return ntstatus_to_werror(status);
+ }
+
+ ret = ldb_dn_set_extended_component(dn, "SID", &sid_blob);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return WERR_FOOBAR;
+ }
+ }
+
+ *out = data_blob_string_const(ldb_dn_get_extended_linearized(mem_ctx, dn, 1));
+ talloc_free(tmp_ctx);
+ W_ERROR_HAVE_NO_MEMORY(out->data);
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_DN_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ WERROR status = dsdb_syntax_one_DN_drsuapi_to_ldb(out->values, ctx->ldb, attr->syntax,
+ in->value_ctr.values[i].blob,
+ &out->values[i]);
+ if (!W_ERROR_IS_OK(status)) {
+ return status;
+ }
+
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_DN_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ out->attid = dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values = in->num_values;
+ out->value_ctr.values = talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ struct drsuapi_DsReplicaObjectIdentifier3 id3;
+ enum ndr_err_code ndr_err;
+ struct ldb_dn *dn;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ NTSTATUS status;
+
+ W_ERROR_HAVE_NO_MEMORY(tmp_ctx);
+
+ out->value_ctr.values[i].blob = &blobs[i];
+
+ dn = ldb_dn_from_ldb_val(tmp_ctx, ctx->ldb, &in->values[i]);
+
+ W_ERROR_HAVE_NO_MEMORY(dn);
+
+ ZERO_STRUCT(id3);
+
+ status = dsdb_get_extended_dn_guid(dn, &id3.guid, "GUID");
+ if (!NT_STATUS_IS_OK(status) &&
+ !NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) {
+ talloc_free(tmp_ctx);
+ return ntstatus_to_werror(status);
+ }
+
+ status = dsdb_get_extended_dn_sid(dn, &id3.sid, "SID");
+ if (!NT_STATUS_IS_OK(status) &&
+ !NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) {
+ talloc_free(tmp_ctx);
+ return ntstatus_to_werror(status);
+ }
+
+ id3.dn = ldb_dn_get_linearized(dn);
+
+ ndr_err = ndr_push_struct_blob(&blobs[i], blobs, &id3, (ndr_push_flags_fn_t)ndr_push_drsuapi_DsReplicaObjectIdentifier3);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ status = ndr_map_error2ntstatus(ndr_err);
+ talloc_free(tmp_ctx);
+ return ntstatus_to_werror(status);
+ }
+ talloc_free(tmp_ctx);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_DN_validate_one_val(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_val *val,
+ TALLOC_CTX *mem_ctx,
+ struct dsdb_dn **_dsdb_dn)
+{
+ static const char * const extended_list[] = { "GUID", "SID", NULL };
+ enum ndr_err_code ndr_err;
+ struct GUID guid;
+ struct dom_sid sid;
+ const DATA_BLOB *sid_blob;
+ struct dsdb_dn *dsdb_dn;
+ struct ldb_dn *dn;
+ char *dn_str;
+ struct ldb_dn *dn2;
+ char *dn2_str;
+ int num_components;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ NTSTATUS status;
+
+ W_ERROR_HAVE_NO_MEMORY(tmp_ctx);
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ dsdb_dn = dsdb_dn_parse(tmp_ctx, ctx->ldb, val,
+ attr->syntax->ldap_oid);
+ if (!dsdb_dn) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ dn = dsdb_dn->dn;
+
+ dn2 = ldb_dn_copy(tmp_ctx, dn);
+ if (dn == NULL) {
+ talloc_free(tmp_ctx);
+ return WERR_NOT_ENOUGH_MEMORY;
+ }
+
+ num_components = ldb_dn_get_comp_num(dn);
+
+ status = dsdb_get_extended_dn_guid(dn, &guid, "GUID");
+ if (NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) {
+ num_components++;
+ } else if (!NT_STATUS_IS_OK(status)) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ sid_blob = ldb_dn_get_extended_component(dn, "SID");
+ if (sid_blob) {
+ num_components++;
+ ndr_err = ndr_pull_struct_blob_all(sid_blob,
+ tmp_ctx,
+ &sid,
+ (ndr_pull_flags_fn_t)ndr_pull_dom_sid);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ }
+
+ /* Do not allow links to the RootDSE */
+ if (num_components == 0) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ /*
+ * We need to check that only "GUID" and "SID" are
+ * specified as extended components, we do that
+ * by comparing the dn's after removing all components
+ * from one dn and only the allowed subset from the other
+ * one.
+ */
+ ldb_dn_extended_filter(dn, extended_list);
+
+ dn_str = ldb_dn_get_extended_linearized(tmp_ctx, dn, 0);
+ if (dn_str == NULL) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ dn2_str = ldb_dn_get_extended_linearized(tmp_ctx, dn2, 0);
+ if (dn2_str == NULL) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ if (strcmp(dn_str, dn2_str) != 0) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ *_dsdb_dn = talloc_move(mem_ctx, &dsdb_dn);
+ talloc_free(tmp_ctx);
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_DN_validate_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ unsigned int i;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ for (i=0; i < in->num_values; i++) {
+ WERROR status;
+ struct dsdb_dn *dsdb_dn;
+ TALLOC_CTX *tmp_ctx = talloc_new(ctx->ldb);
+ W_ERROR_HAVE_NO_MEMORY(tmp_ctx);
+
+ status = dsdb_syntax_DN_validate_one_val(ctx,
+ attr,
+ &in->values[i],
+ tmp_ctx, &dsdb_dn);
+ if (!W_ERROR_IS_OK(status)) {
+ talloc_free(tmp_ctx);
+ return status;
+ }
+
+ if (dsdb_dn->dn_format != DSDB_NORMAL_DN) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ talloc_free(tmp_ctx);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_DN_BINARY_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+ int ret;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ struct drsuapi_DsReplicaObjectIdentifier3Binary id3;
+ enum ndr_err_code ndr_err;
+ DATA_BLOB guid_blob;
+ struct ldb_dn *dn;
+ struct dsdb_dn *dsdb_dn;
+ NTSTATUS status;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ if (!tmp_ctx) {
+ W_ERROR_HAVE_NO_MEMORY(tmp_ctx);
+ }
+
+ if (in->value_ctr.values[i].blob == NULL) {
+ talloc_free(tmp_ctx);
+ return WERR_FOOBAR;
+ }
+
+ if (in->value_ctr.values[i].blob->length == 0) {
+ talloc_free(tmp_ctx);
+ return WERR_FOOBAR;
+ }
+
+
+ /* windows sometimes sends an extra two pad bytes here */
+ ndr_err = ndr_pull_struct_blob(in->value_ctr.values[i].blob,
+ tmp_ctx, &id3,
+ (ndr_pull_flags_fn_t)ndr_pull_drsuapi_DsReplicaObjectIdentifier3Binary);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ status = ndr_map_error2ntstatus(ndr_err);
+ talloc_free(tmp_ctx);
+ return ntstatus_to_werror(status);
+ }
+
+ dn = ldb_dn_new(tmp_ctx, ctx->ldb, id3.dn);
+ if (!dn) {
+ talloc_free(tmp_ctx);
+ /* If this fails, it must be out of memory, as it does not do much parsing */
+ W_ERROR_HAVE_NO_MEMORY(dn);
+ }
+
+ if (!GUID_all_zero(&id3.guid)) {
+ status = GUID_to_ndr_blob(&id3.guid, tmp_ctx, &guid_blob);
+ if (!NT_STATUS_IS_OK(status)) {
+ talloc_free(tmp_ctx);
+ return ntstatus_to_werror(status);
+ }
+
+ ret = ldb_dn_set_extended_component(dn, "GUID", &guid_blob);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return WERR_FOOBAR;
+ }
+ talloc_free(guid_blob.data);
+ }
+
+ if (id3.__ndr_size_sid) {
+ DATA_BLOB sid_blob;
+ ndr_err = ndr_push_struct_blob(&sid_blob, tmp_ctx, &id3.sid,
+ (ndr_push_flags_fn_t)ndr_push_dom_sid);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ status = ndr_map_error2ntstatus(ndr_err);
+ talloc_free(tmp_ctx);
+ return ntstatus_to_werror(status);
+ }
+
+ ret = ldb_dn_set_extended_component(dn, "SID", &sid_blob);
+ if (ret != LDB_SUCCESS) {
+ talloc_free(tmp_ctx);
+ return WERR_FOOBAR;
+ }
+ }
+
+ /* set binary stuff */
+ dsdb_dn = dsdb_dn_construct(tmp_ctx, dn, id3.binary, attr->syntax->ldap_oid);
+ if (!dsdb_dn) {
+ if (errno == EINVAL) {
+ /*
+ * This might be Object(OR-Name)
+ * failing because of a non empty
+ * binary part.
+ */
+ talloc_free(tmp_ctx);
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+ talloc_free(tmp_ctx);
+ W_ERROR_HAVE_NO_MEMORY(dsdb_dn);
+ }
+ out->values[i] = data_blob_string_const(dsdb_dn_get_extended_linearized(out->values, dsdb_dn, 1));
+ talloc_free(tmp_ctx);
+ W_ERROR_HAVE_NO_MEMORY(out->values[i].data);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_DN_BINARY_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ out->attid = dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values = in->num_values;
+ out->value_ctr.values = talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ struct drsuapi_DsReplicaObjectIdentifier3Binary id3;
+ enum ndr_err_code ndr_err;
+ const DATA_BLOB *sid_blob;
+ struct dsdb_dn *dsdb_dn;
+ TALLOC_CTX *tmp_ctx = talloc_new(mem_ctx);
+ NTSTATUS status;
+
+ W_ERROR_HAVE_NO_MEMORY(tmp_ctx);
+
+ out->value_ctr.values[i].blob = &blobs[i];
+
+ dsdb_dn = dsdb_dn_parse(tmp_ctx, ctx->ldb, &in->values[i], attr->syntax->ldap_oid);
+
+ if (!dsdb_dn) {
+ talloc_free(tmp_ctx);
+ return ntstatus_to_werror(NT_STATUS_INVALID_PARAMETER);
+ }
+
+ ZERO_STRUCT(id3);
+
+ status = dsdb_get_extended_dn_guid(dsdb_dn->dn, &id3.guid, "GUID");
+ if (!NT_STATUS_IS_OK(status) &&
+ !NT_STATUS_EQUAL(status, NT_STATUS_OBJECT_NAME_NOT_FOUND)) {
+ talloc_free(tmp_ctx);
+ return ntstatus_to_werror(status);
+ }
+
+ sid_blob = ldb_dn_get_extended_component(dsdb_dn->dn, "SID");
+ if (sid_blob) {
+
+ ndr_err = ndr_pull_struct_blob_all(sid_blob,
+ tmp_ctx, &id3.sid,
+ (ndr_pull_flags_fn_t)ndr_pull_dom_sid);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ status = ndr_map_error2ntstatus(ndr_err);
+ talloc_free(tmp_ctx);
+ return ntstatus_to_werror(status);
+ }
+ }
+
+ id3.dn = ldb_dn_get_linearized(dsdb_dn->dn);
+
+ /* get binary stuff */
+ id3.binary = dsdb_dn->extra_part;
+
+ ndr_err = ndr_push_struct_blob(&blobs[i], blobs, &id3, (ndr_push_flags_fn_t)ndr_push_drsuapi_DsReplicaObjectIdentifier3Binary);
+ if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
+ status = ndr_map_error2ntstatus(ndr_err);
+ talloc_free(tmp_ctx);
+ return ntstatus_to_werror(status);
+ }
+ talloc_free(tmp_ctx);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_DN_BINARY_validate_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ unsigned int i;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ for (i=0; i < in->num_values; i++) {
+ WERROR status;
+ struct dsdb_dn *dsdb_dn;
+ TALLOC_CTX *tmp_ctx = talloc_new(ctx->ldb);
+ W_ERROR_HAVE_NO_MEMORY(tmp_ctx);
+
+ status = dsdb_syntax_DN_validate_one_val(ctx,
+ attr,
+ &in->values[i],
+ tmp_ctx, &dsdb_dn);
+ if (!W_ERROR_IS_OK(status)) {
+ talloc_free(tmp_ctx);
+ return status;
+ }
+
+ if (dsdb_dn->dn_format != DSDB_BINARY_DN) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ status = dsdb_syntax_DATA_BLOB_validate_one_val(ctx,
+ attr,
+ &dsdb_dn->extra_part);
+ if (!W_ERROR_IS_OK(status)) {
+ talloc_free(tmp_ctx);
+ return status;
+ }
+
+ talloc_free(tmp_ctx);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_DN_STRING_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ return dsdb_syntax_DN_BINARY_drsuapi_to_ldb(ctx,
+ attr,
+ in,
+ mem_ctx,
+ out);
+}
+
+static WERROR dsdb_syntax_DN_STRING_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ return dsdb_syntax_DN_BINARY_ldb_to_drsuapi(ctx,
+ attr,
+ in,
+ mem_ctx,
+ out);
+}
+
+static WERROR dsdb_syntax_DN_STRING_validate_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ unsigned int i;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ for (i=0; i < in->num_values; i++) {
+ WERROR status;
+ struct dsdb_dn *dsdb_dn;
+ TALLOC_CTX *tmp_ctx = talloc_new(ctx->ldb);
+ W_ERROR_HAVE_NO_MEMORY(tmp_ctx);
+
+ status = dsdb_syntax_DN_validate_one_val(ctx,
+ attr,
+ &in->values[i],
+ tmp_ctx, &dsdb_dn);
+ if (!W_ERROR_IS_OK(status)) {
+ talloc_free(tmp_ctx);
+ return status;
+ }
+
+ if (dsdb_dn->dn_format != DSDB_STRING_DN) {
+ talloc_free(tmp_ctx);
+ return WERR_DS_INVALID_ATTRIBUTE_SYNTAX;
+ }
+
+ status = dsdb_syntax_UNICODE_validate_one_val(ctx,
+ attr,
+ &dsdb_dn->extra_part);
+ if (!W_ERROR_IS_OK(status)) {
+ talloc_free(tmp_ctx);
+ return status;
+ }
+
+ talloc_free(tmp_ctx);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_PRESENTATION_ADDRESS_drsuapi_to_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out)
+{
+ unsigned int i;
+
+ out->flags = 0;
+ out->name = talloc_strdup(mem_ctx, attr->lDAPDisplayName);
+ W_ERROR_HAVE_NO_MEMORY(out->name);
+
+ out->num_values = in->value_ctr.num_values;
+ out->values = talloc_array(mem_ctx, struct ldb_val, out->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->values);
+
+ for (i=0; i < out->num_values; i++) {
+ size_t len;
+ size_t converted_size = 0;
+ char *str;
+
+ if (in->value_ctr.values[i].blob == NULL) {
+ return WERR_FOOBAR;
+ }
+
+ if (in->value_ctr.values[i].blob->length < 4) {
+ return WERR_FOOBAR;
+ }
+
+ len = IVAL(in->value_ctr.values[i].blob->data, 0);
+
+ if (len != in->value_ctr.values[i].blob->length) {
+ return WERR_FOOBAR;
+ }
+
+ if (!convert_string_talloc(out->values, CH_UTF16, CH_UNIX,
+ in->value_ctr.values[i].blob->data+4,
+ in->value_ctr.values[i].blob->length-4,
+ (void **)&str, &converted_size)) {
+ return WERR_FOOBAR;
+ }
+
+ out->values[i] = data_blob_string_const(str);
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_PRESENTATION_ADDRESS_ldb_to_drsuapi(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in,
+ TALLOC_CTX *mem_ctx,
+ struct drsuapi_DsReplicaAttribute *out)
+{
+ unsigned int i;
+ DATA_BLOB *blobs;
+
+ if (attr->attributeID_id == DRSUAPI_ATTID_INVALID) {
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ out->attid = dsdb_attribute_get_attid(attr,
+ ctx->is_schema_nc);
+ out->value_ctr.num_values = in->num_values;
+ out->value_ctr.values = talloc_array(mem_ctx,
+ struct drsuapi_DsAttributeValue,
+ in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(out->value_ctr.values);
+
+ blobs = talloc_array(mem_ctx, DATA_BLOB, in->num_values);
+ W_ERROR_HAVE_NO_MEMORY(blobs);
+
+ for (i=0; i < in->num_values; i++) {
+ uint8_t *data;
+ size_t ret;
+
+ out->value_ctr.values[i].blob = &blobs[i];
+
+ if (!convert_string_talloc(blobs, CH_UNIX, CH_UTF16,
+ in->values[i].data,
+ in->values[i].length,
+ (void **)&data, &ret)) {
+ return WERR_FOOBAR;
+ }
+
+ blobs[i] = data_blob_talloc(blobs, NULL, 4 + ret);
+ W_ERROR_HAVE_NO_MEMORY(blobs[i].data);
+
+ SIVAL(blobs[i].data, 0, 4 + ret);
+
+ if (ret > 0) {
+ memcpy(blobs[i].data + 4, data, ret);
+ talloc_free(data);
+ }
+ }
+
+ return WERR_OK;
+}
+
+static WERROR dsdb_syntax_PRESENTATION_ADDRESS_validate_ldb(const struct dsdb_syntax_ctx *ctx,
+ const struct dsdb_attribute *attr,
+ const struct ldb_message_element *in)
+{
+ return dsdb_syntax_UNICODE_validate_ldb(ctx,
+ attr,
+ in);
+}
+
+#define OMOBJECTCLASS(val) { .length = sizeof(val) - 1, .data = discard_const_p(uint8_t, val) }
+
+static const struct dsdb_syntax dsdb_syntaxes[] = {
+ {
+ .name = "Boolean",
+ .ldap_oid = LDB_SYNTAX_BOOLEAN,
+ .oMSyntax = 1,
+ .attributeSyntax_oid = "2.5.5.8",
+ .drsuapi_to_ldb = dsdb_syntax_BOOL_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_BOOL_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_BOOL_validate_ldb,
+ .equality = "booleanMatch",
+ .comment = "Boolean",
+ .auto_normalise = true
+ },{
+ .name = "Integer",
+ .ldap_oid = LDB_SYNTAX_INTEGER,
+ .oMSyntax = 2,
+ .attributeSyntax_oid = "2.5.5.9",
+ .drsuapi_to_ldb = dsdb_syntax_INT32_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_INT32_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_INT32_validate_ldb,
+ .equality = "integerMatch",
+ .comment = "Integer",
+ .ldb_syntax = LDB_SYNTAX_SAMBA_INT32,
+ .auto_normalise = true
+ },{
+ .name = "String(Octet)",
+ .ldap_oid = LDB_SYNTAX_OCTET_STRING,
+ .oMSyntax = 4,
+ .attributeSyntax_oid = "2.5.5.10",
+ .drsuapi_to_ldb = dsdb_syntax_DATA_BLOB_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_DATA_BLOB_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_DATA_BLOB_validate_ldb,
+ .equality = "octetStringMatch",
+ .comment = "Octet String",
+ .userParameters = true,
+ .ldb_syntax = LDB_SYNTAX_SAMBA_OCTET_STRING
+ },{
+ .name = "String(Sid)",
+ .ldap_oid = LDB_SYNTAX_OCTET_STRING,
+ .oMSyntax = 4,
+ .attributeSyntax_oid = "2.5.5.17",
+ .drsuapi_to_ldb = dsdb_syntax_DATA_BLOB_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_DATA_BLOB_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_DATA_BLOB_validate_ldb,
+ .equality = "octetStringMatch",
+ .comment = "Octet String - Security Identifier (SID)",
+ .ldb_syntax = LDB_SYNTAX_SAMBA_SID
+ },{
+ .name = "String(Object-Identifier)",
+ .ldap_oid = "1.3.6.1.4.1.1466.115.121.1.38",
+ .oMSyntax = 6,
+ .attributeSyntax_oid = "2.5.5.2",
+ .drsuapi_to_ldb = dsdb_syntax_OID_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_OID_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_OID_validate_ldb,
+ .equality = "caseIgnoreMatch", /* Would use "objectIdentifierMatch" but most are ldap attribute/class names */
+ .comment = "OID String",
+ .ldb_syntax = LDB_SYNTAX_DIRECTORY_STRING
+ },{
+ .name = "Enumeration",
+ .ldap_oid = LDB_SYNTAX_INTEGER,
+ .oMSyntax = 10,
+ .attributeSyntax_oid = "2.5.5.9",
+ .drsuapi_to_ldb = dsdb_syntax_INT32_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_INT32_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_INT32_validate_ldb,
+ .ldb_syntax = LDB_SYNTAX_SAMBA_INT32,
+ .auto_normalise = true
+ },{
+ /* not used in w2k3 forest */
+ .name = "String(Numeric)",
+ .ldap_oid = "1.3.6.1.4.1.1466.115.121.1.36",
+ .oMSyntax = 18,
+ .attributeSyntax_oid = "2.5.5.6",
+ .drsuapi_to_ldb = dsdb_syntax_DATA_BLOB_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_DATA_BLOB_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_DATA_BLOB_validate_ldb,
+ .equality = "numericStringMatch",
+ .substring = "numericStringSubstringsMatch",
+ .comment = "Numeric String",
+ .ldb_syntax = LDB_SYNTAX_DIRECTORY_STRING,
+ },{
+ .name = "String(Printable)",
+ .ldap_oid = "1.3.6.1.4.1.1466.115.121.1.44",
+ .oMSyntax = 19,
+ .attributeSyntax_oid = "2.5.5.5",
+ .drsuapi_to_ldb = dsdb_syntax_DATA_BLOB_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_DATA_BLOB_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_DATA_BLOB_validate_ldb,
+ .ldb_syntax = LDB_SYNTAX_SAMBA_OCTET_STRING,
+ },{
+ .name = "String(Teletex)",
+ .ldap_oid = "1.2.840.113556.1.4.905",
+ .oMSyntax = 20,
+ .attributeSyntax_oid = "2.5.5.4",
+ .drsuapi_to_ldb = dsdb_syntax_DATA_BLOB_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_DATA_BLOB_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_DATA_BLOB_validate_ldb,
+ .equality = "caseIgnoreMatch",
+ .substring = "caseIgnoreSubstringsMatch",
+ .comment = "Case Insensitive String",
+ .ldb_syntax = LDB_SYNTAX_DIRECTORY_STRING,
+ },{
+ .name = "String(IA5)",
+ .ldap_oid = "1.3.6.1.4.1.1466.115.121.1.26",
+ .oMSyntax = 22,
+ .attributeSyntax_oid = "2.5.5.5",
+ .drsuapi_to_ldb = dsdb_syntax_DATA_BLOB_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_DATA_BLOB_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_DATA_BLOB_validate_ldb,
+ .equality = "caseExactIA5Match",
+ .comment = "Printable String",
+ .ldb_syntax = LDB_SYNTAX_SAMBA_OCTET_STRING,
+ },{
+ .name = "String(UTC-Time)",
+ .ldap_oid = "1.3.6.1.4.1.1466.115.121.1.53",
+ .oMSyntax = 23,
+ .attributeSyntax_oid = "2.5.5.11",
+ .drsuapi_to_ldb = dsdb_syntax_NTTIME_UTC_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_NTTIME_UTC_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_NTTIME_UTC_validate_ldb,
+ .equality = "generalizedTimeMatch",
+ .comment = "UTC Time",
+ .auto_normalise = true
+ },{
+ .name = "String(Generalized-Time)",
+ .ldap_oid = "1.3.6.1.4.1.1466.115.121.1.24",
+ .oMSyntax = 24,
+ .attributeSyntax_oid = "2.5.5.11",
+ .drsuapi_to_ldb = dsdb_syntax_NTTIME_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_NTTIME_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_NTTIME_validate_ldb,
+ .equality = "generalizedTimeMatch",
+ .comment = "Generalized Time",
+ .auto_normalise = true
+ },{
+ /* not used in w2k3 schema */
+ .name = "String(Case Sensitive)",
+ .ldap_oid = "1.2.840.113556.1.4.1362",
+ .oMSyntax = 27,
+ .attributeSyntax_oid = "2.5.5.3",
+ .drsuapi_to_ldb = dsdb_syntax_DATA_BLOB_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_DATA_BLOB_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_DATA_BLOB_validate_ldb,
+ .equality = "caseExactMatch",
+ .substring = "caseExactSubstringsMatch",
+ /* TODO (kim): according to LDAP rfc we should be using same comparison
+ * as Directory String (LDB_SYNTAX_DIRECTORY_STRING), but case sensitive.
+ * But according to ms docs binary compare should do the job:
+ * http://msdn.microsoft.com/en-us/library/cc223200(v=PROT.10).aspx */
+ .ldb_syntax = LDB_SYNTAX_SAMBA_OCTET_STRING,
+ },{
+ .name = "String(Unicode)",
+ .ldap_oid = LDB_SYNTAX_DIRECTORY_STRING,
+ .oMSyntax = 64,
+ .attributeSyntax_oid = "2.5.5.12",
+ .drsuapi_to_ldb = dsdb_syntax_UNICODE_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_UNICODE_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_UNICODE_validate_ldb,
+ .equality = "caseIgnoreMatch",
+ .substring = "caseIgnoreSubstringsMatch",
+ .comment = "Directory String",
+ },{
+ .name = "Interval/LargeInteger",
+ .ldap_oid = "1.2.840.113556.1.4.906",
+ .oMSyntax = 65,
+ .attributeSyntax_oid = "2.5.5.16",
+ .drsuapi_to_ldb = dsdb_syntax_INT64_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_INT64_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_INT64_validate_ldb,
+ .equality = "integerMatch",
+ .comment = "Large Integer",
+ .ldb_syntax = LDB_SYNTAX_ORDERED_INTEGER,
+ .auto_normalise = true
+ },{
+ .name = "String(NT-Sec-Desc)",
+ .ldap_oid = LDB_SYNTAX_SAMBA_SECURITY_DESCRIPTOR,
+ .oMSyntax = 66,
+ .attributeSyntax_oid = "2.5.5.15",
+ .drsuapi_to_ldb = dsdb_syntax_DATA_BLOB_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_DATA_BLOB_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_DATA_BLOB_validate_ldb,
+ },{
+ .name = "Object(DS-DN)",
+ .ldap_oid = LDB_SYNTAX_DN,
+ .oMSyntax = 127,
+ .oMObjectClass = OMOBJECTCLASS("\x2b\x0c\x02\x87\x73\x1c\x00\x85\x4a"),
+ .attributeSyntax_oid = "2.5.5.1",
+ .drsuapi_to_ldb = dsdb_syntax_DN_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_DN_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_DN_validate_ldb,
+ .equality = "distinguishedNameMatch",
+ .comment = "Object(DS-DN) == a DN",
+ },{
+ .name = "Object(DN-Binary)",
+ .ldap_oid = DSDB_SYNTAX_BINARY_DN,
+ .oMSyntax = 127,
+ .oMObjectClass = OMOBJECTCLASS("\x2a\x86\x48\x86\xf7\x14\x01\x01\x01\x0b"),
+ .attributeSyntax_oid = "2.5.5.7",
+ .drsuapi_to_ldb = dsdb_syntax_DN_BINARY_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_DN_BINARY_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_DN_BINARY_validate_ldb,
+ .equality = "octetStringMatch",
+ .comment = "OctetString: Binary+DN",
+ },{
+ /* not used in w2k3 schema, but used in Exchange schema*/
+ .name = "Object(OR-Name)",
+ .ldap_oid = DSDB_SYNTAX_OR_NAME,
+ .oMSyntax = 127,
+ .oMObjectClass = OMOBJECTCLASS("\x56\x06\x01\x02\x05\x0b\x1D"),
+ .attributeSyntax_oid = "2.5.5.7",
+ .drsuapi_to_ldb = dsdb_syntax_DN_BINARY_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_DN_BINARY_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_DN_validate_ldb,
+ .equality = "distinguishedNameMatch",
+ .ldb_syntax = LDB_SYNTAX_DN,
+ },{
+ /*
+ * TODO: verify if DATA_BLOB is correct here...!
+ *
+ * repsFrom and repsTo are the only attributes using
+ * this attribute syntax, but they're not replicated...
+ */
+ .name = "Object(Replica-Link)",
+ .ldap_oid = "1.3.6.1.4.1.1466.115.121.1.40",
+ .oMSyntax = 127,
+ .oMObjectClass = OMOBJECTCLASS("\x2a\x86\x48\x86\xf7\x14\x01\x01\x01\x06"),
+ .attributeSyntax_oid = "2.5.5.10",
+ .drsuapi_to_ldb = dsdb_syntax_DATA_BLOB_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_DATA_BLOB_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_DATA_BLOB_validate_ldb,
+ },{
+ .name = "Object(Presentation-Address)",
+ .ldap_oid = "1.3.6.1.4.1.1466.115.121.1.43",
+ .oMSyntax = 127,
+ .oMObjectClass = OMOBJECTCLASS("\x2b\x0c\x02\x87\x73\x1c\x00\x85\x5c"),
+ .attributeSyntax_oid = "2.5.5.13",
+ .drsuapi_to_ldb = dsdb_syntax_PRESENTATION_ADDRESS_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_PRESENTATION_ADDRESS_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_PRESENTATION_ADDRESS_validate_ldb,
+ .comment = "Presentation Address",
+ .ldb_syntax = LDB_SYNTAX_DIRECTORY_STRING,
+ },{
+ /* not used in w2k3 schema */
+ .name = "Object(Access-Point)",
+ .ldap_oid = DSDB_SYNTAX_ACCESS_POINT,
+ .oMSyntax = 127,
+ .oMObjectClass = OMOBJECTCLASS("\x2b\x0c\x02\x87\x73\x1c\x00\x85\x3e"),
+ .attributeSyntax_oid = "2.5.5.14",
+ .drsuapi_to_ldb = dsdb_syntax_FOOBAR_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_FOOBAR_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_FOOBAR_validate_ldb,
+ .ldb_syntax = LDB_SYNTAX_DIRECTORY_STRING,
+ },{
+ /* not used in w2k3 schema */
+ .name = "Object(DN-String)",
+ .ldap_oid = DSDB_SYNTAX_STRING_DN,
+ .oMSyntax = 127,
+ .oMObjectClass = OMOBJECTCLASS("\x2a\x86\x48\x86\xf7\x14\x01\x01\x01\x0c"),
+ .attributeSyntax_oid = "2.5.5.14",
+ .drsuapi_to_ldb = dsdb_syntax_DN_STRING_drsuapi_to_ldb,
+ .ldb_to_drsuapi = dsdb_syntax_DN_STRING_ldb_to_drsuapi,
+ .validate_ldb = dsdb_syntax_DN_STRING_validate_ldb,
+ .equality = "octetStringMatch",
+ .comment = "OctetString: String+DN",
+ }
+};
+
+const struct dsdb_syntax *find_syntax_map_by_ad_oid(const char *ad_oid)
+{
+ unsigned int i;
+ for (i=0; i < ARRAY_SIZE(dsdb_syntaxes); i++) {
+ if (strcasecmp(ad_oid, dsdb_syntaxes[i].attributeSyntax_oid) == 0) {
+ return &dsdb_syntaxes[i];
+ }
+ }
+ return NULL;
+}
+
+const struct dsdb_syntax *find_syntax_map_by_ad_syntax(int oMSyntax)
+{
+ unsigned int i;
+ for (i=0; i < ARRAY_SIZE(dsdb_syntaxes); i++) {
+ if (oMSyntax == dsdb_syntaxes[i].oMSyntax) {
+ return &dsdb_syntaxes[i];
+ }
+ }
+ return NULL;
+}
+
+const struct dsdb_syntax *find_syntax_map_by_standard_oid(const char *standard_oid)
+{
+ unsigned int i;
+ for (i=0; i < ARRAY_SIZE(dsdb_syntaxes); i++) {
+ if (strcasecmp(standard_oid, dsdb_syntaxes[i].ldap_oid) == 0) {
+ return &dsdb_syntaxes[i];
+ }
+ }
+ return NULL;
+}
+
+const struct dsdb_syntax *dsdb_syntax_for_attribute(const struct dsdb_attribute *attr)
+{
+ unsigned int i;
+
+ for (i=0; i < ARRAY_SIZE(dsdb_syntaxes); i++) {
+ /*
+ * We must pretend that userParameters was declared
+ * binary string, so we can store the 'UTF16' (not
+ * really string) structure as given over SAMR to samba
+ */
+ if (dsdb_syntaxes[i].userParameters &&
+ (strcasecmp(attr->lDAPDisplayName, "userParameters") == 0))
+ {
+ return &dsdb_syntaxes[i];
+ }
+ if (attr->oMSyntax != dsdb_syntaxes[i].oMSyntax) continue;
+
+ if (attr->oMObjectClass.length != dsdb_syntaxes[i].oMObjectClass.length) continue;
+
+ if (attr->oMObjectClass.length) {
+ int ret;
+ ret = memcmp(attr->oMObjectClass.data,
+ dsdb_syntaxes[i].oMObjectClass.data,
+ attr->oMObjectClass.length);
+ if (ret != 0) continue;
+ }
+
+ if (strcmp(attr->attributeSyntax_oid, dsdb_syntaxes[i].attributeSyntax_oid) != 0) continue;
+
+ return &dsdb_syntaxes[i];
+ }
+
+ return NULL;
+}
+
+WERROR dsdb_attribute_drsuapi_remote_to_local(const struct dsdb_syntax_ctx *ctx,
+ enum drsuapi_DsAttributeId remote_attid_as_enum,
+ enum drsuapi_DsAttributeId *local_attid_as_enum,
+ const struct dsdb_attribute **_sa)
+{
+ TALLOC_CTX *frame = talloc_stackframe();
+ const struct dsdb_attribute *sa;
+ uint32_t attid_local;
+ bool ok;
+
+ if (ctx->pfm_remote == NULL) {
+ smb_panic(__location__);
+ }
+
+ switch (dsdb_pfm_get_attid_type(remote_attid_as_enum)) {
+ case DSDB_ATTID_TYPE_PFM:
+ /* map remote ATTID to local ATTID */
+ ok = dsdb_syntax_attid_from_remote_attid(ctx, frame,
+ remote_attid_as_enum,
+ &attid_local);
+ if (!ok) {
+ DEBUG(0,(__location__ ": Can't find local ATTID for 0x%08X\n",
+ remote_attid_as_enum));
+ TALLOC_FREE(frame);
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+ break;
+ case DSDB_ATTID_TYPE_INTID:
+ /* use IntId value directly */
+ attid_local = remote_attid_as_enum;
+ break;
+ default:
+ /* we should never get here */
+ DEBUG(0,(__location__ ": Invalid ATTID type passed for conversion - 0x%08X\n",
+ remote_attid_as_enum));
+ TALLOC_FREE(frame);
+ return WERR_INVALID_PARAMETER;
+ }
+
+ sa = dsdb_attribute_by_attributeID_id(ctx->schema, attid_local);
+ if (!sa) {
+ int dbg_level = ctx->schema->resolving_in_progress ? 10 : 0;
+ DEBUG(dbg_level,(__location__ ": Unknown local attributeID_id 0x%08X remote 0x%08X%s\n",
+ attid_local, remote_attid_as_enum,
+ ctx->schema->resolving_in_progress ? "resolving in progress" : ""));
+ TALLOC_FREE(frame);
+ return WERR_DS_ATT_NOT_DEF_IN_SCHEMA;
+ }
+
+ /*
+ * We return the same class of attid as we were given. That
+ * is, we trust the remote server not to use an
+ * msDS-IntId value in the schema partition
+ */
+ if (local_attid_as_enum != NULL) {
+ *local_attid_as_enum = (enum drsuapi_DsAttributeId)attid_local;
+ }
+
+ if (_sa != NULL) {
+ *_sa = sa;
+ }
+
+ TALLOC_FREE(frame);
+ return WERR_OK;
+}
+
+WERROR dsdb_attribute_drsuapi_to_ldb(struct ldb_context *ldb,
+ const struct dsdb_schema *schema,
+ const struct dsdb_schema_prefixmap *pfm_remote,
+ const struct drsuapi_DsReplicaAttribute *in,
+ TALLOC_CTX *mem_ctx,
+ struct ldb_message_element *out,
+ enum drsuapi_DsAttributeId *local_attid_as_enum)
+{
+ struct dsdb_syntax_ctx syntax_ctx;
+ const struct dsdb_attribute *sa = NULL;
+ WERROR werr;
+
+ /* use default syntax conversion context */
+ dsdb_syntax_ctx_init(&syntax_ctx, ldb, schema);
+ syntax_ctx.pfm_remote = pfm_remote;
+
+ werr = dsdb_attribute_drsuapi_remote_to_local(&syntax_ctx,
+ in->attid,
+ local_attid_as_enum,
+ &sa);
+ if (!W_ERROR_IS_OK(werr)) {
+ return werr;
+ }
+
+ return sa->syntax->drsuapi_to_ldb(&syntax_ctx, sa, in, mem_ctx, out);
+}
diff --git a/source4/dsdb/schema/tests/schema_syntax.c b/source4/dsdb/schema/tests/schema_syntax.c
new file mode 100644
index 0000000..7eba102
--- /dev/null
+++ b/source4/dsdb/schema/tests/schema_syntax.c
@@ -0,0 +1,271 @@
+/*
+ Unix SMB/CIFS implementation.
+
+ Test DSDB syntax functions
+
+ Copyright (C) Andrew Bartlet <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 "lib/events/events.h"
+#include <ldb.h>
+#include <ldb_errors.h>
+#include "lib/ldb-samba/ldif_handlers.h"
+#include "ldb_wrap.h"
+#include "dsdb/samdb/samdb.h"
+#include "param/param.h"
+#include "torture/smbtorture.h"
+#include "torture/local/proto.h"
+#include "param/provision.h"
+
+
+struct torture_dsdb_syntax {
+ struct ldb_context *ldb;
+ struct dsdb_schema *schema;
+};
+
+DATA_BLOB hexstr_to_data_blob(TALLOC_CTX *mem_ctx, const char *string)
+{
+ DATA_BLOB binary = data_blob_talloc(mem_ctx, NULL, strlen(string)/2);
+ binary.length = strhex_to_str((char *)binary.data, binary.length, string, strlen(string));
+ return binary;
+}
+
+static bool torture_syntax_add_OR_Name(struct torture_context *tctx,
+ struct ldb_context *ldb,
+ struct dsdb_schema *schema)
+{
+ WERROR werr;
+ int ldb_res;
+ struct ldb_ldif *ldif;
+ const char *ldif_str = "dn: CN=ms-Exch-Auth-Orig,CN=Schema,CN=Configuration,DC=kma-exch,DC=devel\n"
+ "changetype: add\n"
+ "cn: ms-Exch-Auth-Orig\n"
+ "attributeID: 1.2.840.113556.1.2.129\n"
+ "attributeSyntax: 2.5.5.7\n"
+ "isSingleValued: FALSE\n"
+ "linkID: 110\n"
+ "showInAdvancedViewOnly: TRUE\n"
+ "adminDisplayName: ms-Exch-Auth-Orig\n"
+ "oMObjectClass:: VgYBAgULHQ==\n"
+ "adminDescription: ms-Exch-Auth-Orig\n"
+ "oMSyntax: 127\n"
+ "searchFlags: 16\n"
+ "lDAPDisplayName: authOrig\n"
+ "name: ms-Exch-Auth-Orig\n"
+ "objectGUID:: 7tqEWktjAUqsZXqsFPQpRg==\n"
+ "schemaIDGUID:: l3PfqOrF0RG7ywCAx2ZwwA==\n"
+ "attributeSecurityGUID:: VAGN5Pi80RGHAgDAT7lgUA==\n"
+ "isMemberOfPartialAttributeSet: TRUE\n";
+
+ ldif = ldb_ldif_read_string(ldb, &ldif_str);
+ torture_assert(tctx, ldif, "Failed to parse LDIF for authOrig");
+
+ werr = dsdb_set_attribute_from_ldb(ldb, schema, ldif->msg);
+ ldb_ldif_read_free(ldb, ldif);
+ torture_assert_werr_ok(tctx, werr, "dsdb_set_attribute_from_ldb() failed!");
+
+ ldb_res = dsdb_set_schema(ldb, schema, SCHEMA_WRITE);
+ torture_assert_int_equal(tctx, ldb_res, LDB_SUCCESS, "dsdb_set_schema() failed");
+
+ return true;
+};
+
+static bool torture_test_syntax(struct torture_context *torture,
+ struct torture_dsdb_syntax *priv,
+ const char *oid,
+ const char *attr_string,
+ const char *ldb_str,
+ const char *drs_str)
+{
+ TALLOC_CTX *tmp_ctx = talloc_new(torture);
+ DATA_BLOB drs_binary = hexstr_to_data_blob(tmp_ctx, drs_str);
+ DATA_BLOB ldb_blob = data_blob_string_const(ldb_str);
+ struct drsuapi_DsReplicaAttribute drs, drs2;
+ struct drsuapi_DsAttributeValue val;
+ const struct dsdb_syntax *syntax;
+ const struct dsdb_attribute *attr;
+ struct ldb_message_element el;
+ struct ldb_context *ldb = priv->ldb;
+ struct dsdb_schema *schema = priv->schema;
+ struct dsdb_syntax_ctx syntax_ctx;
+
+ /* use default syntax conversion context */
+ dsdb_syntax_ctx_init(&syntax_ctx, ldb, schema);
+
+ drs.value_ctr.num_values = 1;
+ drs.value_ctr.values = &val;
+ val.blob = &drs_binary;
+
+ torture_assert(torture, syntax = find_syntax_map_by_standard_oid(oid), "Failed to find syntax handler");
+ torture_assert(torture, attr = dsdb_attribute_by_lDAPDisplayName(schema, attr_string), "Failed to find attribute handler");
+ torture_assert_str_equal(torture, attr->syntax->name, syntax->name, "Syntax from schema not as expected");
+
+
+ torture_assert_werr_ok(torture, syntax->drsuapi_to_ldb(&syntax_ctx, attr, &drs, tmp_ctx, &el), "Failed to convert from DRS to ldb format");
+
+ torture_assert_data_blob_equal(torture, el.values[0], ldb_blob, "Incorrect conversion from DRS to ldb format");
+
+ torture_assert_werr_ok(torture, syntax->validate_ldb(&syntax_ctx, attr, &el), "Failed to validate ldb format");
+
+ torture_assert_werr_ok(torture, syntax->ldb_to_drsuapi(&syntax_ctx, attr, &el, tmp_ctx, &drs2), "Failed to convert from ldb to DRS format");
+
+ torture_assert(torture, drs2.value_ctr.values[0].blob, "No blob returned from conversion");
+
+ torture_assert_data_blob_equal(torture, *drs2.value_ctr.values[0].blob, drs_binary, "Incorrect conversion from ldb to DRS format");
+ return true;
+}
+
+static bool torture_dsdb_drs_DN_BINARY(struct torture_context *torture, struct torture_dsdb_syntax *priv)
+{
+ bool ret;
+ const char *ldb_str = "B:32:A9D1CA15768811D1ADED00C04FD8D5CD:<GUID=a8378c29-6319-45b3-b216-6a3108452d6c>;CN=Users,DC=ad,DC=ruth,DC=abartlet,DC=net";
+ const char *drs_str = "8C00000000000000298C37A81963B345B2166A3108452D6C000000000000000000000000000000000000000000000000000000002900000043004E003D00550073006500720073002C00440043003D00610064002C00440043003D0072007500740068002C00440043003D00610062006100720074006C00650074002C00440043003D006E0065007400000014000000A9D1CA15768811D1ADED00C04FD8D5CD";
+ const char *ldb_str2 = "B:8:00000002:<GUID=2b475208-3180-4ad4-b6bb-a26cfb44ac50>;<SID=S-1-5-21-3686369990-3108025515-1819299124>;DC=ad,DC=ruth,DC=abartlet,DC=net";
+ const char *drs_str2 = "7A000000180000000852472B8031D44AB6BBA26CFB44AC50010400000000000515000000C68AB9DBABB440B9344D706C0000000020000000440043003D00610064002C00440043003D0072007500740068002C00440043003D00610062006100720074006C00650074002C00440043003D006E0065007400000000000800000000000002";
+ ret = torture_test_syntax(torture, priv, DSDB_SYNTAX_BINARY_DN, "wellKnownObjects", ldb_str, drs_str);
+ if (!ret) return false;
+ return torture_test_syntax(torture, priv, DSDB_SYNTAX_BINARY_DN, "msDS-HasInstantiatedNCs", ldb_str2, drs_str2);
+}
+
+static bool torture_dsdb_drs_DN(struct torture_context *torture, struct torture_dsdb_syntax *priv)
+{
+ const char *ldb_str = "<GUID=fbee08fd-6f75-4bd4-af3f-e4f063a6379e>;OU=Domain Controllers,DC=ad,DC=naomi,DC=abartlet,DC=net";
+ const char *drs_str = "A800000000000000FD08EEFB756FD44BAF3FE4F063A6379E00000000000000000000000000000000000000000000000000000000370000004F0055003D0044006F006D00610069006E00200043006F006E00740072006F006C006C006500720073002C00440043003D00610064002C00440043003D006E0061006F006D0069002C00440043003D00610062006100720074006C00650074002C00440043003D006E00650074000000";
+ if (!torture_test_syntax(torture, priv, LDB_SYNTAX_DN, "lastKnownParent", ldb_str, drs_str)) {
+ return false;
+ }
+
+ /* extended_dn with GUID and SID in it */
+ ldb_str = "<GUID=23cc7d16-3da0-4f3a-9921-0ad60a99230f>;<SID=S-1-5-21-3427639452-1671929926-2759570404-500>;CN=Administrator,CN=Users,DC=kma-exch,DC=devel";
+ drs_str = "960000001C000000167DCC23A03D3A4F99210AD60A99230F0105000000000005150000009CA04DCC46A0A763E4B37BA4F40100002E00000043004E003D00410064006D0069006E006900730074007200610074006F0072002C0043004E003D00550073006500720073002C00440043003D006B006D0061002D0065007800630068002C00440043003D0064006500760065006C000000";
+ return torture_test_syntax(torture, priv, LDB_SYNTAX_DN, "lastKnownParent", ldb_str, drs_str);
+}
+
+static bool torture_dsdb_drs_OR_Name(struct torture_context *torture, struct torture_dsdb_syntax *priv)
+{
+ const char *ldb_str = "<GUID=23cc7d16-3da0-4f3a-9921-0ad60a99230f>;<SID=S-1-5-21-3427639452-1671929926-2759570404-500>;CN=Administrator,CN=Users,DC=kma-exch,DC=devel";
+ const char *drs_str = "960000001C000000167DCC23A03D3A4F99210AD60A99230F0105000000000005150000009CA04DCC46A0A763E4B37BA4F40100002E00000043004E003D00410064006D0069006E006900730074007200610074006F0072002C0043004E003D00550073006500720073002C00440043003D006B006D0061002D0065007800630068002C00440043003D0064006500760065006C000000000004000000";
+ return torture_test_syntax(torture, priv, DSDB_SYNTAX_OR_NAME, "authOrig", ldb_str, drs_str);
+}
+
+static bool torture_dsdb_drs_INT32(struct torture_context *torture, struct torture_dsdb_syntax *priv)
+{
+ const char *ldb_str = "532480";
+ const char *drs_str = "00200800";
+ return torture_test_syntax(torture, priv, LDB_SYNTAX_INTEGER, "userAccountControl", ldb_str, drs_str);
+}
+
+static bool torture_dsdb_drs_INT64(struct torture_context *torture, struct torture_dsdb_syntax *priv)
+{
+ const char *ldb_str = "129022979538281250";
+ const char *drs_str = "22E33D5FB761CA01";
+ return torture_test_syntax(torture, priv, "1.2.840.113556.1.4.906", "pwdLastSet", ldb_str, drs_str);
+}
+
+static bool torture_dsdb_drs_NTTIME(struct torture_context *torture, struct torture_dsdb_syntax *priv)
+{
+ const char *ldb_str = "20091109003446.0Z";
+ const char *drs_str = "A6F4070103000000";
+ return torture_test_syntax(torture, priv, "1.3.6.1.4.1.1466.115.121.1.24", "whenCreated", ldb_str, drs_str);
+}
+
+static bool torture_dsdb_drs_BOOL(struct torture_context *torture, struct torture_dsdb_syntax *priv)
+{
+ const char *ldb_str = "TRUE";
+ const char *drs_str = "01000000";
+ return torture_test_syntax(torture, priv, LDB_SYNTAX_BOOLEAN, "isDeleted", ldb_str, drs_str);
+}
+
+static bool torture_dsdb_drs_UNICODE(struct torture_context *torture, struct torture_dsdb_syntax *priv)
+{
+ const char *ldb_str = "primaryTelexNumber,Numéro de télex";
+ const char *drs_str = "7000720069006D00610072007900540065006C00650078004E0075006D006200650072002C004E0075006D00E90072006F0020006400650020007400E9006C0065007800";
+ return torture_test_syntax(torture, priv, LDB_SYNTAX_DIRECTORY_STRING, "attributeDisplayNames", ldb_str, drs_str);
+}
+
+/*
+ * DSDB-SYNTAX fixture setup/teardown handlers implementation
+ */
+static bool torture_dsdb_syntax_tcase_setup(struct torture_context *tctx, void **data)
+{
+ struct torture_dsdb_syntax *priv;
+
+ priv = talloc_zero(tctx, struct torture_dsdb_syntax);
+ torture_assert(tctx, priv, "No memory");
+
+ priv->ldb = provision_get_schema(priv, tctx->lp_ctx, NULL, NULL);
+ torture_assert(tctx, priv->ldb, "Failed to load schema from disk");
+
+ priv->schema = dsdb_get_schema(priv->ldb, NULL);
+ torture_assert(tctx, priv->schema, "Failed to fetch schema");
+
+ /* add 'authOrig' attribute with OR-Name syntax to schema */
+ if (!torture_syntax_add_OR_Name(tctx, priv->ldb, priv->schema)) {
+ return false;
+ }
+
+ *data = priv;
+ return true;
+}
+
+static bool torture_dsdb_syntax_tcase_teardown(struct torture_context *tctx, void *data)
+{
+ struct torture_dsdb_syntax *priv;
+
+ priv = talloc_get_type_abort(data, struct torture_dsdb_syntax);
+ talloc_unlink(priv, priv->ldb);
+ talloc_free(priv);
+
+ return true;
+}
+
+/**
+ * DSDB-SYNTAX test suite creation
+ */
+struct torture_suite *torture_dsdb_syntax(TALLOC_CTX *mem_ctx)
+{
+ typedef bool (*pfn_run)(struct torture_context *, void *);
+
+ struct torture_tcase *tc;
+ struct torture_suite *suite = torture_suite_create(mem_ctx, "dsdb.syntax");
+
+ if (suite == NULL) {
+ return NULL;
+ }
+
+ tc = torture_suite_add_tcase(suite, "tc");
+ if (!tc) {
+ return NULL;
+ }
+
+ torture_tcase_set_fixture(tc,
+ torture_dsdb_syntax_tcase_setup,
+ torture_dsdb_syntax_tcase_teardown);
+
+ torture_tcase_add_simple_test(tc, "DN-BINARY", (pfn_run)torture_dsdb_drs_DN_BINARY);
+ torture_tcase_add_simple_test(tc, "DN", (pfn_run)torture_dsdb_drs_DN);
+ torture_tcase_add_simple_test(tc, "OR-Name", (pfn_run)torture_dsdb_drs_OR_Name);
+ torture_tcase_add_simple_test(tc, "INT32", (pfn_run)torture_dsdb_drs_INT32);
+ torture_tcase_add_simple_test(tc, "INT64", (pfn_run)torture_dsdb_drs_INT64);
+ torture_tcase_add_simple_test(tc, "NTTIME", (pfn_run)torture_dsdb_drs_NTTIME);
+ torture_tcase_add_simple_test(tc, "BOOL", (pfn_run)torture_dsdb_drs_BOOL);
+ torture_tcase_add_simple_test(tc, "UNICODE", (pfn_run)torture_dsdb_drs_UNICODE);
+
+ suite->description = talloc_strdup(suite, "DSDB syntax tests");
+
+ return suite;
+}
diff --git a/source4/dsdb/tests/python/acl.py b/source4/dsdb/tests/python/acl.py
new file mode 100755
index 0000000..cc2b31c
--- /dev/null
+++ b/source4/dsdb/tests/python/acl.py
@@ -0,0 +1,5795 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This is unit with tests for LDAP access checks
+
+import optparse
+import sys
+import base64
+import re
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests import DynamicTestCase
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+from samba.common import get_string
+
+import samba.getopt as options
+from samba.join import DCJoinContext
+
+from ldb import (
+ SCOPE_BASE, SCOPE_ONELEVEL, SCOPE_SUBTREE, LdbError, ERR_NO_SUCH_OBJECT,
+ ERR_UNWILLING_TO_PERFORM, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+from ldb import ERR_CONSTRAINT_VIOLATION
+from ldb import ERR_OPERATIONS_ERROR
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_REPLACE, FLAG_MOD_ADD, FLAG_MOD_DELETE
+from samba.dcerpc import security, drsuapi, misc
+
+from samba.auth import system_session
+from samba import gensec, sd_utils, werror
+from samba.samdb import SamDB
+from samba.credentials import Credentials, DONT_USE_KERBEROS
+import samba.tests
+from samba.tests import delete_force
+import samba.dsdb
+from samba.tests.password_test import PasswordCommon
+from samba.ndr import ndr_pack
+
+parser = optparse.OptionParser("acl.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+ start = host.rindex("://")
+ host = host.lstrip(start + 3)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+#
+# Tests start here
+#
+
+
+class AclTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(AclTests, self).setUp()
+
+ strict_checking = samba.tests.env_get_var_value('STRICT_CHECKING', allow_missing=True)
+ if strict_checking is None:
+ strict_checking = '1'
+ self.strict_checking = bool(int(strict_checking))
+
+ self.ldb_admin = SamDB(ldaphost, credentials=creds, session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb_admin.domain_dn()
+ self.domain_sid = security.dom_sid(self.ldb_admin.get_domain_sid())
+ self.user_pass = "samba123@"
+ self.configuration_dn = self.ldb_admin.get_config_basedn().get_linearized()
+ self.sd_utils = sd_utils.SDUtils(self.ldb_admin)
+ self.addCleanup(self.delete_admin_connection)
+ # used for anonymous login
+ self.creds_tmp = Credentials()
+ self.creds_tmp.set_username("")
+ self.creds_tmp.set_password("")
+ self.creds_tmp.set_domain(creds.get_domain())
+ self.creds_tmp.set_realm(creds.get_realm())
+ self.creds_tmp.set_workstation(creds.get_workstation())
+ print("baseDN: %s" % self.base_dn)
+
+ # set AttributeAuthorizationOnLDAPAdd and BlockOwnerImplicitRights
+ self.set_heuristic(samba.dsdb.DS_HR_ATTR_AUTHZ_ON_LDAP_ADD, b'11')
+
+ def set_heuristic(self, index, values):
+ self.assertGreater(index, 0)
+ self.assertLess(index, 30)
+ self.assertIsInstance(values, bytes)
+
+ # Get the old "dSHeuristics" if it was set
+ dsheuristics = self.ldb_admin.get_dsheuristics()
+ # Reset the "dSHeuristics" as they were before
+ self.addCleanup(self.ldb_admin.set_dsheuristics, dsheuristics)
+ # Set the "dSHeuristics" to activate the correct behaviour
+ default_heuristics = b"000000000100000000020000000003"
+ if dsheuristics is None:
+ dsheuristics = b""
+ dsheuristics += default_heuristics[len(dsheuristics):]
+ dsheuristics = (dsheuristics[:index - 1] +
+ values +
+ dsheuristics[index - 1 + len(values):])
+ self.ldb_admin.set_dsheuristics(dsheuristics)
+
+ def get_user_dn(self, name):
+ return "CN=%s,CN=Users,%s" % (name, self.base_dn)
+
+ def get_ldb_connection(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop
+ ldb_target = SamDB(url=ldaphost, credentials=creds_tmp, lp=lp)
+ return ldb_target
+
+ # Test if we have any additional groups for users than default ones
+ def assert_user_no_group_member(self, username):
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)" % self.get_user_dn(username))
+ try:
+ self.assertEqual(res[0]["memberOf"][0], "")
+ except KeyError:
+ pass
+ else:
+ self.fail()
+
+ def delete_admin_connection(self):
+ del self.sd_utils
+ del self.ldb_admin
+
+# tests on ldap add operations
+
+
+class AclAddTests(AclTests):
+
+ def setUp(self):
+ super(AclAddTests, self).setUp()
+ # Domain admin that will be creator of OU parent-child structure
+ self.usr_admin_owner = "acl_add_user1"
+ # Second domain admin that will not be creator of OU parent-child structure
+ self.usr_admin_not_owner = "acl_add_user2"
+ # Regular user
+ self.regular_user = "acl_add_user3"
+ self.regular_user2 = "acl_add_user4"
+ self.regular_user3 = "acl_add_user5"
+ self.test_user1 = "test_add_user1"
+ self.test_user2 = "test_add_user2"
+ self.test_user3 = "test_add_user3"
+ self.test_user4 = "test_add_user4"
+ self.test_group1 = "test_add_group1"
+ self.ou1 = "OU=test_add_ou1"
+ self.ou2 = "OU=test_add_ou2,%s" % self.ou1
+ delete_force(self.ldb_admin, self.get_user_dn(self.usr_admin_owner))
+ delete_force(self.ldb_admin, self.get_user_dn(self.usr_admin_not_owner))
+ delete_force(self.ldb_admin, self.get_user_dn(self.regular_user))
+ delete_force(self.ldb_admin, self.get_user_dn(self.regular_user2))
+ self.ldb_admin.newuser(self.usr_admin_owner, self.user_pass)
+ self.ldb_admin.newuser(self.usr_admin_not_owner, self.user_pass)
+ self.ldb_admin.newuser(self.regular_user, self.user_pass)
+ self.ldb_admin.newuser(self.regular_user2, self.user_pass)
+
+ # add admins to the Domain Admins group
+ self.ldb_admin.add_remove_group_members("Domain Admins", [self.usr_admin_owner],
+ add_members_operation=True)
+ self.ldb_admin.add_remove_group_members("Domain Admins", [self.usr_admin_not_owner],
+ add_members_operation=True)
+
+ self.ldb_owner = self.get_ldb_connection(self.usr_admin_owner, self.user_pass)
+ self.ldb_notowner = self.get_ldb_connection(self.usr_admin_not_owner, self.user_pass)
+ self.ldb_user = self.get_ldb_connection(self.regular_user, self.user_pass)
+ self.ldb_user2 = self.get_ldb_connection(self.regular_user2, self.user_pass)
+
+ def tearDown(self):
+ super(AclAddTests, self).tearDown()
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" %
+ (self.test_user1, self.ou2, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" %
+ (self.test_user1, self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" %
+ (self.test_user2, self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" %
+ (self.test_user3, self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" %
+ (self.test_user4, self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" %
+ (self.test_group1, self.ou2, self.base_dn))
+ delete_force(self.ldb_admin, "CN=test_computer2,%s,%s" %
+ (self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "CN=test_computer1,%s,%s" %
+ (self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "%s,%s" % (self.ou2, self.base_dn))
+ delete_force(self.ldb_admin, "%s,%s" % (self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, self.get_user_dn(self.usr_admin_owner))
+ delete_force(self.ldb_admin, self.get_user_dn(self.usr_admin_not_owner))
+ delete_force(self.ldb_admin, self.get_user_dn(self.regular_user))
+ delete_force(self.ldb_admin, self.get_user_dn(self.regular_user2))
+ delete_force(self.ldb_admin, self.get_user_dn("test_add_anonymous"))
+
+ del self.ldb_notowner
+ del self.ldb_owner
+ del self.ldb_user
+ del self.ldb_user2
+
+ # Make sure top OU is deleted (and so everything under it)
+ def assert_top_ou_deleted(self):
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s,%s)" % (
+ "OU=test_add_ou1", self.base_dn))
+ self.assertEqual(len(res), 0)
+
+ def test_add_u1(self):
+ """Testing OU with the rights of Domain Admin not creator of the OU """
+ self.assert_top_ou_deleted()
+ # Change descriptor for top level OU
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ self.ldb_owner.create_ou("OU=test_add_ou2,OU=test_add_ou1," + self.base_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.usr_admin_not_owner))
+ mod = "(D;CI;WPCC;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ # Test user and group creation with another domain admin's credentials
+ self.ldb_notowner.newuser(self.test_user1, self.user_pass, userou=self.ou2)
+ self.ldb_notowner.newgroup("test_add_group1", groupou="OU=test_add_ou2,OU=test_add_ou1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ # Make sure we HAVE created the two objects -- user and group
+ # !!! We should not be able to do that, but however because of ACE ordering our inherited Deny ACE
+ # !!! comes after explicit (A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA) that comes from somewhere
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s,%s)" % ("CN=test_add_user1,OU=test_add_ou2,OU=test_add_ou1", self.base_dn))
+ self.assertGreater(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s,%s)" % ("CN=test_add_group1,OU=test_add_ou2,OU=test_add_ou1", self.base_dn))
+ self.assertGreater(len(res), 0)
+
+ def test_add_u2(self):
+ """Testing OU with the regular user that has no rights granted over the OU """
+ self.assert_top_ou_deleted()
+ # Create a parent-child OU structure with domain admin credentials
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ self.ldb_owner.create_ou("OU=test_add_ou2,OU=test_add_ou1," + self.base_dn)
+ # Test user and group creation with regular user credentials
+ try:
+ self.ldb_user.newuser(self.test_user1, self.user_pass, userou=self.ou2)
+ self.ldb_user.newgroup("test_add_group1", groupou="OU=test_add_ou2,OU=test_add_ou1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+ # Make sure we HAVEN'T created any of two objects -- user or group
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s,%s)" % ("CN=test_add_user1,OU=test_add_ou2,OU=test_add_ou1", self.base_dn))
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s,%s)" % ("CN=test_add_group1,OU=test_add_ou2,OU=test_add_ou1", self.base_dn))
+ self.assertEqual(len(res), 0)
+
+ def test_add_u3(self):
+ """Testing OU with the rights of regular user granted the right 'Create User child objects' """
+ self.assert_top_ou_deleted()
+ # Change descriptor for top level OU
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = "(OA;CI;CC;bf967aba-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ self.ldb_owner.create_ou("OU=test_add_ou2,OU=test_add_ou1," + self.base_dn)
+ # Test user and group creation with granted user only to one of the objects
+ self.ldb_user.newuser(self.test_user1, self.user_pass, userou=self.ou2, setpassword=False)
+ try:
+ self.ldb_user.newgroup("test_add_group1", groupou="OU=test_add_ou2,OU=test_add_ou1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+ # Make sure we HAVE created the one of two objects -- user
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s,%s)" %
+ ("CN=test_add_user1,OU=test_add_ou2,OU=test_add_ou1",
+ self.base_dn))
+ self.assertNotEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s,%s)" %
+ ("CN=test_add_group1,OU=test_add_ou2,OU=test_add_ou1",
+ self.base_dn))
+ self.assertEqual(len(res), 0)
+
+ def test_add_u4(self):
+ """ 4 Testing OU with the rights of Domain Admin creator of the OU"""
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ self.ldb_owner.create_ou("OU=test_add_ou2,OU=test_add_ou1," + self.base_dn)
+ self.ldb_owner.newuser(self.test_user1, self.user_pass, userou=self.ou2)
+ self.ldb_owner.newgroup("test_add_group1", groupou="OU=test_add_ou2,OU=test_add_ou1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ # Make sure we have successfully created the two objects -- user and group
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s,%s)" % ("CN=test_add_user1,OU=test_add_ou2,OU=test_add_ou1", self.base_dn))
+ self.assertGreater(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s,%s)" % ("CN=test_add_group1,OU=test_add_ou2,OU=test_add_ou1", self.base_dn))
+ self.assertGreater(len(res), 0)
+
+ def test_add_c1(self):
+ """Testing adding a computer object with the rights of regular user granted the right 'Create Computer child objects' """
+ self.assert_top_ou_deleted()
+ # Change descriptor for top level OU
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = f"(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ mod = f"(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_SERVICE_PRINCIPAL_NAME};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+
+ # Add a computer object, specifying an explicit SD to grant WP to the creator
+ print("Test adding a user with explicit nTSecurityDescriptor")
+ wp_ace = "(A;;WP;;;%s)" % str(user_sid)
+ tmp_desc = security.descriptor.from_sddl("D:%s" % wp_ace, self.domain_sid)
+ dn = "CN=%s,OU=test_add_ou1,%s" % (self.test_user1, self.base_dn)
+ samaccountname = self.test_user1 + "$"
+ # This should fail, the user has no WD or WO
+ try:
+ self.ldb_user.add({
+ "dn": dn,
+ "objectclass": "computer",
+ "sAMAccountName": samaccountname,
+ "userAccountControl": str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ "servicePrincipalName": "host/" + self.test_user1,
+ "nTSecurityDescriptor": ndr_pack(tmp_desc)})
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_add_c2(self):
+ """Testing adding a computer object with the rights of regular user granted the right 'Create User child objects' and WO"""
+ self.assert_top_ou_deleted()
+ # Change descriptor for top level OU
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = f"(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ mod = f"(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_SERVICE_PRINCIPAL_NAME};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ # Grant WO, we should still not be able to specify a DACL
+ mod = "(A;CI;WO;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ # Add a computer object, specifying an explicit SD to grant WP to the creator
+ print("Test adding a user with explicit nTSecurityDescriptor")
+ wp_ace = "(A;;WP;;;%s)" % str(user_sid)
+ tmp_desc = security.descriptor.from_sddl("D:%s" % wp_ace, self.domain_sid)
+ dn = "CN=%s,OU=test_add_ou1,%s" % (self.test_user1, self.base_dn)
+ samaccountname = self.test_user1 + "$"
+ # This should fail, the user has no WD
+ try:
+ self.ldb_user.add({
+ "dn": dn,
+ "objectclass": "computer",
+ "sAMAccountName": samaccountname,
+ "userAccountControl": str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ "servicePrincipalName": "host/" + self.test_user1,
+ "nTSecurityDescriptor": ndr_pack(tmp_desc)})
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ # We still cannot modify the owner or group
+ sd_sddl = f"O:{user_sid}G:{user_sid}"
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ "dn": dn,
+ "objectclass": "computer",
+ "sAMAccountName": samaccountname,
+ "userAccountControl": str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ "servicePrincipalName": "host/" + self.test_user1,
+ "nTSecurityDescriptor": ndr_pack(tmp_desc)})
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_add_c3(self):
+ """Testing adding a computer object with the rights of regular user granted the right 'Create Computer child objects' and WD"""
+ self.assert_top_ou_deleted()
+ # Change descriptor for top level OU
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = f"(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ mod = f"(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_SERVICE_PRINCIPAL_NAME};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ # Grant WD, we should still not be able to specify a DACL
+ mod = "(A;CI;WD;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ # Add a computer object, specifying an explicit SD to grant WP to the creator
+ print("Test adding a user with explicit nTSecurityDescriptor")
+ wp_ace = "(A;;WP;;;%s)" % str(user_sid)
+ sd_sddl = f"O:{user_sid}G:BA"
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ dn = "CN=%s,OU=test_add_ou1,%s" % (self.test_user1, self.base_dn)
+ samaccountname = self.test_user1 + "$"
+ # The user has no WO, but this succeeds, because WD means we skip further per-attribute checks
+ try:
+ self.ldb_user.add({
+ "dn": dn,
+ "objectclass": "computer",
+ "sAMAccountName": samaccountname,
+ "userAccountControl": str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ "servicePrincipalName": "host/" + self.test_user1,
+ "nTSecurityDescriptor": ndr_pack(tmp_desc)})
+ except LdbError as e3:
+ self.fail(str(e3))
+
+ # we should be able to modify the DACL
+ tmp_desc = security.descriptor.from_sddl("D:%s" % wp_ace, self.domain_sid)
+ dn = "CN=%s,OU=test_add_ou1,%s" % (self.test_user2, self.base_dn)
+ samaccountname = self.test_user2 + "$"
+ try:
+ self.ldb_user.add({
+ "dn": dn,
+ "objectclass": "computer",
+ "sAMAccountName": samaccountname,
+ "userAccountControl": str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ "servicePrincipalName": "host/" + self.test_user2,
+ "nTSecurityDescriptor": ndr_pack(tmp_desc)})
+ except LdbError as e3:
+ self.fail(str(e3))
+
+ # verify the ace is present
+ new_sd = self.sd_utils.get_sd_as_sddl("CN=test_add_user2,OU=test_add_ou1,%s" %
+ self.base_dn)
+ self.assertIn(wp_ace, new_sd)
+
+ def test_add_c4(self):
+ """Testing adding a computer object with the rights of regular user granted the right 'Create User child objects' and WDWO"""
+ self.assert_top_ou_deleted()
+ # Change descriptor for top level OU
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = f"(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ mod = f"(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_SERVICE_PRINCIPAL_NAME};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ # Grant WD and WO, we should be able to update the SD
+ mod = "(A;CI;WDWO;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ # Add a computer object, specifying an explicit SD to grant WP to the creator
+ print("Test adding a user with explicit nTSecurityDescriptor")
+ wp_ace = "(A;;WP;;;%s)" % str(user_sid)
+ sd_sddl = "O:%sG:BAD:(A;;WP;;;%s)" % (str(user_sid), str(user_sid))
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ dn = "CN=%s,OU=test_add_ou1,%s" % (self.test_user1, self.base_dn)
+ samaccountname = self.test_user1 + "$"
+ try:
+ self.ldb_user.add({
+ "dn": dn,
+ "objectclass": "computer",
+ "sAMAccountName": samaccountname,
+ "userAccountControl": str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ "servicePrincipalName": "host/" + self.test_user1,
+ "nTSecurityDescriptor": ndr_pack(tmp_desc)})
+ except LdbError as e3:
+ self.fail(str(e3))
+
+ # verify the owner and group is present
+ new_sd = self.sd_utils.get_sd_as_sddl("CN=test_add_user1,OU=test_add_ou1,%s" %
+ self.base_dn)
+ self.assertIn(f"O:{user_sid}G:BA", new_sd)
+ self.assertIn(wp_ace, new_sd)
+
+ def test_add_c5(self):
+ """Testing adding a computer with an optional attribute """
+ self.assert_top_ou_deleted()
+ # Change descriptor for top level OU
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = f"(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ # servicePrincipalName
+ mod = f"(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_SERVICE_PRINCIPAL_NAME};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ dn = "CN=%s,OU=test_add_ou1,%s" % (self.test_user3, self.base_dn)
+ samaccountname = self.test_user3 + "$"
+ try:
+ self.ldb_user.add({
+ "dn": dn,
+ "objectclass": "computer",
+ "sAMAccountName": samaccountname,
+ "userAccountControl": str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ "servicePrincipalName": "host/" + self.test_user3,
+ "department": "Ministry of Silly Walks"})
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ # grant WP for that attribute and try again
+ mod = f"(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_DEPARTMENT};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ try:
+ self.ldb_user.add({
+ "dn": dn,
+ "objectclass": "computer",
+ "sAMAccountName": samaccountname,
+ "userAccountControl": str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ "servicePrincipalName": "host/" + self.test_user3,
+ "department": "Ministry of Silly Walks"})
+ except LdbError as e3:
+ self.fail(str(e3))
+
+ def test_add_c6(self):
+ """Test creating a computer with a mandatory attribute(sAMAccountName)"""
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = f"(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ # servicePrincipalName
+ mod = f"(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_SERVICE_PRINCIPAL_NAME};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ # userAccountControl
+ mod = f"(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_USER_ACCOUNT_CONTROL};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ dn = "CN=%s,OU=test_add_ou1,%s" % (self.test_user4, self.base_dn)
+ samaccountname = self.test_user4 + "$"
+ try:
+ self.ldb_user.add({
+ "dn": dn,
+ "objectclass": "computer",
+ "sAMAccountName": samaccountname,
+ "userAccountControl": str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ "servicePrincipalName": "host/" + self.test_user4})
+ except LdbError as e3:
+ self.fail(str(e3))
+
+ def test_add_computer1(self):
+ """Testing Computer with the rights of regular user granted the right 'Create Computer child objects' """
+ self.assert_top_ou_deleted()
+ # Change descriptor for top level OU
+ self.ldb_owner.create_ou("OU=test_add_ou1," + self.base_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = f"(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ mod = f"(OA;CI;SW;{samba.dsdb.DS_GUID_SCHEMA_ATTR_SERVICE_PRINCIPAL_NAME};;CO)"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+
+ # add a Computer object with servicePrincipalName
+ # Creator-Owner has SW from the default SD
+ dn = "CN=test_computer1,OU=test_add_ou1,%s" % (self.base_dn)
+ samaccountname = "test_computer1$"
+ try:
+ self.ldb_user.add({
+ "dn": dn,
+ "objectclass": "computer",
+ "sAMAccountName": samaccountname,
+ "userAccountControl": str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ "servicePrincipalName": "nosuchservice/abcd/abcd"})
+ except LdbError as e3:
+ (num, _) = e3.args
+ if self.strict_checking:
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.assertIn(num, (ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ ERR_CONSTRAINT_VIOLATION))
+ else:
+ self.fail()
+
+ # Inherited Deny from the parent will not work, because of ordering rules
+ mod = f"(OD;CI;SW;{samba.dsdb.DS_GUID_SCHEMA_ATTR_SERVICE_PRINCIPAL_NAME};;{user_sid})"
+ self.sd_utils.dacl_add_ace("OU=test_add_ou1," + self.base_dn, mod)
+ try:
+ self.ldb_user.add({
+ "dn": dn,
+ "objectclass": "computer",
+ "sAMAccountName": samaccountname,
+ "userAccountControl": str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ "servicePrincipalName": "nosuchservice/abcd/abcd"})
+ except LdbError as e3:
+ (num, _) = e3.args
+ if self.strict_checking:
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.assertIn(num, (ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ ERR_CONSTRAINT_VIOLATION))
+ else:
+ self.fail()
+
+ def test_add_optional_attr(self):
+ '''Show that adding a computer object with an optional attribute is disallowed'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'msSFU30Name': 'foo',
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_domain_admins(self):
+ '''Show that adding a computer object with an optional attribute is allowed if the user is a Domain Administrator'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.ldb_admin.add_remove_group_members('Domain Admins',
+ [self.regular_user],
+ add_members_operation=True)
+ ldb_domain_admin = self.get_ldb_connection(self.regular_user,
+ self.user_pass)
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ try:
+ ldb_domain_admin.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'msSFU30Name': 'foo',
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_enterprise_admins(self):
+ '''Show that adding a computer object with an optional attribute is allowed if the user is an Enterprise Administrator'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.ldb_admin.add_remove_group_members('Enterprise Admins',
+ [self.regular_user],
+ add_members_operation=True)
+ ldb_enterprise_admin = self.get_ldb_connection(self.regular_user,
+ self.user_pass)
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ try:
+ ldb_enterprise_admin.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'msSFU30Name': 'foo',
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_non_computer(self):
+ '''Show that adding a non-computer object with an optional attribute is allowed'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_USER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = self.test_user1
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'user',
+ 'sAMAccountName': account_name,
+ 'msSFU30Name': 'foo',
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_derived_computer(self):
+ '''Show that adding an object derived from computer with an optional attribute is disallowed'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_MANAGED_SERVICE_ACCOUNT};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'msDS-ManagedServiceAccount',
+ 'sAMAccountName': account_name,
+ 'msSFU30Name': 'foo',
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_write_dac(self):
+ '''Show that adding a computer object with an optional attribute is allowed if the security descriptor gives WRITE_DAC access'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(A;CI;WD;;;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'msSFU30Name': 'foo',
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_system_must_contain(self):
+ '''Show that adding a computer object with only systemMustContain attributes is allowed'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'instanceType': '4',
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_system_must_contain_denied(self):
+ '''Show that adding a computer object with only systemMustContain attributes is allowed, even when explicitly denied'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(D;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_INSTANCE_TYPE};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'instanceType': '4',
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_unicode_pwd(self):
+ '''Show that adding a computer object with a unicodePwd is allowed'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ password = 'Secret007'
+ utf16pw = f'"{password}"'.encode('utf-16-le')
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'unicodePwd': utf16pw,
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_user_password(self):
+ '''Show that adding a computer object with a userPassword is allowed'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ password = 'Secret007'
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'userPassword': password,
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_user_password_denied(self):
+ '''Show that adding a computer object with a userPassword is allowed, even when explicitly denied'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(D;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_USER_PASSWORD};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ password = 'Secret007'
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'userPassword': password,
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_clear_text_password(self):
+ '''Show that adding a computer object with a clearTextPassword is allowed
+
+Note: this does not work on Windows.'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ password = 'Secret007'.encode('utf-16-le')
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'clearTextPassword': password,
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_disallowed_attr(self):
+ '''Show that adding a computer object with a denied attribute is disallowed'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(D;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_MS_SFU_30};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'msSFU30Name': 'foo',
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_allowed_attr(self):
+ '''Show that adding a computer object with an allowed attribute is allowed'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_MS_SFU_30};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'msSFU30Name': 'foo',
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_optional_attr_heuristic_0(self):
+ '''Show that adding a computer object with an optional attribute is allowed when AttributeAuthorizationOnLDAPAdd == 0'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_ATTR_AUTHZ_ON_LDAP_ADD, b'0')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'msSFU30Name': 'foo',
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_optional_attr_heuristic_2(self):
+ '''Show that adding a computer object with an optional attribute is allowed when AttributeAuthorizationOnLDAPAdd == 2'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_ATTR_AUTHZ_ON_LDAP_ADD, b'2')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'msSFU30Name': 'foo',
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_security_descriptor_implicit_right(self):
+ '''Show that adding a computer object with a security descriptor is allowed when BlockOwnerImplicitRights != 1'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'0')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = f'O:{user_sid}G:{user_sid}'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_security_descriptor_implicit_right_optional_attr(self):
+ '''Show that adding a computer object with a security descriptor and an optional attribute is disallowed when BlockOwnerImplicitRights != 1'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'0')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = f'O:{user_sid}G:{user_sid}'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'msSFU30Name': 'foo',
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_security_descriptor_explicit_right(self):
+ '''Show that a computer object with a security descriptor can be added if BlockOwnerImplicitRights == 1 and WRITE_DAC is granted'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(A;CI;WD;;;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = (f'O:{user_sid}G:{user_sid}'
+ f'D:(A;;WP;;;{user_sid})')
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_security_descriptor_explicit_right_no_owner_disallow(self):
+ '''Show that a computer object with a security descriptor can be added if BlockOwnerImplicitRights == 1, WRITE_DAC is granted, and WRITE_OWNER is denied'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(A;CI;WD;;;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(D;CI;WO;;;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = f'D:(A;;WP;;;{user_sid})'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_security_descriptor_explicit_right_owner_disallow(self):
+ '''Show that a computer object with a security descriptor containing an owner and group can be added if BlockOwnerImplicitRights == 1, WRITE_DAC is granted, and WRITE_OWNER is denied'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(A;CI;WD;;;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(D;CI;WO;;;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = (f'O:{user_sid}G:{user_sid}'
+ f'D:(A;;WP;;;{user_sid})')
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_security_descriptor_explicit_right_sacl(self):
+ '''Show that adding a computer object with a security descriptor containing a SACL is disallowed if BlockOwnerImplicitRights == 1 and WRITE_DAC is granted'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(A;CI;WD;;;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = (f'O:{user_sid}G:{user_sid}'
+ f'D:(A;;WP;;;{user_sid})S:(A;;WP;;;{user_sid})')
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ num, estr = err.args
+ if self.strict_checking:
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn(f'{werror.WERR_PRIVILEGE_NOT_HELD:08X}', estr)
+ else:
+ self.assertIn(num, (ERR_CONSTRAINT_VIOLATION,
+ ERR_INSUFFICIENT_ACCESS_RIGHTS))
+ else:
+ self.fail('expected to fail')
+
+ def test_add_security_descriptor_explicit_right_owner_not_us(self):
+ '''Show that adding a computer object with a security descriptor owned by another is disallowed if BlockOwnerImplicitRights == 1 and WRITE_DAC is granted'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(A;CI;WD;;;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = 'O:BA'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn(f'{werror.WERR_INVALID_OWNER:08X}', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_security_descriptor_explicit_right_owner_not_us_admin(self):
+ '''Show that adding a computer object with a security descriptor owned by another is allowed if BlockOwnerImplicitRights == 1, WRITE_DAC is granted, and we are in Domain Admins'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(A;CI;WD;;;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = 'O:BA'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_no_implicit_right(self):
+ '''Show that adding a computer object without a security descriptor is allowed when BlockOwnerImplicitRights == 1'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ })
+ except LdbError as err:
+ self.fail(err)
+
+ def test_add_security_descriptor_owner(self):
+ '''Show that adding a computer object with a security descriptor containing an owner is disallowed when BlockOwnerImplicitRights == 1'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = f'O:{user_sid}'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_security_descriptor_owner_implicit(self):
+ '''Show that adding a computer object with a security descriptor containing an owner is disallowed when BlockOwnerImplicitRights == 1, even when we are the owner of the OU security descriptor'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+
+ ou_controls = [
+ f'sd_flags:1:{security.SECINFO_OWNER|security.SECINFO_DACL}']
+ ou_sddl = (f'O:{user_sid}'
+ f'D:(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.sd_utils.modify_sd_on_dn(f'{self.ou1},{self.base_dn}', ou_desc,
+ controls=ou_controls)
+
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = f'O:{user_sid}'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_security_descriptor_owner_explicit_right(self):
+ '''Show that adding a computer object with a security descriptor containing an owner is disallowed when BlockOwnerImplicitRights == 1, even with WO'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(A;CI;WO;;;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = f'O:{user_sid}'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_security_descriptor_group(self):
+ '''Show that adding a computer object with a security descriptor containing an group is disallowed when BlockOwnerImplicitRights == 1'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = f'G:{user_sid}'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_security_descriptor_group_explicit_right(self):
+ '''Show that adding a computer object with a security descriptor containing an group is disallowed when BlockOwnerImplicitRights == 1, even with WO'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(A;CI;WO;;;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = f'G:{user_sid}'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_security_descriptor_group_implicit(self):
+ '''Show that adding a computer object with a security descriptor containing an group is disallowed when BlockOwnerImplicitRights == 1, even when we are the owner of the OU security descriptor'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+
+ ou_controls = [
+ f'sd_flags:1:{security.SECINFO_OWNER|security.SECINFO_DACL}']
+ ou_sddl = (f'O:{user_sid}'
+ f'D:(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.sd_utils.modify_sd_on_dn(f'{self.ou1},{self.base_dn}', ou_desc,
+ controls=ou_controls)
+
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = f'G:{user_sid}'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_security_descriptor_dacl(self):
+ '''Show that adding a computer object with a security descriptor containing a DACL is disallowed when BlockOwnerImplicitRights == 1'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = f'D:(A;;WP;;;{user_sid})'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_security_descriptor_dacl_implicit(self):
+ '''Show that adding a computer object with a security descriptor containing a DACL is disallowed when BlockOwnerImplicitRights == 1, even when we are the owner of the OU security descriptor'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+
+ ou_controls = [
+ f'sd_flags:1:{security.SECINFO_OWNER|security.SECINFO_DACL}']
+ ou_sddl = (f'O:{user_sid}'
+ f'D:(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.sd_utils.modify_sd_on_dn(f'{self.ou1},{self.base_dn}', ou_desc,
+ controls=ou_controls)
+
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = f'D:(A;;WP;;;{user_sid})'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_security_descriptor_sacl(self):
+ '''Show that adding a computer object with a security descriptor containing a SACL is disallowed when BlockOwnerImplicitRights == 1'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(
+ f'{self.ou1},{self.base_dn}',
+ f'(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ sd_sddl = f'S:(A;;WP;;;{user_sid})'
+ tmp_desc = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_security_descriptor_empty(self):
+ '''Show that adding a computer object with an empty security descriptor is disallowed when BlockOwnerImplicitRights == 1, even when we are the owner of the OU security descriptor'''
+
+ self.assert_top_ou_deleted()
+ self.ldb_owner.create_ou(f'{self.ou1},{self.base_dn}')
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'1')
+
+ user_sid = self.sd_utils.get_object_sid(
+ self.get_user_dn(self.regular_user))
+
+ ou_controls = [
+ f'sd_flags:1:{security.SECINFO_OWNER|security.SECINFO_DACL}']
+ ou_sddl = (f'O:{user_sid}'
+ f'D:(OA;CI;CC;{samba.dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})')
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.sd_utils.modify_sd_on_dn(f'{self.ou1},{self.base_dn}', ou_desc,
+ controls=ou_controls)
+
+ dn = f'CN={self.test_user1},{self.ou1},{self.base_dn}'
+ account_name = f'{self.test_user1}$'
+ tmp_desc = security.descriptor.from_sddl('', self.domain_sid)
+ try:
+ self.ldb_user.add({
+ 'dn': dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': account_name,
+ 'ntSecurityDescriptor': ndr_pack(tmp_desc),
+ })
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ if self.strict_checking:
+ self.assertIn('000021CC', estr)
+ else:
+ self.fail('expected to fail')
+
+ def test_add_anonymous(self):
+ """Test add operation with anonymous user"""
+ anonymous = SamDB(url=ldaphost, credentials=self.creds_tmp, lp=lp)
+ try:
+ anonymous.newuser("test_add_anonymous", self.user_pass)
+ except LdbError as e2:
+ (num, _) = e2.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ else:
+ self.fail()
+
+# tests on ldap modify operations
+
+
+class AclModifyTests(AclTests):
+
+ def setUp(self):
+ super(AclModifyTests, self).setUp()
+ self.user_with_wp = "acl_mod_user1"
+ self.user_with_sm = "acl_mod_user2"
+ self.user_with_group_sm = "acl_mod_user3"
+ self.ldb_admin.newuser(self.user_with_wp, self.user_pass)
+ self.ldb_admin.newuser(self.user_with_sm, self.user_pass)
+ self.ldb_admin.newuser(self.user_with_group_sm, self.user_pass)
+ self.ldb_user = self.get_ldb_connection(self.user_with_wp, self.user_pass)
+ self.ldb_user2 = self.get_ldb_connection(self.user_with_sm, self.user_pass)
+ self.ldb_user3 = self.get_ldb_connection(self.user_with_group_sm, self.user_pass)
+ self.user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.user_with_wp))
+ self.ldb_admin.newgroup("test_modify_group2", grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ self.ldb_admin.newgroup("test_modify_group3", grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ self.ldb_admin.newuser("test_modify_user2", self.user_pass)
+
+ def tearDown(self):
+ super(AclModifyTests, self).tearDown()
+ delete_force(self.ldb_admin, self.get_user_dn("test_modify_user1"))
+ delete_force(self.ldb_admin, "CN=test_modify_group1,CN=Users," + self.base_dn)
+ delete_force(self.ldb_admin, "CN=test_modify_group2,CN=Users," + self.base_dn)
+ delete_force(self.ldb_admin, "CN=test_modify_group3,CN=Users," + self.base_dn)
+ delete_force(self.ldb_admin, "CN=test_mod_hostname,OU=test_modify_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "CN=test_modify_ou1_user,OU=test_modify_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_modify_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, self.get_user_dn(self.user_with_wp))
+ delete_force(self.ldb_admin, self.get_user_dn(self.user_with_sm))
+ delete_force(self.ldb_admin, self.get_user_dn(self.user_with_group_sm))
+ delete_force(self.ldb_admin, self.get_user_dn("test_modify_user2"))
+ delete_force(self.ldb_admin, self.get_user_dn("test_anonymous"))
+
+ del self.ldb_user
+ del self.ldb_user2
+ del self.ldb_user3
+
+ def get_sd_rights_effective(self, samdb, dn):
+ res = samdb.search(dn,
+ scope=SCOPE_BASE,
+ attrs=['sDRightsEffective'])
+ sd_rights = res[0].get('sDRightsEffective', idx=0)
+ if sd_rights is not None:
+ sd_rights = int(sd_rights)
+
+ return sd_rights
+
+ def test_modify_u1(self):
+ """5 Modify one attribute if you have DS_WRITE_PROPERTY for it"""
+ mod = "(OA;;WP;bf967953-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.user_sid)
+ # First test object -- User
+ print("Testing modify on User object")
+ self.ldb_admin.newuser("test_modify_user1", self.user_pass)
+ self.sd_utils.dacl_add_ace(self.get_user_dn("test_modify_user1"), mod)
+ ldif = """
+dn: """ + self.get_user_dn("test_modify_user1") + """
+changetype: modify
+replace: displayName
+displayName: test_changed"""
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % self.get_user_dn("test_modify_user1"))
+ self.assertEqual(str(res[0]["displayName"][0]), "test_changed")
+ # Second test object -- Group
+ print("Testing modify on Group object")
+ self.ldb_admin.newgroup("test_modify_group1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ self.sd_utils.dacl_add_ace("CN=test_modify_group1,CN=Users," + self.base_dn, mod)
+ ldif = """
+dn: CN=test_modify_group1,CN=Users,""" + self.base_dn + """
+changetype: modify
+replace: displayName
+displayName: test_changed"""
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)" % str("CN=test_modify_group1,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["displayName"][0]), "test_changed")
+ # Third test object -- Organizational Unit
+ print("Testing modify on OU object")
+ #delete_force(self.ldb_admin, "OU=test_modify_ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=test_modify_ou1," + self.base_dn)
+ self.sd_utils.dacl_add_ace("OU=test_modify_ou1," + self.base_dn, mod)
+ ldif = """
+dn: OU=test_modify_ou1,""" + self.base_dn + """
+changetype: modify
+replace: displayName
+displayName: test_changed"""
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)" % str("OU=test_modify_ou1," + self.base_dn))
+ self.assertEqual(str(res[0]["displayName"][0]), "test_changed")
+
+ def test_modify_u2(self):
+ """6 Modify two attributes as you have DS_WRITE_PROPERTY granted only for one of them"""
+ mod = "(OA;;WP;bf967953-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.user_sid)
+ # First test object -- User
+ print("Testing modify on User object")
+ #delete_force(self.ldb_admin, self.get_user_dn("test_modify_user1"))
+ self.ldb_admin.newuser("test_modify_user1", self.user_pass)
+ self.sd_utils.dacl_add_ace(self.get_user_dn("test_modify_user1"), mod)
+ # Modify on attribute you have rights for
+ ldif = """
+dn: """ + self.get_user_dn("test_modify_user1") + """
+changetype: modify
+replace: displayName
+displayName: test_changed"""
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" %
+ self.get_user_dn("test_modify_user1"))
+ self.assertEqual(str(res[0]["displayName"][0]), "test_changed")
+ # Modify on attribute you do not have rights for granted
+ ldif = """
+dn: """ + self.get_user_dn("test_modify_user1") + """
+changetype: modify
+replace: url
+url: www.samba.org"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+ # Second test object -- Group
+ print("Testing modify on Group object")
+ self.ldb_admin.newgroup("test_modify_group1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ self.sd_utils.dacl_add_ace("CN=test_modify_group1,CN=Users," + self.base_dn, mod)
+ ldif = """
+dn: CN=test_modify_group1,CN=Users,""" + self.base_dn + """
+changetype: modify
+replace: displayName
+displayName: test_changed"""
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" %
+ str("CN=test_modify_group1,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["displayName"][0]), "test_changed")
+ # Modify on attribute you do not have rights for granted
+ ldif = """
+dn: CN=test_modify_group1,CN=Users,""" + self.base_dn + """
+changetype: modify
+replace: url
+url: www.samba.org"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e4:
+ (num, _) = e4.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+ # Modify on attribute you do not have rights for granted while also modifying something you do have rights for
+ ldif = """
+dn: CN=test_modify_group1,CN=Users,""" + self.base_dn + """
+changetype: modify
+replace: url
+url: www.samba.org
+replace: displayName
+displayName: test_changed"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e5:
+ (num, _) = e5.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+ # Second test object -- Organizational Unit
+ print("Testing modify on OU object")
+ self.ldb_admin.create_ou("OU=test_modify_ou1," + self.base_dn)
+ self.sd_utils.dacl_add_ace("OU=test_modify_ou1," + self.base_dn, mod)
+ ldif = """
+dn: OU=test_modify_ou1,""" + self.base_dn + """
+changetype: modify
+replace: displayName
+displayName: test_changed"""
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % str("OU=test_modify_ou1,"
+ + self.base_dn))
+ self.assertEqual(str(res[0]["displayName"][0]), "test_changed")
+ # Modify on attribute you do not have rights for granted
+ ldif = """
+dn: OU=test_modify_ou1,""" + self.base_dn + """
+changetype: modify
+replace: url
+url: www.samba.org"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e6:
+ (num, _) = e6.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+
+ def test_modify_u3(self):
+ """7 Modify one attribute as you have no what so ever rights granted"""
+ # First test object -- User
+ print("Testing modify on User object")
+ self.ldb_admin.newuser("test_modify_user1", self.user_pass)
+ # Modify on attribute you do not have rights for granted
+ ldif = """
+dn: """ + self.get_user_dn("test_modify_user1") + """
+changetype: modify
+replace: url
+url: www.samba.org"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e7:
+ (num, _) = e7.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+
+ # Second test object -- Group
+ print("Testing modify on Group object")
+ self.ldb_admin.newgroup("test_modify_group1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ # Modify on attribute you do not have rights for granted
+ ldif = """
+dn: CN=test_modify_group1,CN=Users,""" + self.base_dn + """
+changetype: modify
+replace: url
+url: www.samba.org"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e8:
+ (num, _) = e8.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+
+ # Second test object -- Organizational Unit
+ print("Testing modify on OU object")
+ #delete_force(self.ldb_admin, "OU=test_modify_ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=test_modify_ou1," + self.base_dn)
+ # Modify on attribute you do not have rights for granted
+ ldif = """
+dn: OU=test_modify_ou1,""" + self.base_dn + """
+changetype: modify
+replace: url
+url: www.samba.org"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e9:
+ (num, _) = e9.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+
+ def test_modify_u4(self):
+ """11 Grant WP to PRINCIPAL_SELF and test modify"""
+ ldif = """
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+add: adminDescription
+adminDescription: blah blah blah"""
+ try:
+ self.ldb_user.modify_ldif(ldif)
+ except LdbError as e10:
+ (num, _) = e10.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+
+ mod = "(OA;;WP;bf967919-0de6-11d0-a285-00aa003049e2;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ # Modify on attribute you have rights for
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)"
+ % self.get_user_dn(self.user_with_wp), attrs=["adminDescription"])
+ self.assertEqual(str(res[0]["adminDescription"][0]), "blah blah blah")
+
+ def test_modify_u5(self):
+ """12 test self membership"""
+ ldif = """
+dn: CN=test_modify_group2,CN=Users,""" + self.base_dn + """
+changetype: modify
+add: Member
+Member: """ + self.get_user_dn(self.user_with_sm)
+# the user has no rights granted, this should fail
+ try:
+ self.ldb_user2.modify_ldif(ldif)
+ except LdbError as e11:
+ (num, _) = e11.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This 'modify' operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+
+# grant self-membership, should be able to add himself
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.user_with_sm))
+ mod = "(OA;;SW;bf9679c0-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace("CN=test_modify_group2,CN=Users," + self.base_dn, mod)
+ self.ldb_user2.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)"
+ % ("CN=test_modify_group2,CN=Users," + self.base_dn), attrs=["Member"])
+ self.assertEqual(str(res[0]["Member"][0]), self.get_user_dn(self.user_with_sm))
+# but not other users
+ ldif = """
+dn: CN=test_modify_group2,CN=Users,""" + self.base_dn + """
+changetype: modify
+add: Member
+Member: CN=test_modify_user2,CN=Users,""" + self.base_dn
+ try:
+ self.ldb_user2.modify_ldif(ldif)
+ except LdbError as e12:
+ (num, _) = e12.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_modify_u6(self):
+ """13 test self membership"""
+ ldif = """
+dn: CN=test_modify_group2,CN=Users,""" + self.base_dn + """
+changetype: modify
+add: Member
+Member: """ + self.get_user_dn(self.user_with_sm) + """
+Member: CN=test_modify_user2,CN=Users,""" + self.base_dn
+
+# grant self-membership, should be able to add himself but not others at the same time
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.user_with_sm))
+ mod = "(OA;;SW;bf9679c0-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace("CN=test_modify_group2,CN=Users," + self.base_dn, mod)
+ try:
+ self.ldb_user2.modify_ldif(ldif)
+ except LdbError as e13:
+ (num, _) = e13.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_modify_u7(self):
+ """13 User with WP modifying Member"""
+# a second user is given write property permission
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.user_with_wp))
+ mod = "(A;;WP;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace("CN=test_modify_group2,CN=Users," + self.base_dn, mod)
+ ldif = """
+dn: CN=test_modify_group2,CN=Users,""" + self.base_dn + """
+changetype: modify
+add: Member
+Member: """ + self.get_user_dn(self.user_with_wp)
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)"
+ % ("CN=test_modify_group2,CN=Users," + self.base_dn), attrs=["Member"])
+ self.assertEqual(str(res[0]["Member"][0]), self.get_user_dn(self.user_with_wp))
+ ldif = """
+dn: CN=test_modify_group2,CN=Users,""" + self.base_dn + """
+changetype: modify
+delete: Member"""
+ self.ldb_user.modify_ldif(ldif)
+ ldif = """
+dn: CN=test_modify_group2,CN=Users,""" + self.base_dn + """
+changetype: modify
+add: Member
+Member: CN=test_modify_user2,CN=Users,""" + self.base_dn
+ self.ldb_user.modify_ldif(ldif)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)"
+ % ("CN=test_modify_group2,CN=Users," + self.base_dn), attrs=["Member"])
+ self.assertEqual(str(res[0]["Member"][0]), "CN=test_modify_user2,CN=Users," + self.base_dn)
+
+ def test_modify_dacl_explicit_user(self):
+ '''Modify the DACL of a user's security descriptor when we have RIGHT_WRITE_DAC'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ username = 'test_modify_ou1_user'
+ user_dn = Dn(self.ldb_admin, f'CN={username},{ou_dn}')
+
+ sd_sddl = 'D:(A;;WP;;;BA)'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ self.ldb_admin.newuser(username, self.user_pass,
+ userou=f'OU={ou_name}',
+ sd=descriptor)
+
+ new_sddl = f'D:(A;;WP;;;{self.user_sid})'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ controls = [f'sd_flags:1:{security.SECINFO_DACL}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = 0
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the DACL.
+ message = Message(user_dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # The update fails since we don't have WRITE_DAC.
+ try:
+ self.ldb_user.modify(message, controls=controls)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ if self.strict_checking:
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # Grant ourselves WRITE_DAC.
+ write_dac_sddl = f'(A;CI;WD;;;{self.user_sid})'
+ self.sd_utils.dacl_add_ace(ou_dn, write_dac_sddl)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = security.SECINFO_DACL
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update fails if we don't specify the controls.
+ try:
+ self.ldb_user.modify(message)
+ except LdbError as err:
+ if self.strict_checking:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # The update succeeds when specifying the controls.
+ self.ldb_user.modify(message, controls=controls)
+
+ def test_modify_dacl_explicit_computer(self):
+ '''Modify the DACL of a computer's security descriptor when we have RIGHT_WRITE_DAC'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = Dn(self.ldb_admin, f'CN={account_name},{ou_dn}')
+
+ sd_sddl = 'D:(A;;WP;;;BA)'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ 'nTSecurityDescriptor': ndr_pack(descriptor),
+ })
+
+ new_sddl = f'D:(A;;WP;;;{self.user_sid})'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ controls = [f'sd_flags:1:{security.SECINFO_DACL}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the DACL.
+ message = Message(dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # The update fails since we don't have WRITE_DAC.
+ try:
+ self.ldb_user.modify(message, controls=controls)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ if self.strict_checking:
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # Grant ourselves WRITE_DAC.
+ write_dac_sddl = f'(A;CI;WD;;;{self.user_sid})'
+ self.sd_utils.dacl_add_ace(ou_dn, write_dac_sddl)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update fails if we don't specify the controls.
+ try:
+ self.ldb_user.modify(message)
+ except LdbError as err:
+ if self.strict_checking:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # The update succeeds when specifying the controls.
+ self.ldb_user.modify(message, controls=controls)
+
+ def test_modify_dacl_owner_user(self):
+ '''Modify the DACL of a user's security descriptor when we are its owner'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ username = 'test_modify_ou1_user'
+ user_dn = Dn(self.ldb_admin, f'CN={username},{ou_dn}')
+
+ sd_sddl = 'O:BA'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ self.ldb_admin.newuser(username, self.user_pass,
+ userou=f'OU={ou_name}',
+ sd=descriptor)
+
+ new_sddl = f'D:(A;;WP;;;{self.user_sid})'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ owner_controls = [f'sd_flags:1:{security.SECINFO_OWNER}']
+ dacl_controls = [f'sd_flags:1:{security.SECINFO_DACL}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = 0
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the DACL.
+ message = Message(user_dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # The update fails since we are not the owner.
+ try:
+ self.ldb_user.modify(message, controls=dacl_controls)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ if self.strict_checking:
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # Make ourselves the owner of the security descriptor.
+ owner_sddl = f'O:{self.user_sid}'
+ owner_desc = security.descriptor.from_sddl(owner_sddl, self.domain_sid)
+ self.sd_utils.modify_sd_on_dn(user_dn, owner_desc,
+ controls=owner_controls)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = security.SECINFO_DACL
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update fails if we don't specify the controls.
+ try:
+ self.ldb_user.modify(message)
+ except LdbError as err:
+ if self.strict_checking:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # The update succeeds when specifying the controls.
+ self.ldb_user.modify(message, controls=dacl_controls)
+
+ def test_modify_dacl_owner_computer_implicit_right_blocked(self):
+ '''Show that we cannot modify the DACL of a computer's security descriptor when we are its owner and BlockOwnerImplicitRights == 1'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = Dn(self.ldb_admin, f'CN={account_name},{ou_dn}')
+
+ sd_sddl = 'O:BA'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ 'nTSecurityDescriptor': ndr_pack(descriptor),
+ })
+
+ new_sddl = f'D:(A;;WP;;;{self.user_sid})'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ owner_controls = [f'sd_flags:1:{security.SECINFO_OWNER}']
+ dacl_controls = [f'sd_flags:1:{security.SECINFO_DACL}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the DACL.
+ message = Message(dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # The update fails since we are not the owner.
+ try:
+ self.ldb_user.modify(message, controls=dacl_controls)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ if self.strict_checking:
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # Make ourselves the owner of the security descriptor.
+ owner_sddl = f'O:{self.user_sid}'
+ owner_desc = security.descriptor.from_sddl(owner_sddl, self.domain_sid)
+ self.sd_utils.modify_sd_on_dn(dn, owner_desc,
+ controls=owner_controls)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update fails even when specifying the controls.
+ try:
+ self.ldb_user.modify(message, controls=dacl_controls)
+ except LdbError as err:
+ if self.strict_checking:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ def test_modify_dacl_owner_computer_implicit_right_allowed(self):
+ '''Modify the DACL of a computer's security descriptor when we are its owner and BlockOwnerImplicitRights != 1'''
+
+ self.set_heuristic(samba.dsdb.DS_HR_BLOCK_OWNER_IMPLICIT_RIGHTS, b'0')
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = Dn(self.ldb_admin, f'CN={account_name},{ou_dn}')
+
+ sd_sddl = 'O:BA'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ 'nTSecurityDescriptor': ndr_pack(descriptor),
+ })
+
+ new_sddl = f'D:(A;;WP;;;{self.user_sid})'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ owner_controls = [f'sd_flags:1:{security.SECINFO_OWNER}']
+ dacl_controls = [f'sd_flags:1:{security.SECINFO_DACL}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the DACL.
+ message = Message(dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # The update fails since we are not the owner.
+ try:
+ self.ldb_user.modify(message, controls=dacl_controls)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ if self.strict_checking:
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # Make ourselves the owner of the security descriptor.
+ owner_sddl = f'O:{self.user_sid}'
+ owner_desc = security.descriptor.from_sddl(owner_sddl, self.domain_sid)
+ self.sd_utils.modify_sd_on_dn(dn, owner_desc,
+ controls=owner_controls)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update fails if we don't specify the controls.
+ try:
+ self.ldb_user.modify(message)
+ except LdbError as err:
+ if self.strict_checking:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # The update succeeds when specifying the controls.
+ self.ldb_user.modify(message, controls=dacl_controls)
+
+ def test_modify_owner_explicit_user(self):
+ '''Modify the owner of a user's security descriptor when we have RIGHT_WRITE_OWNER'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ username = 'test_modify_ou1_user'
+ user_dn = Dn(self.ldb_admin, f'CN={username},{ou_dn}')
+
+ sd_sddl = 'O:BA'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ self.ldb_admin.newuser(username, self.user_pass,
+ userou=f'OU={ou_name}',
+ sd=descriptor)
+
+ # Try to modify the owner to ourselves.
+ new_sddl = f'O:{self.user_sid}'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ owner_controls = [f'sd_flags:1:{security.SECINFO_OWNER}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = 0
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the owner.
+ message = Message(user_dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # The update fails since we don't have WRITE_OWNER.
+ try:
+ self.ldb_user.modify(message, controls=owner_controls)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ if self.strict_checking:
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # Grant ourselves WRITE_OWNER.
+ owner_sddl = f'(A;CI;WO;;;{self.user_sid})'
+ self.sd_utils.dacl_add_ace(ou_dn, owner_sddl)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = security.SECINFO_OWNER | security.SECINFO_GROUP
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update fails if we don't specify the controls.
+ try:
+ self.ldb_user.modify(message)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ if self.strict_checking:
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # The update succeeds when specifying the controls.
+ self.ldb_user.modify(message, controls=owner_controls)
+
+ def test_modify_owner_explicit_computer(self):
+ '''Modify the owner of a computer's security descriptor when we have RIGHT_WRITE_OWNER'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = Dn(self.ldb_admin, f'CN={account_name},{ou_dn}')
+
+ sd_sddl = 'O:BA'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ 'nTSecurityDescriptor': ndr_pack(descriptor),
+ })
+
+ # Try to modify the owner to ourselves.
+ new_sddl = f'O:{self.user_sid}'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ owner_controls = [f'sd_flags:1:{security.SECINFO_OWNER}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the owner.
+ message = Message(dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # The update fails since we don't have WRITE_OWNER.
+ try:
+ self.ldb_user.modify(message, controls=owner_controls)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ if self.strict_checking:
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # Grant ourselves WRITE_OWNER.
+ owner_sddl = f'(A;CI;WO;;;{self.user_sid})'
+ self.sd_utils.dacl_add_ace(ou_dn, owner_sddl)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update fails if we don't specify the controls.
+ try:
+ self.ldb_user.modify(message)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ if self.strict_checking:
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # The update succeeds when specifying the controls.
+ self.ldb_user.modify(message, controls=owner_controls)
+
+ def test_modify_group_explicit_user(self):
+ '''Modify the group of a user's security descriptor when we have RIGHT_WRITE_OWNER'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ username = 'test_modify_ou1_user'
+ user_dn = Dn(self.ldb_admin, f'CN={username},{ou_dn}')
+
+ sd_sddl = 'O:BA'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ self.ldb_admin.newuser(username, self.user_pass,
+ userou=f'OU={ou_name}',
+ sd=descriptor)
+
+ # Try to modify the group to ourselves.
+ new_sddl = f'G:{self.user_sid}'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ group_controls = [f'sd_flags:1:{security.SECINFO_GROUP}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = 0
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the group.
+ message = Message(user_dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # The update fails since we don't have WRITE_OWNER.
+ try:
+ self.ldb_user.modify(message, controls=group_controls)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ if self.strict_checking:
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # Grant ourselves WRITE_OWNER.
+ owner_sddl = f'(A;CI;WO;;;{self.user_sid})'
+ self.sd_utils.dacl_add_ace(ou_dn, owner_sddl)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = security.SECINFO_OWNER | security.SECINFO_GROUP
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update fails if we don't specify the controls.
+ try:
+ self.ldb_user.modify(message)
+ except LdbError as err:
+ if self.strict_checking:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # The update succeeds when specifying the controls.
+ self.ldb_user.modify(message, controls=group_controls)
+
+ def test_modify_group_explicit_computer(self):
+ '''Modify the group of a computer's security descriptor when we have RIGHT_WRITE_OWNER'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = Dn(self.ldb_admin, f'CN={account_name},{ou_dn}')
+
+ sd_sddl = 'O:BA'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ 'nTSecurityDescriptor': ndr_pack(descriptor),
+ })
+
+ # Try to modify the group to ourselves.
+ new_sddl = f'G:{self.user_sid}'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ group_controls = [f'sd_flags:1:{security.SECINFO_GROUP}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the group.
+ message = Message(dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # The update fails since we don't have WRITE_OWNER.
+ try:
+ self.ldb_user.modify(message, controls=group_controls)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ if self.strict_checking:
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # Grant ourselves WRITE_OWNER.
+ owner_sddl = f'(A;CI;WO;;;{self.user_sid})'
+ self.sd_utils.dacl_add_ace(ou_dn, owner_sddl)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update fails if we don't specify the controls.
+ try:
+ self.ldb_user.modify(message)
+ except LdbError as err:
+ if self.strict_checking:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ self.assertIn(f'{werror.WERR_ACCESS_DENIED:08X}', estr)
+ else:
+ self.fail()
+
+ # The update succeeds when specifying the controls.
+ self.ldb_user.modify(message, controls=group_controls)
+
+ def test_modify_owner_other_user(self):
+ '''Show we cannot set the owner of a user's security descriptor to another SID'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ username = 'test_modify_ou1_user'
+ user_dn = Dn(self.ldb_admin, f'CN={username},{ou_dn}')
+
+ sd_sddl = 'O:BA'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ self.ldb_admin.newuser(username, self.user_pass,
+ userou=f'OU={ou_name}',
+ sd=descriptor)
+
+ # Try to modify the owner to someone other than ourselves.
+ new_sddl = 'O:BA'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ owner_controls = [f'sd_flags:1:{security.SECINFO_OWNER}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = 0
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the owner.
+ message = Message(user_dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # Grant ourselves WRITE_OWNER.
+ owner_sddl = f'(A;CI;WO;;;{self.user_sid})'
+ self.sd_utils.dacl_add_ace(ou_dn, owner_sddl)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = security.SECINFO_OWNER | security.SECINFO_GROUP
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update fails when trying to specify another user.
+ try:
+ self.ldb_user.modify(message, controls=owner_controls)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ if self.strict_checking:
+ self.assertIn(f'{werror.WERR_INVALID_OWNER:08X}', estr)
+ else:
+ self.fail('expected an error')
+
+ def test_modify_owner_other_computer(self):
+ '''Show we cannot set the owner of a computer's security descriptor to another SID'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = Dn(self.ldb_admin, f'CN={account_name},{ou_dn}')
+
+ sd_sddl = 'O:BA'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ 'nTSecurityDescriptor': ndr_pack(descriptor),
+ })
+
+ # Try to modify the owner to someone other than ourselves.
+ new_sddl = 'O:BA'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ owner_controls = [f'sd_flags:1:{security.SECINFO_OWNER}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the owner.
+ message = Message(dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # Grant ourselves WRITE_OWNER.
+ owner_sddl = f'(A;CI;WO;;;{self.user_sid})'
+ self.sd_utils.dacl_add_ace(ou_dn, owner_sddl)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update fails when trying to specify another user.
+ try:
+ self.ldb_user.modify(message, controls=owner_controls)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ if self.strict_checking:
+ self.assertIn(f'{werror.WERR_INVALID_OWNER:08X}', estr)
+ else:
+ self.fail('expected an error')
+
+ def test_modify_owner_other_admin_user(self):
+ '''Show a domain admin cannot set the owner of a user's security descriptor to another SID'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ username = 'test_modify_ou1_user'
+ user_dn = Dn(self.ldb_admin, f'CN={username},{ou_dn}')
+
+ sd_sddl = 'O:BA'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ self.ldb_admin.newuser(username, self.user_pass,
+ userou=f'OU={ou_name}',
+ sd=descriptor)
+
+ # Try to modify the owner to someone other than ourselves.
+ new_sddl = 'O:BA'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ owner_controls = [f'sd_flags:1:{security.SECINFO_OWNER}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = 0
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the owner.
+ message = Message(user_dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # Grant ourselves WRITE_OWNER.
+ owner_sddl = f'(A;CI;WO;;;{self.user_sid})'
+ self.sd_utils.dacl_add_ace(ou_dn, owner_sddl)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = security.SECINFO_OWNER | security.SECINFO_GROUP
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update succeeds as admin when trying to specify another user.
+ self.ldb_admin.modify(message, controls=owner_controls)
+
+ def test_modify_owner_other_admin_computer(self):
+ '''Show a domain admin cannot set the owner of a computer's security descriptor to another SID'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = Dn(self.ldb_admin, f'CN={account_name},{ou_dn}')
+
+ sd_sddl = 'O:BA'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ 'nTSecurityDescriptor': ndr_pack(descriptor),
+ })
+
+ # Try to modify the owner to someone other than ourselves.
+ new_sddl = 'O:BA'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ owner_controls = [f'sd_flags:1:{security.SECINFO_OWNER}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the owner.
+ message = Message(dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # Grant ourselves WRITE_OWNER.
+ owner_sddl = f'(A;CI;WO;;;{self.user_sid})'
+ self.sd_utils.dacl_add_ace(ou_dn, owner_sddl)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update succeeds as admin when trying to specify another user.
+ self.ldb_admin.modify(message, controls=owner_controls)
+
+ def test_modify_owner_admin_user(self):
+ '''Show a domain admin can set the owner of a user's security descriptor to Domain Admins'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ username = 'test_modify_ou1_user'
+ user_dn = Dn(self.ldb_admin, f'CN={username},{ou_dn}')
+
+ sd_sddl = 'O:BA'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ self.ldb_admin.newuser(username, self.user_pass,
+ userou=f'OU={ou_name}',
+ sd=descriptor)
+
+ # Try to modify the owner to Domain Admins.
+ new_sddl = 'O:DA'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ owner_controls = [f'sd_flags:1:{security.SECINFO_OWNER}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = 0
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the owner.
+ message = Message(user_dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # Grant ourselves WRITE_OWNER.
+ owner_sddl = f'(A;CI;WO;;;{self.user_sid})'
+ self.sd_utils.dacl_add_ace(ou_dn, owner_sddl)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, user_dn)
+ expected_rights = security.SECINFO_OWNER | security.SECINFO_GROUP
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update succeeds as admin when specifying Domain Admins.
+ self.ldb_admin.modify(message, controls=owner_controls)
+
+ def test_modify_owner_admin_computer(self):
+ '''Show a domain admin can set the owner of a computer's security descriptor to Domain Admins'''
+
+ ou_name = 'test_modify_ou1'
+ ou_dn = f'OU={ou_name},{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = Dn(self.ldb_admin, f'CN={account_name},{ou_dn}')
+
+ sd_sddl = 'O:BA'
+ descriptor = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ ou_sddl = f'D:(OA;CI;WP;{samba.dsdb.DS_GUID_SCHEMA_ATTR_NT_SECURITY_DESCRIPTOR};;{self.user_sid})'
+ ou_desc = security.descriptor.from_sddl(ou_sddl, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, name=ou_name, sd=ou_desc)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ 'nTSecurityDescriptor': ndr_pack(descriptor),
+ })
+
+ # Try to modify the owner to Domain Admins.
+ new_sddl = 'O:DA'
+ new_desc = security.descriptor.from_sddl(new_sddl, self.domain_sid)
+
+ owner_controls = [f'sd_flags:1:{security.SECINFO_OWNER}']
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The user should not be able to modify the owner.
+ message = Message(dn)
+ message['nTSecurityDescriptor'] = MessageElement(
+ ndr_pack(new_desc),
+ FLAG_MOD_REPLACE,
+ 'nTSecurityDescriptor')
+
+ # Grant ourselves WRITE_OWNER.
+ owner_sddl = f'(A;CI;WO;;;{self.user_sid})'
+ self.sd_utils.dacl_add_ace(ou_dn, owner_sddl)
+
+ # Check our effective rights.
+ effective_rights = self.get_sd_rights_effective(self.ldb_user, dn)
+ expected_rights = None
+ self.assertEqual(expected_rights, effective_rights)
+
+ # The update succeeds as admin when specifying Domain Admins.
+ self.ldb_admin.modify(message, controls=owner_controls)
+
+ def test_modify_anonymous(self):
+ """Test add operation with anonymous user"""
+ anonymous = SamDB(url=ldaphost, credentials=self.creds_tmp, lp=lp)
+ self.ldb_admin.newuser("test_anonymous", "samba123@")
+ m = Message()
+ m.dn = Dn(anonymous, self.get_user_dn("test_anonymous"))
+
+ m["description"] = MessageElement("sambauser2",
+ FLAG_MOD_ADD,
+ "description")
+ try:
+ anonymous.modify(m)
+ except LdbError as e14:
+ (num, _) = e14.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name(self):
+ '''Test modifying dNSHostName with validated write'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_dns_host_name_no_validated_write(self):
+ '''Test modifying dNSHostName without validated write'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_invalid(self):
+ '''Test modifying dNSHostName to an invalid value'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = 'invalid'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_invalid_wp(self):
+ '''Test modifying dNSHostName to an invalid value when we have WP'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Write Property.
+ mod = (f'(OA;CI;WP;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = 'invalid'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_dns_host_name_invalid_non_computer(self):
+ '''Test modifying dNSHostName to an invalid value on a non-computer'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'user',
+ 'sAMAccountName': f'{account_name}',
+ })
+
+ host_name = 'invalid'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INSUFFICIENT_ACCESS_RIGHTS, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_no_value(self):
+ '''Test modifying dNSHostName with validated write with no value'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement([],
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_OPERATIONS_ERROR, num)
+ else:
+ # Windows accepts this.
+ pass
+
+ def test_modify_dns_host_name_empty_string(self):
+ '''Test modifying dNSHostName with validated write of an empty string'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement('\0',
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_dollar(self):
+ '''Test modifying dNSHostName with validated write of a value including a dollar'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}$.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_account_no_dollar(self):
+ '''Test modifying dNSHostName with validated write with no dollar in sAMAccountName'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}',
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_dns_host_name_no_suffix(self):
+ '''Test modifying dNSHostName with validated write of a value missing the suffix'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_wrong_prefix(self):
+ '''Test modifying dNSHostName with validated write of a value with the wrong prefix'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'invalid.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_wrong_suffix(self):
+ '''Test modifying dNSHostName with validated write of a value with the wrong suffix'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}.invalid.example.com'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_case(self):
+ '''Test modifying dNSHostName with validated write of a value with irregular case'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+ host_name = host_name.capitalize()
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_dns_host_name_allowed_suffixes(self):
+ '''Test modifying dNSHostName with validated write and an allowed suffix'''
+
+ allowed_suffix = 'suffix.that.is.allowed'
+
+ # Add the allowed suffix.
+
+ res = self.ldb_admin.search(self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=['msDS-AllowedDNSSuffixes'])
+ self.assertEqual(1, len(res))
+ old_allowed_suffixes = res[0].get('msDS-AllowedDNSSuffixes')
+
+ def modify_allowed_suffixes(suffixes):
+ if suffixes is None:
+ suffixes = []
+ flag = FLAG_MOD_DELETE
+ else:
+ flag = FLAG_MOD_REPLACE
+
+ m = Message(Dn(self.ldb_admin, self.base_dn))
+ m['msDS-AllowedDNSSuffixes'] = MessageElement(
+ suffixes,
+ flag,
+ 'msDS-AllowedDNSSuffixes')
+ self.ldb_admin.modify(m)
+
+ self.addCleanup(modify_allowed_suffixes, old_allowed_suffixes)
+
+ if old_allowed_suffixes is None:
+ allowed_suffixes = []
+ else:
+ allowed_suffixes = list(old_allowed_suffixes)
+
+ if (allowed_suffix not in allowed_suffixes and
+ allowed_suffix.encode('utf-8') not in allowed_suffixes):
+ allowed_suffixes.append(allowed_suffix)
+
+ modify_allowed_suffixes(allowed_suffixes)
+
+ # Create the account and run the test.
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}.{allowed_suffix}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_dns_host_name_spn(self):
+ '''Test modifying dNSHostName and SPN with validated write'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_VALIDATE_SPN};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+ spn = f'host/{host_name}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['0'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ m['1'] = MessageElement(spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_spn_matching_dns_host_name_invalid(self):
+ '''Test modifying SPN with validated write, matching a valid dNSHostName '''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Write Property.
+ mod = (f'(OA;CI;WP;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_VALIDATE_SPN};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ invalid_host_name = 'invalid'
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+ spn = f'host/{host_name}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['0'] = MessageElement(invalid_host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ m['1'] = MessageElement(spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ m['2'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+ def test_modify_spn_matching_dns_host_name_original(self):
+ '''Test modifying SPN with validated write, matching the original dNSHostName '''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_VALIDATE_SPN};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ original_host_name = 'invalid_host_name'
+ original_spn = 'host/{original_host_name}'
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ 'dNSHostName': original_host_name,
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['0'] = MessageElement(original_spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ m['1'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_spn_matching_account_name_original(self):
+ '''Test modifying dNSHostName and SPN with validated write, matching the original sAMAccountName'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ sam_account_name = '3e0abfd0-126a-11d0-a060-00aa006c33ed'
+
+ # Grant Write Property.
+ mod = (f'(OA;CI;WP;{sam_account_name};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_VALIDATE_SPN};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ new_account_name = 'test_mod_hostname2'
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+ spn = f'host/{host_name}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['0'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ m['1'] = MessageElement(spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ m['2'] = MessageElement(f'{new_account_name}$',
+ FLAG_MOD_REPLACE,
+ 'sAMAccountName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ else:
+ self.fail()
+
+ def test_modify_dns_host_name_spn_matching_account_name_new(self):
+ '''Test modifying dNSHostName and SPN with validated write, matching the new sAMAccountName'''
+
+ ou_dn = f'OU=test_modify_ou1,{self.base_dn}'
+
+ account_name = 'test_mod_hostname'
+ dn = f'CN={account_name},{ou_dn}'
+
+ self.ldb_admin.create_ou(ou_dn)
+
+ sam_account_name = '3e0abfd0-126a-11d0-a060-00aa006c33ed'
+
+ # Grant Write Property.
+ mod = (f'(OA;CI;WP;{sam_account_name};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ # Grant Validated Write.
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_DNS_HOST_NAME};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ mod = (f'(OA;CI;SW;{security.GUID_DRS_VALIDATE_SPN};;'
+ f'{self.user_sid})')
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+
+ # Create the account.
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': f'{account_name}$',
+ })
+
+ new_account_name = 'test_mod_hostname2'
+ new_host_name = f'{new_account_name}.{self.ldb_user.domain_dns_name()}'
+ new_spn = f'host/{new_host_name}'
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['0'] = MessageElement(new_spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ m['1'] = MessageElement(new_host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+ m['2'] = MessageElement(f'{new_account_name}$',
+ FLAG_MOD_REPLACE,
+ 'sAMAccountName')
+ try:
+ self.ldb_user.modify(m)
+ except LdbError:
+ self.fail()
+
+# enable these when we have search implemented
+
+
+class AclSearchTests(AclTests):
+
+ def setUp(self):
+ super(AclSearchTests, self).setUp()
+
+ # permit password changes during this test
+ PasswordCommon.allow_password_changes(self, self.ldb_admin)
+
+ self.u1 = "search_u1"
+ self.u2 = "search_u2"
+ self.u3 = "search_u3"
+ self.group1 = "group1"
+ self.ldb_admin.newuser(self.u1, self.user_pass)
+ self.ldb_admin.newuser(self.u2, self.user_pass)
+ self.ldb_admin.newuser(self.u3, self.user_pass)
+ self.ldb_admin.newgroup(self.group1, grouptype=samba.dsdb.GTYPE_SECURITY_GLOBAL_GROUP)
+ self.ldb_admin.add_remove_group_members(self.group1, [self.u2],
+ add_members_operation=True)
+ self.ldb_user = self.get_ldb_connection(self.u1, self.user_pass)
+ self.ldb_user2 = self.get_ldb_connection(self.u2, self.user_pass)
+ self.ldb_user3 = self.get_ldb_connection(self.u3, self.user_pass)
+ self.full_list = [Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou3,OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou4,OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn)]
+ self.user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.u1))
+ self.group_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.group1))
+
+ def create_clean_ou(self, object_dn):
+ """ Base repeating setup for unittests to follow """
+ res = self.ldb_admin.search(base=self.base_dn, scope=SCOPE_SUBTREE,
+ expression="distinguishedName=%s" % object_dn)
+ # Make sure top testing OU has been deleted before starting the test
+ self.assertEqual(len(res), 0)
+ self.ldb_admin.create_ou(object_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ # Make sure there are inheritable ACEs initially
+ self.assertTrue("CI" in desc_sddl or "OI" in desc_sddl)
+ # Find and remove all inherit ACEs
+ res = re.findall(r"\(.*?\)", desc_sddl)
+ res = [x for x in res if ("CI" in x) or ("OI" in x)]
+ for x in res:
+ desc_sddl = desc_sddl.replace(x, "")
+ # Add flag 'protected' in both DACL and SACL so no inherit ACEs
+ # can propagate from above
+ # remove SACL, we are not interested
+ desc_sddl = desc_sddl.replace(":AI", ":AIP")
+ self.sd_utils.modify_sd_on_dn(object_dn, desc_sddl)
+ # Verify all inheritable ACEs are gone
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ self.assertNotIn("CI", desc_sddl)
+ self.assertNotIn("OI", desc_sddl)
+
+ def tearDown(self):
+ super(AclSearchTests, self).tearDown()
+ delete_force(self.ldb_admin, "OU=test_search_ou2,OU=test_search_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_search_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=ou4,OU=ou2,OU=ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=ou3,OU=ou2,OU=ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=ou1," + self.base_dn)
+ delete_force(self.ldb_admin, self.get_user_dn("search_u1"))
+ delete_force(self.ldb_admin, self.get_user_dn("search_u2"))
+ delete_force(self.ldb_admin, self.get_user_dn("search_u3"))
+ delete_force(self.ldb_admin, self.get_user_dn("group1"))
+
+ del self.ldb_user
+ del self.ldb_user2
+ del self.ldb_user3
+
+ def test_search_anonymous1(self):
+ """Verify access of rootDSE with the correct request"""
+ anonymous = SamDB(url=ldaphost, credentials=self.creds_tmp, lp=lp)
+ res = anonymous.search("", expression="(objectClass=*)", scope=SCOPE_BASE)
+ self.assertEqual(len(res), 1)
+ # verify some of the attributes
+ # don't care about values
+ self.assertIn("ldapServiceName", res[0])
+ self.assertIn("namingContexts", res[0])
+ self.assertIn("isSynchronized", res[0])
+ self.assertIn("dsServiceName", res[0])
+ self.assertIn("supportedSASLMechanisms", res[0])
+ self.assertIn("isGlobalCatalogReady", res[0])
+ self.assertIn("domainControllerFunctionality", res[0])
+ self.assertIn("serverName", res[0])
+
+ def test_search_anonymous2(self):
+ """Make sure we cannot access anything else"""
+ anonymous = SamDB(url=ldaphost, credentials=self.creds_tmp, lp=lp)
+ try:
+ anonymous.search("", expression="(objectClass=*)", scope=SCOPE_SUBTREE)
+ except LdbError as e15:
+ (num, _) = e15.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ else:
+ self.fail()
+ try:
+ anonymous.search(self.base_dn, expression="(objectClass=*)", scope=SCOPE_SUBTREE)
+ except LdbError as e16:
+ (num, _) = e16.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ else:
+ self.fail()
+ try:
+ anonymous.search(anonymous.get_config_basedn(), expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ except LdbError as e17:
+ (num, _) = e17.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ else:
+ self.fail()
+
+ def test_search_anonymous3(self):
+ """Set dsHeuristics and repeat"""
+ self.ldb_admin.set_dsheuristics("0000002")
+ self.ldb_admin.create_ou("OU=test_search_ou1," + self.base_dn)
+ mod = "(A;CI;LC;;;AN)"
+ self.sd_utils.dacl_add_ace("OU=test_search_ou1," + self.base_dn, mod)
+ self.ldb_admin.create_ou("OU=test_search_ou2,OU=test_search_ou1," + self.base_dn)
+ anonymous = SamDB(url=ldaphost, credentials=self.creds_tmp, lp=lp)
+ res = anonymous.search("OU=test_search_ou2,OU=test_search_ou1," + self.base_dn,
+ expression="(objectClass=*)", scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1)
+ self.assertIn("dn", res[0])
+ self.assertEqual(res[0]["dn"], Dn(self.ldb_admin,
+ "OU=test_search_ou2,OU=test_search_ou1," + self.base_dn))
+ res = anonymous.search(anonymous.get_config_basedn(), expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1)
+ self.assertIn("dn", res[0])
+ self.assertEqual(res[0]["dn"], Dn(self.ldb_admin, self.configuration_dn))
+
+ def test_search1(self):
+ """Make sure users can see us if given LC to user and group"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ mod = "(A;;LC;;;%s)(A;;LC;;;%s)" % (str(self.user_sid), str(self.group_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ tmp_desc = security.descriptor.from_sddl("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" + mod,
+ self.domain_sid)
+ self.ldb_admin.create_ou("OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou4,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+
+ # regular users must see only ou1 and ou2
+ res = self.ldb_user3.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 2)
+ ok_list = [Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou1," + self.base_dn)]
+
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ # these users should see all ous
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 6)
+ res_list = [x["dn"] for x in res if x["dn"] in self.full_list]
+ self.assertEqual(sorted(res_list), sorted(self.full_list))
+
+ res = self.ldb_user2.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 6)
+ res_list = [x["dn"] for x in res if x["dn"] in self.full_list]
+ self.assertEqual(sorted(res_list), sorted(self.full_list))
+
+ def test_search2(self):
+ """Make sure users can't see us if access is explicitly denied"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=ou2,OU=ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=ou3,OU=ou2,OU=ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=ou4,OU=ou2,OU=ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn)
+ self.ldb_admin.create_ou("OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn)
+ mod = "(D;;LC;;;%s)(D;;LC;;;%s)" % (str(self.user_sid), str(self.group_sid))
+ self.sd_utils.dacl_add_ace("OU=ou2,OU=ou1," + self.base_dn, mod)
+ res = self.ldb_user3.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ # this user should see all ous
+ res_list = [x["dn"] for x in res if x["dn"] in self.full_list]
+ self.assertEqual(sorted(res_list), sorted(self.full_list))
+
+ # these users should see ou1, 2, 5 and 6 but not 3 and 4
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ ok_list = [Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn)]
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ res = self.ldb_user2.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 4)
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ def test_search3(self):
+ """Make sure users can't see ous if access is explicitly denied - 2"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ mod = "(A;CI;LC;;;%s)(A;CI;LC;;;%s)" % (str(self.user_sid), str(self.group_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ tmp_desc = security.descriptor.from_sddl("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" + mod,
+ self.domain_sid)
+ self.ldb_admin.create_ou("OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou4,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+
+ print("Testing correct behavior on nonaccessible search base")
+ try:
+ self.ldb_user3.search("OU=ou3,OU=ou2,OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_BASE)
+ except LdbError as e18:
+ (num, _) = e18.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+ else:
+ self.fail()
+
+ mod = "(D;;LC;;;%s)(D;;LC;;;%s)" % (str(self.user_sid), str(self.group_sid))
+ self.sd_utils.dacl_add_ace("OU=ou2,OU=ou1," + self.base_dn, mod)
+
+ ok_list = [Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou1," + self.base_dn)]
+
+ res = self.ldb_user3.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ ok_list = [Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn)]
+
+ # should not see ou3 and ou4, but should see ou5 and ou6
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 4)
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ res = self.ldb_user2.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 4)
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ def test_search4(self):
+ """There is no difference in visibility if the user is also creator"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ mod = "(A;CI;CCWD;;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ tmp_desc = security.descriptor.from_sddl("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" + mod,
+ self.domain_sid)
+ self.ldb_user.create_ou("OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_user.create_ou("OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_user.create_ou("OU=ou4,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_user.create_ou("OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_user.create_ou("OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+
+ ok_list = [Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn),
+ Dn(self.ldb_admin, "OU=ou1," + self.base_dn)]
+ res = self.ldb_user3.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 2)
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 2)
+ res_list = [x["dn"] for x in res if x["dn"] in ok_list]
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ def test_search5(self):
+ """Make sure users can see only attributes they are allowed to see"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ mod = "(A;CI;LC;;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ tmp_desc = security.descriptor.from_sddl("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" + mod,
+ self.domain_sid)
+ self.ldb_admin.create_ou("OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ # assert user can only see dn
+ res = self.ldb_user.search("OU=ou2,OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ ok_list = ['dn']
+ self.assertEqual(len(res), 1)
+ res_list = list(res[0].keys())
+ self.assertEqual(res_list, ok_list)
+
+ res = self.ldb_user.search("OU=ou2,OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_BASE, attrs=["ou"])
+
+ self.assertEqual(len(res), 1)
+ res_list = list(res[0].keys())
+ self.assertEqual(res_list, ok_list)
+
+ # give read property on ou and assert user can only see dn and ou
+ mod = "(OA;;RP;bf9679f0-0de6-11d0-a285-00aa003049e2;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ self.sd_utils.dacl_add_ace("OU=ou2,OU=ou1," + self.base_dn, mod)
+ res = self.ldb_user.search("OU=ou2,OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+ ok_list = ['dn', 'ou']
+ self.assertEqual(len(res), 1)
+ res_list = list(res[0].keys())
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ # give read property on Public Information and assert user can see ou and other members
+ mod = "(OA;;RP;e48d0154-bcf8-11d1-8702-00c04fb96050;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ self.sd_utils.dacl_add_ace("OU=ou2,OU=ou1," + self.base_dn, mod)
+ res = self.ldb_user.search("OU=ou2,OU=ou1," + self.base_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE)
+
+ ok_list = ['dn', 'objectClass', 'ou', 'distinguishedName', 'name', 'objectGUID', 'objectCategory']
+ res_list = list(res[0].keys())
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ def test_search6(self):
+ """If an attribute that cannot be read is used in a filter, it is as if the attribute does not exist"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ mod = "(A;CI;LCCCWD;;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ tmp_desc = security.descriptor.from_sddl("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" + mod,
+ self.domain_sid)
+ self.ldb_admin.create_ou("OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_user.create_ou("OU=ou3,OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(ou=ou3)",
+ scope=SCOPE_SUBTREE)
+ # nothing should be returned as ou is not accessible
+ self.assertEqual(len(res), 0)
+
+ # give read property on ou and assert user can only see dn and ou
+ mod = "(OA;;RP;bf9679f0-0de6-11d0-a285-00aa003049e2;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou3,OU=ou2,OU=ou1," + self.base_dn, mod)
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(ou=ou3)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1)
+ ok_list = ['dn', 'ou']
+ res_list = list(res[0].keys())
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ # give read property on Public Information and assert user can see ou and other members
+ mod = "(OA;;RP;e48d0154-bcf8-11d1-8702-00c04fb96050;;%s)" % (str(self.user_sid))
+ self.sd_utils.dacl_add_ace("OU=ou2,OU=ou1," + self.base_dn, mod)
+ res = self.ldb_user.search("OU=ou1," + self.base_dn, expression="(ou=ou2)",
+ scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1)
+ ok_list = ['dn', 'objectClass', 'ou', 'distinguishedName', 'name', 'objectGUID', 'objectCategory']
+ res_list = list(res[0].keys())
+ self.assertEqual(sorted(res_list), sorted(ok_list))
+
+ def assert_search_on_attr(self, dn, samdb, attr, expected_list):
+
+ expected_num = len(expected_list)
+ res = samdb.search(dn, expression="(%s=*)" % attr, scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), expected_num)
+
+ res_list = [ x["dn"] for x in res if x["dn"] in expected_list ]
+ self.assertEqual(sorted(res_list), sorted(expected_list))
+
+ def test_search7(self):
+ """Checks object search visibility when users don't have full rights"""
+ self.create_clean_ou("OU=ou1," + self.base_dn)
+ mod = "(A;;LC;;;%s)(A;;LC;;;%s)" % (str(self.user_sid),
+ str(self.group_sid))
+ self.sd_utils.dacl_add_ace("OU=ou1," + self.base_dn, mod)
+ tmp_desc = security.descriptor.from_sddl("D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)" + mod,
+ self.domain_sid)
+ self.ldb_admin.create_ou("OU=ou2,OU=ou1," + self.base_dn, sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou3,OU=ou2,OU=ou1," + self.base_dn,
+ sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou4,OU=ou2,OU=ou1," + self.base_dn,
+ sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou5,OU=ou3,OU=ou2,OU=ou1," + self.base_dn,
+ sd=tmp_desc)
+ self.ldb_admin.create_ou("OU=ou6,OU=ou4,OU=ou2,OU=ou1," + self.base_dn,
+ sd=tmp_desc)
+
+ ou2_dn = Dn(self.ldb_admin, "OU=ou2,OU=ou1," + self.base_dn)
+ ou1_dn = Dn(self.ldb_admin, "OU=ou1," + self.base_dn)
+
+ # even though unprivileged users can't read these attributes for OU2,
+ # the object should still be visible in searches, because they have
+ # 'List Contents' rights still. This isn't really disclosive because
+ # ALL objects have these attributes
+ visible_attrs = ["objectClass", "distinguishedName", "name",
+ "objectGUID"]
+ two_objects = [ou2_dn, ou1_dn]
+
+ for attr in visible_attrs:
+ # a regular user should just see the 2 objects
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_user3, attr,
+ expected_list=two_objects)
+
+ # whereas the following users have LC rights for all the objects,
+ # so they should see them all
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_user, attr,
+ expected_list=self.full_list)
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_user2, attr,
+ expected_list=self.full_list)
+
+ # however when searching on the following attributes, objects will not
+ # be visible unless the user has Read Property rights
+ hidden_attrs = ["objectCategory", "instanceType", "ou", "uSNChanged",
+ "uSNCreated", "whenCreated"]
+ one_object = [ou1_dn]
+
+ for attr in hidden_attrs:
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_user3, attr,
+ expected_list=one_object)
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_user, attr,
+ expected_list=one_object)
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_user2, attr,
+ expected_list=one_object)
+
+ # admin has RP rights so can still see all the objects
+ self.assert_search_on_attr(str(ou1_dn), self.ldb_admin, attr,
+ expected_list=self.full_list)
+
+
+# tests on ldap delete operations
+
+
+class AclDeleteTests(AclTests):
+
+ def setUp(self):
+ super(AclDeleteTests, self).setUp()
+ self.regular_user = "acl_delete_user1"
+ # Create regular user
+ self.ldb_admin.newuser(self.regular_user, self.user_pass)
+ self.ldb_user = self.get_ldb_connection(self.regular_user, self.user_pass)
+
+ def tearDown(self):
+ super(AclDeleteTests, self).tearDown()
+ delete_force(self.ldb_admin, self.get_user_dn("test_delete_user1"))
+ delete_force(self.ldb_admin, self.get_user_dn(self.regular_user))
+ delete_force(self.ldb_admin, self.get_user_dn("test_anonymous"))
+
+ del self.ldb_user
+
+ def test_delete_u1(self):
+ """User is prohibited by default to delete another User object"""
+ # Create user that we try to delete
+ self.ldb_admin.newuser("test_delete_user1", self.user_pass)
+ # Here delete User object should ALWAYS through exception
+ try:
+ self.ldb_user.delete(self.get_user_dn("test_delete_user1"))
+ except LdbError as e19:
+ (num, _) = e19.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_delete_u2(self):
+ """User's group has RIGHT_DELETE to another User object"""
+ user_dn = self.get_user_dn("test_delete_user1")
+ # Create user that we try to delete
+ self.ldb_admin.newuser("test_delete_user1", self.user_pass)
+ mod = "(A;;SD;;;AU)"
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ # Try to delete User object
+ self.ldb_user.delete(user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+
+ def test_delete_u3(self):
+ """User identified by SID has RIGHT_DELETE to another User object"""
+ user_dn = self.get_user_dn("test_delete_user1")
+ # Create user that we try to delete
+ self.ldb_admin.newuser("test_delete_user1", self.user_pass)
+ mod = "(A;;SD;;;%s)" % self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ # Try to delete User object
+ self.ldb_user.delete(user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+
+ def test_delete_anonymous(self):
+ """Test add operation with anonymous user"""
+ anonymous = SamDB(url=ldaphost, credentials=self.creds_tmp, lp=lp)
+ self.ldb_admin.newuser("test_anonymous", "samba123@")
+
+ try:
+ anonymous.delete(self.get_user_dn("test_anonymous"))
+ except LdbError as e20:
+ (num, _) = e20.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ else:
+ self.fail()
+
+# tests on ldap rename operations
+
+
+class AclRenameTests(AclTests):
+
+ def setUp(self):
+ super(AclRenameTests, self).setUp()
+ self.regular_user = "acl_rename_user1"
+ self.ou1 = "OU=test_rename_ou1"
+ self.ou2 = "OU=test_rename_ou2"
+ self.ou3 = "OU=test_rename_ou3,%s" % self.ou2
+ self.testuser1 = "test_rename_user1"
+ self.testuser2 = "test_rename_user2"
+ self.testuser3 = "test_rename_user3"
+ self.testuser4 = "test_rename_user4"
+ self.testuser5 = "test_rename_user5"
+ # Create regular user
+ self.ldb_admin.newuser(self.regular_user, self.user_pass)
+ self.ldb_user = self.get_ldb_connection(self.regular_user, self.user_pass)
+
+ def tearDown(self):
+ super(AclRenameTests, self).tearDown()
+ # Rename OU3
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser1, self.ou3, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser2, self.ou3, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser5, self.ou3, self.base_dn))
+ delete_force(self.ldb_admin, "%s,%s" % (self.ou3, self.base_dn))
+ # Rename OU2
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser1, self.ou2, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser2, self.ou2, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser5, self.ou2, self.base_dn))
+ delete_force(self.ldb_admin, "%s,%s" % (self.ou2, self.base_dn))
+ # Rename OU1
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser1, self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser2, self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "CN=%s,%s,%s" % (self.testuser5, self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "OU=test_rename_ou3,%s,%s" % (self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, "%s,%s" % (self.ou1, self.base_dn))
+ delete_force(self.ldb_admin, self.get_user_dn(self.regular_user))
+
+ del self.ldb_user
+
+ def test_rename_u1(self):
+ """Regular user fails to rename 'User object' within single OU"""
+ # Create OU structure
+ self.ldb_admin.create_ou("OU=test_rename_ou1," + self.base_dn)
+ self.ldb_admin.newuser(self.testuser1, self.user_pass, userou=self.ou1)
+ try:
+ self.ldb_user.rename("CN=%s,%s,%s" % (self.testuser1, self.ou1, self.base_dn),
+ "CN=%s,%s,%s" % (self.testuser5, self.ou1, self.base_dn))
+ except LdbError as e21:
+ (num, _) = e21.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_rename_u2(self):
+ """Grant WRITE_PROPERTY to AU so regular user can rename 'User object' within single OU"""
+ ou_dn = "OU=test_rename_ou1," + self.base_dn
+ user_dn = "CN=test_rename_user1," + ou_dn
+ rename_user_dn = "CN=test_rename_user5," + ou_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou_dn)
+ self.ldb_admin.newuser(self.testuser1, self.user_pass, userou=self.ou1)
+ mod = "(A;;WP;;;AU)"
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ # Rename 'User object' having WP to AU
+ self.ldb_user.rename(user_dn, rename_user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % rename_user_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u3(self):
+ """Test rename with rights granted to 'User object' SID"""
+ ou_dn = "OU=test_rename_ou1," + self.base_dn
+ user_dn = "CN=test_rename_user1," + ou_dn
+ rename_user_dn = "CN=test_rename_user5," + ou_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou_dn)
+ self.ldb_admin.newuser(self.testuser1, self.user_pass, userou=self.ou1)
+ sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = "(A;;WP;;;%s)" % str(sid)
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ # Rename 'User object' having WP to AU
+ self.ldb_user.rename(user_dn, rename_user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % rename_user_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u4(self):
+ """Rename 'User object' cross OU with WP, SD and CC right granted on reg. user to AU"""
+ ou1_dn = "OU=test_rename_ou1," + self.base_dn
+ ou2_dn = "OU=test_rename_ou2," + self.base_dn
+ user_dn = "CN=test_rename_user2," + ou1_dn
+ rename_user_dn = "CN=test_rename_user5," + ou2_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou1_dn)
+ self.ldb_admin.create_ou(ou2_dn)
+ self.ldb_admin.newuser(self.testuser2, self.user_pass, userou=self.ou1)
+ mod = "(A;;WPSD;;;AU)"
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(ou2_dn, mod)
+ # Rename 'User object' having SD and CC to AU
+ self.ldb_user.rename(user_dn, rename_user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % rename_user_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u5(self):
+ """Test rename with rights granted to 'User object' SID"""
+ ou1_dn = "OU=test_rename_ou1," + self.base_dn
+ ou2_dn = "OU=test_rename_ou2," + self.base_dn
+ user_dn = "CN=test_rename_user2," + ou1_dn
+ rename_user_dn = "CN=test_rename_user5," + ou2_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou1_dn)
+ self.ldb_admin.create_ou(ou2_dn)
+ self.ldb_admin.newuser(self.testuser2, self.user_pass, userou=self.ou1)
+ sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = "(A;;WPSD;;;%s)" % str(sid)
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ mod = "(A;;CC;;;%s)" % str(sid)
+ self.sd_utils.dacl_add_ace(ou2_dn, mod)
+ # Rename 'User object' having SD and CC to AU
+ self.ldb_user.rename(user_dn, rename_user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % rename_user_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u6(self):
+ """Rename 'User object' cross OU with WP, DC and CC right granted on OU & user to AU"""
+ ou1_dn = "OU=test_rename_ou1," + self.base_dn
+ ou2_dn = "OU=test_rename_ou2," + self.base_dn
+ user_dn = "CN=test_rename_user2," + ou1_dn
+ rename_user_dn = "CN=test_rename_user2," + ou2_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou1_dn)
+ self.ldb_admin.create_ou(ou2_dn)
+ #mod = "(A;CI;DCWP;;;AU)"
+ mod = "(A;;DC;;;AU)"
+ self.sd_utils.dacl_add_ace(ou1_dn, mod)
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(ou2_dn, mod)
+ self.ldb_admin.newuser(self.testuser2, self.user_pass, userou=self.ou1)
+ mod = "(A;;WP;;;AU)"
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ # Rename 'User object' having SD and CC to AU
+ self.ldb_user.rename(user_dn, rename_user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % rename_user_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u7(self):
+ """Rename 'User object' cross OU (second level) with WP, DC and CC right granted on OU to AU"""
+ ou1_dn = "OU=test_rename_ou1," + self.base_dn
+ ou2_dn = "OU=test_rename_ou2," + self.base_dn
+ ou3_dn = "OU=test_rename_ou3," + ou2_dn
+ user_dn = "CN=test_rename_user2," + ou1_dn
+ rename_user_dn = "CN=test_rename_user5," + ou3_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou1_dn)
+ self.ldb_admin.create_ou(ou2_dn)
+ self.ldb_admin.create_ou(ou3_dn)
+ mod = "(A;CI;WPDC;;;AU)"
+ self.sd_utils.dacl_add_ace(ou1_dn, mod)
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(ou3_dn, mod)
+ self.ldb_admin.newuser(self.testuser2, self.user_pass, userou=self.ou1)
+ # Rename 'User object' having SD and CC to AU
+ self.ldb_user.rename(user_dn, rename_user_dn)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % user_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % rename_user_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u8(self):
+ """Test rename on an object with and without modify access on the RDN attribute"""
+ ou1_dn = "OU=test_rename_ou1," + self.base_dn
+ ou2_dn = "OU=test_rename_ou2," + ou1_dn
+ ou3_dn = "OU=test_rename_ou3," + ou1_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou1_dn)
+ self.ldb_admin.create_ou(ou2_dn)
+ sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = "(OA;;WP;bf967a0e-0de6-11d0-a285-00aa003049e2;;%s)" % str(sid)
+ self.sd_utils.dacl_add_ace(ou2_dn, mod)
+ mod = "(OD;;WP;bf9679f0-0de6-11d0-a285-00aa003049e2;;%s)" % str(sid)
+ self.sd_utils.dacl_add_ace(ou2_dn, mod)
+ try:
+ self.ldb_user.rename(ou2_dn, ou3_dn)
+ except LdbError as e22:
+ (num, _) = e22.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ # This rename operation should always throw ERR_INSUFFICIENT_ACCESS_RIGHTS
+ self.fail()
+ sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+ mod = "(A;;WP;bf9679f0-0de6-11d0-a285-00aa003049e2;;%s)" % str(sid)
+ self.sd_utils.dacl_add_ace(ou2_dn, mod)
+ self.ldb_user.rename(ou2_dn, ou3_dn)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)" % ou2_dn)
+ self.assertEqual(len(res), 0)
+ res = self.ldb_admin.search(self.base_dn, expression="(distinguishedName=%s)" % ou3_dn)
+ self.assertNotEqual(len(res), 0)
+
+ def test_rename_u9(self):
+ """Rename 'User object' cross OU, with explicit deny on sd and dc"""
+ ou1_dn = "OU=test_rename_ou1," + self.base_dn
+ ou2_dn = "OU=test_rename_ou2," + self.base_dn
+ user_dn = "CN=test_rename_user2," + ou1_dn
+ rename_user_dn = "CN=test_rename_user5," + ou2_dn
+ # Create OU structure
+ self.ldb_admin.create_ou(ou1_dn)
+ self.ldb_admin.create_ou(ou2_dn)
+ self.ldb_admin.newuser(self.testuser2, self.user_pass, userou=self.ou1)
+ mod = "(D;;SD;;;DA)"
+ self.sd_utils.dacl_add_ace(user_dn, mod)
+ mod = "(D;;DC;;;DA)"
+ self.sd_utils.dacl_add_ace(ou1_dn, mod)
+ # Rename 'User object' having SD and CC to AU
+ try:
+ self.ldb_admin.rename(user_dn, rename_user_dn)
+ except LdbError as e23:
+ (num, _) = e23.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+ # add an allow ace so we can delete this ou
+ mod = "(A;;DC;;;DA)"
+ self.sd_utils.dacl_add_ace(ou1_dn, mod)
+
+
+# tests on Control Access Rights
+class AclCARTests(AclTests):
+
+ def setUp(self):
+ super(AclCARTests, self).setUp()
+
+ # Get the old "dSHeuristics" if it was set
+ dsheuristics = self.ldb_admin.get_dsheuristics()
+ # Reset the "dSHeuristics" as they were before
+ self.addCleanup(self.ldb_admin.set_dsheuristics, dsheuristics)
+ # Set the "dSHeuristics" to activate the correct "userPassword" behaviour
+ self.ldb_admin.set_dsheuristics("000000001")
+ # Get the old "minPwdAge"
+ minPwdAge = self.ldb_admin.get_minPwdAge()
+ # Reset the "minPwdAge" as it was before
+ self.addCleanup(self.ldb_admin.set_minPwdAge, minPwdAge)
+ # Set it temporarily to "0"
+ self.ldb_admin.set_minPwdAge("0")
+
+ self.user_with_wp = "acl_car_user1"
+ self.user_with_pc = "acl_car_user2"
+ self.ldb_admin.newuser(self.user_with_wp, self.user_pass)
+ self.ldb_admin.newuser(self.user_with_pc, self.user_pass)
+ self.ldb_user = self.get_ldb_connection(self.user_with_wp, self.user_pass)
+ self.ldb_user2 = self.get_ldb_connection(self.user_with_pc, self.user_pass)
+
+ def tearDown(self):
+ super(AclCARTests, self).tearDown()
+ delete_force(self.ldb_admin, self.get_user_dn(self.user_with_wp))
+ delete_force(self.ldb_admin, self.get_user_dn(self.user_with_pc))
+
+ del self.ldb_user
+ del self.ldb_user2
+
+ def test_change_password1(self):
+ """Try a password change operation without any CARs given"""
+ # users have change password by default - remove for negative testing
+ desc = self.sd_utils.read_sd_on_dn(self.get_user_dn(self.user_with_wp))
+ sddl = desc.as_sddl(self.domain_sid)
+ sddl = sddl.replace("(OA;;CR;ab721a53-1e2f-11d0-9819-00aa0040529b;;WD)", "")
+ sddl = sddl.replace("(OA;;CR;ab721a53-1e2f-11d0-9819-00aa0040529b;;PS)", "")
+ self.sd_utils.modify_sd_on_dn(self.get_user_dn(self.user_with_wp), sddl)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"samba123@\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ except LdbError as e24:
+ (num, _) = e24.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ # for some reason we get constraint violation instead of insufficient access error
+ self.fail()
+
+ def test_change_password2(self):
+ """Make sure WP has no influence"""
+ desc = self.sd_utils.read_sd_on_dn(self.get_user_dn(self.user_with_wp))
+ sddl = desc.as_sddl(self.domain_sid)
+ sddl = sddl.replace("(OA;;CR;ab721a53-1e2f-11d0-9819-00aa0040529b;;WD)", "")
+ sddl = sddl.replace("(OA;;CR;ab721a53-1e2f-11d0-9819-00aa0040529b;;PS)", "")
+ self.sd_utils.modify_sd_on_dn(self.get_user_dn(self.user_with_wp), sddl)
+ mod = "(A;;WP;;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"samba123@\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ except LdbError as e25:
+ (num, _) = e25.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ # for some reason we get constraint violation instead of insufficient access error
+ self.fail()
+
+ def test_change_password3(self):
+ """Make sure WP has no influence"""
+ mod = "(D;;WP;;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"samba123@\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+
+ def test_change_password5(self):
+ """Make sure rights have no influence on dBCSPwd"""
+ desc = self.sd_utils.read_sd_on_dn(self.get_user_dn(self.user_with_wp))
+ sddl = desc.as_sddl(self.domain_sid)
+ sddl = sddl.replace("(OA;;CR;ab721a53-1e2f-11d0-9819-00aa0040529b;;WD)", "")
+ sddl = sddl.replace("(OA;;CR;ab721a53-1e2f-11d0-9819-00aa0040529b;;PS)", "")
+ self.sd_utils.modify_sd_on_dn(self.get_user_dn(self.user_with_wp), sddl)
+ mod = "(D;;WP;;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: dBCSPwd
+dBCSPwd: XXXXXXXXXXXXXXXX
+add: dBCSPwd
+dBCSPwd: YYYYYYYYYYYYYYYY
+""")
+ except LdbError as e26:
+ (num, _) = e26.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ else:
+ self.fail()
+
+ def test_change_password6(self):
+ """Test uneven delete/adds"""
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ except LdbError as e27:
+ (num, _) = e27.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+ mod = "(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ # This fails on Windows 2000 domain level with constraint violation
+ except LdbError as e28:
+ (num, _) = e28.args
+ self.assertIn(num, (ERR_CONSTRAINT_VIOLATION,
+ ERR_UNWILLING_TO_PERFORM))
+ else:
+ self.fail()
+
+ def test_change_password7(self):
+ """Try a password change operation without any CARs given"""
+ # users have change password by default - remove for negative testing
+ desc = self.sd_utils.read_sd_on_dn(self.get_user_dn(self.user_with_wp))
+ sddl = desc.as_sddl(self.domain_sid)
+ self.sd_utils.modify_sd_on_dn(self.get_user_dn(self.user_with_wp), sddl)
+ # first change our own password
+ self.ldb_user2.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_pc) + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"samba123@\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ # then someone else's
+ self.ldb_user2.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"samba123@\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+
+ def test_reset_password1(self):
+ """Try a user password reset operation (unicodePwd) before and after granting CAR"""
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ except LdbError as e29:
+ (num, _) = e29.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+ mod = "(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')).decode('utf8') + """
+""")
+
+ def test_reset_password2(self):
+ """Try a user password reset operation (userPassword) before and after granting CAR"""
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ except LdbError as e30:
+ (num, _) = e30.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+ mod = "(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ # This fails on Windows 2000 domain level with constraint violation
+ except LdbError as e31:
+ (num, _) = e31.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ pass # Not self.fail() as we normally want success.
+
+ def test_reset_password3(self):
+ """Grant WP and see what happens (unicodePwd)"""
+ mod = "(A;;WP;;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ except LdbError as e32:
+ (num, _) = e32.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_reset_password4(self):
+ """Grant WP and see what happens (userPassword)"""
+ mod = "(A;;WP;;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ except LdbError as e33:
+ (num, _) = e33.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_reset_password5(self):
+ """Explicitly deny WP but grant CAR (unicodePwd)"""
+ mod = "(D;;WP;;;PS)(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')).decode('utf8') + """
+""")
+
+ def test_reset_password6(self):
+ """Explicitly deny WP but grant CAR (userPassword)"""
+ mod = "(D;;WP;;;PS)(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;PS)"
+ self.sd_utils.dacl_add_ace(self.get_user_dn(self.user_with_wp), mod)
+ try:
+ self.ldb_user.modify_ldif("""
+dn: """ + self.get_user_dn(self.user_with_wp) + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ # This fails on Windows 2000 domain level with constraint violation
+ except LdbError as e34:
+ (num, _) = e34.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ pass # Not self.fail() as we normally want success
+
+
+class AclExtendedTests(AclTests):
+
+ def setUp(self):
+ super(AclExtendedTests, self).setUp()
+ # regular user, will be the creator
+ self.u1 = "ext_u1"
+ # regular user
+ self.u2 = "ext_u2"
+ # admin user
+ self.u3 = "ext_u3"
+ self.ldb_admin.newuser(self.u1, self.user_pass)
+ self.ldb_admin.newuser(self.u2, self.user_pass)
+ self.ldb_admin.newuser(self.u3, self.user_pass)
+ self.ldb_admin.add_remove_group_members("Domain Admins", [self.u3],
+ add_members_operation=True)
+ self.ldb_user1 = self.get_ldb_connection(self.u1, self.user_pass)
+ self.ldb_user2 = self.get_ldb_connection(self.u2, self.user_pass)
+ self.ldb_user3 = self.get_ldb_connection(self.u3, self.user_pass)
+ self.user_sid1 = self.sd_utils.get_object_sid(self.get_user_dn(self.u1))
+ self.user_sid2 = self.sd_utils.get_object_sid(self.get_user_dn(self.u2))
+
+ def tearDown(self):
+ super(AclExtendedTests, self).tearDown()
+ delete_force(self.ldb_admin, self.get_user_dn(self.u1))
+ delete_force(self.ldb_admin, self.get_user_dn(self.u2))
+ delete_force(self.ldb_admin, self.get_user_dn(self.u3))
+ delete_force(self.ldb_admin, "CN=ext_group1,OU=ext_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "ou=ext_ou1," + self.base_dn)
+
+ del self.ldb_user1
+ del self.ldb_user2
+ del self.ldb_user3
+
+ def test_ntSecurityDescriptor(self):
+ # create empty ou
+ self.ldb_admin.create_ou("ou=ext_ou1," + self.base_dn)
+ # give u1 Create children access
+ mod = "(A;;CC;;;%s)" % str(self.user_sid1)
+ self.sd_utils.dacl_add_ace("OU=ext_ou1," + self.base_dn, mod)
+ mod = "(A;;LC;;;%s)" % str(self.user_sid2)
+ self.sd_utils.dacl_add_ace("OU=ext_ou1," + self.base_dn, mod)
+ # create a group under that, grant RP to u2
+ self.ldb_user1.newgroup("ext_group1", groupou="OU=ext_ou1",
+ grouptype=samba.dsdb.GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)
+ mod = "(A;;RP;;;%s)" % str(self.user_sid2)
+ self.sd_utils.dacl_add_ace("CN=ext_group1,OU=ext_ou1," + self.base_dn, mod)
+ # u2 must not read the descriptor
+ res = self.ldb_user2.search("CN=ext_group1,OU=ext_ou1," + self.base_dn,
+ SCOPE_BASE, None, ["nTSecurityDescriptor"])
+ self.assertNotEqual(len(res), 0)
+ self.assertNotIn("nTSecurityDescriptor", res[0].keys())
+ # grant RC to u2 - still no access
+ mod = "(A;;RC;;;%s)" % str(self.user_sid2)
+ self.sd_utils.dacl_add_ace("CN=ext_group1,OU=ext_ou1," + self.base_dn, mod)
+ res = self.ldb_user2.search("CN=ext_group1,OU=ext_ou1," + self.base_dn,
+ SCOPE_BASE, None, ["nTSecurityDescriptor"])
+ self.assertNotEqual(len(res), 0)
+ self.assertNotIn("nTSecurityDescriptor", res[0].keys())
+ # u3 is member of administrators group, should be able to read sd
+ res = self.ldb_user3.search("CN=ext_group1,OU=ext_ou1," + self.base_dn,
+ SCOPE_BASE, None, ["nTSecurityDescriptor"])
+ self.assertEqual(len(res), 1)
+ self.assertIn("nTSecurityDescriptor", res[0].keys())
+
+
+class AclUndeleteTests(AclTests):
+
+ def setUp(self):
+ super(AclUndeleteTests, self).setUp()
+ self.regular_user = "undeleter1"
+ self.ou1 = "OU=undeleted_ou,"
+ self.testuser1 = "to_be_undeleted1"
+ self.testuser2 = "to_be_undeleted2"
+ self.testuser3 = "to_be_undeleted3"
+ self.testuser4 = "to_be_undeleted4"
+ self.testuser5 = "to_be_undeleted5"
+ self.testuser6 = "to_be_undeleted6"
+
+ self.new_dn_ou = "CN=" + self.testuser4 + "," + self.ou1 + self.base_dn
+
+ # Create regular user
+ self.testuser1_dn = self.get_user_dn(self.testuser1)
+ self.testuser2_dn = self.get_user_dn(self.testuser2)
+ self.testuser3_dn = self.get_user_dn(self.testuser3)
+ self.testuser4_dn = self.get_user_dn(self.testuser4)
+ self.testuser5_dn = self.get_user_dn(self.testuser5)
+ self.deleted_dn1 = self.create_delete_user(self.testuser1)
+ self.deleted_dn2 = self.create_delete_user(self.testuser2)
+ self.deleted_dn3 = self.create_delete_user(self.testuser3)
+ self.deleted_dn4 = self.create_delete_user(self.testuser4)
+ self.deleted_dn5 = self.create_delete_user(self.testuser5)
+
+ self.ldb_admin.create_ou(self.ou1 + self.base_dn)
+
+ self.ldb_admin.newuser(self.regular_user, self.user_pass)
+ self.ldb_admin.add_remove_group_members("Domain Admins", [self.regular_user],
+ add_members_operation=True)
+ self.ldb_user = self.get_ldb_connection(self.regular_user, self.user_pass)
+ self.sid = self.sd_utils.get_object_sid(self.get_user_dn(self.regular_user))
+
+ def tearDown(self):
+ super(AclUndeleteTests, self).tearDown()
+ delete_force(self.ldb_admin, self.get_user_dn(self.regular_user))
+ delete_force(self.ldb_admin, self.get_user_dn(self.testuser1))
+ delete_force(self.ldb_admin, self.get_user_dn(self.testuser2))
+ delete_force(self.ldb_admin, self.get_user_dn(self.testuser3))
+ delete_force(self.ldb_admin, self.get_user_dn(self.testuser4))
+ delete_force(self.ldb_admin, self.get_user_dn(self.testuser5))
+ delete_force(self.ldb_admin, self.new_dn_ou)
+ delete_force(self.ldb_admin, self.ou1 + self.base_dn)
+
+ del self.ldb_user
+
+ def GUID_string(self, guid):
+ return get_string(ldb.schema_format_value("objectGUID", guid))
+
+ def create_delete_user(self, new_user):
+ self.ldb_admin.newuser(new_user, self.user_pass)
+
+ res = self.ldb_admin.search(expression="(objectClass=*)",
+ base=self.get_user_dn(new_user),
+ scope=SCOPE_BASE,
+ controls=["show_deleted:1"])
+ guid = res[0]["objectGUID"][0]
+ self.ldb_admin.delete(self.get_user_dn(new_user))
+ res = self.ldb_admin.search(base="<GUID=%s>" % self.GUID_string(guid),
+ scope=SCOPE_BASE, controls=["show_deleted:1"])
+ self.assertEqual(len(res), 1)
+ return str(res[0].dn)
+
+ def undelete_deleted(self, olddn, newdn):
+ msg = Message()
+ msg.dn = Dn(self.ldb_user, olddn)
+ msg["isDeleted"] = MessageElement([], FLAG_MOD_DELETE, "isDeleted")
+ msg["distinguishedName"] = MessageElement([newdn], FLAG_MOD_REPLACE, "distinguishedName")
+ self.ldb_user.modify(msg, ["show_recycled:1"])
+
+ def undelete_deleted_with_mod(self, olddn, newdn):
+ msg = Message()
+ msg.dn = Dn(ldb, olddn)
+ msg["isDeleted"] = MessageElement([], FLAG_MOD_DELETE, "isDeleted")
+ msg["distinguishedName"] = MessageElement([newdn], FLAG_MOD_REPLACE, "distinguishedName")
+ msg["url"] = MessageElement(["www.samba.org"], FLAG_MOD_REPLACE, "url")
+ self.ldb_user.modify(msg, ["show_deleted:1"])
+
+ def test_undelete(self):
+ # it appears the user has to have LC on the old parent to be able to move the object
+ # otherwise we get no such object. Since only System can modify the SD on deleted object
+ # we cannot grant this permission via LDAP, and this leaves us with "negative" tests at the moment
+
+ # deny write property on rdn, should fail
+ mod = "(OD;;WP;bf967a0e-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.deleted_dn1, mod)
+ try:
+ self.undelete_deleted(self.deleted_dn1, self.testuser1_dn)
+ self.fail()
+ except LdbError as e35:
+ (num, _) = e35.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ # seems that permissions on isDeleted and distinguishedName are irrelevant
+ mod = "(OD;;WP;bf96798f-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.deleted_dn2, mod)
+ mod = "(OD;;WP;bf9679e4-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.deleted_dn2, mod)
+ self.undelete_deleted(self.deleted_dn2, self.testuser2_dn)
+
+ # attempt undelete with simultaneous addition of url, WP to which is denied
+ mod = "(OD;;WP;9a9a0221-4a5b-11d1-a9c3-0000f80367c1;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.deleted_dn3, mod)
+ try:
+ self.undelete_deleted_with_mod(self.deleted_dn3, self.testuser3_dn)
+ self.fail()
+ except LdbError as e36:
+ (num, _) = e36.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ # undelete in an ou, in which we have no right to create children
+ mod = "(D;;CC;;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.ou1 + self.base_dn, mod)
+ try:
+ self.undelete_deleted(self.deleted_dn4, self.new_dn_ou)
+ self.fail()
+ except LdbError as e37:
+ (num, _) = e37.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ # delete is not required
+ mod = "(D;;SD;;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.deleted_dn5, mod)
+ self.undelete_deleted(self.deleted_dn5, self.testuser5_dn)
+
+ # deny Reanimate-Tombstone, should fail
+ mod = "(OD;;CR;45ec5156-db7e-47bb-b53f-dbeb2d03c40f;;%s)" % str(self.sid)
+ self.sd_utils.dacl_add_ace(self.base_dn, mod)
+ try:
+ self.undelete_deleted(self.deleted_dn4, self.testuser4_dn)
+ self.fail()
+ except LdbError as e38:
+ (num, _) = e38.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ self.sd_utils.dacl_delete_aces(self.base_dn, mod)
+
+
+class AclSPNTests(AclTests):
+
+ def setUp(self):
+ super(AclSPNTests, self).setUp()
+ self.dcname = "TESTSRV8"
+ self.rodcname = "TESTRODC8"
+ self.computername = "testcomp8"
+ self.test_user = "spn_test_user8"
+ self.computerdn = "CN=%s,CN=computers,%s" % (self.computername, self.base_dn)
+ self.user_object = "user_with_spn"
+ self.user_object_dn = "CN=%s,CN=Users,%s" % (self.user_object, self.base_dn)
+ self.dc_dn = "CN=%s,OU=Domain Controllers,%s" % (self.dcname, self.base_dn)
+ self.site = "Default-First-Site-Name"
+ self.rodcctx = DCJoinContext(server=host, creds=creds, lp=lp,
+ site=self.site, netbios_name=self.rodcname,
+ targetdir=None, domain=None)
+ self.dcctx = DCJoinContext(server=host, creds=creds, lp=lp,
+ site=self.site, netbios_name=self.dcname,
+ targetdir=None, domain=None)
+ self.ldb_admin.newuser(self.test_user, self.user_pass)
+ self.ldb_user1 = self.get_ldb_connection(self.test_user, self.user_pass)
+ self.user_sid1 = self.sd_utils.get_object_sid(self.get_user_dn(self.test_user))
+ self.create_computer(self.computername, self.dcctx.dnsdomain)
+ self.create_rodc(self.rodcctx)
+ self.create_dc(self.dcctx)
+
+ def tearDown(self):
+ super(AclSPNTests, self).tearDown()
+ self.rodcctx.cleanup_old_join()
+ self.dcctx.cleanup_old_join()
+ delete_force(self.ldb_admin, "cn=%s,cn=computers,%s" % (self.computername, self.base_dn))
+ delete_force(self.ldb_admin, self.get_user_dn(self.test_user))
+ delete_force(self.ldb_admin, self.user_object_dn)
+
+ del self.ldb_user1
+
+ def replace_spn(self, _ldb, dn, spn):
+ print("Setting spn %s on %s" % (spn, dn))
+ res = self.ldb_admin.search(dn, expression="(objectClass=*)",
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ if "servicePrincipalName" in res[0].keys():
+ flag = FLAG_MOD_REPLACE
+ else:
+ flag = FLAG_MOD_ADD
+
+ msg = Message()
+ msg.dn = Dn(self.ldb_admin, dn)
+ msg["servicePrincipalName"] = MessageElement(spn, flag,
+ "servicePrincipalName")
+ _ldb.modify(msg)
+
+ def create_computer(self, computername, domainname):
+ dn = "CN=%s,CN=computers,%s" % (computername, self.base_dn)
+ samaccountname = computername + "$"
+ dnshostname = "%s.%s" % (computername, domainname)
+ self.ldb_admin.add({
+ "dn": dn,
+ "objectclass": "computer",
+ "sAMAccountName": samaccountname,
+ "userAccountControl": str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ "dNSHostName": dnshostname})
+
+ # same as for join_RODC, but do not set any SPNs
+ def create_rodc(self, ctx):
+ ctx.nc_list = [ctx.base_dn, ctx.config_dn, ctx.schema_dn]
+ ctx.full_nc_list = [ctx.base_dn, ctx.config_dn, ctx.schema_dn]
+ ctx.krbtgt_dn = "CN=krbtgt_%s,CN=Users,%s" % (ctx.myname, ctx.base_dn)
+
+ ctx.never_reveal_sid = ["<SID=%s-%s>" % (ctx.domsid, security.DOMAIN_RID_RODC_DENY),
+ "<SID=%s>" % security.SID_BUILTIN_ADMINISTRATORS,
+ "<SID=%s>" % security.SID_BUILTIN_SERVER_OPERATORS,
+ "<SID=%s>" % security.SID_BUILTIN_BACKUP_OPERATORS,
+ "<SID=%s>" % security.SID_BUILTIN_ACCOUNT_OPERATORS]
+ ctx.reveal_sid = "<SID=%s-%s>" % (ctx.domsid, security.DOMAIN_RID_RODC_ALLOW)
+
+ mysid = ctx.get_mysid()
+ admin_dn = "<SID=%s>" % mysid
+ ctx.managedby = admin_dn
+
+ ctx.userAccountControl = (samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
+ samba.dsdb.UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION |
+ samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT)
+
+ ctx.connection_dn = "CN=RODC Connection (FRS),%s" % ctx.ntds_dn
+ ctx.secure_channel_type = misc.SEC_CHAN_RODC
+ ctx.RODC = True
+ ctx.replica_flags = (drsuapi.DRSUAPI_DRS_INIT_SYNC |
+ drsuapi.DRSUAPI_DRS_PER_SYNC |
+ drsuapi.DRSUAPI_DRS_GET_ANC |
+ drsuapi.DRSUAPI_DRS_NEVER_SYNCED |
+ drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING)
+
+ ctx.join_add_objects()
+
+ def create_dc(self, ctx):
+ ctx.nc_list = [ctx.base_dn, ctx.config_dn, ctx.schema_dn]
+ ctx.full_nc_list = [ctx.base_dn, ctx.config_dn, ctx.schema_dn]
+ ctx.userAccountControl = samba.dsdb.UF_SERVER_TRUST_ACCOUNT | samba.dsdb.UF_TRUSTED_FOR_DELEGATION
+ ctx.secure_channel_type = misc.SEC_CHAN_BDC
+ ctx.replica_flags = (drsuapi.DRSUAPI_DRS_WRIT_REP |
+ drsuapi.DRSUAPI_DRS_INIT_SYNC |
+ drsuapi.DRSUAPI_DRS_PER_SYNC |
+ drsuapi.DRSUAPI_DRS_FULL_SYNC_IN_PROGRESS |
+ drsuapi.DRSUAPI_DRS_NEVER_SYNCED)
+
+ ctx.join_add_objects()
+
+ def dc_spn_test(self, ctx):
+ netbiosdomain = self.dcctx.get_domain_name()
+ try:
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "HOST/%s/%s" % (ctx.myname, netbiosdomain))
+ except LdbError as e39:
+ (num, _) = e39.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ mod = "(OA;;SW;f3a64788-5306-11d1-a9c5-0000f80367c1;;%s)" % str(self.user_sid1)
+ self.sd_utils.dacl_add_ace(ctx.acct_dn, mod)
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "HOST/%s/%s" % (ctx.myname, netbiosdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "HOST/%s" % (ctx.myname))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "HOST/%s.%s/%s" %
+ (ctx.myname, ctx.dnsdomain, netbiosdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "HOST/%s/%s" % (ctx.myname, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "HOST/%s.%s/%s" %
+ (ctx.myname, ctx.dnsdomain, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "GC/%s.%s/%s" %
+ (ctx.myname, ctx.dnsdomain, ctx.dnsforest))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s/%s" % (ctx.myname, netbiosdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s.%s/%s" %
+ (ctx.myname, ctx.dnsdomain, netbiosdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s" % (ctx.myname))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s/%s" % (ctx.myname, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s.%s/%s" %
+ (ctx.myname, ctx.dnsdomain, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "DNS/%s/%s" % (ctx.myname, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "RestrictedKrbHost/%s/%s" %
+ (ctx.myname, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "RestrictedKrbHost/%s" %
+ (ctx.myname))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "Dfsr-12F9A27C-BF97-4787-9364-D31B6C55EB04/%s/%s" %
+ (ctx.myname, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "NtFrs-88f5d2bd-b646-11d2-a6d3-00c04fc9b232/%s/%s" %
+ (ctx.myname, ctx.dnsdomain))
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s._msdcs.%s" %
+ (ctx.ntds_guid, ctx.dnsdomain))
+
+ # the following spns do not match the restrictions and should fail
+ try:
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s.%s/ForestDnsZones.%s" %
+ (ctx.myname, ctx.dnsdomain, ctx.dnsdomain))
+ except LdbError as e40:
+ (num, _) = e40.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "ldap/%s.%s/DomainDnsZones.%s" %
+ (ctx.myname, ctx.dnsdomain, ctx.dnsdomain))
+ except LdbError as e41:
+ (num, _) = e41.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "nosuchservice/%s/%s" % ("abcd", "abcd"))
+ except LdbError as e42:
+ (num, _) = e42.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "GC/%s.%s/%s" %
+ (ctx.myname, ctx.dnsdomain, netbiosdomain))
+ except LdbError as e43:
+ (num, _) = e43.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, ctx.acct_dn, "E3514235-4B06-11D1-AB04-00C04FC2DCD2/%s/%s" %
+ (ctx.ntds_guid, ctx.dnsdomain))
+ except LdbError as e44:
+ (num, _) = e44.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+
+ def test_computer_spn(self):
+ # with WP, any value can be set
+ netbiosdomain = self.dcctx.get_domain_name()
+ self.replace_spn(self.ldb_admin, self.computerdn, "HOST/%s/%s" %
+ (self.computername, netbiosdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "HOST/%s" % (self.computername))
+ self.replace_spn(self.ldb_admin, self.computerdn, "HOST/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, netbiosdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "HOST/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "HOST/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "GC/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsforest))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s/%s" % (self.computername, netbiosdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s.%s/ForestDnsZones.%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s.%s/DomainDnsZones.%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, netbiosdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s" % (self.computername))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "ldap/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "DNS/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "RestrictedKrbHost/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "RestrictedKrbHost/%s" %
+ (self.computername))
+ self.replace_spn(self.ldb_admin, self.computerdn, "Dfsr-12F9A27C-BF97-4787-9364-D31B6C55EB04/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "NtFrs-88f5d2bd-b646-11d2-a6d3-00c04fc9b232/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ self.replace_spn(self.ldb_admin, self.computerdn, "nosuchservice/%s/%s" % ("abcd", "abcd"))
+
+ # user has neither WP nor Validated-SPN, access denied expected
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s/%s" % (self.computername, netbiosdomain))
+ except LdbError as e45:
+ (num, _) = e45.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ mod = "(OA;;SW;f3a64788-5306-11d1-a9c5-0000f80367c1;;%s)" % str(self.user_sid1)
+ self.sd_utils.dacl_add_ace(self.computerdn, mod)
+ # grant Validated-SPN and check which values are accepted
+ # see 3.1.1.5.3.1.1.4 servicePrincipalName for reference
+
+ # for regular computer objects we shouldalways get constraint violation
+
+ # This does not pass against Windows, although it should according to docs
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s" % (self.computername))
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s.%s" %
+ (self.computername, self.dcctx.dnsdomain))
+
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s/%s" % (self.computername, netbiosdomain))
+ except LdbError as e46:
+ (num, _) = e46.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, netbiosdomain))
+ except LdbError as e47:
+ (num, _) = e47.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s/%s" %
+ (self.computername, self.dcctx.dnsdomain))
+ except LdbError as e48:
+ (num, _) = e48.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "HOST/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsdomain))
+ except LdbError as e49:
+ (num, _) = e49.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "GC/%s.%s/%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsforest))
+ except LdbError as e50:
+ (num, _) = e50.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "ldap/%s/%s" % (self.computername, netbiosdomain))
+ except LdbError as e51:
+ (num, _) = e51.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+ try:
+ self.replace_spn(self.ldb_user1, self.computerdn, "ldap/%s.%s/ForestDnsZones.%s" %
+ (self.computername, self.dcctx.dnsdomain, self.dcctx.dnsdomain))
+ except LdbError as e52:
+ (num, _) = e52.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+
+ def test_spn_rwdc(self):
+ self.dc_spn_test(self.dcctx)
+
+ def test_spn_rodc(self):
+ self.dc_spn_test(self.rodcctx)
+
+ def test_user_spn(self):
+ #grant SW to a regular user and try to set the spn on a user object
+ #should get ERR_INSUFFICIENT_ACCESS_RIGHTS, since Validate-SPN only applies to computer
+ self.ldb_admin.newuser(self.user_object, self.user_pass)
+ mod = "(OA;;SW;f3a64788-5306-11d1-a9c5-0000f80367c1;;%s)" % str(self.user_sid1)
+ self.sd_utils.dacl_add_ace(self.user_object_dn, mod)
+ try:
+ self.replace_spn(self.ldb_user1, self.user_object_dn, "nosuchservice/%s/%s" % ("abcd", "abcd"))
+ except LdbError as e60:
+ (num, _) = e60.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_delete_add_spn(self):
+ # Grant Validated-SPN property.
+ mod = f'(OA;;SW;{security.GUID_DRS_VALIDATE_SPN};;{self.user_sid1})'
+ self.sd_utils.dacl_add_ace(self.computerdn, mod)
+
+ spn_base = f'HOST/{self.computername}'
+
+ allowed_spn = f'{spn_base}.{self.dcctx.dnsdomain}'
+ not_allowed_spn = f'{spn_base}/{self.dcctx.get_domain_name()}'
+
+ # Ensure we are able to add an allowed SPN.
+ msg = Message(Dn(self.ldb_user1, self.computerdn))
+ msg['servicePrincipalName'] = MessageElement(allowed_spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ self.ldb_user1.modify(msg)
+
+ # Ensure we are not able to add a disallowed SPN.
+ msg = Message(Dn(self.ldb_user1, self.computerdn))
+ msg['servicePrincipalName'] = MessageElement(not_allowed_spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ try:
+ self.ldb_user1.modify(msg)
+ except LdbError as e:
+ num, _ = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail(f'able to add disallowed SPN {not_allowed_spn}')
+
+ # Ensure that deleting an existing SPN followed by adding a disallowed
+ # SPN fails.
+ msg = Message(Dn(self.ldb_user1, self.computerdn))
+ msg['0'] = MessageElement([],
+ FLAG_MOD_DELETE,
+ 'servicePrincipalName')
+ msg['1'] = MessageElement(not_allowed_spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ try:
+ self.ldb_user1.modify(msg)
+ except LdbError as e:
+ num, _ = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail(f'able to add disallowed SPN {not_allowed_spn}')
+
+ def test_delete_disallowed_spn(self):
+ # Grant Validated-SPN property.
+ mod = f'(OA;;SW;{security.GUID_DRS_VALIDATE_SPN};;{self.user_sid1})'
+ self.sd_utils.dacl_add_ace(self.computerdn, mod)
+
+ spn_base = f'HOST/{self.computername}'
+
+ not_allowed_spn = f'{spn_base}/{self.dcctx.get_domain_name()}'
+
+ # Add a disallowed SPN as admin.
+ msg = Message(Dn(self.ldb_admin, self.computerdn))
+ msg['servicePrincipalName'] = MessageElement(not_allowed_spn,
+ FLAG_MOD_ADD,
+ 'servicePrincipalName')
+ self.ldb_admin.modify(msg)
+
+ # Ensure we are able to delete a disallowed SPN.
+ msg = Message(Dn(self.ldb_user1, self.computerdn))
+ msg['servicePrincipalName'] = MessageElement(not_allowed_spn,
+ FLAG_MOD_DELETE,
+ 'servicePrincipalName')
+ try:
+ self.ldb_user1.modify(msg)
+ except LdbError:
+ self.fail(f'unable to delete disallowed SPN {not_allowed_spn}')
+
+
+# tests SEC_ADS_LIST vs. SEC_ADS_LIST_OBJECT
+@DynamicTestCase
+class AclVisibiltyTests(AclTests):
+
+ envs = {
+ "No": False,
+ "Do": True,
+ }
+ modes = {
+ "Allow": False,
+ "Deny": True,
+ }
+ perms = {
+ "nn": 0,
+ "Cn": security.SEC_ADS_LIST,
+ "nO": security.SEC_ADS_LIST_OBJECT,
+ "CO": security.SEC_ADS_LIST | security.SEC_ADS_LIST_OBJECT,
+ }
+
+ @classmethod
+ def setUpDynamicTestCases(cls):
+ for le in cls.envs.keys():
+ for lm in cls.modes.keys():
+ for l1 in cls.perms.keys():
+ for l2 in cls.perms.keys():
+ for l3 in cls.perms.keys():
+ tname = "%s_%s_%s_%s_%s" % (le, lm, l1, l2, l3)
+ ve = cls.envs[le]
+ vm = cls.modes[lm]
+ v1 = cls.perms[l1]
+ v2 = cls.perms[l2]
+ v3 = cls.perms[l3]
+ targs = (tname, ve, vm, v1, v2, v3)
+ cls.generate_dynamic_test("test_visibility",
+ tname, *targs)
+ return
+
+ def setUp(self):
+ super(AclVisibiltyTests, self).setUp()
+
+ # Get the old "dSHeuristics" if it was set
+ self.dsheuristics = self.ldb_admin.get_dsheuristics()
+ # Reset the "dSHeuristics" as they were before
+ self.addCleanup(self.ldb_admin.set_dsheuristics, self.dsheuristics)
+
+ # Domain Admins and SYSTEM get full access
+ self.sddl_dacl = "D:PAI(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;SY)"
+ self.set_dacl_control = ["sd_flags:1:%d" % security.SECINFO_DACL]
+
+ self.level_idxs = [ 1, 2, 3, 4 ]
+ self.oul1 = "OU=acl_visibility_oul1"
+ self.oul1_dn_str = "%s,%s" % (self.oul1, self.base_dn)
+ self.oul2 = "OU=oul2,%s" % self.oul1
+ self.oul2_dn_str = "%s,%s" % (self.oul2, self.base_dn)
+ self.oul3 = "OU=oul3,%s" % self.oul2
+ self.oul3_dn_str = "%s,%s" % (self.oul3, self.base_dn)
+ self.user_name = "acl_visibility_user"
+ self.user_dn_str = "CN=%s,%s" % (self.user_name, self.oul3_dn_str)
+ delete_force(self.ldb_admin, self.user_dn_str)
+ delete_force(self.ldb_admin, self.oul3_dn_str)
+ delete_force(self.ldb_admin, self.oul2_dn_str)
+ delete_force(self.ldb_admin, self.oul1_dn_str)
+ self.ldb_admin.create_ou(self.oul1_dn_str)
+ self.sd_utils.modify_sd_on_dn(self.oul1_dn_str,
+ self.sddl_dacl,
+ controls=self.set_dacl_control)
+ self.ldb_admin.create_ou(self.oul2_dn_str)
+ self.sd_utils.modify_sd_on_dn(self.oul2_dn_str,
+ self.sddl_dacl,
+ controls=self.set_dacl_control)
+ self.ldb_admin.create_ou(self.oul3_dn_str)
+ self.sd_utils.modify_sd_on_dn(self.oul3_dn_str,
+ self.sddl_dacl,
+ controls=self.set_dacl_control)
+
+ self.ldb_admin.newuser(self.user_name, self.user_pass, userou=self.oul3)
+ self.user_sid = self.sd_utils.get_object_sid(self.user_dn_str)
+ self.ldb_user = self.get_ldb_connection(self.user_name, self.user_pass)
+
+ def tearDown(self):
+ super(AclVisibiltyTests, self).tearDown()
+ delete_force(self.ldb_admin, self.user_dn_str)
+ delete_force(self.ldb_admin, self.oul3_dn_str)
+ delete_force(self.ldb_admin, self.oul2_dn_str)
+ delete_force(self.ldb_admin, self.oul1_dn_str)
+
+ del self.ldb_user
+
+ def _test_visibility_with_args(self,
+ tname,
+ fDoListObject,
+ modeDeny,
+ l1_allow,
+ l2_allow,
+ l3_allow):
+ l1_deny = 0
+ l2_deny = 0
+ l3_deny = 0
+ if modeDeny:
+ l1_deny = ~l1_allow
+ l2_deny = ~l2_allow
+ l3_deny = ~l3_allow
+ print("Testing: fDoListObject=%s, modeDeny=%s, l1_allow=0x%02x, l2_allow=0x%02x, l3_allow=0x%02x)" % (
+ fDoListObject, modeDeny, l1_allow, l2_allow, l3_allow))
+ if fDoListObject:
+ self.ldb_admin.set_dsheuristics("001")
+ else:
+ self.ldb_admin.set_dsheuristics("000")
+
+ def _generate_dacl(allow, deny):
+ dacl = self.sddl_dacl
+ drights = ""
+ if deny & security.SEC_ADS_LIST:
+ drights += "LC"
+ if deny & security.SEC_ADS_LIST_OBJECT:
+ drights += "LO"
+ if len(drights) > 0:
+ dacl += "(D;;%s;;;%s)" % (drights, self.user_sid)
+ arights = ""
+ if allow & security.SEC_ADS_LIST:
+ arights += "LC"
+ if allow & security.SEC_ADS_LIST_OBJECT:
+ arights += "LO"
+ if len(arights) > 0:
+ dacl += "(A;;%s;;;%s)" % (arights, self.user_sid)
+ print("dacl: %s" % dacl)
+ return dacl
+
+ l1_dacl = _generate_dacl(l1_allow, l1_deny)
+ l2_dacl = _generate_dacl(l2_allow, l2_deny)
+ l3_dacl = _generate_dacl(l3_allow, l3_deny)
+ self.sd_utils.modify_sd_on_dn(self.oul1_dn_str,
+ l1_dacl,
+ controls=self.set_dacl_control)
+ self.sd_utils.modify_sd_on_dn(self.oul2_dn_str,
+ l2_dacl,
+ controls=self.set_dacl_control)
+ self.sd_utils.modify_sd_on_dn(self.oul3_dn_str,
+ l3_dacl,
+ controls=self.set_dacl_control)
+
+ def _generate_levels(_l1_allow,
+ _l1_deny,
+ _l2_allow,
+ _l2_deny,
+ _l3_allow,
+ _l3_deny):
+ _l0_allow = security.SEC_ADS_LIST | security.SEC_ADS_LIST_OBJECT | security.SEC_ADS_READ_PROP
+ _l0_deny = 0
+ _l4_allow = security.SEC_ADS_LIST | security.SEC_ADS_LIST_OBJECT | security.SEC_ADS_READ_PROP
+ _l4_deny = 0
+ _levels = [{
+ "dn": str(self.base_dn),
+ "allow": _l0_allow,
+ "deny": _l0_deny,
+ },{
+ "dn": str(self.oul1_dn_str),
+ "allow": _l1_allow,
+ "deny": _l1_deny,
+ },{
+ "dn": str(self.oul2_dn_str),
+ "allow": _l2_allow,
+ "deny": _l2_deny,
+ },{
+ "dn": str(self.oul3_dn_str),
+ "allow": _l3_allow,
+ "deny": _l3_deny,
+ },{
+ "dn": str(self.user_dn_str),
+ "allow": _l4_allow,
+ "deny": _l4_deny,
+ }]
+ return _levels
+
+ def _generate_admin_levels():
+ _l1_allow = security.SEC_ADS_LIST | security.SEC_ADS_READ_PROP
+ _l1_deny = 0
+ _l2_allow = security.SEC_ADS_LIST | security.SEC_ADS_READ_PROP
+ _l2_deny = 0
+ _l3_allow = security.SEC_ADS_LIST | security.SEC_ADS_READ_PROP
+ _l3_deny = 0
+ return _generate_levels(_l1_allow, _l1_deny,
+ _l2_allow, _l2_deny,
+ _l3_allow, _l3_deny)
+
+ def _generate_user_levels():
+ return _generate_levels(l1_allow, l1_deny,
+ l2_allow, l2_deny,
+ l3_allow, l3_deny)
+
+ admin_levels = _generate_admin_levels()
+ user_levels = _generate_user_levels()
+
+ def _msg_require_name(msg, idx, e):
+ self.assertIn("name", msg)
+ self.assertEqual(len(msg["name"]), 1)
+
+ def _msg_no_name(msg, idx, e):
+ self.assertNotIn("name", msg)
+
+ def _has_right(allow, deny, bit):
+ if allow & bit:
+ if not (deny & bit):
+ return True
+ return False
+
+ def _is_visible(p_allow, p_deny, o_allow, o_deny):
+ plc = _has_right(p_allow, p_deny, security.SEC_ADS_LIST)
+ if plc:
+ return True
+ if not fDoListObject:
+ return False
+ plo = _has_right(p_allow, p_deny, security.SEC_ADS_LIST_OBJECT)
+ if not plo:
+ return False
+ olo = _has_right(o_allow, o_deny, security.SEC_ADS_LIST_OBJECT)
+ if not olo:
+ return False
+ return True
+
+ def _generate_expected(scope, base_level, levels):
+ expected = {}
+
+ p = levels[base_level-1]
+ o = levels[base_level]
+ base_visible = _is_visible(p["allow"], p["deny"],
+ o["allow"], o["deny"])
+
+ if scope == SCOPE_BASE:
+ lmin = base_level
+ lmax = base_level
+ elif scope == SCOPE_ONELEVEL:
+ lmin = base_level+1
+ lmax = base_level+1
+ else:
+ lmin = base_level
+ lmax = len(levels)
+
+ next_idx = 0
+ for li in self.level_idxs:
+ if li < lmin:
+ continue
+ if li > lmax:
+ break
+ p = levels[li-1]
+ o = levels[li]
+ visible = _is_visible(p["allow"], p["deny"],
+ o["allow"], o["deny"])
+ if not visible:
+ continue
+ read = _has_right(o["allow"], o["deny"], security.SEC_ADS_READ_PROP)
+ if read:
+ check_msg_fn = _msg_require_name
+ else:
+ check_msg_fn = _msg_no_name
+ expected[o["dn"]] = {
+ "idx": next_idx,
+ "check_msg_fn": check_msg_fn,
+ }
+ next_idx += 1
+
+ if len(expected) == 0 and not base_visible:
+ # This means we're expecting NO_SUCH_OBJECT
+ return None
+ return expected
+
+ def _verify_result_array(results,
+ description,
+ expected):
+ print("%s Results: %d" % (description, len(results)))
+ for msg in results:
+ print("%s" % msg)
+ self.assertIsNotNone(expected)
+ print("%s Expected: %d" % (description, len(expected)))
+ for e in expected:
+ print("%s" % e)
+ self.assertEqual(len(results), len(expected))
+ idx = 0
+ found = {}
+ for msg in results:
+ dn_str = str(msg.dn)
+ self.assertIn(dn_str, expected)
+ self.assertNotIn(dn_str, found)
+ found[dn_str] = idx
+ e = expected[dn_str]
+ if self.strict_checking:
+ self.assertEqual(idx, int(e["idx"]))
+ if "check_msg_fn" in e:
+ check_msg_fn = e["check_msg_fn"]
+ check_msg_fn(msg, idx, e)
+ idx += 1
+
+ return
+
+ for li in self.level_idxs:
+ base_dn = admin_levels[li]["dn"]
+ for scope in [SCOPE_BASE, SCOPE_ONELEVEL, SCOPE_SUBTREE]:
+ print("\nTesting SCOPE[%d] %s" % (scope, base_dn))
+ admin_expected = _generate_expected(scope, li, admin_levels)
+ admin_res = self.ldb_admin.search(base_dn, scope=scope, attrs=["name"])
+ _verify_result_array(admin_res, "Admin", admin_expected)
+
+ user_expected = _generate_expected(scope, li, user_levels)
+ try:
+ user_res = self.ldb_user.search(base_dn, scope=scope, attrs=["name"])
+ except LdbError as e:
+ (num, _) = e.args
+ if user_expected is None:
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+ print("User: NO_SUCH_OBJECT")
+ continue
+ self.fail(e)
+ _verify_result_array(user_res, "User", user_expected)
+
+# Important unit running information
+
+ldb = SamDB(ldaphost, credentials=creds, session_info=system_session(lp), lp=lp)
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/acl_modify.py b/source4/dsdb/tests/python/acl_modify.py
new file mode 100755
index 0000000..cb59901
--- /dev/null
+++ b/source4/dsdb/tests/python/acl_modify.py
@@ -0,0 +1,179 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+
+import optparse
+import sys
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from ldb import ERR_INSUFFICIENT_ACCESS_RIGHTS
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+
+from samba.auth import system_session
+from samba import gensec
+from samba.samdb import SamDB
+from samba.credentials import Credentials, DONT_USE_KERBEROS
+import samba.tests
+import samba.dsdb
+
+
+parser = optparse.OptionParser("acl_modify.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+ start = host.rindex("://")
+ host = host.lstrip(start + 3)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+#
+# Tests start here
+#
+
+
+class AclTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(AclTests, self).setUp()
+
+ self.ldb_admin = SamDB(ldaphost, credentials=creds, session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb_admin.domain_dn()
+ print("baseDN: %s" % self.base_dn)
+
+ def get_ldb_connection(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop
+ ldb_target = SamDB(url=ldaphost, credentials=creds_tmp, lp=lp)
+ return ldb_target
+
+
+class AclModifyTests(AclTests):
+
+ def setup_computer_with_hostname(self, account_name):
+ ou_dn = f'OU={account_name},{self.base_dn}'
+ dn = f'CN={account_name},{ou_dn}'
+
+ user, password = "mouse", "mus musculus 123!"
+ self.addCleanup(self.ldb_admin.deleteuser, user)
+
+ self.ldb_admin.newuser(user, password)
+ self.ldb_user = self.get_ldb_connection(user, password)
+
+ self.addCleanup(self.ldb_admin.delete, ou_dn,
+ controls=["tree_delete:0"])
+ self.ldb_admin.create_ou(ou_dn)
+
+ self.ldb_admin.add({
+ 'dn': dn,
+ 'objectClass': 'computer',
+ 'sAMAccountName': account_name + '$',
+ })
+
+ host_name = f'{account_name}.{self.ldb_user.domain_dns_name()}'
+
+ m = Message(Dn(self.ldb_admin, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_REPLACE,
+ 'dNSHostName')
+
+ self.ldb_admin.modify(m)
+ return host_name, dn
+
+ def test_modify_delete_dns_host_name_specified(self):
+ '''Test deleting dNSHostName'''
+ account_name = self.id().rsplit(".", 1)[1][:63]
+ host_name, dn = self.setup_computer_with_hostname(account_name)
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement(host_name,
+ FLAG_MOD_DELETE,
+ 'dNSHostName')
+
+ self.assertRaisesLdbError(
+ ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "User able to delete dNSHostName (with specified name)",
+ self.ldb_user.modify, m)
+
+ def test_modify_delete_dns_host_name_unspecified(self):
+ '''Test deleting dNSHostName'''
+ account_name = self.id().rsplit(".", 1)[1][:63]
+ host_name, dn = self.setup_computer_with_hostname(account_name)
+
+ m = Message(Dn(self.ldb_user, dn))
+ m['dNSHostName'] = MessageElement([],
+ FLAG_MOD_DELETE,
+ 'dNSHostName')
+
+ self.assertRaisesLdbError(
+ ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "User able to delete dNSHostName (without specified name)",
+ self.ldb_user.modify, m)
+
+ def test_modify_delete_dns_host_name_ldif_specified(self):
+ '''Test deleting dNSHostName'''
+ account_name = self.id().rsplit(".", 1)[1][:63]
+ host_name, dn = self.setup_computer_with_hostname(account_name)
+
+ ldif = f"""
+dn: {dn}
+changetype: modify
+delete: dNSHostName
+dNSHostName: {host_name}
+"""
+ self.assertRaisesLdbError(
+ ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "User able to delete dNSHostName (with specified name)",
+ self.ldb_user.modify_ldif, ldif)
+
+ def test_modify_delete_dns_host_name_ldif_unspecified(self):
+ '''Test deleting dNSHostName'''
+ account_name = self.id().rsplit(".", 1)[1][:63]
+ host_name, dn = self.setup_computer_with_hostname(account_name)
+
+ ldif = f"""
+dn: {dn}
+changetype: modify
+delete: dNSHostName
+"""
+ self.assertRaisesLdbError(
+ ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "User able to delete dNSHostName (without specific name)",
+ self.ldb_user.modify_ldif, ldif)
+
+
+ldb = SamDB(ldaphost, credentials=creds, session_info=system_session(lp), lp=lp)
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ad_dc_medley_performance.py b/source4/dsdb/tests/python/ad_dc_medley_performance.py
new file mode 100644
index 0000000..39f9e67
--- /dev/null
+++ b/source4/dsdb/tests/python/ad_dc_medley_performance.py
@@ -0,0 +1,523 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+sys.path.insert(0, 'bin/python')
+
+import os
+import samba
+import samba.getopt as options
+import random
+import tempfile
+import shutil
+import time
+import itertools
+
+from samba.netcmd.main import samba_tool
+
+# We try to use the test infrastructure of Samba 4.3+, but if it
+# doesn't work, we are probably in a back-ported patch and trying to
+# run on 4.1 or something.
+#
+# Don't copy this horror into ordinary tests -- it is special for
+# performance tests that want to apply to old versions.
+try:
+ from samba.tests.subunitrun import SubunitOptions, TestProgram
+ ANCIENT_SAMBA = False
+except ImportError:
+ ANCIENT_SAMBA = True
+ samba.ensure_external_module("testtools", "testtools")
+ samba.ensure_external_module("subunit", "subunit/python")
+ from subunit.run import SubunitTestRunner
+ import unittest
+
+from samba.samdb import SamDB
+from samba.auth import system_session
+from ldb import Message, MessageElement, Dn, LdbError
+from ldb import FLAG_MOD_ADD, FLAG_MOD_DELETE
+from ldb import SCOPE_BASE, SCOPE_SUBTREE
+from ldb import ERR_NO_SUCH_OBJECT
+
+parser = optparse.OptionParser("ad_dc_medley_performance.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+sambaopts.add_option("-p", "--use-paged-search", action="store_true",
+ help="Use paged search module")
+
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+if not ANCIENT_SAMBA:
+ subunitopts = SubunitOptions(parser)
+ parser.add_option_group(subunitopts)
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+random.seed(1)
+
+
+class PerfTestException(Exception):
+ pass
+
+
+BATCH_SIZE = 2000
+LINK_BATCH_SIZE = 1000
+DELETE_BATCH_SIZE = 50
+N_GROUPS = 29
+
+
+class GlobalState(object):
+ next_user_id = 0
+ n_groups = 0
+ next_linked_user = 0
+ next_relinked_user = 0
+ next_linked_user_3 = 0
+ next_removed_link_0 = 0
+ test_number = 0
+ active_links = set()
+
+
+class UserTests(samba.tests.TestCase):
+
+ def add_if_possible(self, *args, **kwargs):
+ """In these tests sometimes things are left in the database
+ deliberately, so we don't worry if we fail to add them a second
+ time."""
+ try:
+ self.ldb.add(*args, **kwargs)
+ except LdbError:
+ pass
+
+ def setUp(self):
+ super(UserTests, self).setUp()
+ self.state = GlobalState # the class itself, not an instance
+ self.lp = lp
+
+ kwargs = {}
+ if opts.use_paged_search:
+ kwargs["options"] = ["modules:paged_searches"]
+
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp, **kwargs)
+ self.base_dn = self.ldb.domain_dn()
+ self.ou = "OU=pid%s,%s" % (os.getpid(), self.base_dn)
+ self.ou_users = "OU=users,%s" % self.ou
+ self.ou_groups = "OU=groups,%s" % self.ou
+ self.ou_computers = "OU=computers,%s" % self.ou
+
+ self.state.test_number += 1
+ random.seed(self.state.test_number)
+
+ def tearDown(self):
+ super(UserTests, self).tearDown()
+
+ def test_00_00_do_nothing(self):
+ # this gives us an idea of the overhead
+ pass
+
+ def test_00_01_do_nothing_relevant(self):
+ # takes around 1 second on i7-4770
+ j = 0
+ for i in range(30000000):
+ j += i
+
+ def test_00_02_do_nothing_sleepily(self):
+ time.sleep(1)
+
+ def test_00_03_add_ous_and_groups(self):
+ # initialise the database
+ for dn in (self.ou,
+ self.ou_users,
+ self.ou_groups,
+ self.ou_computers):
+ self.ldb.add({
+ "dn": dn,
+ "objectclass": "organizationalUnit"
+ })
+
+ for i in range(N_GROUPS):
+ self.ldb.add({
+ "dn": "cn=g%d,%s" % (i, self.ou_groups),
+ "objectclass": "group"
+ })
+
+ self.state.n_groups = N_GROUPS
+
+ def _add_users(self, start, end):
+ for i in range(start, end):
+ self.ldb.add({
+ "dn": "cn=u%d,%s" % (i, self.ou_users),
+ "objectclass": "user"
+ })
+
+ def _add_users_ldif(self, start, end):
+ lines = []
+ for i in range(start, end):
+ lines.append("dn: cn=u%d,%s" % (i, self.ou_users))
+ lines.append("objectclass: user")
+ lines.append("")
+ self.ldb.add_ldif('\n'.join(lines))
+
+ def _test_join(self):
+ tmpdir = tempfile.mkdtemp()
+ if '://' in host:
+ server = host.split('://', 1)[1]
+ else:
+ server = host
+ result = samba_tool('domain', 'join',
+ creds.get_realm(),
+ "dc", "-U%s%%%s" % (creds.get_username(),
+ creds.get_password()),
+ '--targetdir=%s' % tmpdir,
+ '--server=%s' % server)
+ self.assertIsNone(result)
+
+ shutil.rmtree(tmpdir)
+
+ def _test_unindexed_search(self):
+ expressions = [
+ ('(&(objectclass=user)(description='
+ 'Built-in account for adminstering the computer/domain))'),
+ '(description=Built-in account for adminstering the computer/domain)',
+ '(objectCategory=*)',
+ '(samaccountname=Administrator*)'
+ ]
+ for expression in expressions:
+ t = time.time()
+ for i in range(25):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d %s took %s' % (i, expression,
+ time.time() - t),
+ file=sys.stderr)
+
+ def _test_indexed_search(self):
+ expressions = ['(objectclass=group)',
+ '(samaccountname=Administrator)'
+ ]
+ for expression in expressions:
+ t = time.time()
+ for i in range(4000):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d runs %s took %s' % (i, expression,
+ time.time() - t),
+ file=sys.stderr)
+
+ def _test_base_search(self):
+ for dn in [self.base_dn, self.ou, self.ou_users,
+ self.ou_groups, self.ou_computers]:
+ for i in range(4000):
+ try:
+ self.ldb.search(dn,
+ scope=SCOPE_BASE,
+ attrs=['cn'])
+ except LdbError as e:
+ (num, msg) = e.args
+ if num != ERR_NO_SUCH_OBJECT:
+ raise
+
+ def _test_base_search_failing(self):
+ pattern = 'missing%d' + self.ou
+ for i in range(4000):
+ try:
+ self.ldb.search(pattern % i,
+ scope=SCOPE_BASE,
+ attrs=['cn'])
+ except LdbError as e:
+ (num, msg) = e.args
+ if num != ERR_NO_SUCH_OBJECT:
+ raise
+
+ def search_expression_list(self, expressions, rounds,
+ attrs=None,
+ scope=SCOPE_SUBTREE):
+ if attrs is None:
+ attrs = ['cn']
+ for expression in expressions:
+ t = time.time()
+ for i in range(rounds):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=scope,
+ attrs=attrs)
+ print('%d runs %s took %s' % (i, expression,
+ time.time() - t),
+ file=sys.stderr)
+
+ def _test_complex_search(self, n=100):
+ classes = ['samaccountname', 'objectCategory', 'dn', 'member']
+ values = ['*', '*t*', 'g*', 'user']
+ comparators = ['=', '<=', '>='] # '~=' causes error
+ maybe_not = ['!(', '']
+ joiners = ['&', '|']
+
+ # The number of permutations is 18432, which is not huge but
+ # would take hours to search. So we take a sample.
+ all_permutations = list(itertools.product(joiners,
+ classes, classes,
+ values, values,
+ comparators, comparators,
+ maybe_not, maybe_not))
+
+ expressions = []
+
+ for (j, c1, c2, v1, v2,
+ o1, o2, n1, n2) in random.sample(all_permutations, n):
+ expression = ''.join(['(', j,
+ '(', n1, c1, o1, v1,
+ '))' if n1 else ')',
+ '(', n2, c2, o2, v2,
+ '))' if n2 else ')',
+ ')'])
+ expressions.append(expression)
+
+ self.search_expression_list(expressions, 1)
+
+ def _test_member_search(self, rounds=10):
+ expressions = []
+ for d in range(20):
+ expressions.append('(member=cn=u%d,%s)' % (d + 500, self.ou_users))
+ expressions.append('(member=u%d*)' % (d + 700,))
+
+ self.search_expression_list(expressions, rounds)
+
+ def _test_memberof_search(self, rounds=200):
+ expressions = []
+ for i in range(min(self.state.n_groups, rounds)):
+ expressions.append('(memberOf=cn=g%d,%s)' % (i, self.ou_groups))
+ expressions.append('(memberOf=cn=g%d*)' % (i,))
+ expressions.append('(memberOf=cn=*%s*)' % self.ou_groups)
+
+ self.search_expression_list(expressions, 2)
+
+ def _test_add_many_users(self, n=BATCH_SIZE):
+ s = self.state.next_user_id
+ e = s + n
+ self._add_users(s, e)
+ self.state.next_user_id = e
+
+ def _test_add_many_users_ldif(self, n=BATCH_SIZE):
+ s = self.state.next_user_id
+ e = s + n
+ self._add_users_ldif(s, e)
+ self.state.next_user_id = e
+
+ def _link_user_and_group(self, u, g):
+ link = (u, g)
+ if link in self.state.active_links:
+ return False
+
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=g%d,%s" % (g, self.ou_groups))
+ m["member"] = MessageElement("cn=u%d,%s" % (u, self.ou_users),
+ FLAG_MOD_ADD, "member")
+ self.ldb.modify(m)
+ self.state.active_links.add(link)
+ return True
+
+ def _unlink_user_and_group(self, u, g):
+ link = (u, g)
+ if link not in self.state.active_links:
+ return False
+
+ user = "cn=u%d,%s" % (u, self.ou_users)
+ group = "CN=g%d,%s" % (g, self.ou_groups)
+ m = Message()
+ m.dn = Dn(self.ldb, group)
+ m["member"] = MessageElement(user, FLAG_MOD_DELETE, "member")
+ self.ldb.modify(m)
+ self.state.active_links.remove(link)
+ return True
+
+ def _test_link_many_users(self, n=LINK_BATCH_SIZE):
+ # this links unevenly, putting more users in the first group
+ # and fewer in the last.
+ ng = self.state.n_groups
+ nu = self.state.next_user_id
+ while n:
+ u = random.randrange(nu)
+ g = random.randrange(random.randrange(ng) + 1)
+ if self._link_user_and_group(u, g):
+ n -= 1
+
+ def _test_link_many_users_batch(self, n=(LINK_BATCH_SIZE * 10)):
+ # this links unevenly, putting more users in the first group
+ # and fewer in the last.
+ ng = self.state.n_groups
+ nu = self.state.next_user_id
+ messages = []
+ for g in range(ng):
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=g%d,%s" % (g, self.ou_groups))
+ messages.append(m)
+
+ while n:
+ u = random.randrange(nu)
+ g = random.randrange(random.randrange(ng) + 1)
+ link = (u, g)
+ if link in self.state.active_links:
+ continue
+ m = messages[g]
+ m["member%s" % u] = MessageElement("cn=u%d,%s" %
+ (u, self.ou_users),
+ FLAG_MOD_ADD, "member")
+ self.state.active_links.add(link)
+ n -= 1
+
+ for m in messages:
+ try:
+ self.ldb.modify(m)
+ except LdbError as e:
+ print(e)
+ print(m)
+
+ def _test_remove_some_links(self, n=(LINK_BATCH_SIZE // 2)):
+ victims = random.sample(list(self.state.active_links), n)
+ for x in victims:
+ self._unlink_user_and_group(*x)
+
+ test_00_11_join_empty_dc = _test_join
+
+ test_00_12_adding_users_2000 = _test_add_many_users
+
+ test_00_20_join_unlinked_2k_users = _test_join
+ test_00_21_unindexed_search_2k_users = _test_unindexed_search
+ test_00_22_indexed_search_2k_users = _test_indexed_search
+
+ test_00_23_complex_search_2k_users = _test_complex_search
+ test_00_24_member_search_2k_users = _test_member_search
+ test_00_25_memberof_search_2k_users = _test_memberof_search
+
+ test_00_27_base_search_2k_users = _test_base_search
+ test_00_28_base_search_failing_2k_users = _test_base_search_failing
+
+ test_01_01_link_2k_users = _test_link_many_users
+ test_01_02_link_2k_users_batch = _test_link_many_users_batch
+
+ test_02_10_join_2k_linked_dc = _test_join
+ test_02_11_unindexed_search_2k_linked_dc = _test_unindexed_search
+ test_02_12_indexed_search_2k_linked_dc = _test_indexed_search
+
+ test_04_01_remove_some_links_2k = _test_remove_some_links
+
+ test_05_01_adding_users_after_links_4k_ldif = _test_add_many_users_ldif
+
+ test_06_04_link_users_4k = _test_link_many_users
+ test_06_05_link_users_4k_batch = _test_link_many_users_batch
+
+ test_07_01_adding_users_after_links_6k = _test_add_many_users
+
+ def _test_ldif_well_linked_group(self, link_chance=1.0):
+ g = self.state.n_groups
+ self.state.n_groups += 1
+ lines = ["dn: CN=g%d,%s" % (g, self.ou_groups),
+ "objectclass: group"]
+
+ for i in range(self.state.next_user_id):
+ if random.random() <= link_chance:
+ lines.append("member: cn=u%d,%s" % (i, self.ou_users))
+ self.state.active_links.add((i, g))
+
+ lines.append("")
+ self.ldb.add_ldif('\n'.join(lines))
+
+ test_09_01_add_fully_linked_group = _test_ldif_well_linked_group
+
+ def test_09_02_add_exponentially_diminishing_linked_groups(self):
+ linkage = 0.8
+ while linkage > 0.01:
+ self._test_ldif_well_linked_group(linkage)
+ linkage *= 0.75
+
+ test_09_04_link_users_6k = _test_link_many_users
+
+ test_10_01_unindexed_search_6k_users = _test_unindexed_search
+ test_10_02_indexed_search_6k_users = _test_indexed_search
+
+ test_10_27_base_search_6k_users = _test_base_search
+ test_10_28_base_search_failing_6k_users = _test_base_search_failing
+
+ def test_10_03_complex_search_6k_users(self):
+ self._test_complex_search(n=50)
+
+ def test_10_04_member_search_6k_users(self):
+ self._test_member_search(rounds=1)
+
+ def test_10_05_memberof_search_6k_users(self):
+ self._test_memberof_search(rounds=5)
+
+ test_11_02_join_full_dc = _test_join
+
+ test_12_01_remove_some_links_6k = _test_remove_some_links
+
+ def _test_delete_many_users(self, n=DELETE_BATCH_SIZE):
+ e = self.state.next_user_id
+ s = max(0, e - n)
+ self.state.next_user_id = s
+ for i in range(s, e):
+ self.ldb.delete("cn=u%d,%s" % (i, self.ou_users))
+
+ for x in tuple(self.state.active_links):
+ if s >= x[0] > e:
+ self.state.active_links.remove(x)
+
+ test_20_01_delete_users_6k = _test_delete_many_users
+
+ def test_21_01_delete_10_groups(self):
+ for i in range(self.state.n_groups - 10, self.state.n_groups):
+ self.ldb.delete("cn=g%d,%s" % (i, self.ou_groups))
+ self.state.n_groups -= 10
+ for x in tuple(self.state.active_links):
+ if x[1] >= self.state.n_groups:
+ self.state.active_links.remove(x)
+
+ test_21_02_delete_users_5950 = _test_delete_many_users
+
+ def test_22_01_delete_all_groups(self):
+ for i in range(self.state.n_groups):
+ self.ldb.delete("cn=g%d,%s" % (i, self.ou_groups))
+ self.state.n_groups = 0
+ self.state.active_links = set()
+
+ # XXX assert the state is as we think, using searches
+
+ def test_23_01_delete_users_5900_after_groups(self):
+ # we do not delete everything because it takes too long
+ n = 4 * DELETE_BATCH_SIZE
+ self._test_delete_many_users(n=n)
+
+ test_24_02_join_after_partial_cleanup = _test_join
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+if ANCIENT_SAMBA:
+ runner = SubunitTestRunner()
+ if not runner.run(unittest.TestLoader().loadTestsFromTestCase(
+ UserTests)).wasSuccessful():
+ sys.exit(1)
+ sys.exit(0)
+else:
+ TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ad_dc_multi_bind.py b/source4/dsdb/tests/python/ad_dc_multi_bind.py
new file mode 100644
index 0000000..93a1c30
--- /dev/null
+++ b/source4/dsdb/tests/python/ad_dc_multi_bind.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import optparse
+import sys
+sys.path.insert(0, 'bin/python')
+
+import os
+import samba
+import samba.getopt as options
+
+# We try to use the test infrastructure of Samba 4.3+, but if it
+# doesn't work, we are probably in a back-ported patch and trying to
+# run on 4.1 or something.
+#
+# Don't copy this horror into ordinary tests -- it is special for
+# performance tests that want to apply to old versions.
+try:
+ from samba.tests.subunitrun import SubunitOptions, TestProgram
+ ANCIENT_SAMBA = False
+except ImportError:
+ ANCIENT_SAMBA = True
+ samba.ensure_external_module("testtools", "testtools")
+ samba.ensure_external_module("subunit", "subunit/python")
+ from subunit.run import SubunitTestRunner
+ import unittest
+
+from samba.samdb import SamDB
+from samba.auth import system_session
+from ldb import SCOPE_BASE
+
+parser = optparse.OptionParser("ad_dc_multi_bind.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+if not ANCIENT_SAMBA:
+ subunitopts = SubunitOptions(parser)
+ parser.add_option_group(subunitopts)
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+opts, args = parser.parse_args()
+
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+class UserTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(UserTests, self).setUp()
+ self.lp = lp
+
+ def tearDown(self):
+ super(UserTests, self).tearDown()
+
+ def test_1000_binds(self):
+
+ for x in range(1, 1000):
+ samdb = SamDB(host, credentials=creds,
+ session_info=system_session(self.lp), lp=self.lp)
+ samdb.search(base=samdb.domain_dn(),
+ scope=SCOPE_BASE, attrs=["*"])
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+if ANCIENT_SAMBA:
+ runner = SubunitTestRunner()
+ if not runner.run(unittest.TestLoader().loadTestsFromTestCase(
+ UserTests)).wasSuccessful():
+ sys.exit(1)
+ sys.exit(0)
+else:
+ TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ad_dc_performance.py b/source4/dsdb/tests/python/ad_dc_performance.py
new file mode 100644
index 0000000..b8863b5
--- /dev/null
+++ b/source4/dsdb/tests/python/ad_dc_performance.py
@@ -0,0 +1,340 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+sys.path.insert(0, 'bin/python')
+
+import os
+import samba
+import samba.getopt as options
+import random
+import tempfile
+import shutil
+import time
+
+from samba.netcmd.main import samba_tool
+
+# We try to use the test infrastructure of Samba 4.3+, but if it
+# doesn't work, we are probably in a back-ported patch and trying to
+# run on 4.1 or something.
+#
+# Don't copy this horror into ordinary tests -- it is special for
+# performance tests that want to apply to old versions.
+try:
+ from samba.tests.subunitrun import SubunitOptions, TestProgram
+ ANCIENT_SAMBA = False
+except ImportError:
+ ANCIENT_SAMBA = True
+ samba.ensure_external_module("testtools", "testtools")
+ samba.ensure_external_module("subunit", "subunit/python")
+ from subunit.run import SubunitTestRunner
+ import unittest
+
+from samba.samdb import SamDB
+from samba.auth import system_session
+from ldb import Message, MessageElement, Dn, LdbError
+from ldb import FLAG_MOD_ADD, FLAG_MOD_DELETE
+from ldb import SCOPE_SUBTREE
+
+parser = optparse.OptionParser("ad_dc_performance.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+if not ANCIENT_SAMBA:
+ subunitopts = SubunitOptions(parser)
+ parser.add_option_group(subunitopts)
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+opts, args = parser.parse_args()
+
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+random.seed(1)
+
+
+class PerfTestException(Exception):
+ pass
+
+
+BATCH_SIZE = 1000
+N_GROUPS = 5
+
+
+class GlobalState(object):
+ next_user_id = 0
+ n_groups = 0
+ next_linked_user = 0
+ next_relinked_user = 0
+ next_linked_user_3 = 0
+ next_removed_link_0 = 0
+
+
+class UserTests(samba.tests.TestCase):
+
+ def add_if_possible(self, *args, **kwargs):
+ """In these tests sometimes things are left in the database
+ deliberately, so we don't worry if we fail to add them a second
+ time."""
+ try:
+ self.ldb.add(*args, **kwargs)
+ except LdbError:
+ pass
+
+ def setUp(self):
+ super(UserTests, self).setUp()
+ self.state = GlobalState # the class itself, not an instance
+ self.lp = lp
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.domain_dn()
+ self.ou = "OU=pid%s,%s" % (os.getpid(), self.base_dn)
+ self.ou_users = "OU=users,%s" % self.ou
+ self.ou_groups = "OU=groups,%s" % self.ou
+ self.ou_computers = "OU=computers,%s" % self.ou
+
+ for dn in (self.ou, self.ou_users, self.ou_groups,
+ self.ou_computers):
+ self.add_if_possible({
+ "dn": dn,
+ "objectclass": "organizationalUnit"})
+
+ def tearDown(self):
+ super(UserTests, self).tearDown()
+
+ def test_00_00_do_nothing(self):
+ # this gives us an idea of the overhead
+ pass
+
+ def _prepare_n_groups(self, n):
+ self.state.n_groups = n
+ for i in range(n):
+ self.add_if_possible({
+ "dn": "cn=g%d,%s" % (i, self.ou_groups),
+ "objectclass": "group"})
+
+ def _add_users(self, start, end):
+ for i in range(start, end):
+ self.ldb.add({
+ "dn": "cn=u%d,%s" % (i, self.ou_users),
+ "objectclass": "user"})
+
+ def _test_join(self):
+ tmpdir = tempfile.mkdtemp()
+ if '://' in host:
+ server = host.split('://', 1)[1]
+ else:
+ server = host
+ result = samba_tool('domain', 'join',
+ creds.get_realm(),
+ "dc", "-U%s%%%s" % (creds.get_username(),
+ creds.get_password()),
+ '--targetdir=%s' % tmpdir,
+ '--server=%s' % server)
+ self.assertIsNone(result)
+
+ shutil.rmtree(tmpdir)
+
+ def _test_unindexed_search(self):
+ expressions = [
+ ('(&(objectclass=user)(description='
+ 'Built-in account for adminstering the computer/domain))'),
+ '(description=Built-in account for adminstering the computer/domain)',
+ '(objectCategory=*)',
+ '(samaccountname=Administrator*)'
+ ]
+ for expression in expressions:
+ t = time.time()
+ for i in range(10):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d %s took %s' % (i, expression,
+ time.time() - t), file=sys.stderr)
+
+ def _test_indexed_search(self):
+ expressions = ['(objectclass=group)',
+ '(samaccountname=Administrator)'
+ ]
+ for expression in expressions:
+ t = time.time()
+ for i in range(100):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d runs %s took %s' % (i, expression,
+ time.time() - t), file=sys.stderr)
+
+ def _test_add_many_users(self, n=BATCH_SIZE):
+ s = self.state.next_user_id
+ e = s + n
+ self._add_users(s, e)
+ self.state.next_user_id = e
+
+ test_00_00_join_empty_dc = _test_join
+
+ test_00_01_adding_users_1000 = _test_add_many_users
+ test_00_02_adding_users_2000 = _test_add_many_users
+ test_00_03_adding_users_3000 = _test_add_many_users
+
+ test_00_10_join_unlinked_dc = _test_join
+ test_00_11_unindexed_search_3k_users = _test_unindexed_search
+ test_00_12_indexed_search_3k_users = _test_indexed_search
+
+ def _link_user_and_group(self, u, g):
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=g%d,%s" % (g, self.ou_groups))
+ m["member"] = MessageElement("cn=u%d,%s" % (u, self.ou_users),
+ FLAG_MOD_ADD, "member")
+ self.ldb.modify(m)
+
+ def _unlink_user_and_group(self, u, g):
+ user = "cn=u%d,%s" % (u, self.ou_users)
+ group = "CN=g%d,%s" % (g, self.ou_groups)
+ m = Message()
+ m.dn = Dn(self.ldb, group)
+ m["member"] = MessageElement(user, FLAG_MOD_DELETE, "member")
+ self.ldb.modify(m)
+
+ def _test_link_many_users(self, n=BATCH_SIZE):
+ self._prepare_n_groups(N_GROUPS)
+ s = self.state.next_linked_user
+ e = s + n
+ for i in range(s, e):
+ g = i % N_GROUPS
+ self._link_user_and_group(i, g)
+ self.state.next_linked_user = e
+
+ test_01_01_link_users_1000 = _test_link_many_users
+ test_01_02_link_users_2000 = _test_link_many_users
+ test_01_03_link_users_3000 = _test_link_many_users
+
+ def _test_link_many_users_offset_1(self, n=BATCH_SIZE):
+ s = self.state.next_relinked_user
+ e = s + n
+ for i in range(s, e):
+ g = (i + 1) % N_GROUPS
+ self._link_user_and_group(i, g)
+ self.state.next_relinked_user = e
+
+ test_02_01_link_users_again_1000 = _test_link_many_users_offset_1
+ test_02_02_link_users_again_2000 = _test_link_many_users_offset_1
+ test_02_03_link_users_again_3000 = _test_link_many_users_offset_1
+
+ test_02_10_join_partially_linked_dc = _test_join
+ test_02_11_unindexed_search_partially_linked_dc = _test_unindexed_search
+ test_02_12_indexed_search_partially_linked_dc = _test_indexed_search
+
+ def _test_link_many_users_3_groups(self, n=BATCH_SIZE, groups=3):
+ s = self.state.next_linked_user_3
+ e = s + n
+ self.state.next_linked_user_3 = e
+ for i in range(s, e):
+ g = (i + 2) % groups
+ if g not in (i % N_GROUPS, (i + 1) % N_GROUPS):
+ self._link_user_and_group(i, g)
+
+ test_03_01_link_users_again_1000_few_groups = _test_link_many_users_3_groups
+ test_03_02_link_users_again_2000_few_groups = _test_link_many_users_3_groups
+ test_03_03_link_users_again_3000_few_groups = _test_link_many_users_3_groups
+
+ def _test_remove_links_0(self, n=BATCH_SIZE):
+ s = self.state.next_removed_link_0
+ e = s + n
+ self.state.next_removed_link_0 = e
+ for i in range(s, e):
+ g = i % N_GROUPS
+ self._unlink_user_and_group(i, g)
+
+ test_04_01_remove_some_links_1000 = _test_remove_links_0
+ test_04_02_remove_some_links_2000 = _test_remove_links_0
+ test_04_03_remove_some_links_3000 = _test_remove_links_0
+
+ # back to using _test_add_many_users
+ test_05_01_adding_users_after_links_4000 = _test_add_many_users
+
+ # reset the link count, to replace the original links
+ def test_06_01_relink_users_1000(self):
+ self.state.next_linked_user = 0
+ self._test_link_many_users()
+
+ test_06_02_link_users_2000 = _test_link_many_users
+ test_06_03_link_users_3000 = _test_link_many_users
+ test_06_04_link_users_4000 = _test_link_many_users
+ test_06_05_link_users_again_4000 = _test_link_many_users_offset_1
+ test_06_06_link_users_again_4000_few_groups = _test_link_many_users_3_groups
+
+ test_07_01_adding_users_after_links_5000 = _test_add_many_users
+
+ def _test_link_random_users_and_groups(self, n=BATCH_SIZE, groups=100):
+ self._prepare_n_groups(groups)
+ for i in range(n):
+ u = random.randrange(self.state.next_user_id)
+ g = random.randrange(groups)
+ try:
+ self._link_user_and_group(u, g)
+ except LdbError:
+ pass
+
+ test_08_01_link_random_users_100_groups = _test_link_random_users_and_groups
+ test_08_02_link_random_users_100_groups = _test_link_random_users_and_groups
+
+ test_10_01_unindexed_search_full_dc = _test_unindexed_search
+ test_10_02_indexed_search_full_dc = _test_indexed_search
+ test_11_02_join_full_dc = _test_join
+
+ def test_20_01_delete_50_groups(self):
+ for i in range(self.state.n_groups - 50, self.state.n_groups):
+ self.ldb.delete("cn=g%d,%s" % (i, self.ou_groups))
+ self.state.n_groups -= 50
+
+ def _test_delete_many_users(self, n=BATCH_SIZE):
+ e = self.state.next_user_id
+ s = max(0, e - n)
+ self.state.next_user_id = s
+ for i in range(s, e):
+ self.ldb.delete("cn=u%d,%s" % (i, self.ou_users))
+
+ test_21_01_delete_users_5000_lightly_linked = _test_delete_many_users
+ test_21_02_delete_users_4000_lightly_linked = _test_delete_many_users
+ test_21_03_delete_users_3000 = _test_delete_many_users
+
+ def test_22_01_delete_all_groups(self):
+ for i in range(self.state.n_groups):
+ self.ldb.delete("cn=g%d,%s" % (i, self.ou_groups))
+ self.state.n_groups = 0
+
+ test_23_01_delete_users_after_groups_2000 = _test_delete_many_users
+ test_23_00_delete_users_after_groups_1000 = _test_delete_many_users
+
+ test_24_02_join_after_cleanup = _test_join
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+if ANCIENT_SAMBA:
+ runner = SubunitTestRunner()
+ if not runner.run(unittest.TestLoader().loadTestsFromTestCase(
+ UserTests)).wasSuccessful():
+ sys.exit(1)
+ sys.exit(0)
+else:
+ TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ad_dc_provision_performance.py b/source4/dsdb/tests/python/ad_dc_provision_performance.py
new file mode 100644
index 0000000..bfe9226
--- /dev/null
+++ b/source4/dsdb/tests/python/ad_dc_provision_performance.py
@@ -0,0 +1,131 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import optparse
+import sys
+sys.path.insert(0, 'bin/python')
+
+import os
+import samba
+import samba.getopt as options
+import random
+import tempfile
+import shutil
+import subprocess
+
+from samba.netcmd.main import samba_tool
+
+# We try to use the test infrastructure of Samba 4.3+, but if it
+# doesn't work, we are probably in a back-ported patch and trying to
+# run on 4.1 or something.
+#
+# Don't copy this horror into ordinary tests -- it is special for
+# performance tests that want to apply to old versions.
+try:
+ from samba.tests.subunitrun import SubunitOptions, TestProgram
+ ANCIENT_SAMBA = False
+except ImportError:
+ ANCIENT_SAMBA = True
+ samba.ensure_external_module("testtools", "testtools")
+ samba.ensure_external_module("subunit", "subunit/python")
+ from subunit.run import SubunitTestRunner
+ import unittest
+
+parser = optparse.OptionParser("ad_dc_provision_performance.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+if not ANCIENT_SAMBA:
+ subunitopts = SubunitOptions(parser)
+ parser.add_option_group(subunitopts)
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+opts, args = parser.parse_args()
+
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+random.seed(1)
+
+
+class PerfTestException(Exception):
+ pass
+
+
+class UserTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(UserTests, self).setUp()
+ self.tmpdir = tempfile.mkdtemp()
+
+ def tearDown(self):
+ shutil.rmtree(self.tmpdir)
+
+ def test_00_00_do_nothing(self):
+ # this gives us an idea of the overhead
+ pass
+
+ def _test_provision_subprocess(self, options=None, subdir=None):
+ if subdir is None:
+ d = self.tmpdir
+ else:
+ d = os.path.join(self.tmpdir, str(subdir))
+ os.mkdir(d)
+
+ cmd = ['bin/samba-tool', 'domain', 'provision', '--targetdir',
+ d, '--realm=realm.com', '--use-ntvfs', '--domain=dom']
+
+ if options:
+ options.extend(options)
+ subprocess.check_call(cmd)
+
+ test_01_00_provision_subprocess = _test_provision_subprocess
+
+ def test_01_00_provision_subprocess_overwrite(self):
+ for i in range(2):
+ self._test_provision_subprocess()
+
+ def test_02_00_provision_cmd_sambatool(self):
+ result = samba_tool('domain', 'provision',
+ '--targetdir=%s' % self.tmpdir,
+ '--use-ntvfs')
+ self.assertIsNone(result)
+
+ def test_03_00_provision_server_role(self):
+ for role in ('member', 'server', 'member', 'standalone'):
+ self._test_provision_subprocess(options=['--server-role', role],
+ subdir=role)
+
+ def test_04_00_provision_blank(self):
+ for i in range(2):
+ self._test_provision_subprocess(options=['--blank'],
+ subdir=i)
+
+ def test_05_00_provision_partitions_only(self):
+ self._test_provision_subprocess(options=['--partitions-only'])
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+if ANCIENT_SAMBA:
+ runner = SubunitTestRunner()
+ if not runner.run(unittest.TestLoader().loadTestsFromTestCase(
+ UserTests)).wasSuccessful():
+ sys.exit(1)
+ sys.exit(0)
+else:
+ TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ad_dc_search_performance.py b/source4/dsdb/tests/python/ad_dc_search_performance.py
new file mode 100644
index 0000000..aabcc22
--- /dev/null
+++ b/source4/dsdb/tests/python/ad_dc_search_performance.py
@@ -0,0 +1,296 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+sys.path.insert(0, 'bin/python')
+
+import os
+import samba
+import samba.getopt as options
+import random
+import time
+import itertools
+
+# We try to use the test infrastructure of Samba 4.3+, but if it
+# doesn't work, we are probably in a back-ported patch and trying to
+# run on 4.1 or something.
+#
+# Don't copy this horror into ordinary tests -- it is special for
+# performance tests that want to apply to old versions.
+try:
+ from samba.tests.subunitrun import SubunitOptions, TestProgram
+ ANCIENT_SAMBA = False
+except ImportError:
+ ANCIENT_SAMBA = True
+ samba.ensure_external_module("testtools", "testtools")
+ samba.ensure_external_module("subunit", "subunit/python")
+ from subunit.run import SubunitTestRunner
+ import unittest
+
+from samba.samdb import SamDB
+from samba.auth import system_session
+from ldb import Message, MessageElement, Dn, LdbError
+from ldb import FLAG_MOD_ADD
+from ldb import SCOPE_SUBTREE
+
+parser = optparse.OptionParser("ad_dc_search_performance.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+if not ANCIENT_SAMBA:
+ subunitopts = SubunitOptions(parser)
+ parser.add_option_group(subunitopts)
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+opts, args = parser.parse_args()
+
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+random.seed(1)
+
+
+class PerfTestException(Exception):
+ pass
+
+
+BATCH_SIZE = 1000
+N_GROUPS = 5
+
+
+class GlobalState(object):
+ next_user_id = 0
+ n_groups = 0
+ next_linked_user = 0
+ next_relinked_user = 0
+ next_linked_user_3 = 0
+ next_removed_link_0 = 0
+
+
+class UserTests(samba.tests.TestCase):
+
+ def add_if_possible(self, *args, **kwargs):
+ """In these tests sometimes things are left in the database
+ deliberately, so we don't worry if we fail to add them a second
+ time."""
+ try:
+ self.ldb.add(*args, **kwargs)
+ except LdbError:
+ pass
+
+ def setUp(self):
+ super(UserTests, self).setUp()
+ self.state = GlobalState # the class itself, not an instance
+ self.lp = lp
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.domain_dn()
+ self.ou = "OU=pid%s,%s" % (os.getpid(), self.base_dn)
+ self.ou_users = "OU=users,%s" % self.ou
+ self.ou_groups = "OU=groups,%s" % self.ou
+ self.ou_computers = "OU=computers,%s" % self.ou
+
+ for dn in (self.ou, self.ou_users, self.ou_groups,
+ self.ou_computers):
+ self.add_if_possible({
+ "dn": dn,
+ "objectclass": "organizationalUnit"})
+
+ def tearDown(self):
+ super(UserTests, self).tearDown()
+
+ def test_00_00_do_nothing(self):
+ # this gives us an idea of the overhead
+ pass
+
+ def _prepare_n_groups(self, n):
+ self.state.n_groups = n
+ for i in range(n):
+ self.add_if_possible({
+ "dn": "cn=g%d,%s" % (i, self.ou_groups),
+ "objectclass": "group"})
+
+ def _add_users(self, start, end):
+ for i in range(start, end):
+ self.ldb.add({
+ "dn": "cn=u%d,%s" % (i, self.ou_users),
+ "objectclass": "user"})
+
+ def _add_users_ldif(self, start, end):
+ lines = []
+ for i in range(start, end):
+ lines.append("dn: cn=u%d,%s" % (i, self.ou_users))
+ lines.append("objectclass: user")
+ lines.append("")
+ self.ldb.add_ldif('\n'.join(lines))
+
+ def _test_unindexed_search(self):
+ expressions = [
+ ('(&(objectclass=user)(description='
+ 'Built-in account for adminstering the computer/domain))'),
+ '(description=Built-in account for adminstering the computer/domain)',
+ '(objectCategory=*)',
+ '(samaccountname=Administrator*)'
+ ]
+ for expression in expressions:
+ t = time.time()
+ for i in range(50):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d %s took %s' % (i, expression,
+ time.time() - t),
+ file=sys.stderr)
+
+ def _test_indexed_search(self):
+ expressions = ['(objectclass=group)',
+ '(samaccountname=Administrator)'
+ ]
+ for expression in expressions:
+ t = time.time()
+ for i in range(10000):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d runs %s took %s' % (i, expression,
+ time.time() - t),
+ file=sys.stderr)
+
+ def _test_complex_search(self):
+ classes = ['samaccountname', 'objectCategory', 'dn', 'member']
+ values = ['*', '*t*', 'g*', 'user']
+ comparators = ['=', '<=', '>='] # '~=' causes error
+ maybe_not = ['!(', '']
+ joiners = ['&', '|']
+
+ # The number of permutations is 18432, which is not huge but
+ # would take hours to search. So we take a sample.
+ all_permutations = list(itertools.product(joiners,
+ classes, classes,
+ values, values,
+ comparators, comparators,
+ maybe_not, maybe_not))
+ random.seed(1)
+
+ for (j, c1, c2, v1, v2,
+ o1, o2, n1, n2) in random.sample(all_permutations, 100):
+ expression = ''.join(['(', j,
+ '(', n1, c1, o1, v1,
+ '))' if n1 else ')',
+ '(', n2, c2, o2, v2,
+ '))' if n2 else ')',
+ ')'])
+ print(expression)
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+
+ def _test_member_search(self, rounds=10):
+ expressions = []
+ for d in range(50):
+ expressions.append('(member=cn=u%d,%s)' % (d + 500, self.ou_users))
+ expressions.append('(member=u%d*)' % (d + 700,))
+ for i in range(N_GROUPS):
+ expressions.append('(memberOf=cn=g%d,%s)' % (i, self.ou_groups))
+ expressions.append('(memberOf=cn=g%d*)' % (i,))
+ expressions.append('(memberOf=cn=*%s*)' % self.ou_groups)
+
+ for expression in expressions:
+ t = time.time()
+ for i in range(rounds):
+ self.ldb.search(self.ou,
+ expression=expression,
+ scope=SCOPE_SUBTREE,
+ attrs=['cn'])
+ print('%d runs %s took %s' % (i, expression,
+ time.time() - t),
+ file=sys.stderr)
+
+ def _test_add_many_users(self, n=BATCH_SIZE):
+ s = self.state.next_user_id
+ e = s + n
+ self._add_users(s, e)
+ self.state.next_user_id = e
+
+ def _test_add_many_users_ldif(self, n=BATCH_SIZE):
+ s = self.state.next_user_id
+ e = s + n
+ self._add_users_ldif(s, e)
+ self.state.next_user_id = e
+
+ def _link_user_and_group(self, u, g):
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=g%d,%s" % (g, self.ou_groups))
+ m["member"] = MessageElement("cn=u%d,%s" % (u, self.ou_users),
+ FLAG_MOD_ADD, "member")
+ self.ldb.modify(m)
+
+ def _test_link_many_users(self, n=BATCH_SIZE):
+ self._prepare_n_groups(N_GROUPS)
+ s = self.state.next_linked_user
+ e = s + n
+ for i in range(s, e):
+ # put everyone in group 0, and one other group
+ g = i % (N_GROUPS - 1) + 1
+ self._link_user_and_group(i, g)
+ self._link_user_and_group(i, 0)
+ self.state.next_linked_user = e
+
+ test_00_01_adding_users_1000 = _test_add_many_users
+
+ test_00_10_complex_search_1k_users = _test_complex_search
+ test_00_11_unindexed_search_1k_users = _test_unindexed_search
+ test_00_12_indexed_search_1k_users = _test_indexed_search
+ test_00_13_member_search_1k_users = _test_member_search
+
+ test_01_02_adding_users_2000_ldif = _test_add_many_users_ldif
+ test_01_03_adding_users_3000 = _test_add_many_users
+
+ test_01_10_complex_search_3k_users = _test_complex_search
+ test_01_11_unindexed_search_3k_users = _test_unindexed_search
+ test_01_12_indexed_search_3k_users = _test_indexed_search
+
+ def test_01_13_member_search_3k_users(self):
+ self._test_member_search(rounds=5)
+
+ test_02_01_link_users_1000 = _test_link_many_users
+ test_02_02_link_users_2000 = _test_link_many_users
+ test_02_03_link_users_3000 = _test_link_many_users
+
+ test_03_10_complex_search_linked_users = _test_complex_search
+ test_03_11_unindexed_search_linked_users = _test_unindexed_search
+ test_03_12_indexed_search_linked_users = _test_indexed_search
+
+ def test_03_13_member_search_linked_users(self):
+ self._test_member_search(rounds=2)
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+if ANCIENT_SAMBA:
+ runner = SubunitTestRunner()
+ if not runner.run(unittest.TestLoader().loadTestsFromTestCase(
+ UserTests)).wasSuccessful():
+ sys.exit(1)
+ sys.exit(0)
+else:
+ TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/asq.py b/source4/dsdb/tests/python/asq.py
new file mode 100644
index 0000000..72fa8bb
--- /dev/null
+++ b/source4/dsdb/tests/python/asq.py
@@ -0,0 +1,225 @@
+#!/usr/bin/env python3
+#
+# Test ASQ LDAP control behaviour in Samba
+# Copyright (C) Andrew Bartlett 2019-2020
+#
+# Based on Unit tests for the notification control
+# Copyright (C) Stefan Metzmacher 2016
+#
+# 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/>.
+
+import optparse
+import sys
+import os
+import random
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba import ldb
+from samba.samdb import SamDB
+from samba.ndr import ndr_unpack
+from samba import gensec
+from samba.credentials import Credentials
+import samba.tests
+
+from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, LdbError
+from ldb import ERR_TIME_LIMIT_EXCEEDED, ERR_ADMIN_LIMIT_EXCEEDED, ERR_UNWILLING_TO_PERFORM
+from ldb import Message
+
+parser = optparse.OptionParser("asq.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+url = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+class ASQLDAPTest(samba.tests.TestCase):
+
+ def setUp(self):
+ super(ASQLDAPTest, self).setUp()
+ self.ldb = samba.Ldb(url, credentials=creds, session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.get_default_basedn()
+ self.NAME_ASQ="asq_" + format(random.randint(0, 99999), "05")
+ self.OU_NAME_ASQ= self.NAME_ASQ + "_ou"
+ self.ou_dn = ldb.Dn(self.ldb, "ou=" + self.OU_NAME_ASQ + "," + str(self.base_dn))
+
+ samba.tests.delete_force(self.ldb, self.ou_dn,
+ controls=['tree_delete:1'])
+
+ self.ldb.add({
+ "dn": self.ou_dn,
+ "objectclass": "organizationalUnit",
+ "ou": self.OU_NAME_ASQ})
+
+ self.members = []
+ self.members2 = []
+
+ for x in range(20):
+ name = self.NAME_ASQ + "_" + str(x)
+ dn = ldb.Dn(self.ldb,
+ "cn=" + name + "," + str(self.ou_dn))
+ self.members.append(dn)
+ self.ldb.add({
+ "dn": dn,
+ "objectclass": "group"})
+
+ for x in range(20):
+ name = self.NAME_ASQ + "_" + str(x + 20)
+ dn = ldb.Dn(self.ldb,
+ "cn=" + name + "," + str(self.ou_dn))
+ self.members2.append(dn)
+ self.ldb.add({
+ "dn": dn,
+ "objectclass": "group",
+ "member": [str(x) for x in self.members]})
+
+ name = self.NAME_ASQ + "_" + str(x + 40)
+ self.top_dn = ldb.Dn(self.ldb,
+ "cn=" + name + "," + str(self.ou_dn))
+ self.ldb.add({
+ "dn": self.top_dn,
+ "objectclass": "group",
+ "member": [str(x) for x in self.members2]})
+
+ def tearDown(self):
+ samba.tests.delete_force(self.ldb, self.ou_dn,
+ controls=['tree_delete:1'])
+
+ def test_asq(self):
+ """Testing ASQ behaviour.
+
+ ASQ is very strange, it turns a BASE search into a search for
+ all the objects pointed to by the specified attribute,
+ returning multiple entries!
+
+ """
+
+ msgs = self.ldb.search(base=self.top_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=["objectGUID", "cn", "member"],
+ controls=["asq:1:member"])
+
+ self.assertEqual(len(msgs), 20)
+
+ for msg in msgs:
+ self.assertNotEqual(msg.dn, self.top_dn)
+ self.assertIn(msg.dn, self.members2)
+ for group in msg["member"]:
+ self.assertIn(ldb.Dn(self.ldb, str(group)),
+ self.members)
+
+ def test_asq_paged(self):
+ """Testing ASQ behaviour with paged_results set.
+
+ ASQ is very strange, it turns a BASE search into a search for
+ all the objects pointed to by the specified attribute,
+ returning multiple entries!
+
+ """
+
+ msgs = self.ldb.search(base=self.top_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=["objectGUID", "cn", "member"],
+ controls=["asq:1:member",
+ "paged_results:1:1024"])
+
+ self.assertEqual(len(msgs), 20)
+
+ for msg in msgs:
+ self.assertNotEqual(msg.dn, self.top_dn)
+ self.assertIn(msg.dn, self.members2)
+ for group in msg["member"]:
+ self.assertIn(ldb.Dn(self.ldb, str(group)),
+ self.members)
+
+ def test_asq_vlv(self):
+ """Testing ASQ behaviour with VLV set.
+
+ ASQ is very strange, it turns a BASE search into a search for
+ all the objects pointed to by the specified attribute,
+ returning multiple entries!
+
+ """
+
+ sort_control = "server_sort:1:0:cn"
+
+ msgs = self.ldb.search(base=self.top_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=["objectGUID", "cn", "member"],
+ controls=["asq:1:member",
+ sort_control,
+ "vlv:1:20:20:11:0"])
+
+ self.assertEqual(len(msgs), 20)
+
+ for msg in msgs:
+ self.assertNotEqual(msg.dn, self.top_dn)
+ self.assertIn(msg.dn, self.members2)
+ for group in msg["member"]:
+ self.assertIn(ldb.Dn(self.ldb, str(group)),
+ self.members)
+
+ def test_asq_vlv_paged(self):
+ """Testing ASQ behaviour with VLV and paged_results set.
+
+ ASQ is very strange, it turns a BASE search into a search for
+ all the objects pointed to by the specified attribute,
+ returning multiple entries!
+
+ Thankfully combining both of these gives
+ unavailable-critical-extension against Windows 1709
+
+ """
+
+ sort_control = "server_sort:1:0:cn"
+
+ try:
+ msgs = self.ldb.search(base=self.top_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=["objectGUID", "cn", "member"],
+ controls=["asq:1:member",
+ sort_control,
+ "vlv:1:20:20:11:0",
+ "paged_results:1:1024"])
+ self.fail("should have failed with LDAP_UNAVAILABLE_CRITICAL_EXTENSION")
+ except ldb.LdbError as e:
+ (enum, estr) = e.args
+ self.assertEqual(enum, ldb.ERR_UNSUPPORTED_CRITICAL_EXTENSION)
+
+if "://" not in url:
+ if os.path.isfile(url):
+ url = "tdb://%s" % url
+ else:
+ url = "ldap://%s" % url
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/attr_from_server.py b/source4/dsdb/tests/python/attr_from_server.py
new file mode 100644
index 0000000..aca356b
--- /dev/null
+++ b/source4/dsdb/tests/python/attr_from_server.py
@@ -0,0 +1,149 @@
+# -*- coding: utf-8 -*-
+#
+# Tests a corner-case involving the fromServer attribute, which is slightly
+# unique: it's an Object(DS-DN) (like a one-way link), but it is also a
+# mandatory attribute (for nTDSConnection). The corner-case is that the
+# fromServer can potentially end up pointing to a non-existent object.
+# This can happen with other one-way links, but these other one-way links
+# are not mandatory attributes.
+#
+# Copyright (C) Andrew Bartlett 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/>.
+#
+import optparse
+import sys
+sys.path.insert(0, "bin/python")
+import samba
+import os
+import time
+import ldb
+import samba.tests
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+from samba.dcerpc import misc
+from samba.provision import DEFAULTSITE
+
+# note we must connect to the local ldb file on disk, in order to
+# add system-only nTDSDSA objects
+parser = optparse.OptionParser("attr_from_server.py <LDB-filepath>")
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+ldb_path = args[0]
+
+
+class FromServerAttrTest(samba.tests.TestCase):
+ def setUp(self):
+ super(FromServerAttrTest, self).setUp()
+ self.ldb = samba.tests.connect_samdb(ldb_path)
+
+ def tearDown(self):
+ super(FromServerAttrTest, self).tearDown()
+
+ def set_attribute(self, dn, attr, value, operation=ldb.FLAG_MOD_ADD):
+ """Modifies an attribute for an object"""
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.ldb, dn)
+ m[attr] = ldb.MessageElement(value, operation, attr)
+ self.ldb.modify(m)
+
+ def get_object_guid(self, dn):
+ res = self.ldb.search(base=dn, attrs=["objectGUID"],
+ scope=ldb.SCOPE_BASE)
+ self.assertTrue(len(res) == 1)
+ return str(misc.GUID(res[0]['objectGUID'][0]))
+
+ def test_dangling_server_attr(self):
+ """
+ Tests a scenario where an object has a fromServer attribute that points
+ to an object that no longer exists.
+ """
+
+ # add a temporary server and its associated NTDS Settings object
+ config_dn = self.ldb.get_config_basedn()
+ sites_dn = "CN=Sites,{0}".format(config_dn)
+ servers_dn = "CN=Servers,CN={0},{1}".format(DEFAULTSITE, sites_dn)
+ tmp_server = "CN=TMPSERVER,{0}".format(servers_dn)
+ self.ldb.add({"dn": tmp_server, "objectclass": "server"})
+ server_guid = self.get_object_guid(tmp_server)
+ tmp_ntds_settings = "CN=NTDS Settings,{0}".format(tmp_server)
+ self.ldb.add({"dn": tmp_ntds_settings, "objectClass": "nTDSDSA"},
+ ["relax:0"])
+
+ # add an NTDS connection under the testenv DC that points to the tmp DC
+ testenv_dc = "CN={0},{1}".format(os.environ["SERVER"], servers_dn)
+ ntds_conn = "CN=Test-NTDS-Conn,CN=NTDS Settings,{0}".format(testenv_dc)
+ ldif = """
+dn: {dn}
+objectClass: nTDSConnection
+fromServer: CN=NTDS Settings,{fromServer}
+options: 1
+enabledConnection: TRUE
+""".format(dn=ntds_conn, fromServer=tmp_server)
+ self.ldb.add_ldif(ldif)
+ self.addCleanup(self.ldb.delete, ntds_conn)
+
+ # sanity-check we can modify the NTDS Connection object
+ self.set_attribute(ntds_conn, 'description', 'Test-value')
+
+ # sanity-check we can't modify the fromServer to point to a bad DN
+ try:
+ bad_dn = "CN=NTDS Settings,CN=BAD-DC,{0}".format(servers_dn)
+ self.set_attribute(ntds_conn, 'fromServer', bad_dn,
+ operation=ldb.FLAG_MOD_REPLACE)
+ self.fail("Successfully set fromServer to bad DN")
+ except ldb.LdbError as err:
+ enum = err.args[0]
+ self.assertEqual(enum, ldb.ERR_CONSTRAINT_VIOLATION)
+
+ # delete the tmp server, i.e. pretend we demoted it
+ self.ldb.delete(tmp_server, ["tree_delete:1"])
+
+ # check we can still see the deleted server object
+ search_expr = '(objectGUID={0})'.format(server_guid)
+ res = self.ldb.search(config_dn, scope=ldb.SCOPE_SUBTREE,
+ expression=search_expr,
+ controls=["show_deleted:1"])
+ self.assertTrue(len(res) == 1, "Could not find deleted server entry")
+
+ # now pretend some time has passed and the deleted server object
+ # has been tombstone-expunged from the DB
+ time.sleep(1)
+ current_time = int(time.time())
+ self.ldb.garbage_collect_tombstones([str(config_dn)], current_time,
+ tombstone_lifetime=0)
+
+ # repeat the search to sanity-check the deleted object is really gone
+ res = self.ldb.search(config_dn, scope=ldb.SCOPE_SUBTREE,
+ expression=search_expr,
+ controls=["show_deleted:1"])
+ self.assertTrue(len(res) == 0, "Did not expunge deleted server")
+
+ # the nTDSConnection now has a (mandatory) fromServer attribute that
+ # points to an object that no longer exists. Now try to modify an
+ # unrelated attribute on the nTDSConnection
+ try:
+ self.set_attribute(ntds_conn, 'description', 'Test-value-2',
+ operation=ldb.FLAG_MOD_REPLACE)
+ except ldb.LdbError as err:
+ print(err)
+ self.fail("Could not modify NTDS connection")
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/confidential_attr.py b/source4/dsdb/tests/python/confidential_attr.py
new file mode 100755
index 0000000..edbc959
--- /dev/null
+++ b/source4/dsdb/tests/python/confidential_attr.py
@@ -0,0 +1,1137 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Tests that confidential attributes (or attributes protected by a ACL that
+# denies read access) cannot be guessed through wildcard DB searches.
+#
+# Copyright (C) Catalyst.Net Ltd
+#
+# 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/>.
+#
+import optparse
+import sys
+sys.path.insert(0, "bin/python")
+
+import samba
+import random
+import statistics
+import time
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+import samba.getopt as options
+from ldb import SCOPE_BASE, SCOPE_SUBTREE
+from samba.dsdb import SEARCH_FLAG_CONFIDENTIAL, SEARCH_FLAG_RODC_ATTRIBUTE, SEARCH_FLAG_PRESERVEONDELETE
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_REPLACE, FLAG_MOD_ADD
+from samba.auth import system_session
+from samba import gensec, sd_utils
+from samba.samdb import SamDB
+from samba.credentials import Credentials, DONT_USE_KERBEROS
+from samba.dcerpc import security
+
+import samba.tests
+import samba.dsdb
+
+parser = optparse.OptionParser("confidential_attr.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+ start = host.rindex("://")
+ host = host.lstrip(start + 3)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+#
+# Tests start here
+#
+class ConfidentialAttrCommon(samba.tests.TestCase):
+
+ def setUp(self):
+ super(ConfidentialAttrCommon, self).setUp()
+
+ self.ldb_admin = SamDB(ldaphost, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+ self.user_pass = "samba123@"
+ self.base_dn = self.ldb_admin.domain_dn()
+ self.schema_dn = self.ldb_admin.get_schema_basedn()
+ self.sd_utils = sd_utils.SDUtils(self.ldb_admin)
+
+ # the tests work by setting the 'Confidential' bit in the searchFlags
+ # for an existing schema attribute. This only works against Windows if
+ # the systemFlags does not have FLAG_SCHEMA_BASE_OBJECT set for the
+ # schema attribute being modified. There are only a few attributes that
+ # meet this criteria (most of which only apply to 'user' objects)
+ self.conf_attr = "homePostalAddress"
+ attr_cn = "CN=Address-Home"
+ # schemaIdGuid for homePostalAddress (used for ACE tests)
+ self.conf_attr_guid = "16775781-47f3-11d1-a9c3-0000f80367c1"
+ self.conf_attr_sec_guid = "77b5b886-944a-11d1-aebd-0000f80367c1"
+ self.attr_dn = "{0},{1}".format(attr_cn, self.schema_dn)
+
+ userou = "OU=conf-attr-test"
+ self.ou = "{0},{1}".format(userou, self.base_dn)
+ samba.tests.delete_force(self.ldb_admin, self.ou, controls=['tree_delete:1'])
+ self.ldb_admin.create_ou(self.ou)
+ self.addCleanup(samba.tests.delete_force, self.ldb_admin, self.ou, controls=['tree_delete:1'])
+
+ # use a common username prefix, so we can use sAMAccountName=CATC-* as
+ # a search filter to only return the users we're interested in
+ self.user_prefix = "catc-"
+
+ # add a test object with this attribute set
+ self.conf_value = "abcdef"
+ self.conf_user = "{0}conf-user".format(self.user_prefix)
+ self.ldb_admin.newuser(self.conf_user, self.user_pass, userou=userou)
+ self.conf_dn = self.get_user_dn(self.conf_user)
+ self.add_attr(self.conf_dn, self.conf_attr, self.conf_value)
+
+ # add a sneaky user that will try to steal our secrets
+ self.user = "{0}sneaky-user".format(self.user_prefix)
+ self.ldb_admin.newuser(self.user, self.user_pass, userou=userou)
+ self.ldb_user = self.get_ldb_connection(self.user, self.user_pass)
+
+ self.all_users = [self.user, self.conf_user]
+
+ # add some other users that also have confidential attributes, so we
+ # check we don't disclose their details, particularly in '!' searches
+ for i in range(1, 3):
+ username = "{0}other-user{1}".format(self.user_prefix, i)
+ self.ldb_admin.newuser(username, self.user_pass, userou=userou)
+ userdn = self.get_user_dn(username)
+ self.add_attr(userdn, self.conf_attr, "xyz{0}".format(i))
+ self.all_users.append(username)
+
+ # there are 4 users in the OU, plus the OU itself
+ self.test_dn = self.ou
+ self.total_objects = len(self.all_users) + 1
+ self.objects_with_attr = 3
+
+ # sanity-check the flag is not already set (this'll cause problems if
+ # previous test run didn't clean up properly)
+ search_flags = int(self.get_attr_search_flags(self.attr_dn))
+ if search_flags & SEARCH_FLAG_CONFIDENTIAL|SEARCH_FLAG_RODC_ATTRIBUTE:
+ self.set_attr_search_flags(self.attr_dn, str(search_flags &~ (SEARCH_FLAG_CONFIDENTIAL|SEARCH_FLAG_RODC_ATTRIBUTE)))
+ search_flags = int(self.get_attr_search_flags(self.attr_dn))
+ self.assertEqual(0, search_flags & (SEARCH_FLAG_CONFIDENTIAL|SEARCH_FLAG_RODC_ATTRIBUTE),
+ f"{self.conf_attr} searchFlags did not reset to omit SEARCH_FLAG_CONFIDENTIAL and SEARCH_FLAG_RODC_ATTRIBUTE ({search_flags})")
+
+ def add_attr(self, dn, attr, value):
+ m = Message()
+ m.dn = Dn(self.ldb_admin, dn)
+ m[attr] = MessageElement(value, FLAG_MOD_ADD, attr)
+ self.ldb_admin.modify(m)
+
+ def set_attr_search_flags(self, attr_dn, flags):
+ """Modifies the searchFlags for an object in the schema"""
+ m = Message()
+ m.dn = Dn(self.ldb_admin, attr_dn)
+ m['searchFlags'] = MessageElement(flags, FLAG_MOD_REPLACE,
+ 'searchFlags')
+ self.ldb_admin.modify(m)
+
+ # note we have to update the schema for this change to take effect (on
+ # Windows, at least)
+ self.ldb_admin.set_schema_update_now()
+
+ def get_attr_search_flags(self, attr_dn):
+ """Marks the attribute under test as being confidential"""
+ res = self.ldb_admin.search(attr_dn, scope=SCOPE_BASE,
+ attrs=['searchFlags'])
+ return res[0]['searchFlags'][0]
+
+ def make_attr_confidential(self):
+ """Marks the attribute under test as being confidential"""
+
+ # work out the original 'searchFlags' value before we overwrite it
+ old_value = self.get_attr_search_flags(self.attr_dn)
+
+ self.set_attr_search_flags(self.attr_dn, str(SEARCH_FLAG_CONFIDENTIAL))
+
+ # reset the value after the test completes
+ self.addCleanup(self.set_attr_search_flags, self.attr_dn, old_value)
+
+ def get_user_dn(self, name):
+ return "CN={0},{1}".format(name, self.ou)
+
+ def get_user_sid_string(self, username):
+ user_dn = self.get_user_dn(username)
+ user_sid = self.sd_utils.get_object_sid(user_dn)
+ return str(user_sid)
+
+ def get_ldb_connection(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ features = creds_tmp.get_gensec_features() | gensec.FEATURE_SEAL
+ creds_tmp.set_gensec_features(features)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS)
+ ldb_target = SamDB(url=ldaphost, credentials=creds_tmp, lp=lp)
+ return ldb_target
+
+ def assert_search_result(self, expected_num, expr, samdb):
+
+ # try asking for different attributes back: None/all, the confidential
+ # attribute itself, and a random unrelated attribute
+ attr_filters = [None, ["*"], [self.conf_attr], ['name']]
+ for attr in attr_filters:
+ res = samdb.search(self.test_dn, expression=expr,
+ scope=SCOPE_SUBTREE, attrs=attr)
+ self.assertEqual(len(res), expected_num,
+ "%u results, not %u for search %s, attr %s" %
+ (len(res), expected_num, expr, str(attr)))
+
+ # return a selection of searches that match exactly against the test object
+ def get_exact_match_searches(self):
+ first_char = self.conf_value[:1]
+ last_char = self.conf_value[-1:]
+ test_attr = self.conf_attr
+
+ searches = [
+ # search for the attribute using a sub-string wildcard
+ # (which could reveal the attribute's actual value)
+ "({0}={1}*)".format(test_attr, first_char),
+ "({0}=*{1})".format(test_attr, last_char),
+
+ # sanity-check equality against an exact match on value
+ "({0}={1})".format(test_attr, self.conf_value),
+
+ # '~=' searches don't work against Samba
+ # sanity-check an approx search against an exact match on value
+ # "({0}~={1})".format(test_attr, self.conf_value),
+
+ # check wildcard in an AND search...
+ "(&({0}={1}*)(objectclass=*))".format(test_attr, first_char),
+
+ # ...an OR search (against another term that will never match)
+ "(|({0}={1}*)(objectclass=banana))".format(test_attr, first_char)]
+
+ return searches
+
+ # return searches that match any object with the attribute under test
+ def get_match_all_searches(self):
+ searches = [
+ # check a full wildcard against the confidential attribute
+ # (which could reveal the attribute's presence/absence)
+ "({0}=*)".format(self.conf_attr),
+
+ # check wildcard in an AND search...
+ "(&(objectclass=*)({0}=*))".format(self.conf_attr),
+
+ # ...an OR search (against another term that will never match)
+ "(|(objectclass=banana)({0}=*))".format(self.conf_attr),
+
+ # check <=, and >= expressions that would normally find a match
+ "({0}>=0)".format(self.conf_attr),
+ "({0}<=ZZZZZZZZZZZ)".format(self.conf_attr)]
+
+ return searches
+
+ def assert_conf_attr_searches(self, has_rights_to=0, samdb=None):
+ """Check searches against the attribute under test work as expected"""
+
+ if samdb is None:
+ samdb = self.ldb_user
+
+ if has_rights_to == "all":
+ has_rights_to = self.objects_with_attr
+
+ # these first few searches we just expect to match against the one
+ # object under test that we're trying to guess the value of
+ expected_num = 1 if has_rights_to > 0 else 0
+ for search in self.get_exact_match_searches():
+ self.assert_search_result(expected_num, search, samdb)
+
+ # these next searches will match any objects we have rights to see
+ expected_num = has_rights_to
+ for search in self.get_match_all_searches():
+ self.assert_search_result(expected_num, search, samdb)
+
+ # The following are double negative searches (i.e. NOT non-matching-
+ # condition) which will therefore match ALL objects, including the test
+ # object(s).
+ def get_negative_match_all_searches(self):
+ first_char = self.conf_value[:1]
+ last_char = self.conf_value[-1:]
+ not_first_char = chr(ord(first_char) + 1)
+ not_last_char = chr(ord(last_char) + 1)
+
+ searches = [
+ "(!({0}={1}*))".format(self.conf_attr, not_first_char),
+ "(!({0}=*{1}))".format(self.conf_attr, not_last_char)]
+ return searches
+
+ # the following searches will not match against the test object(s). So
+ # a user with sufficient rights will see an inverse sub-set of objects.
+ # (An unprivileged user would either see all objects on Windows, or no
+ # objects on Samba)
+ def get_inverse_match_searches(self):
+ first_char = self.conf_value[:1]
+ last_char = self.conf_value[-1:]
+ searches = [
+ "(!({0}={1}*))".format(self.conf_attr, first_char),
+ "(!({0}=*{1}))".format(self.conf_attr, last_char)]
+ return searches
+
+ def negative_searches_all_rights(self, total_objects=None):
+ expected_results = {}
+
+ if total_objects is None:
+ total_objects = self.total_objects
+
+ # these searches should match ALL objects (including the OU)
+ for search in self.get_negative_match_all_searches():
+ expected_results[search] = total_objects
+
+ # a ! wildcard should only match the objects without the attribute
+ search = "(!({0}=*))".format(self.conf_attr)
+ expected_results[search] = total_objects - self.objects_with_attr
+
+ # whereas the inverse searches should match all objects *except* the
+ # one under test
+ for search in self.get_inverse_match_searches():
+ expected_results[search] = total_objects - 1
+
+ return expected_results
+
+ # Returns the expected negative (i.e. '!') search behaviour when talking to
+ # a DC, i.e. we assert that users
+ # without rights always see ALL objects in '!' searches
+ def negative_searches_return_all(self, has_rights_to=0,
+ total_objects=None):
+ """Asserts user without rights cannot see objects in '!' searches"""
+ expected_results = {}
+
+ if total_objects is None:
+ total_objects = self.total_objects
+
+ # Windows 'hides' objects by always returning all of them, so negative
+ # searches that match all objects will simply return all objects
+ for search in self.get_negative_match_all_searches():
+ expected_results[search] = total_objects
+
+ # if we're matching on everything except the one object under test
+ # (i.e. the inverse subset), we'll still see all objects if
+ # has_rights_to == 0. Or we'll see all bar one if has_rights_to == 1.
+ inverse_searches = self.get_inverse_match_searches()
+ inverse_searches += ["(!({0}=*))".format(self.conf_attr)]
+
+ for search in inverse_searches:
+ expected_results[search] = total_objects - has_rights_to
+
+ return expected_results
+
+ # Returns the expected negative (i.e. '!') search behaviour. This varies
+ # depending on what type of DC we're talking to (i.e. Windows or Samba)
+ # and what access rights the user has.
+ # Note we only handle has_rights_to="all", 1 (the test object), or 0 (i.e.
+ # we don't have rights to any objects)
+ def negative_search_expected_results(self, has_rights_to, total_objects=None):
+
+ if has_rights_to == "all":
+ expect_results = self.negative_searches_all_rights(total_objects)
+
+ else:
+ expect_results = self.negative_searches_return_all(has_rights_to,
+ total_objects)
+ return expect_results
+
+ def assert_negative_searches(self, has_rights_to=0, samdb=None):
+ """Asserts user without rights cannot see objects in '!' searches"""
+
+ if samdb is None:
+ samdb = self.ldb_user
+
+ # build a dictionary of key=search-expr, value=expected_num assertions
+ expected_results = self.negative_search_expected_results(has_rights_to)
+
+ for search, expected_num in expected_results.items():
+ self.assert_search_result(expected_num, search, samdb)
+
+ def assert_attr_returned(self, expect_attr, samdb, attrs):
+ # does a query that should always return a successful result, and
+ # checks whether the confidential attribute is present
+ res = samdb.search(self.conf_dn, expression="(objectClass=*)",
+ scope=SCOPE_SUBTREE, attrs=attrs)
+ self.assertEqual(1, len(res))
+
+ attr_returned = False
+ for msg in res:
+ if self.conf_attr in msg:
+ attr_returned = True
+ self.assertEqual(expect_attr, attr_returned)
+
+ def assert_attr_visible(self, expect_attr, samdb=None):
+ if samdb is None:
+ samdb = self.ldb_user
+
+ # sanity-check confidential attribute is/isn't returned as expected
+ # based on the filter attributes we ask for
+ self.assert_attr_returned(expect_attr, samdb, attrs=None)
+ self.assert_attr_returned(expect_attr, samdb, attrs=["*"])
+ self.assert_attr_returned(expect_attr, samdb, attrs=[self.conf_attr])
+
+ # filtering on a different attribute should never return the conf_attr
+ self.assert_attr_returned(expect_attr=False, samdb=samdb,
+ attrs=['name'])
+
+ def assert_attr_visible_to_admin(self):
+ # sanity-check the admin user can always see the confidential attribute
+ self.assert_conf_attr_searches(has_rights_to="all",
+ samdb=self.ldb_admin)
+ self.assert_negative_searches(has_rights_to="all",
+ samdb=self.ldb_admin)
+ self.assert_attr_visible(expect_attr=True, samdb=self.ldb_admin)
+
+
+class ConfidentialAttrTest(ConfidentialAttrCommon):
+ def test_basic_search(self):
+ """Basic test confidential attributes aren't disclosed via searches"""
+
+ # check we can see a non-confidential attribute in a basic searches
+ self.assert_conf_attr_searches(has_rights_to="all")
+ self.assert_negative_searches(has_rights_to="all")
+ self.assert_attr_visible(expect_attr=True)
+
+ # now make the attribute confidential. Repeat the tests and check that
+ # an ordinary user can't see the attribute, or indirectly match on the
+ # attribute via the search expression
+ self.make_attr_confidential()
+
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_negative_searches(has_rights_to=0)
+ self.assert_attr_visible(expect_attr=False)
+
+ # sanity-check we haven't hidden the attribute from the admin as well
+ self.assert_attr_visible_to_admin()
+
+ def _test_search_with_allow_acl(self, allow_ace):
+ """Checks a ACE with 'CR' rights can override a confidential attr"""
+ # make the test attribute confidential and check user can't see it
+ self.make_attr_confidential()
+
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_negative_searches(has_rights_to=0)
+ self.assert_attr_visible(expect_attr=False)
+
+ # apply the allow ACE to the object under test
+ self.sd_utils.dacl_add_ace(self.conf_dn, allow_ace)
+
+ # the user should now be able to see the attribute for the one object
+ # we gave it rights to
+ self.assert_conf_attr_searches(has_rights_to=1)
+ self.assert_negative_searches(has_rights_to=1)
+ self.assert_attr_visible(expect_attr=True)
+
+ # sanity-check the admin can still see the attribute
+ self.assert_attr_visible_to_admin()
+
+ def test_search_with_attr_acl_override(self):
+ """Make the confidential attr visible via an OA attr ACE"""
+
+ # set the SEC_ADS_CONTROL_ACCESS bit ('CR') for the user for the
+ # attribute under test, so the user can see it once more
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(OA;;CR;{0};;{1})".format(self.conf_attr_guid, user_sid)
+
+ self._test_search_with_allow_acl(ace)
+
+ def test_search_with_propset_acl_override(self):
+ """Make the confidential attr visible via a Property-set ACE"""
+
+ # set the SEC_ADS_CONTROL_ACCESS bit ('CR') for the user for the
+ # property-set containing the attribute under test (i.e. the
+ # attributeSecurityGuid), so the user can see it once more
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(OA;;CR;{0};;{1})".format(self.conf_attr_sec_guid, user_sid)
+
+ self._test_search_with_allow_acl(ace)
+
+ def test_search_with_acl_override(self):
+ """Make the confidential attr visible via a general 'allow' ACE"""
+
+ # set the allow SEC_ADS_CONTROL_ACCESS bit ('CR') for the user
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(A;;CR;;;{0})".format(user_sid)
+
+ self._test_search_with_allow_acl(ace)
+
+ def test_search_with_blanket_oa_acl(self):
+ """Make the confidential attr visible via a non-specific OA ACE"""
+
+ # this just checks that an Object Access (OA) ACE without a GUID
+ # specified will work the same as an 'Access' (A) ACE
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(OA;;CR;;;{0})".format(user_sid)
+
+ self._test_search_with_allow_acl(ace)
+
+ def _test_search_with_neutral_acl(self, neutral_ace):
+ """Checks that a user does NOT gain access via an unrelated ACE"""
+
+ # make the test attribute confidential and check user can't see it
+ self.make_attr_confidential()
+
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_negative_searches(has_rights_to=0)
+ self.assert_attr_visible(expect_attr=False)
+
+ # apply the ACE to the object under test
+ self.sd_utils.dacl_add_ace(self.conf_dn, neutral_ace)
+
+ # this should make no difference to the user's ability to see the attr
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_negative_searches(has_rights_to=0)
+ self.assert_attr_visible(expect_attr=False)
+
+ # sanity-check the admin can still see the attribute
+ self.assert_attr_visible_to_admin()
+
+ def test_search_with_neutral_acl(self):
+ """Give the user all rights *except* CR for any attributes"""
+
+ # give the user all rights *except* CR and check it makes no difference
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(A;;RPWPCCDCLCLORCWOWDSDDTSW;;;{0})".format(user_sid)
+ self._test_search_with_neutral_acl(ace)
+
+ def test_search_with_neutral_attr_acl(self):
+ """Give the user all rights *except* CR for the attribute under test"""
+
+ # giving user all OA rights *except* CR should make no difference
+ user_sid = self.get_user_sid_string(self.user)
+ rights = "RPWPCCDCLCLORCWOWDSDDTSW"
+ ace = "(OA;;{0};{1};;{2})".format(rights, self.conf_attr_guid, user_sid)
+ self._test_search_with_neutral_acl(ace)
+
+ def test_search_with_neutral_cr_acl(self):
+ """Give the user CR rights for *another* unrelated attribute"""
+
+ # giving user object-access CR rights to an unrelated attribute
+ user_sid = self.get_user_sid_string(self.user)
+ # use the GUID for sAMAccountName here (for no particular reason)
+ unrelated_attr = "3e0abfd0-126a-11d0-a060-00aa006c33ed"
+ ace = "(OA;;CR;{0};;{1})".format(unrelated_attr, user_sid)
+ self._test_search_with_neutral_acl(ace)
+
+
+# Check that a Deny ACL on an attribute doesn't reveal confidential info
+class ConfidentialAttrTestDenyAcl(ConfidentialAttrCommon):
+
+ def assert_not_in_result(self, res, exclude_dn):
+ for msg in res:
+ self.assertNotEqual(msg.dn, exclude_dn,
+ "Search revealed object {0}".format(exclude_dn))
+
+ # deny ACL tests are slightly different as we are only denying access to
+ # the one object under test (rather than any objects with that attribute).
+ # Therefore we need an extra check that we don't reveal the test object
+ # in the search, if we're not supposed to
+ def assert_search_result(self, expected_num, expr, samdb,
+ excl_testobj=False):
+
+ # try asking for different attributes back: None/all, the confidential
+ # attribute itself, and a random unrelated attribute
+ attr_filters = [None, ["*"], [self.conf_attr], ['name']]
+ for attr in attr_filters:
+ res = samdb.search(self.test_dn, expression=expr,
+ scope=SCOPE_SUBTREE, attrs=attr)
+ self.assertEqual(len(res), expected_num,
+ "%u results, not %u for search %s, attr %s" %
+ (len(res), expected_num, expr, str(attr)))
+
+ # assert we haven't revealed the hidden test-object
+ if excl_testobj:
+ self.assert_not_in_result(res, exclude_dn=self.conf_dn)
+
+ # we make a few tweaks to the regular version of this function to cater to
+ # denying specifically one object via an ACE
+ def assert_conf_attr_searches(self, has_rights_to=0, samdb=None):
+ """Check searches against the attribute under test work as expected"""
+
+ if samdb is None:
+ samdb = self.ldb_user
+
+ # make sure the test object is not returned if we've been denied rights
+ # to it via an ACE
+ excl_testobj = has_rights_to == "deny-one"
+
+ # these first few searches we just expect to match against the one
+ # object under test that we're trying to guess the value of
+ expected_num = 1 if has_rights_to == "all" else 0
+
+ for search in self.get_exact_match_searches():
+ self.assert_search_result(expected_num, search, samdb,
+ excl_testobj)
+
+ # these next searches will match any objects with the attribute that
+ # we have rights to see (i.e. all except the object under test)
+ if has_rights_to == "all":
+ expected_num = self.objects_with_attr
+ elif has_rights_to == "deny-one":
+ expected_num = self.objects_with_attr - 1
+
+ for search in self.get_match_all_searches():
+ self.assert_search_result(expected_num, search, samdb,
+ excl_testobj)
+
+ # override method specifically for deny ACL test cases
+ def negative_searches_return_all(self, has_rights_to=0,
+ total_objects=None):
+ expected_results = {}
+
+ # When a user lacks access rights to an object, Windows 'hides' it in
+ # '!' searches by always returning it, regardless of whether it matches
+ searches = self.get_negative_match_all_searches()
+ searches += self.get_inverse_match_searches()
+ for search in searches:
+ expected_results[search] = self.total_objects
+
+ # in the wildcard case, the one object we don't have rights to gets
+ # bundled in with the objects that don't have the attribute at all
+ search = "(!({0}=*))".format(self.conf_attr)
+ has_rights_to = self.objects_with_attr - 1
+ expected_results[search] = self.total_objects - has_rights_to
+ return expected_results
+
+ # override method specifically for deny ACL test cases
+ def assert_negative_searches(self, has_rights_to=0, samdb=None):
+ """Asserts user without rights cannot see objects in '!' searches"""
+
+ if samdb is None:
+ samdb = self.ldb_user
+
+ # As the deny ACL is only denying access to one particular object, add
+ # an extra check that the denied object is not returned. (We can only
+ # assert this if the '!'/negative search behaviour is to suppress any
+ # objects we don't have access rights to)
+ excl_testobj = False
+
+ # build a dictionary of key=search-expr, value=expected_num assertions
+ expected_results = self.negative_search_expected_results(has_rights_to)
+
+ for search, expected_num in expected_results.items():
+ self.assert_search_result(expected_num, search, samdb,
+ excl_testobj=excl_testobj)
+
+ def _test_search_with_deny_acl(self, ace):
+ # check the user can see the attribute initially
+ self.assert_conf_attr_searches(has_rights_to="all")
+ self.assert_negative_searches(has_rights_to="all")
+ self.assert_attr_visible(expect_attr=True)
+
+ # add the ACE that denies access to the attr under test
+ self.sd_utils.dacl_add_ace(self.conf_dn, ace)
+
+ # the user shouldn't be able to see the attribute anymore
+ self.assert_conf_attr_searches(has_rights_to="deny-one")
+ self.assert_negative_searches(has_rights_to="deny-one")
+ self.assert_attr_visible(expect_attr=False)
+
+ # sanity-check we haven't hidden the attribute from the admin as well
+ self.assert_attr_visible_to_admin()
+
+ def test_search_with_deny_attr_acl(self):
+ """Checks a deny ACE works the same way as a confidential attribute"""
+
+ # add an ACE that denies the user Read Property (RP) access to the attr
+ # (which is similar to making the attribute confidential)
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(OD;;RP;{0};;{1})".format(self.conf_attr_guid, user_sid)
+
+ # check the user cannot see the attribute anymore
+ self._test_search_with_deny_acl(ace)
+
+ def test_search_with_deny_acl(self):
+ """Checks a blanket deny ACE denies access to an object's attributes"""
+
+ # add an blanket deny ACE for Read Property (RP) rights
+ user_dn = self.get_user_dn(self.user)
+ user_sid = self.sd_utils.get_object_sid(user_dn)
+ ace = "(D;;RP;;;{0})".format(str(user_sid))
+
+ # check the user cannot see the attribute anymore
+ self._test_search_with_deny_acl(ace)
+
+ def test_search_with_deny_propset_acl(self):
+ """Checks a deny ACE on the attribute's Property-Set"""
+
+ # add an blanket deny ACE for Read Property (RP) rights
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(OD;;RP;{0};;{1})".format(self.conf_attr_sec_guid, user_sid)
+
+ # check the user cannot see the attribute anymore
+ self._test_search_with_deny_acl(ace)
+
+ def test_search_with_blanket_oa_deny_acl(self):
+ """Checks a non-specific 'OD' ACE works the same as a 'D' ACE"""
+
+ # this just checks that adding a 'Object Deny' (OD) ACE without
+ # specifying a GUID will work the same way as a 'Deny' (D) ACE
+ user_sid = self.get_user_sid_string(self.user)
+ ace = "(OD;;RP;;;{0})".format(user_sid)
+
+ # check the user cannot see the attribute anymore
+ self._test_search_with_deny_acl(ace)
+
+
+# Check that using the dirsync controls doesn't reveal confidential attributes
+class ConfidentialAttrTestDirsync(ConfidentialAttrCommon):
+
+ def setUp(self):
+ super(ConfidentialAttrTestDirsync, self).setUp()
+ self.dirsync = ["dirsync:1:1:1000"]
+
+ # because we need to search on the base DN when using the dirsync
+ # controls, we need an extra filter for the inverse ('!') search,
+ # so we don't get thousands of objects returned
+ self.extra_filter = \
+ "(&(samaccountname={0}*)(!(isDeleted=*)))".format(self.user_prefix)
+ self.single_obj_filter = \
+ "(&(samaccountname={0})(!(isDeleted=*)))".format(self.conf_user)
+
+ self.attr_filters = [None, ["*"], ["name"]]
+
+ # Note dirsync behaviour is slightly different for the attribute under
+ # test - when you have full access rights, it only returns the objects
+ # that actually have this attribute (i.e. it doesn't return an empty
+ # message with just the DN). So we add the 'name' attribute into the
+ # attribute filter to avoid complicating our assertions further
+ self.attr_filters += [[self.conf_attr, "name"]]
+
+ # override method specifically for dirsync, i.e. add dirsync controls
+ def assert_search_result(self, expected_num, expr, samdb, base_dn=None):
+
+ # Note dirsync must always search on the partition base DN
+ base_dn = self.base_dn
+
+ # we need an extra filter for dirsync because:
+ # - we search on the base DN, so otherwise the '!' searches return
+ # thousands of unrelated results, and
+ # - we make the test attribute preserve-on-delete in one case, so we
+ # want to weed out results from any previous test runs
+ search = "(&{0}{1})".format(expr, self.extra_filter)
+
+ # If we expect to return multiple results, only check the first
+ if expected_num > 0:
+ attr_filters = [self.attr_filters[0]]
+ else:
+ attr_filters = self.attr_filters
+
+ for attr in attr_filters:
+ res = samdb.search(base_dn, expression=search, scope=SCOPE_SUBTREE,
+ attrs=attr, controls=self.dirsync)
+ self.assertEqual(len(res), expected_num,
+ "%u results, not %u for search %s, attr %s" %
+ (len(res), expected_num, search, str(attr)))
+
+ # override method specifically for dirsync, i.e. add dirsync controls
+ def assert_attr_returned(self, expect_attr, samdb, attrs,
+ no_result_ok=False):
+
+ # When using dirsync, the base DN we search on needs to be a naming
+ # context. Add an extra filter to ignore all the objects we aren't
+ # interested in
+ expr = self.single_obj_filter
+ res = samdb.search(self.base_dn, expression=expr, scope=SCOPE_SUBTREE,
+ attrs=attrs, controls=self.dirsync)
+ if not no_result_ok:
+ self.assertEqual(1, len(res))
+
+ attr_returned = False
+ for msg in res:
+ if self.conf_attr in msg and len(msg[self.conf_attr]) > 0:
+ attr_returned = True
+ self.assertEqual(expect_attr, attr_returned)
+
+ # override method specifically for dirsync (it has slightly different
+ # behaviour to normal when requesting specific attributes)
+ def assert_attr_visible(self, expect_attr, samdb=None):
+ if samdb is None:
+ samdb = self.ldb_user
+
+ # sanity-check confidential attribute is/isn't returned as expected
+ # based on the filter attributes we ask for
+ self.assert_attr_returned(expect_attr, samdb, attrs=None)
+ self.assert_attr_returned(expect_attr, samdb, attrs=["*"])
+
+ if expect_attr:
+ self.assert_attr_returned(expect_attr, samdb,
+ attrs=[self.conf_attr])
+ else:
+ # The behaviour with dirsync when asking solely for an attribute
+ # that you don't have rights to is a bit strange. Samba returns
+ # no result rather than an empty message with just the DN.
+ # Presumably this is due to dirsync module behaviour. It's not
+ # disclosive in that the DC behaves the same way as if you asked
+ # for a garbage/non-existent attribute
+ self.assert_attr_returned(expect_attr, samdb,
+ attrs=[self.conf_attr],
+ no_result_ok=True)
+ self.assert_attr_returned(expect_attr, samdb,
+ attrs=["garbage"], no_result_ok=True)
+
+ # filtering on a different attribute should never return the conf_attr
+ self.assert_attr_returned(expect_attr=False, samdb=samdb,
+ attrs=['name'])
+
+ # override method specifically for dirsync (total object count differs)
+ def assert_negative_searches(self, has_rights_to=0, samdb=None):
+ """Asserts user without rights cannot see objects in '!' searches"""
+
+ if samdb is None:
+ samdb = self.ldb_user
+
+ # because dirsync uses an extra filter, the total objects we expect
+ # here only includes the user objects (not the parent OU)
+ total_objects = len(self.all_users)
+ expected_results = self.negative_search_expected_results(has_rights_to,
+ total_objects)
+
+ for search, expected_num in expected_results.items():
+ self.assert_search_result(expected_num, search, samdb)
+
+ def test_search_with_dirsync(self):
+ """Checks dirsync controls don't reveal confidential attributes"""
+
+ self.assert_conf_attr_searches(has_rights_to="all")
+ self.assert_attr_visible(expect_attr=True)
+ self.assert_negative_searches(has_rights_to="all")
+
+ # make the test attribute confidential and check user can't see it,
+ # even if they use the dirsync controls
+ self.make_attr_confidential()
+
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_attr_visible(expect_attr=False)
+ self.assert_negative_searches(has_rights_to=0)
+
+ # as a final sanity-check, make sure the admin can still see the attr
+ self.assert_conf_attr_searches(has_rights_to="all",
+ samdb=self.ldb_admin)
+ self.assert_attr_visible(expect_attr=True, samdb=self.ldb_admin)
+ self.assert_negative_searches(has_rights_to="all",
+ samdb=self.ldb_admin)
+
+ def get_guid_string(self, dn):
+ """Returns an object's GUID (in string format)"""
+ res = self.ldb_admin.search(base=dn, attrs=["objectGUID"],
+ scope=SCOPE_BASE)
+ guid = res[0]['objectGUID'][0]
+ return self.ldb_admin.schema_format_value("objectGUID", guid).decode('utf-8')
+
+ def make_attr_preserve_on_delete(self):
+ """Marks the attribute under test as being preserve on delete"""
+
+ # work out the original 'searchFlags' value before we overwrite it
+ search_flags = int(self.get_attr_search_flags(self.attr_dn))
+
+ # check we've already set the confidential flag
+ self.assertNotEqual(0, search_flags & SEARCH_FLAG_CONFIDENTIAL)
+ search_flags |= SEARCH_FLAG_PRESERVEONDELETE
+
+ self.set_attr_search_flags(self.attr_dn, str(search_flags))
+
+ def change_attr_under_test(self, attr_name, attr_cn):
+ # change the attribute that the test code uses
+ self.conf_attr = attr_name
+ self.attr_dn = "{0},{1}".format(attr_cn, self.schema_dn)
+
+ # set the new attribute for the user-under-test
+ self.add_attr(self.conf_dn, self.conf_attr, self.conf_value)
+
+ # 2 other users also have the attribute-under-test set (to a randomish
+ # value). Set the new attribute for them now (normally this gets done
+ # in the setUp())
+ for username in self.all_users:
+ if "other-user" in username:
+ dn = self.get_user_dn(username)
+ self.add_attr(dn, self.conf_attr, "xyz-blah")
+
+ def test_search_with_dirsync_deleted_objects(self):
+ """Checks dirsync doesn't reveal confidential info for deleted objs"""
+
+ # change the attribute we're testing (we'll preserve on delete for this
+ # test case, which means the attribute-under-test hangs around after
+ # the test case finishes, and would interfere with the searches for
+ # subsequent other test cases)
+ self.change_attr_under_test("carLicense", "CN=carLicense")
+
+ # Windows dirsync behaviour is a little strange when you request
+ # attributes that deleted objects no longer have, so just request 'all
+ # attributes' to simplify the test logic
+ self.attr_filters = [None, ["*"]]
+
+ # normally dirsync uses extra filters to exclude deleted objects that
+ # we're not interested in. Override these filters so they WILL include
+ # deleted objects, but only from this particular test run. We can do
+ # this by matching lastKnownParent against this test case's OU, which
+ # will match any deleted child objects.
+ ou_guid = self.get_guid_string(self.ou)
+ deleted_filter = "(lastKnownParent=<GUID={0}>)".format(ou_guid)
+
+ # the extra-filter will get combined via AND with the search expression
+ # we're testing, i.e. filter on the confidential attribute AND only
+ # include non-deleted objects, OR deleted objects from this test run
+ exclude_deleted_objs_filter = self.extra_filter
+ self.extra_filter = "(|{0}{1})".format(exclude_deleted_objs_filter,
+ deleted_filter)
+
+ # for matching on a single object, the search expresseion becomes:
+ # match exactly by account-name AND either a non-deleted object OR a
+ # deleted object from this test run
+ match_by_name = "(samaccountname={0})".format(self.conf_user)
+ not_deleted = "(!(isDeleted=*))"
+ self.single_obj_filter = "(&{0}(|{1}{2}))".format(match_by_name,
+ not_deleted,
+ deleted_filter)
+
+ # check that the search filters work as expected
+ self.assert_conf_attr_searches(has_rights_to="all")
+ self.assert_attr_visible(expect_attr=True)
+ self.assert_negative_searches(has_rights_to="all")
+
+ # make the test attribute confidential *and* preserve on delete.
+ self.make_attr_confidential()
+ self.make_attr_preserve_on_delete()
+
+ # check we can't see the objects now, even with using dirsync controls
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_attr_visible(expect_attr=False)
+ self.assert_negative_searches(has_rights_to=0)
+
+ # now delete the users (except for the user whose LDB connection
+ # we're currently using)
+ for user in self.all_users:
+ if user is not self.user:
+ self.ldb_admin.delete(self.get_user_dn(user))
+
+ # check we still can't see the objects
+ self.assert_conf_attr_searches(has_rights_to=0)
+ self.assert_negative_searches(has_rights_to=0)
+
+ def test_timing_attack(self):
+ # Create the machine account.
+ mach_name = f'conf_timing_{random.randint(0, 0xffff)}'
+ mach_dn = Dn(self.ldb_admin, f'CN={mach_name},{self.ou}')
+ details = {
+ 'dn': mach_dn,
+ 'objectclass': 'computer',
+ 'sAMAccountName': f'{mach_name}$',
+ }
+ self.ldb_admin.add(details)
+
+ # Get the machine account's GUID.
+ res = self.ldb_admin.search(mach_dn,
+ attrs=['objectGUID'],
+ scope=SCOPE_BASE)
+ mach_guid = res[0].get('objectGUID', idx=0)
+
+ # Now we can create an msFVE-RecoveryInformation object that is a child
+ # of the machine account object.
+ recovery_dn = Dn(self.ldb_admin, str(mach_dn))
+ recovery_dn.add_child('CN=recovery_info')
+
+ secret_pw = 'Secret007'
+ not_secret_pw = 'Secret008'
+
+ secret_pw_utf8 = secret_pw.encode('utf-8')
+
+ # The crucial attribute, msFVE-RecoveryPassword, is a confidential
+ # attribute.
+ conf_attr = 'msFVE-RecoveryPassword'
+
+ m = Message(recovery_dn)
+ m['objectClass'] = 'msFVE-RecoveryInformation'
+ m['msFVE-RecoveryGuid'] = mach_guid
+ m[conf_attr] = secret_pw
+ self.ldb_admin.add(m)
+
+ attrs = [conf_attr]
+
+ # Search for the confidential attribute as administrator, ensuring it
+ # is visible.
+ res = self.ldb_admin.search(recovery_dn,
+ attrs=attrs,
+ scope=SCOPE_BASE)
+ self.assertEqual(1, len(res))
+ pw = res[0].get(conf_attr, idx=0)
+ self.assertEqual(secret_pw_utf8, pw)
+
+ # Repeat the search with an expression matching on the confidential
+ # attribute. This should also work.
+ res = self.ldb_admin.search(
+ recovery_dn,
+ attrs=attrs,
+ expression=f'({conf_attr}={secret_pw})',
+ scope=SCOPE_BASE)
+ self.assertEqual(1, len(res))
+ pw = res[0].get(conf_attr, idx=0)
+ self.assertEqual(secret_pw_utf8, pw)
+
+ # Search for the attribute as an unprivileged user. It should not be
+ # visible.
+ user_res = self.ldb_user.search(recovery_dn,
+ attrs=attrs,
+ scope=SCOPE_BASE)
+ pw = user_res[0].get(conf_attr, idx=0)
+ # The attribute should be None.
+ self.assertIsNone(pw)
+
+ # We use LDAP_MATCHING_RULE_TRANSITIVE_EVAL to create a search
+ # expression that takes a long time to execute, by setting off another
+ # search each time it is evaluated. It makes no difference that the
+ # object on which we're searching has no 'member' attribute.
+ dummy_dn = 'cn=user,cn=users,dc=samba,dc=example,dc=com'
+ slow_subexpr = f'(member:1.2.840.113556.1.4.1941:={dummy_dn})'
+ slow_expr = f'(|{slow_subexpr * 100})'
+
+ # The full search expression. It comprises a match on the confidential
+ # attribute joined by an AND to our slow search expression, The AND
+ # operator is short-circuiting, so if our first subexpression fails to
+ # match, we'll bail out of the search early. Otherwise, we'll evaluate
+ # the slow part; as its subexpressions are joined by ORs, and will all
+ # fail to match, every one of them will need to be evaluated. By
+ # measuring how long the search takes, we'll be able to infer whether
+ # the confidential attribute matched or not.
+
+ # This is bad if we are not an administrator, and are able to use this
+ # to determine the values of confidential attributes. Therefore we need
+ # to ensure we can't observe any difference in timing.
+ correct_expr = f'(&({conf_attr}={secret_pw}){slow_expr})'
+ wrong_expr = f'(&({conf_attr}={not_secret_pw}){slow_expr})'
+
+ def standard_uncertainty_bounds(times):
+ mean = statistics.mean(times)
+ stdev = statistics.stdev(times, mean)
+
+ return (mean - stdev, mean + stdev)
+
+ # Perform a number of searches with both correct and incorrect
+ # expressions, and return the uncertainty bounds for each.
+ def time_searches(samdb):
+ warmup_samples = 3
+ samples = 10
+ matching_times = []
+ non_matching_times = []
+
+ for _ in range(warmup_samples):
+ samdb.search(recovery_dn,
+ attrs=attrs,
+ expression=correct_expr,
+ scope=SCOPE_BASE)
+
+ for _ in range(samples):
+ # Measure the time taken for a search, for both a matching and
+ # a non-matching search expression.
+
+ prev = time.time()
+ samdb.search(recovery_dn,
+ attrs=attrs,
+ expression=correct_expr,
+ scope=SCOPE_BASE)
+ now = time.time()
+ matching_times.append(now - prev)
+
+ prev = time.time()
+ samdb.search(recovery_dn,
+ attrs=attrs,
+ expression=wrong_expr,
+ scope=SCOPE_BASE)
+ now = time.time()
+ non_matching_times.append(now - prev)
+
+ matching = standard_uncertainty_bounds(matching_times)
+ non_matching = standard_uncertainty_bounds(non_matching_times)
+ return matching, non_matching
+
+ def assertRangesDistinct(a, b):
+ a0, a1 = a
+ b0, b1 = b
+ self.assertLess(min(a1, b1), max(a0, b0))
+
+ def assertRangesOverlap(a, b):
+ a0, a1 = a
+ b0, b1 = b
+ self.assertGreaterEqual(min(a1, b1), max(a0, b0))
+
+ # For an administrator, the uncertainty bounds for matching and
+ # non-matching searches should be distinct. This shows that the two
+ # cases are distinguishable, and therefore that confidential attributes
+ # are visible.
+ admin_matching, admin_non_matching = time_searches(self.ldb_admin)
+ assertRangesDistinct(admin_matching, admin_non_matching)
+
+ # The user cannot view the confidential attribute, so the uncertainty
+ # bounds for matching and non-matching searches must overlap. The two
+ # cases must be indistinguishable.
+ user_matching, user_non_matching = time_searches(self.ldb_user)
+ assertRangesOverlap(user_matching, user_non_matching)
+
+# Check that using the dirsync controls doesn't reveal confidential
+# "RODC filtered attribute" values to users with only
+# GUID_DRS_GET_CHANGES. The tests is so similar to the Confidential
+# attribute test we base it on that.
+class RodcFilteredAttrDirsync(ConfidentialAttrTestDirsync):
+
+ def setUp(self):
+ super().setUp()
+ self.dirsync = ["dirsync:1:0:1000"]
+
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.user))
+ mod = "(OA;;CR;%s;;%s)" % (security.GUID_DRS_GET_CHANGES,
+ str(user_sid))
+ self.sd_utils.dacl_add_ace(self.base_dn, mod)
+
+ self.ldb_user = self.get_ldb_connection(self.user, self.user_pass)
+
+ self.addCleanup(self.sd_utils.dacl_delete_aces, self.base_dn, mod)
+
+ def make_attr_confidential(self):
+ """Marks the attribute under test as being confidential AND RODC
+ filtered (which should mean it is not visible with only
+ GUID_DRS_GET_CHANGES)
+ """
+
+ # work out the original 'searchFlags' value before we overwrite it
+ old_value = self.get_attr_search_flags(self.attr_dn)
+
+ self.set_attr_search_flags(self.attr_dn, str(SEARCH_FLAG_RODC_ATTRIBUTE|SEARCH_FLAG_CONFIDENTIAL))
+
+ # reset the value after the test completes
+ self.addCleanup(self.set_attr_search_flags, self.attr_dn, old_value)
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/deletetest.py b/source4/dsdb/tests/python/deletetest.py
new file mode 100755
index 0000000..118e6b5
--- /dev/null
+++ b/source4/dsdb/tests/python/deletetest.py
@@ -0,0 +1,565 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+import os
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from ldb import SCOPE_BASE, LdbError
+from ldb import ERR_NO_SUCH_OBJECT, ERR_NOT_ALLOWED_ON_NON_LEAF
+from ldb import ERR_UNWILLING_TO_PERFORM
+from samba.samdb import SamDB
+from samba.tests import delete_force
+from samba import dsdb
+from samba.common import get_string
+
+parser = optparse.OptionParser("deletetest.py [options] <host|file>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+class BaseDeleteTests(samba.tests.TestCase):
+
+ def GUID_string(self, guid):
+ return get_string(self.ldb.schema_format_value("objectGUID", guid))
+
+ def setUp(self):
+ super(BaseDeleteTests, self).setUp()
+ self.ldb = SamDB(host, credentials=creds, session_info=system_session(lp), lp=lp)
+
+ self.base_dn = self.ldb.domain_dn()
+ self.configuration_dn = self.ldb.get_config_basedn().get_linearized()
+
+ def search_guid(self, guid):
+ print("SEARCH by GUID %s" % self.GUID_string(guid))
+
+ res = self.ldb.search(base="<GUID=%s>" % self.GUID_string(guid),
+ scope=SCOPE_BASE,
+ controls=["show_deleted:1"],
+ attrs=["*", "parentGUID"])
+ self.assertEqual(len(res), 1)
+ return res[0]
+
+ def search_dn(self, dn):
+ print("SEARCH by DN %s" % dn)
+
+ res = self.ldb.search(expression="(objectClass=*)",
+ base=dn,
+ scope=SCOPE_BASE,
+ controls=["show_deleted:1"],
+ attrs=["*", "parentGUID"])
+ self.assertEqual(len(res), 1)
+ return res[0]
+
+
+class BasicDeleteTests(BaseDeleteTests):
+
+ def setUp(self):
+ super(BasicDeleteTests, self).setUp()
+
+ def del_attr_values(self, delObj):
+ print("Checking attributes for %s" % delObj["dn"])
+
+ self.assertEqual(str(delObj["isDeleted"][0]), "TRUE")
+ self.assertTrue(not("objectCategory" in delObj))
+ self.assertTrue(not("sAMAccountType" in delObj))
+
+ def preserved_attributes_list(self, liveObj, delObj):
+ print("Checking for preserved attributes list")
+
+ preserved_list = ["nTSecurityDescriptor", "attributeID", "attributeSyntax", "dNReferenceUpdate", "dNSHostName",
+ "flatName", "governsID", "groupType", "instanceType", "lDAPDisplayName", "legacyExchangeDN",
+ "isDeleted", "isRecycled", "lastKnownParent", "msDS-LastKnownRDN", "mS-DS-CreatorSID",
+ "mSMQOwnerID", "nCName", "objectClass", "distinguishedName", "objectGUID", "objectSid",
+ "oMSyntax", "proxiedObjectName", "name", "replPropertyMetaData", "sAMAccountName",
+ "securityIdentifier", "sIDHistory", "subClassOf", "systemFlags", "trustPartner", "trustDirection",
+ "trustType", "trustAttributes", "userAccountControl", "uSNChanged", "uSNCreated", "whenCreated"]
+
+ for a in liveObj:
+ if a in preserved_list:
+ self.assertTrue(a in delObj)
+
+ def check_rdn(self, liveObj, delObj, rdnName):
+ print("Checking for correct rDN")
+ rdn = liveObj[rdnName][0]
+ rdn2 = delObj[rdnName][0]
+ name2 = delObj["name"][0]
+ dn_rdn = delObj.dn.get_rdn_value()
+ guid = liveObj["objectGUID"][0]
+ self.assertEqual(str(rdn2), ("%s\nDEL:%s" % (rdn, self.GUID_string(guid))))
+ self.assertEqual(str(name2), ("%s\nDEL:%s" % (rdn, self.GUID_string(guid))))
+ self.assertEqual(str(name2), dn_rdn)
+
+ def delete_deleted(self, ldb, dn):
+ print("Testing the deletion of the already deleted dn %s" % dn)
+
+ try:
+ ldb.delete(dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ def test_delete_protection(self):
+ """Delete protection tests"""
+
+ print(self.base_dn)
+
+ delete_force(self.ldb, "cn=entry1,cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=entry2,cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectclass": "container"})
+ self.ldb.add({
+ "dn": "cn=entry1,cn=ldaptestcontainer," + self.base_dn,
+ "objectclass": "container"})
+ self.ldb.add({
+ "dn": "cn=entry2,cn=ldaptestcontainer," + self.base_dn,
+ "objectclass": "container"})
+
+ try:
+ self.ldb.delete("cn=ldaptestcontainer," + self.base_dn)
+ self.fail()
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_NON_LEAF)
+
+ self.ldb.delete("cn=ldaptestcontainer," + self.base_dn, ["tree_delete:1"])
+
+ try:
+ res = self.ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE, attrs=[])
+ self.fail()
+ except LdbError as e2:
+ (num, _) = e2.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+ try:
+ res = self.ldb.search("cn=entry1,cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE, attrs=[])
+ self.fail()
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+ try:
+ res = self.ldb.search("cn=entry2,cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE, attrs=[])
+ self.fail()
+ except LdbError as e4:
+ (num, _) = e4.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ delete_force(self.ldb, "cn=entry1,cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=entry2,cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+
+ # Performs some protected object delete testing
+
+ res = self.ldb.search(base="", expression="", scope=SCOPE_BASE,
+ attrs=["dsServiceName", "dNSHostName"])
+ self.assertEqual(len(res), 1)
+
+ # Delete failing since DC's nTDSDSA object is protected
+ try:
+ self.ldb.delete(res[0]["dsServiceName"][0])
+ self.fail()
+ except LdbError as e5:
+ (num, _) = e5.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ res = self.ldb.search(self.base_dn, attrs=["rIDSetReferences"],
+ expression="(&(objectClass=computer)(dNSHostName=" + str(res[0]["dNSHostName"][0]) + "))")
+ self.assertEqual(len(res), 1)
+
+ # Deletes failing since DC's rIDSet object is protected
+ try:
+ self.ldb.delete(res[0]["rIDSetReferences"][0])
+ self.fail()
+ except LdbError as e6:
+ (num, _) = e6.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ try:
+ self.ldb.delete(res[0]["rIDSetReferences"][0], ["tree_delete:1"])
+ self.fail()
+ except LdbError as e7:
+ (num, _) = e7.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Deletes failing since three main crossRef objects are protected
+
+ try:
+ self.ldb.delete("cn=Enterprise Schema,cn=Partitions," + self.configuration_dn)
+ self.fail()
+ except LdbError as e8:
+ (num, _) = e8.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ try:
+ self.ldb.delete("cn=Enterprise Schema,cn=Partitions," + self.configuration_dn, ["tree_delete:1"])
+ self.fail()
+ except LdbError as e9:
+ (num, _) = e9.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ self.ldb.delete("cn=Enterprise Configuration,cn=Partitions," + self.configuration_dn)
+ self.fail()
+ except LdbError as e10:
+ (num, _) = e10.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_NON_LEAF)
+ try:
+ self.ldb.delete("cn=Enterprise Configuration,cn=Partitions," + self.configuration_dn, ["tree_delete:1"])
+ self.fail()
+ except LdbError as e11:
+ (num, _) = e11.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_NON_LEAF)
+
+ res = self.ldb.search("cn=Partitions," + self.configuration_dn, attrs=[],
+ expression="(nCName=%s)" % self.base_dn)
+ self.assertEqual(len(res), 1)
+
+ try:
+ self.ldb.delete(res[0].dn)
+ self.fail()
+ except LdbError as e12:
+ (num, _) = e12.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_NON_LEAF)
+ try:
+ self.ldb.delete(res[0].dn, ["tree_delete:1"])
+ self.fail()
+ except LdbError as e13:
+ (num, _) = e13.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_NON_LEAF)
+
+ # Delete failing since "SYSTEM_FLAG_DISALLOW_DELETE"
+ try:
+ self.ldb.delete("CN=Users," + self.base_dn)
+ self.fail()
+ except LdbError as e14:
+ (num, _) = e14.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Tree-delete failing since "isCriticalSystemObject"
+ try:
+ self.ldb.delete("CN=Computers," + self.base_dn, ["tree_delete:1"])
+ self.fail()
+ except LdbError as e15:
+ (num, _) = e15.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+
+class BasicTreeDeleteTests(BasicDeleteTests):
+
+ def setUp(self):
+ super(BasicTreeDeleteTests, self).setUp()
+
+ # user current time in ms to make unique objects
+ import time
+ marker = str(int(round(time.time() * 1000)))
+ usr1_name = "u_" + marker
+ usr2_name = "u2_" + marker
+ grp_name = "g1_" + marker
+ site_name = "s1_" + marker
+
+ self.usr1 = "cn=%s,cn=users,%s" % (usr1_name, self.base_dn)
+ self.usr2 = "cn=%s,cn=users,%s" % (usr2_name, self.base_dn)
+ self.grp1 = "cn=%s,cn=users,%s" % (grp_name, self.base_dn)
+ self.sit1 = "cn=%s,cn=sites,%s" % (site_name, self.configuration_dn)
+ self.ss1 = "cn=NTDS Site Settings,cn=%s,cn=sites,%s" % (site_name, self.configuration_dn)
+ self.srv1 = "cn=Servers,cn=%s,cn=sites,%s" % (site_name, self.configuration_dn)
+ self.srv2 = "cn=TESTSRV,cn=Servers,cn=%s,cn=sites,%s" % (site_name, self.configuration_dn)
+
+ delete_force(self.ldb, self.usr1)
+ delete_force(self.ldb, self.usr2)
+ delete_force(self.ldb, self.grp1)
+ delete_force(self.ldb, self.ss1)
+ delete_force(self.ldb, self.srv2)
+ delete_force(self.ldb, self.srv1)
+ delete_force(self.ldb, self.sit1)
+
+ self.ldb.add({
+ "dn": self.usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": usr1_name})
+
+ self.ldb.add({
+ "dn": self.usr2,
+ "objectclass": "user",
+ "description": "test user 2 description",
+ "samaccountname": usr2_name})
+
+ self.ldb.add({
+ "dn": self.grp1,
+ "objectclass": "group",
+ "description": "test group",
+ "samaccountname": grp_name,
+ "member": [self.usr1, self.usr2],
+ "isDeleted": "FALSE"})
+
+ self.ldb.add({
+ "dn": self.sit1,
+ "objectclass": "site"})
+
+ self.ldb.add({
+ "dn": self.ss1,
+ "objectclass": ["applicationSiteSettings", "nTDSSiteSettings"]})
+
+ self.ldb.add({
+ "dn": self.srv1,
+ "objectclass": "serversContainer"})
+
+ self.ldb.add({
+ "dn": self.srv2,
+ "objectClass": "server"})
+
+ self.objLive1 = self.search_dn(self.usr1)
+ self.guid1 = self.objLive1["objectGUID"][0]
+
+ self.objLive2 = self.search_dn(self.usr2)
+ self.guid2 = self.objLive2["objectGUID"][0]
+
+ self.objLive3 = self.search_dn(self.grp1)
+ self.guid3 = self.objLive3["objectGUID"][0]
+
+ self.objLive4 = self.search_dn(self.sit1)
+ self.guid4 = self.objLive4["objectGUID"][0]
+
+ self.objLive5 = self.search_dn(self.ss1)
+ self.guid5 = self.objLive5["objectGUID"][0]
+
+ self.objLive6 = self.search_dn(self.srv1)
+ self.guid6 = self.objLive6["objectGUID"][0]
+
+ self.objLive7 = self.search_dn(self.srv2)
+ self.guid7 = self.objLive7["objectGUID"][0]
+
+ self.deleted_objects_config_dn \
+ = self.ldb.get_wellknown_dn(self.ldb.get_config_basedn(),
+ dsdb.DS_GUID_DELETED_OBJECTS_CONTAINER)
+ deleted_objects_config_obj \
+ = self.search_dn(self.deleted_objects_config_dn)
+
+ self.deleted_objects_config_guid \
+ = deleted_objects_config_obj["objectGUID"][0]
+
+ self.deleted_objects_domain_dn \
+ = self.ldb.get_wellknown_dn(self.ldb.get_default_basedn(),
+ dsdb.DS_GUID_DELETED_OBJECTS_CONTAINER)
+ deleted_objects_domain_obj \
+ = self.search_dn(self.deleted_objects_domain_dn)
+
+ self.deleted_objects_domain_guid \
+ = deleted_objects_domain_obj["objectGUID"][0]
+
+ self.deleted_objects_domain_dn \
+ = self.ldb.get_wellknown_dn(self.ldb.get_default_basedn(),
+ dsdb.DS_GUID_DELETED_OBJECTS_CONTAINER)
+ sites_obj = self.search_dn("cn=sites,%s"
+ % self.ldb.get_config_basedn())
+ self.sites_dn = sites_obj.dn
+ self.sites_guid \
+ = sites_obj["objectGUID"][0]
+
+ def test_all(self):
+ """Basic delete tests"""
+
+ self.ldb.delete(self.usr1)
+ self.ldb.delete(self.usr2)
+ self.ldb.delete(self.grp1)
+ self.ldb.delete(self.srv1, ["tree_delete:1"])
+ self.ldb.delete(self.sit1, ["tree_delete:1"])
+
+ self.check_all()
+
+ def test_tree_delete(self):
+ """Basic delete tests,
+ but use just one tree delete for the config records
+ """
+
+ self.ldb.delete(self.usr1)
+ self.ldb.delete(self.usr2)
+ self.ldb.delete(self.grp1)
+ self.ldb.delete(self.sit1, ["tree_delete:1"])
+
+ self.check_all()
+
+ def check_all(self):
+ objDeleted1 = self.search_guid(self.guid1)
+ objDeleted2 = self.search_guid(self.guid2)
+ objDeleted3 = self.search_guid(self.guid3)
+ objDeleted4 = self.search_guid(self.guid4)
+ objDeleted5 = self.search_guid(self.guid5)
+ objDeleted6 = self.search_guid(self.guid6)
+ objDeleted7 = self.search_guid(self.guid7)
+
+ self.del_attr_values(objDeleted1)
+ self.del_attr_values(objDeleted2)
+ self.del_attr_values(objDeleted3)
+ self.del_attr_values(objDeleted4)
+ self.del_attr_values(objDeleted5)
+ self.del_attr_values(objDeleted6)
+ self.del_attr_values(objDeleted7)
+
+ self.preserved_attributes_list(self.objLive1, objDeleted1)
+ self.preserved_attributes_list(self.objLive2, objDeleted2)
+ self.preserved_attributes_list(self.objLive3, objDeleted3)
+ self.preserved_attributes_list(self.objLive4, objDeleted4)
+ self.preserved_attributes_list(self.objLive5, objDeleted5)
+ self.preserved_attributes_list(self.objLive6, objDeleted6)
+ self.preserved_attributes_list(self.objLive7, objDeleted7)
+
+ self.check_rdn(self.objLive1, objDeleted1, "cn")
+ self.check_rdn(self.objLive2, objDeleted2, "cn")
+ self.check_rdn(self.objLive3, objDeleted3, "cn")
+ self.check_rdn(self.objLive4, objDeleted4, "cn")
+ self.check_rdn(self.objLive5, objDeleted5, "cn")
+ self.check_rdn(self.objLive6, objDeleted6, "cn")
+ self.check_rdn(self.objLive7, objDeleted7, "cn")
+
+ self.delete_deleted(self.ldb, self.usr1)
+ self.delete_deleted(self.ldb, self.usr2)
+ self.delete_deleted(self.ldb, self.grp1)
+ self.delete_deleted(self.ldb, self.sit1)
+ self.delete_deleted(self.ldb, self.ss1)
+ self.delete_deleted(self.ldb, self.srv1)
+ self.delete_deleted(self.ldb, self.srv2)
+
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted1.dn))
+ self.assertEqual(objDeleted1.dn.parent(),
+ self.deleted_objects_domain_dn)
+ self.assertEqual(objDeleted1["parentGUID"][0],
+ self.deleted_objects_domain_guid)
+
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted2.dn))
+ self.assertEqual(objDeleted2.dn.parent(),
+ self.deleted_objects_domain_dn)
+ self.assertEqual(objDeleted2["parentGUID"][0],
+ self.deleted_objects_domain_guid)
+
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted3.dn))
+ self.assertEqual(objDeleted3.dn.parent(),
+ self.deleted_objects_domain_dn)
+ self.assertEqual(objDeleted3["parentGUID"][0],
+ self.deleted_objects_domain_guid)
+
+ self.assertFalse("CN=Deleted Objects" in str(objDeleted4.dn))
+ self.assertEqual(objDeleted4.dn.parent(),
+ self.sites_dn)
+ self.assertEqual(objDeleted4["parentGUID"][0],
+ self.sites_guid)
+
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted5.dn))
+ self.assertEqual(objDeleted5.dn.parent(),
+ self.deleted_objects_config_dn)
+ self.assertEqual(objDeleted5["parentGUID"][0],
+ self.deleted_objects_config_guid)
+
+ self.assertFalse("CN=Deleted Objects" in str(objDeleted6.dn))
+ self.assertEqual(objDeleted6.dn.parent(),
+ objDeleted4.dn)
+ self.assertEqual(objDeleted6["parentGUID"][0],
+ objDeleted4["objectGUID"][0])
+
+ self.assertFalse("CN=Deleted Objects" in str(objDeleted7.dn))
+ self.assertEqual(objDeleted7.dn.parent(),
+ objDeleted6.dn)
+ self.assertEqual(objDeleted7["parentGUID"][0],
+ objDeleted6["objectGUID"][0])
+
+ objDeleted1 = self.search_guid(self.guid1)
+ objDeleted2 = self.search_guid(self.guid2)
+ objDeleted3 = self.search_guid(self.guid3)
+ objDeleted4 = self.search_guid(self.guid4)
+ objDeleted5 = self.search_guid(self.guid5)
+ objDeleted6 = self.search_guid(self.guid6)
+ objDeleted7 = self.search_guid(self.guid7)
+
+ self.del_attr_values(objDeleted1)
+ self.del_attr_values(objDeleted2)
+ self.del_attr_values(objDeleted3)
+ self.del_attr_values(objDeleted4)
+ self.del_attr_values(objDeleted5)
+ self.del_attr_values(objDeleted6)
+ self.del_attr_values(objDeleted7)
+
+ self.preserved_attributes_list(self.objLive1, objDeleted1)
+ self.preserved_attributes_list(self.objLive2, objDeleted2)
+ self.preserved_attributes_list(self.objLive3, objDeleted3)
+ self.preserved_attributes_list(self.objLive4, objDeleted4)
+ self.preserved_attributes_list(self.objLive5, objDeleted5)
+ self.preserved_attributes_list(self.objLive6, objDeleted6)
+ self.preserved_attributes_list(self.objLive7, objDeleted7)
+
+ self.check_rdn(self.objLive1, objDeleted1, "cn")
+ self.check_rdn(self.objLive2, objDeleted2, "cn")
+ self.check_rdn(self.objLive3, objDeleted3, "cn")
+ self.check_rdn(self.objLive4, objDeleted4, "cn")
+ self.check_rdn(self.objLive5, objDeleted5, "cn")
+ self.check_rdn(self.objLive6, objDeleted6, "cn")
+ self.check_rdn(self.objLive7, objDeleted7, "cn")
+
+ self.delete_deleted(self.ldb, self.usr1)
+ self.delete_deleted(self.ldb, self.usr2)
+ self.delete_deleted(self.ldb, self.grp1)
+ self.delete_deleted(self.ldb, self.sit1)
+ self.delete_deleted(self.ldb, self.ss1)
+ self.delete_deleted(self.ldb, self.srv1)
+ self.delete_deleted(self.ldb, self.srv2)
+
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted1.dn))
+ self.assertEqual(objDeleted1.dn.parent(),
+ self.deleted_objects_domain_dn)
+ self.assertEqual(objDeleted1["parentGUID"][0],
+ self.deleted_objects_domain_guid)
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted2.dn))
+ self.assertEqual(objDeleted2.dn.parent(),
+ self.deleted_objects_domain_dn)
+ self.assertEqual(objDeleted2["parentGUID"][0],
+ self.deleted_objects_domain_guid)
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted3.dn))
+ self.assertEqual(objDeleted3.dn.parent(),
+ self.deleted_objects_domain_dn)
+ self.assertEqual(objDeleted3["parentGUID"][0],
+ self.deleted_objects_domain_guid)
+ self.assertFalse("CN=Deleted Objects" in str(objDeleted4.dn))
+ self.assertTrue("CN=Deleted Objects" in str(objDeleted5.dn))
+ self.assertEqual(objDeleted5.dn.parent(),
+ self.deleted_objects_config_dn)
+ self.assertEqual(objDeleted5["parentGUID"][0],
+ self.deleted_objects_config_guid)
+ self.assertFalse("CN=Deleted Objects" in str(objDeleted6.dn))
+ self.assertFalse("CN=Deleted Objects" in str(objDeleted7.dn))
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/dirsync.py b/source4/dsdb/tests/python/dirsync.py
new file mode 100755
index 0000000..db39692
--- /dev/null
+++ b/source4/dsdb/tests/python/dirsync.py
@@ -0,0 +1,1107 @@
+#!/usr/bin/env python3
+#
+# Unit tests for dirsync control
+# Copyright (C) Matthieu Patou <mat@matws.net> 2011
+# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2014
+# Copyright (C) Catalyst.Net Ltd
+#
+# 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/>.
+
+
+import optparse
+import sys
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+
+import samba.getopt as options
+import base64
+
+import ldb
+from ldb import LdbError, SCOPE_BASE
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_ADD, FLAG_MOD_DELETE, FLAG_MOD_REPLACE
+from samba.dsdb import SEARCH_FLAG_CONFIDENTIAL, SEARCH_FLAG_RODC_ATTRIBUTE
+from samba.dcerpc import security, misc, drsblobs
+from samba.ndr import ndr_unpack, ndr_pack
+
+from samba.auth import system_session
+from samba import gensec, sd_utils
+from samba.samdb import SamDB
+from samba.credentials import Credentials, DONT_USE_KERBEROS
+import samba.tests
+from samba.tests import delete_force
+
+parser = optparse.OptionParser("dirsync.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args.pop()
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+ start = host.rindex("://")
+ host = host.lstrip(start + 3)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+#
+# Tests start here
+#
+
+
+class DirsyncBaseTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super().setUp()
+ self.ldb_admin = SamDB(ldaphost, credentials=creds, session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb_admin.domain_dn()
+ self.domain_sid = security.dom_sid(self.ldb_admin.get_domain_sid())
+ self.user_pass = samba.generate_random_password(12, 16)
+ self.configuration_dn = self.ldb_admin.get_config_basedn().get_linearized()
+ self.sd_utils = sd_utils.SDUtils(self.ldb_admin)
+ # used for anonymous login
+ print("baseDN: %s" % self.base_dn)
+
+ userou = "OU=dirsync-test"
+ self.ou = f"{userou},{self.base_dn}"
+ samba.tests.delete_force(self.ldb_admin, self.ou, controls=['tree_delete:1'])
+ self.ldb_admin.create_ou(self.ou)
+ self.addCleanup(samba.tests.delete_force, self.ldb_admin, self.ou, controls=['tree_delete:1'])
+
+ # Regular user
+ self.dirsync_user = "test_dirsync_user"
+ self.simple_user = "test_simple_user"
+ self.admin_user = "test_admin_user"
+ self.dirsync_pass = self.user_pass
+ self.simple_pass = self.user_pass
+ self.admin_pass = self.user_pass
+
+ self.ldb_admin.newuser(self.dirsync_user, self.dirsync_pass, userou=userou)
+ self.ldb_admin.newuser(self.simple_user, self.simple_pass, userou=userou)
+ self.ldb_admin.newuser(self.admin_user, self.admin_pass, userou=userou)
+ self.desc_sddl = self.sd_utils.get_sd_as_sddl(self.base_dn)
+
+ user_sid = self.sd_utils.get_object_sid(self.get_user_dn(self.dirsync_user))
+ mod = "(OA;;CR;%s;;%s)" % (security.GUID_DRS_GET_CHANGES,
+ str(user_sid))
+ self.sd_utils.dacl_add_ace(self.base_dn, mod)
+ self.addCleanup(self.sd_utils.dacl_delete_aces, self.base_dn, mod)
+
+ # add admins to the Domain Admins group
+ self.ldb_admin.add_remove_group_members("Domain Admins", [self.admin_user],
+ add_members_operation=True)
+
+ def get_user_dn(self, name):
+ return ldb.Dn(self.ldb_admin, "CN={0},{1}".format(name, self.ou))
+
+ def get_ldb_connection(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop
+ ldb_target = SamDB(url=ldaphost, credentials=creds_tmp, lp=lp)
+ return ldb_target
+
+# tests on ldap add operations
+class SimpleDirsyncTests(DirsyncBaseTests):
+
+ # def test_dirsync_errors(self):
+
+ def test_dirsync_supported(self):
+ """Test the basic of the dirsync is supported"""
+ self.ldb_dirsync = self.get_ldb_connection(self.dirsync_user, self.user_pass)
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = self.ldb_admin.search(self.base_dn, expression="samaccountname=*", controls=["dirsync:1:0:1"])
+ res = self.ldb_dirsync.search(self.base_dn, expression="samaccountname=*", controls=["dirsync:1:0:1"])
+ try:
+ self.ldb_simple.search(self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+ except LdbError as l:
+ self.assertTrue(str(l).find("LDAP_INSUFFICIENT_ACCESS_RIGHTS") != -1)
+
+ def test_parentGUID_referrals(self):
+ res2 = self.ldb_admin.search(self.base_dn, scope=SCOPE_BASE, attrs=["objectGUID"])
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="name=Configuration",
+ controls=["dirsync:1:0:1"])
+ self.assertEqual(res2[0].get("objectGUID"), res[0].get("parentGUID"))
+
+ def test_ok_not_rootdc(self):
+ """Test if it's ok to do dirsync on another NC that is not the root DC"""
+ self.ldb_admin.search(self.ldb_admin.get_config_basedn(),
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+
+ def test_dirsync_errors(self):
+ """Test if dirsync returns the correct LDAP errors in case of pb"""
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self.ldb_dirsync = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ try:
+ self.ldb_simple.search(self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+ except LdbError as l:
+ print(l)
+ self.assertTrue(str(l).find("LDAP_INSUFFICIENT_ACCESS_RIGHTS") != -1)
+
+ try:
+ self.ldb_simple.search("CN=Users,%s" % self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+ except LdbError as l:
+ print(l)
+ self.assertTrue(str(l).find("LDAP_INSUFFICIENT_ACCESS_RIGHTS") != -1)
+
+ try:
+ self.ldb_simple.search("CN=Users,%s" % self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:1:1"])
+ except LdbError as l:
+ print(l)
+ self.assertTrue(str(l).find("LDAP_UNWILLING_TO_PERFORM") != -1)
+
+ try:
+ self.ldb_dirsync.search("CN=Users,%s" % self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+ except LdbError as l:
+ print(l)
+ self.assertTrue(str(l).find("LDAP_INSUFFICIENT_ACCESS_RIGHTS") != -1)
+
+ try:
+ self.ldb_admin.search("CN=Users,%s" % self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+ except LdbError as l:
+ print(l)
+ self.assertTrue(str(l).find("LDAP_INSUFFICIENT_ACCESS_RIGHTS") != -1)
+
+ try:
+ self.ldb_admin.search("CN=Users,%s" % self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:1:1"])
+ except LdbError as l:
+ print(l)
+ self.assertTrue(str(l).find("LDAP_UNWILLING_TO_PERFORM") != -1)
+
+ def test_dirsync_attributes(self):
+ """Check behavior with some attributes """
+ res = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=*",
+ controls=["dirsync:1:0:1"])
+ # Check that nTSecurityDescriptor is returned as it's the case when doing dirsync
+ self.assertTrue(res.msgs[0].get("ntsecuritydescriptor") is not None)
+ # Check that non replicated attributes are not returned
+ self.assertTrue(res.msgs[0].get("badPwdCount") is None)
+ # Check that non forward link are not returned
+ self.assertTrue(res.msgs[0].get("memberof") is None)
+
+ # Asking for instanceType will return also objectGUID
+ res = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=Administrator",
+ attrs=["instanceType"],
+ controls=["dirsync:1:0:1"])
+ self.assertTrue(res.msgs[0].get("objectGUID") is not None)
+ self.assertTrue(res.msgs[0].get("instanceType") is not None)
+
+ # We don't return an entry if asked for objectGUID
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % str(self.base_dn),
+ attrs=["objectGUID"],
+ controls=["dirsync:1:0:1"])
+ self.assertEqual(len(res.msgs), 0)
+
+ # a request on the root of a NC didn't return parentGUID
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % str(self.base_dn),
+ attrs=["name"],
+ controls=["dirsync:1:0:1"])
+ self.assertTrue(res.msgs[0].get("objectGUID") is not None)
+ self.assertTrue(res.msgs[0].get("name") is not None)
+ self.assertTrue(res.msgs[0].get("parentGUID") is None)
+ self.assertTrue(res.msgs[0].get("instanceType") is not None)
+
+ # Asking for name will return also objectGUID and parentGUID
+ # and instanceType and of course name
+ res = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=Administrator",
+ attrs=["name"],
+ controls=["dirsync:1:0:1"])
+ self.assertTrue(res.msgs[0].get("objectGUID") is not None)
+ self.assertTrue(res.msgs[0].get("name") is not None)
+ self.assertTrue(res.msgs[0].get("parentGUID") is not None)
+ self.assertTrue(res.msgs[0].get("instanceType") is not None)
+
+ # Asking for dn will not return not only DN but more like if attrs=*
+ # parentGUID should be returned
+ res = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=Administrator",
+ attrs=["dn"],
+ controls=["dirsync:1:0:1"])
+ count = len(res.msgs[0])
+ res2 = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=Administrator",
+ controls=["dirsync:1:0:1"])
+ count2 = len(res2.msgs[0])
+ self.assertEqual(count, count2)
+
+ # Asking for cn will return nothing on objects that have CN as RDN
+ res = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=Administrator",
+ attrs=["cn"],
+ controls=["dirsync:1:0:1"])
+ self.assertEqual(len(res.msgs), 0)
+ # Asking for parentGUID will return nothing too
+ res = self.ldb_admin.search(self.base_dn,
+ expression="samaccountname=Administrator",
+ attrs=["parentGUID"],
+ controls=["dirsync:1:0:1"])
+ self.assertEqual(len(res.msgs), 0)
+ ouname = "OU=testou,%s" % self.ou
+ self.ouname = ouname
+ self.ldb_admin.create_ou(ouname)
+ delta = Message()
+ delta.dn = Dn(self.ldb_admin, ouname)
+ delta["cn"] = MessageElement("test ou",
+ FLAG_MOD_ADD,
+ "cn")
+ self.ldb_admin.modify(delta)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="name=testou",
+ attrs=["cn"],
+ controls=["dirsync:1:0:1"])
+
+ self.assertEqual(len(res.msgs), 1)
+ self.assertEqual(len(res.msgs[0]), 3)
+ delete_force(self.ldb_admin, ouname)
+
+ def test_dirsync_with_controls(self):
+ """Check that dirsync return correct information when dealing with the NC"""
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % str(self.base_dn),
+ attrs=["name"],
+ controls=["dirsync:1:0:10000", "extended_dn:1", "show_deleted:1"])
+
+ def test_dirsync_basenc(self):
+ """Check that dirsync return correct information when dealing with the NC"""
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % str(self.base_dn),
+ attrs=["name"],
+ controls=["dirsync:1:0:10000"])
+ self.assertEqual(len(res.msgs), 1)
+ self.assertEqual(len(res.msgs[0]), 3)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(distinguishedName=%s)" % str(self.base_dn),
+ attrs=["ntSecurityDescriptor"],
+ controls=["dirsync:1:0:10000"])
+ self.assertEqual(len(res.msgs), 1)
+ self.assertEqual(len(res.msgs[0]), 3)
+
+ def test_dirsync_othernc(self):
+ """Check that dirsync return information for entries that are normally referrals (ie. other NCs)"""
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(objectclass=configuration)",
+ attrs=["name"],
+ controls=["dirsync:1:0:10000"])
+ self.assertEqual(len(res.msgs), 1)
+ self.assertEqual(len(res.msgs[0]), 4)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(objectclass=configuration)",
+ attrs=["ntSecurityDescriptor"],
+ controls=["dirsync:1:0:10000"])
+ self.assertEqual(len(res.msgs), 1)
+ self.assertEqual(len(res.msgs[0]), 3)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(objectclass=domaindns)",
+ attrs=["ntSecurityDescriptor"],
+ controls=["dirsync:1:0:10000"])
+ nb = len(res.msgs)
+
+ # only sub nc returns a result when asked for objectGUID
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(objectclass=domaindns)",
+ attrs=["objectGUID"],
+ controls=["dirsync:1:0:0"])
+ self.assertEqual(len(res.msgs), nb - 1)
+ if nb > 1:
+ self.assertTrue(res.msgs[0].get("objectGUID") is not None)
+ else:
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(objectclass=configuration)",
+ attrs=["objectGUID"],
+ controls=["dirsync:1:0:0"])
+
+ def test_dirsync_send_delta(self):
+ """Check that dirsync return correct delta when sending the last cookie"""
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(samaccountname=test*)(!(isDeleted=*)))",
+ controls=["dirsync:1:0:10000"])
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "0"
+ ctl[3] = "10000"
+ control = str(":".join(ctl))
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(samaccountname=test*)(!(isDeleted=*)))",
+ controls=[control])
+ self.assertEqual(len(res), 0)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=["dirsync:1:0:100000"])
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "0"
+ ctl[3] = "10000"
+ control2 = str(":".join(ctl))
+
+ # Let's create an OU
+ ouname = "OU=testou2,%s" % self.base_dn
+ self.ouname = ouname
+ self.ldb_admin.create_ou(ouname)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=[control2])
+ self.assertEqual(len(res), 1)
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "0"
+ ctl[3] = "10000"
+ control3 = str(":".join(ctl))
+
+ delta = Message()
+ delta.dn = Dn(self.ldb_admin, str(ouname))
+
+ delta["cn"] = MessageElement("test ou",
+ FLAG_MOD_ADD,
+ "cn")
+ self.ldb_admin.modify(delta)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=[control3])
+
+ self.assertEqual(len(res.msgs), 1)
+ # 3 attributes: instanceType, cn and objectGUID
+ self.assertEqual(len(res.msgs[0]), 3)
+
+ delta = Message()
+ delta.dn = Dn(self.ldb_admin, str(ouname))
+ delta["cn"] = MessageElement([],
+ FLAG_MOD_DELETE,
+ "cn")
+ self.ldb_admin.modify(delta)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=[control3])
+
+ self.assertEqual(len(res.msgs), 1)
+ # So we won't have much attribute returned but instanceType and GUID
+ # are.
+ # 3 attributes: instanceType and objectGUID and cn but empty
+ self.assertEqual(len(res.msgs[0]), 3)
+ ouname = "OU=newouname,%s" % self.base_dn
+ self.ldb_admin.rename(str(res[0].dn), str(Dn(self.ldb_admin, ouname)))
+ self.ouname = ouname
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "0"
+ ctl[3] = "10000"
+ control4 = str(":".join(ctl))
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=[control4])
+
+ self.assertTrue(res[0].get("parentGUID") is not None)
+ self.assertTrue(res[0].get("name") is not None)
+ delete_force(self.ldb_admin, ouname)
+
+ def test_dirsync_linkedattributes_OBJECT_SECURITY(self):
+ """Check that dirsync returned deleted objects too"""
+ # Let's search for members
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = self.ldb_simple.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=["dirsync:1:1:1"])
+
+ self.assertTrue(len(res[0].get("member")) > 0)
+ size = len(res[0].get("member"))
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "1"
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+ self.ldb_admin.add_remove_group_members("Administrators", [self.simple_user],
+ add_members_operation=True)
+
+ res = self.ldb_simple.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=[control1])
+
+ self.assertEqual(len(res[0].get("member")), size + 1)
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "1"
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+
+ # remove the user from the group
+ self.ldb_admin.add_remove_group_members("Administrators", [self.simple_user],
+ add_members_operation=False)
+
+ res = self.ldb_simple.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=[control1])
+
+ self.assertEqual(len(res[0].get("member")), size)
+
+ self.ldb_admin.newgroup("testgroup")
+ self.addCleanup(self.ldb_admin.deletegroup, "testgroup")
+ self.ldb_admin.add_remove_group_members("testgroup", [self.simple_user],
+ add_members_operation=True)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=testgroup)",
+ controls=["dirsync:1:0:1"])
+
+ self.assertEqual(len(res[0].get("member")), 1)
+ self.assertTrue(res[0].get("member") != "")
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "0"
+ ctl[3] = "1"
+ control1 = str(":".join(ctl))
+
+ # Check that reasking the same question but with an updated cookie
+ # didn't return any results.
+ print(control1)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=testgroup)",
+ controls=[control1])
+ self.assertEqual(len(res), 0)
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "1"
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+
+ self.ldb_admin.add_remove_group_members("testgroup", [self.simple_user],
+ add_members_operation=False)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=testgroup)",
+ attrs=["member"],
+ controls=[control1])
+
+ self.assertEqual(len(res[0].get("member")), 0)
+
+ def test_dirsync_deleted_items(self):
+ """Check that dirsync returned deleted objects too"""
+ # Let's create an OU
+ ouname = "OU=testou3,%s" % self.base_dn
+ self.ouname = ouname
+ self.ldb_admin.create_ou(ouname)
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=["dirsync:1:0:1"])
+ guid = None
+ for e in res:
+ if str(e["name"]) == "testou3":
+ guid = str(ndr_unpack(misc.GUID, e.get("objectGUID")[0]))
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "0"
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+
+ # So now delete the object and check that
+ # we can see the object but deleted when admin
+ delete_force(self.ldb_admin, ouname)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(objectClass=organizationalUnit)",
+ controls=[control1])
+ self.assertEqual(len(res), 1)
+ guid2 = str(ndr_unpack(misc.GUID, res[0].get("objectGUID")[0]))
+ self.assertEqual(guid2, guid)
+ self.assertTrue(res[0].get("isDeleted"))
+ self.assertTrue(res[0].get("name") is not None)
+
+ def test_cookie_from_others(self):
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=["dirsync:1:0:1"])
+ ctl = str(res.controls[0]).split(":")
+ cookie = ndr_unpack(drsblobs.ldapControlDirSyncCookie, base64.b64decode(str(ctl[4])))
+ cookie.blob.guid1 = misc.GUID("128a99bf-abcd-1234-abcd-1fb625e530db")
+ controls = ["dirsync:1:0:0:%s" % base64.b64encode(ndr_pack(cookie)).decode('utf8')]
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=controls)
+
+ def test_dirsync_linkedattributes_range(self):
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = self.ldb_admin.search(self.base_dn,
+ attrs=["member;range=1-1"],
+ expression="(name=Administrators)",
+ controls=["dirsync:1:0:0"])
+
+ self.assertTrue(len(res) > 0)
+ self.assertTrue(res[0].get("member;range=1-1") is None)
+ self.assertTrue(res[0].get("member") is not None)
+ self.assertTrue(len(res[0].get("member")) > 0)
+
+ def test_dirsync_linkedattributes_range_user(self):
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ try:
+ res = self.ldb_simple.search(self.base_dn,
+ attrs=["member;range=1-1"],
+ expression="(name=Administrators)",
+ controls=["dirsync:1:0:0"])
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail()
+
+ def test_dirsync_linkedattributes(self):
+ flag_incr_linked = 2147483648
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = self.ldb_admin.search(self.base_dn,
+ attrs=["member"],
+ expression="(name=Administrators)",
+ controls=["dirsync:1:%d:1" % flag_incr_linked])
+
+ self.assertTrue(res[0].get("member;range=1-1") is not None)
+ self.assertTrue(len(res[0].get("member;range=1-1")) > 0)
+ size = len(res[0].get("member;range=1-1"))
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "%d" % flag_incr_linked
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+ self.ldb_admin.add_remove_group_members("Administrators", [self.simple_user],
+ add_members_operation=True)
+ self.ldb_admin.add_remove_group_members("Administrators", [self.dirsync_user],
+ add_members_operation=True)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=[control1])
+
+ self.assertEqual(len(res[0].get("member;range=1-1")), 2)
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "%d" % flag_incr_linked
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+
+ # remove the user from the group
+ self.ldb_admin.add_remove_group_members("Administrators", [self.simple_user],
+ add_members_operation=False)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=[control1])
+
+ self.assertEqual(res[0].get("member;range=1-1"), None)
+ self.assertEqual(len(res[0].get("member;range=0-0")), 1)
+
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "%d" % flag_incr_linked
+ ctl[3] = "10000"
+ control2 = str(":".join(ctl))
+
+ self.ldb_admin.add_remove_group_members("Administrators", [self.dirsync_user],
+ add_members_operation=False)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=[control2])
+
+ self.assertEqual(res[0].get("member;range=1-1"), None)
+ self.assertEqual(len(res[0].get("member;range=0-0")), 1)
+
+ res = self.ldb_admin.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=[control1])
+
+ self.assertEqual(res[0].get("member;range=1-1"), None)
+ self.assertEqual(len(res[0].get("member;range=0-0")), 2)
+
+ def test_dirsync_extended_dn(self):
+ """Check that dirsync works together with the extended_dn control"""
+ # Let's search for members
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = self.ldb_simple.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=["dirsync:1:1:1"])
+
+ self.assertTrue(len(res[0].get("member")) > 0)
+ size = len(res[0].get("member"))
+
+ resEX1 = self.ldb_simple.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=["dirsync:1:1:1","extended_dn:1:1"])
+ self.assertTrue(len(resEX1[0].get("member")) > 0)
+ sizeEX1 = len(resEX1[0].get("member"))
+ self.assertEqual(sizeEX1, size)
+ self.assertIn(res[0]["member"][0], resEX1[0]["member"][0])
+ self.assertIn(b"<GUID=", resEX1[0]["member"][0])
+ self.assertIn(b">;<SID=S-1-5-21-", resEX1[0]["member"][0])
+
+ resEX0 = self.ldb_simple.search(self.base_dn,
+ expression="(name=Administrators)",
+ controls=["dirsync:1:1:1","extended_dn:1:0"])
+ self.assertTrue(len(resEX0[0].get("member")) > 0)
+ sizeEX0 = len(resEX0[0].get("member"))
+ self.assertEqual(sizeEX0, size)
+ self.assertIn(res[0]["member"][0], resEX0[0]["member"][0])
+ self.assertIn(b"<GUID=", resEX0[0]["member"][0])
+ self.assertIn(b">;<SID=010500000000000515", resEX0[0]["member"][0])
+
+ def test_dirsync_deleted_items_OBJECT_SECURITY(self):
+ """Check that dirsync returned deleted objects too"""
+ # Let's create an OU
+ self.ldb_simple = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ ouname = "OU=testou3,%s" % self.base_dn
+ self.ouname = ouname
+ self.ldb_admin.create_ou(ouname)
+
+ # Specify LDAP_DIRSYNC_OBJECT_SECURITY
+ res = self.ldb_simple.search(self.base_dn,
+ expression="(&(objectClass=organizationalUnit)(!(isDeleted=*)))",
+ controls=["dirsync:1:1:1"])
+
+ guid = None
+ for e in res:
+ if str(e["name"]) == "testou3":
+ guid = str(ndr_unpack(misc.GUID, e.get("objectGUID")[0]))
+
+ self.assertTrue(guid is not None)
+ ctl = str(res.controls[0]).split(":")
+ ctl[1] = "1"
+ ctl[2] = "1"
+ ctl[3] = "10000"
+ control1 = str(":".join(ctl))
+
+ # So now delete the object and check that
+ # we can see the object but deleted when admin
+ # we just see the objectGUID when simple user
+ delete_force(self.ldb_admin, ouname)
+
+ res = self.ldb_simple.search(self.base_dn,
+ expression="(objectClass=organizationalUnit)",
+ controls=[control1])
+ self.assertEqual(len(res), 1)
+ guid2 = str(ndr_unpack(misc.GUID, res[0].get("objectGUID")[0]))
+ self.assertEqual(guid2, guid)
+ self.assertEqual(str(res[0].dn), "")
+
+class SpecialDirsyncTests(DirsyncBaseTests):
+
+ def setUp(self):
+ super().setUp()
+
+ self.schema_dn = self.ldb_admin.get_schema_basedn()
+
+ # the tests work by setting the 'Confidential' or 'RODC Filtered' bit in the searchFlags
+ # for an existing schema attribute. This only works against Windows if
+ # the systemFlags does not have FLAG_SCHEMA_BASE_OBJECT set for the
+ # schema attribute being modified. There are only a few attributes that
+ # meet this criteria (most of which only apply to 'user' objects)
+ self.conf_attr = "homePostalAddress"
+ attr_cn = "CN=Address-Home"
+ # schemaIdGuid for homePostalAddress (used for ACE tests)
+ self.attr_dn = f"{attr_cn},{self.schema_dn}"
+
+ userou = "OU=conf-attr-test"
+ self.ou = "{0},{1}".format(userou, self.base_dn)
+ samba.tests.delete_force(self.ldb_admin, self.ou, controls=['tree_delete:1'])
+ self.ldb_admin.create_ou(self.ou)
+ self.addCleanup(samba.tests.delete_force, self.ldb_admin, self.ou, controls=['tree_delete:1'])
+
+ # add a test object with this attribute set
+ self.conf_value = "abcdef"
+ self.conf_user = "conf-user"
+ self.ldb_admin.newuser(self.conf_user, self.user_pass, userou=userou)
+ self.conf_dn = self.get_user_dn(self.conf_user)
+ self.add_attr(self.conf_dn, self.conf_attr, self.conf_value)
+
+ # sanity-check the flag is not already set (this'll cause problems if
+ # previous test run didn't clean up properly)
+
+ search_flags = int(self.get_attr_search_flags(self.attr_dn))
+ if search_flags & SEARCH_FLAG_CONFIDENTIAL|SEARCH_FLAG_RODC_ATTRIBUTE:
+ self.set_attr_search_flags(self.attr_dn, str(search_flags &~ (SEARCH_FLAG_CONFIDENTIAL|SEARCH_FLAG_RODC_ATTRIBUTE)))
+ search_flags = int(self.get_attr_search_flags(self.attr_dn))
+ self.assertEqual(0, search_flags & (SEARCH_FLAG_CONFIDENTIAL|SEARCH_FLAG_RODC_ATTRIBUTE),
+ f"{self.conf_attr} searchFlags did not reset to omit SEARCH_FLAG_CONFIDENTIAL and SEARCH_FLAG_RODC_ATTRIBUTE ({search_flags})")
+
+ # work out the original 'searchFlags' value before we overwrite it
+ old_value = self.get_attr_search_flags(self.attr_dn)
+
+ self.set_attr_search_flags(self.attr_dn, str(self.flag_under_test))
+
+ # reset the value after the test completes
+ self.addCleanup(self.set_attr_search_flags, self.attr_dn, old_value)
+
+ def add_attr(self, dn, attr, value):
+ m = Message()
+ m.dn = dn
+ m[attr] = MessageElement(value, FLAG_MOD_ADD, attr)
+ self.ldb_admin.modify(m)
+
+ def set_attr_search_flags(self, attr_dn, flags):
+ """Modifies the searchFlags for an object in the schema"""
+ m = Message()
+ m.dn = Dn(self.ldb_admin, attr_dn)
+ m['searchFlags'] = MessageElement(flags, FLAG_MOD_REPLACE,
+ 'searchFlags')
+ self.ldb_admin.modify(m)
+
+ # note we have to update the schema for this change to take effect (on
+ # Windows, at least)
+ self.ldb_admin.set_schema_update_now()
+
+ def get_attr_search_flags(self, attr_dn):
+ res = self.ldb_admin.search(attr_dn, scope=SCOPE_BASE,
+ attrs=['searchFlags'])
+ return res[0]['searchFlags'][0]
+
+ def find_under_current_ou(self, res):
+ for msg in res:
+ if msg.dn == self.conf_dn:
+ return msg
+ self.fail(f"Failed to find object {self.conf_dn} in {len(res)} results")
+
+
+class ConfidentialDirsyncTests(SpecialDirsyncTests):
+
+ def setUp(self):
+ self.flag_under_test = SEARCH_FLAG_CONFIDENTIAL
+ super().setUp()
+
+ def test_unicodePwd_normal(self):
+ res = self.ldb_admin.search(self.base_dn,
+ attrs=["unicodePwd", "supplementalCredentials", "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})")
+
+ msg = res[0]
+
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ self.assertTrue(msg.get("unicodePwd") is None)
+ self.assertTrue(msg.get("supplementalCredentials") is None)
+
+ def _test_dirsync_unicodePwd(self, ldb_conn, control=None, insist_on_empty_element=False):
+ res = ldb_conn.search(self.base_dn,
+ attrs=["unicodePwd", "supplementalCredentials", "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=[control])
+
+ msg = self.find_under_current_ou(res)
+
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ if insist_on_empty_element:
+ self.assertTrue(msg.get("unicodePwd") is not None)
+ self.assertEqual(len(msg.get("unicodePwd")), 0)
+ self.assertTrue(msg.get("supplementalCredentials") is not None)
+ self.assertEqual(len(msg.get("supplementalCredentials")), 0)
+ else:
+ self.assertTrue(msg.get("unicodePwd") is None
+ or len(msg.get("unicodePwd")) == 0)
+ self.assertTrue(msg.get("supplementalCredentials") is None
+ or len(msg.get("supplementalCredentials")) == 0)
+
+ def test_dirsync_unicodePwd_OBJ_SEC(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_unicodePwd(ldb_conn, control="dirsync:1:1:0")
+
+ def test_dirsync_unicodePwd_OBJ_SEC_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_unicodePwd(ldb_conn, control="dirsync:1:1:0", insist_on_empty_element=True)
+
+ def test_dirsync_unicodePwd_with_GET_CHANGES_OBJ_SEC(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_unicodePwd(ldb_conn, control="dirsync:1:1:0")
+
+ def test_dirsync_unicodePwd_with_GET_CHANGES_OBJ_SEC_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_unicodePwd(ldb_conn, control="dirsync:1:1:0", insist_on_empty_element=True)
+
+ def test_dirsync_unicodePwd_with_GET_CHANGES(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_unicodePwd(ldb_conn, control="dirsync:1:0:0")
+
+ def test_dirsync_unicodePwd_with_GET_CHANGES_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_unicodePwd(ldb_conn, control="dirsync:1:0:0", insist_on_empty_element=True)
+
+ def test_normal(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})")
+
+ msg = res[0]
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ self.assertTrue(msg.get(self.conf_attr) is None)
+
+ def _test_dirsync_OBJECT_SECURITY(self, ldb_conn, insist_on_empty_element=False):
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:1:0"])
+
+ msg = self.find_under_current_ou(res)
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ if insist_on_empty_element:
+ self.assertTrue(msg.get(self.conf_attr) is not None)
+ self.assertEqual(len(msg.get(self.conf_attr)), 0)
+ else:
+ self.assertTrue(msg.get(self.conf_attr) is None
+ or len(msg.get(self.conf_attr)) == 0)
+
+ def test_dirsync_OBJECT_SECURITY(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn)
+
+ def test_dirsync_OBJECT_SECURITY_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn, insist_on_empty_element=True)
+
+ def test_dirsync_with_GET_CHANGES(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:0:0"])
+
+ msg = self.find_under_current_ou(res)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ self.assertTrue(msg.get(self.conf_attr))
+ self.assertEqual(len(msg.get(self.conf_attr)), 1)
+
+ def test_dirsync_with_GET_CHANGES_OBJECT_SECURITY(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn)
+
+ def test_dirsync_with_GET_CHANGES_OBJECT_SECURITY_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn, insist_on_empty_element=True)
+
+class FilteredDirsyncTests(SpecialDirsyncTests):
+
+ def setUp(self):
+ self.flag_under_test = SEARCH_FLAG_RODC_ATTRIBUTE
+ super().setUp()
+
+ def test_attr(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})")
+
+ msg = res[0]
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ self.assertTrue(msg.get(self.conf_attr))
+ self.assertEqual(len(msg.get(self.conf_attr)), 1)
+
+ def _test_dirsync_OBJECT_SECURITY(self, ldb_conn):
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:1:0"])
+
+ msg = self.find_under_current_ou(res)
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ self.assertTrue(msg.get(self.conf_attr))
+ self.assertEqual(len(msg.get(self.conf_attr)), 1)
+
+ def test_dirsync_OBJECT_SECURITY(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn)
+
+ def test_dirsync_OBJECT_SECURITY_with_GET_CHANGES(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn)
+
+ def _test_dirsync_with_GET_CHANGES(self, insist_on_empty_element=False):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ res = ldb_conn.search(self.base_dn,
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:0:0"])
+
+ msg = self.find_under_current_ou(res)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ if insist_on_empty_element:
+ self.assertTrue(msg.get(self.conf_attr) is not None)
+ self.assertEqual(len(msg.get(self.conf_attr)), 0)
+ else:
+ self.assertTrue(msg.get(self.conf_attr) is None
+ or len(msg.get(self.conf_attr)) == 0)
+
+ def test_dirsync_with_GET_CHANGES(self):
+ self._test_dirsync_with_GET_CHANGES()
+
+ def test_dirsync_with_GET_CHANGES_insist_on_empty_element(self):
+ self._test_dirsync_with_GET_CHANGES(insist_on_empty_element=True)
+
+ def test_dirsync_with_GET_CHANGES_attr(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ try:
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:0:0"])
+ self.fail("ldb.search() should have failed with LDAP_INSUFFICIENT_ACCESS_RIGHTS")
+ except ldb.LdbError as e:
+ (errno, errstr) = e.args
+ self.assertEqual(errno, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+class ConfidentialFilteredDirsyncTests(SpecialDirsyncTests):
+
+ def setUp(self):
+ self.flag_under_test = SEARCH_FLAG_RODC_ATTRIBUTE|SEARCH_FLAG_CONFIDENTIAL
+ super().setUp()
+
+ def test_attr(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ res = ldb_conn.search(self.base_dn,
+ attrs=["unicodePwd", "supplementalCredentials", "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})")
+
+ msg = res[0]
+ self.assertTrue(msg.get("samAccountName"))
+ self.assertTrue(msg.get(self.conf_attr) is None)
+
+ def _test_dirsync_OBJECT_SECURITY(self, ldb_conn, insist_on_empty_element=False):
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:1:0"])
+
+ msg = self.find_under_current_ou(res)
+ self.assertTrue("samAccountName" in msg)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ if insist_on_empty_element:
+ self.assertTrue(msg.get(self.conf_attr) is not None)
+ self.assertEqual(len(msg.get(self.conf_attr)), 0)
+ else:
+ self.assertTrue(msg.get(self.conf_attr) is None
+ or len(msg.get(self.conf_attr)) == 0)
+
+ def test_dirsync_OBJECT_SECURITY(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn)
+
+ def test_dirsync_OBJECT_SECURITY_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.simple_user, self.simple_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn, insist_on_empty_element=True)
+
+ def test_dirsync_OBJECT_SECURITY_with_GET_CHANGES(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn)
+
+ def test_dirsync_OBJECT_SECURITY_with_GET_CHANGES_insist_on_empty_element(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ self._test_dirsync_OBJECT_SECURITY(ldb_conn, insist_on_empty_element=True)
+
+ def _test_dirsync_with_GET_CHANGES(self, insist_on_empty_element=False):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ res = ldb_conn.search(self.base_dn,
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:0:0"])
+
+ msg = self.find_under_current_ou(res)
+ # This form ensures this is a case insensitive comparison
+ self.assertTrue(msg.get("samAccountName"))
+ if insist_on_empty_element:
+ self.assertTrue(msg.get(self.conf_attr) is not None)
+ self.assertEqual(len(msg.get(self.conf_attr)), 0)
+ else:
+ self.assertTrue(msg.get(self.conf_attr) is None
+ or len(msg.get(self.conf_attr)) == 0)
+
+ def test_dirsync_with_GET_CHANGES(self):
+ self._test_dirsync_with_GET_CHANGES()
+
+ def test_dirsync_with_GET_CHANGES_insist_on_empty_element(self):
+ self._test_dirsync_with_GET_CHANGES(insist_on_empty_element=True)
+
+ def test_dirsync_with_GET_CHANGES_attr(self):
+ ldb_conn = self.get_ldb_connection(self.dirsync_user, self.dirsync_pass)
+ try:
+ res = ldb_conn.search(self.base_dn,
+ attrs=[self.conf_attr, "samAccountName"],
+ expression=f"(samAccountName={self.conf_user})",
+ controls=["dirsync:1:0:0"])
+ self.fail("ldb.search() should have failed with LDAP_INSUFFICIENT_ACCESS_RIGHTS")
+ except ldb.LdbError as e:
+ (errno, errstr) = e.args
+ self.assertEqual(errno, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+
+if not getattr(opts, "listtests", False):
+ lp = sambaopts.get_loadparm()
+ samba.tests.cmdline_credentials = credopts.get_credentials(lp)
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/dsdb_schema_info.py b/source4/dsdb/tests/python/dsdb_schema_info.py
new file mode 100644
index 0000000..e3178d1
--- /dev/null
+++ b/source4/dsdb/tests/python/dsdb_schema_info.py
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+#
+# Unix SMB/CIFS implementation.
+# Copyright (C) Kamen Mazdrashki <kamenim@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/>.
+#
+
+#
+# Usage:
+# export DC_SERVER=target_dc_or_local_samdb_url
+# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun
+# PYTHONPATH="$PYTHONPATH:$samba4srcdir/lib/ldb/tests/python" $SUBUNITRUN dsdb_schema_info -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD"
+#
+
+import sys
+import time
+import random
+
+sys.path.insert(0, "bin/python")
+import samba.tests
+
+from ldb import SCOPE_BASE, LdbError
+
+import samba.dcerpc.drsuapi
+from samba.dcerpc.drsblobs import schemaInfoBlob
+from samba.ndr import ndr_unpack
+from samba.dcerpc.misc import GUID
+
+
+class SchemaInfoTestCase(samba.tests.TestCase):
+
+ # static SamDB connection
+ sam_db = None
+
+ def setUp(self):
+ super(SchemaInfoTestCase, self).setUp()
+
+ # connect SamDB if we haven't yet
+ if self.sam_db is None:
+ ldb_url = "ldap://%s" % samba.tests.env_get_var_value("DC_SERVER")
+ SchemaInfoTestCase.sam_db = samba.tests.connect_samdb(ldb_url)
+
+ # fetch rootDSE
+ res = self.sam_db.search(base="", expression="", scope=SCOPE_BASE, attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.schema_dn = res[0]["schemaNamingContext"][0]
+ self.base_dn = res[0]["defaultNamingContext"][0]
+ self.forest_level = int(res[0]["forestFunctionality"][0])
+
+ # get DC invocation_id
+ self.invocation_id = GUID(self.sam_db.get_invocation_id())
+
+ def tearDown(self):
+ super(SchemaInfoTestCase, self).tearDown()
+
+ def _getSchemaInfo(self):
+ try:
+ schema_info_data = self.sam_db.searchone(attribute="schemaInfo",
+ basedn=self.schema_dn,
+ expression="(objectClass=*)",
+ scope=SCOPE_BASE)
+ self.assertEqual(len(schema_info_data), 21)
+ schema_info = ndr_unpack(schemaInfoBlob, schema_info_data)
+ self.assertEqual(schema_info.marker, 0xFF)
+ except KeyError:
+ # create default schemaInfo if
+ # attribute value is not created yet
+ schema_info = schemaInfoBlob()
+ schema_info.revision = 0
+ schema_info.invocation_id = self.invocation_id
+ return schema_info
+
+ def _checkSchemaInfo(self, schi_before, schi_after):
+ self.assertEqual(schi_before.revision + 1, schi_after.revision)
+ self.assertEqual(schi_before.invocation_id, schi_after.invocation_id)
+ self.assertEqual(schi_after.invocation_id, self.invocation_id)
+
+ def _ldap_schemaUpdateNow(self):
+ ldif = """
+dn:
+changetype: modify
+add: schemaUpdateNow
+schemaUpdateNow: 1
+"""
+ self.sam_db.modify_ldif(ldif)
+
+ def _make_obj_names(self, prefix):
+ obj_name = prefix + time.strftime("%s", time.gmtime())
+ obj_ldap_name = obj_name.replace("-", "")
+ obj_dn = "CN=%s,%s" % (obj_name, self.schema_dn)
+ return (obj_name, obj_ldap_name, obj_dn)
+
+ def _make_attr_ldif(self, attr_name, attr_dn, sub_oid):
+ ldif = """
+dn: """ + attr_dn + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: 1.3.6.1.4.1.7165.4.6.1.7.%d.""" % sub_oid + str(random.randint(1, 100000)) + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ return ldif
+
+ def test_AddModifyAttribute(self):
+ # get initial schemaInfo
+ schi_before = self._getSchemaInfo()
+
+ # create names for an attribute to add
+ (attr_name, attr_ldap_name, attr_dn) = self._make_obj_names("schemaInfo-Attr-")
+ ldif = self._make_attr_ldif(attr_name, attr_dn, 1)
+
+ # add the new attribute
+ self.sam_db.add_ldif(ldif)
+ self._ldap_schemaUpdateNow()
+ # compare resulting schemaInfo
+ schi_after = self._getSchemaInfo()
+ self._checkSchemaInfo(schi_before, schi_after)
+
+ # rename the Attribute
+ attr_dn_new = attr_dn.replace(attr_name, attr_name + "-NEW")
+ try:
+ self.sam_db.rename(attr_dn, attr_dn_new)
+ except LdbError as e:
+ (num, _) = e.args
+ self.fail("failed to change CN for %s: %s" % (attr_name, _))
+
+ # compare resulting schemaInfo
+ schi_after = self._getSchemaInfo()
+ self._checkSchemaInfo(schi_before, schi_after)
+ pass
+
+ def _make_class_ldif(self, class_name, class_dn, sub_oid):
+ ldif = """
+dn: """ + class_dn + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.7.%d.""" % sub_oid + str(random.randint(1, 100000)) + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ return ldif
+
+ def test_AddModifyClass(self, controls=None, class_pre="schemaInfo-Class-"):
+ if controls is None:
+ controls = []
+
+ # get initial schemaInfo
+ schi_before = self._getSchemaInfo()
+
+ # create names for a Class to add
+ (class_name, class_ldap_name, class_dn) =\
+ self._make_obj_names(class_pre)
+ ldif = self._make_class_ldif(class_name, class_dn, 1)
+
+ # add the new Class
+ self.sam_db.add_ldif(ldif, controls=controls)
+ self._ldap_schemaUpdateNow()
+ # compare resulting schemaInfo
+ schi_after = self._getSchemaInfo()
+ self._checkSchemaInfo(schi_before, schi_after)
+
+ # rename the Class
+ class_dn_new = class_dn.replace(class_name, class_name + "-NEW")
+ try:
+ self.sam_db.rename(class_dn, class_dn_new, controls=controls)
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.fail("failed to change CN for %s: %s" % (class_name, _))
+
+ # compare resulting schemaInfo
+ schi_after = self._getSchemaInfo()
+ self._checkSchemaInfo(schi_before, schi_after)
+
+ def test_AddModifyClassLocalRelaxed(self):
+ lp = self.get_loadparm()
+ self.sam_db = samba.tests.connect_samdb(lp.samdb_url())
+ self.test_AddModifyClass(controls=["relax:0"],
+ class_pre="schemaInfo-Relaxed-")
diff --git a/source4/dsdb/tests/python/large_ldap.py b/source4/dsdb/tests/python/large_ldap.py
new file mode 100644
index 0000000..ab4cebe
--- /dev/null
+++ b/source4/dsdb/tests/python/large_ldap.py
@@ -0,0 +1,338 @@
+#!/usr/bin/env python3
+#
+# Test large LDAP response behaviour in Samba
+# Copyright (C) Andrew Bartlett 2019
+#
+# Based on Unit tests for the notification control
+# Copyright (C) Stefan Metzmacher 2016
+#
+# 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/>.
+
+import optparse
+import sys
+import os
+import random
+import time
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba import ldb, sd_utils
+from samba.samdb import SamDB
+import samba.tests
+
+from ldb import LdbError
+
+parser = optparse.OptionParser("large_ldap.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+url = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+class ManyLDAPTest(samba.tests.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.ldb = SamDB(url, credentials=creds, session_info=system_session(lp), lp=lp)
+ cls.base_dn = cls.ldb.domain_dn()
+ cls.OU_NAME_MANY="many_ou" + format(random.randint(0, 99999), "05")
+ cls.ou_dn = ldb.Dn(cls.ldb, "ou=" + cls.OU_NAME_MANY + "," + str(cls.base_dn))
+
+ samba.tests.delete_force(cls.ldb, cls.ou_dn,
+ controls=['tree_delete:1'])
+
+ cls.ldb.add({
+ "dn": cls.ou_dn,
+ "objectclass": "organizationalUnit",
+ "ou": cls.OU_NAME_MANY})
+
+ for x in range(2000):
+ ou_name = cls.OU_NAME_MANY + str(x)
+ cls.ldb.add({
+ "dn": "ou=" + ou_name + "," + str(cls.ou_dn),
+ "objectclass": "organizationalUnit",
+ "ou": ou_name})
+
+ @classmethod
+ def tearDownClass(cls):
+ samba.tests.delete_force(cls.ldb, cls.ou_dn,
+ controls=['tree_delete:1'])
+
+ def test_unindexed_iterator_search(self):
+ """Testing a search for all the OUs.
+
+ Needed to test that more that IOV_MAX responses can be returned
+ """
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ count = 0
+ search1 = self.ldb.search_iterator(base=self.ou_dn,
+ expression="(ou=" + self.OU_NAME_MANY + "*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "samAccountName"])
+
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ count += 1
+ search1.result()
+
+ # Check we got everything
+ self.assertEqual(count, 2001)
+
+class LargeLDAPTest(samba.tests.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.ldb = SamDB(url, credentials=creds, session_info=system_session(lp), lp=lp)
+ cls.base_dn = cls.ldb.domain_dn()
+
+ cls.sd_utils = sd_utils.SDUtils(cls.ldb)
+ cls.USER_NAME = "large_user" + format(random.randint(0, 99999), "05") + "-"
+ cls.OU_NAME="large_user_ou" + format(random.randint(0, 99999), "05")
+ cls.ou_dn = ldb.Dn(cls.ldb, "ou=" + cls.OU_NAME + "," + str(cls.base_dn))
+
+ samba.tests.delete_force(cls.ldb, cls.ou_dn,
+ controls=['tree_delete:1'])
+
+ cls.ldb.add({
+ "dn": cls.ou_dn,
+ "objectclass": "organizationalUnit",
+ "ou": cls.OU_NAME})
+
+ for x in range(200):
+ user_name = cls.USER_NAME + format(x, "03")
+ cls.ldb.add({
+ "dn": "cn=" + user_name + "," + str(cls.ou_dn),
+ "objectclass": "user",
+ "sAMAccountName": user_name,
+ "jpegPhoto": b'a' * (2 * 1024 * 1024)})
+
+ ace = "(OD;;RP;6bc69afa-7bd9-4184-88f5-28762137eb6a;;S-1-%d)" % x
+ dn = ldb.Dn(cls.ldb, "cn=" + user_name + "," + str(cls.ou_dn))
+
+ # add an ACE that denies access to the above random attr
+ # for a not-existing user. This makes each SD distinct
+ # and so will slow SD parsing.
+ cls.sd_utils.dacl_add_ace(dn, ace)
+
+ @classmethod
+ def tearDownClass(cls):
+ # Remake the connection for tear-down (old Samba drops the socket)
+ cls.ldb = SamDB(url, credentials=creds, session_info=system_session(lp), lp=lp)
+ samba.tests.delete_force(cls.ldb, cls.ou_dn,
+ controls=['tree_delete:1'])
+
+ def test_unindexed_iterator_search(self):
+ """Testing an unindexed search that will break the result size limit"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ count = 0
+ search1 = self.ldb.search_iterator(base=self.ou_dn,
+ expression="(sAMAccountName=" + self.USER_NAME + "*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "samAccountName"])
+
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ count += 1
+
+ search1.result()
+
+ self.assertEqual(count, 200)
+
+ # Now try breaking the 256MB limit
+
+ count_jpeg = 0
+ search1 = self.ldb.search_iterator(base=self.ou_dn,
+ expression="(sAMAccountName=" + self.USER_NAME + "*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "samAccountName", "jpegPhoto"])
+ try:
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ count_jpeg += 1
+ except LdbError as err:
+ enum = err.args[0]
+ self.assertEqual(enum, ldb.ERR_SIZE_LIMIT_EXCEEDED)
+ else:
+ # FIXME: Due to a bug in the client, the second exception to
+ # transmit the iteration error isn't raised. We must still check
+ # that the number of results is fewer than the total count.
+
+ # self.fail('expected to fail with ERR_SIZE_LIMIT_EXCEEDED')
+
+ pass
+
+ # Assert we don't get all the entries but still the error
+ self.assertGreater(count, count_jpeg)
+
+ # Now try for just 100MB (server will do some chunking for this)
+
+ count_jpeg2 = 0
+ try:
+ search1 = self.ldb.search_iterator(base=self.ou_dn,
+ expression="(sAMAccountName=" + self.USER_NAME + "1*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "samAccountName", "jpegPhoto"])
+ except LdbError as e:
+ enum = e.args[0]
+ estr = e.args[1]
+ self.fail(estr)
+
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ count_jpeg2 += 1
+
+ # Assert we got some entries
+ self.assertEqual(count_jpeg2, 100)
+
+ def test_iterator_search(self):
+ """Testing an indexed search that will break the result size limit"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ count = 0
+ search1 = self.ldb.search_iterator(base=self.ou_dn,
+ expression="(&(objectClass=user)(sAMAccountName=" + self.USER_NAME + "*))",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "samAccountName"])
+
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ count += 1
+ search1.result()
+
+ self.assertEqual(count, 200)
+
+ # Now try breaking the 256MB limit
+
+ count_jpeg = 0
+ search1 = self.ldb.search_iterator(base=self.ou_dn,
+ expression="(&(objectClass=user)(sAMAccountName=" + self.USER_NAME + "*))",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "samAccountName", "jpegPhoto"])
+ try:
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ count_jpeg += 1
+ except LdbError as err:
+ enum = err.args[0]
+ self.assertEqual(enum, ldb.ERR_SIZE_LIMIT_EXCEEDED)
+ else:
+ # FIXME: Due to a bug in the client, the second exception to
+ # transmit the iteration error isn't raised. We must still check
+ # that the number of results is fewer than the total count.
+
+ # self.fail('expected to fail with ERR_SIZE_LIMIT_EXCEEDED')
+
+ pass
+
+ # Assert we don't get all the entries but still the error
+ self.assertGreater(count, count_jpeg)
+
+ def test_timeout(self):
+
+ policy_dn = ldb.Dn(self.ldb,
+ 'CN=Default Query Policy,CN=Query-Policies,'
+ 'CN=Directory Service,CN=Windows NT,CN=Services,'
+ f'{self.ldb.get_config_basedn().get_linearized()}')
+
+ # Get the current value of lDAPAdminLimits.
+ res = self.ldb.search(base=policy_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['lDAPAdminLimits'])
+ msg = res[0]
+ admin_limits = msg['lDAPAdminLimits']
+
+ # Ensure we restore the previous value of the attribute.
+ admin_limits.set_flags(ldb.FLAG_MOD_REPLACE)
+ self.addCleanup(self.ldb.modify, msg)
+
+ # Temporarily lower the value of MaxQueryDuration so we can test
+ # timeout behaviour.
+ timeout = 5
+ query_duration = f'MaxQueryDuration={timeout}'.encode()
+
+ admin_limits = [limit for limit in admin_limits
+ if not limit.lower().startswith(b'maxqueryduration=')]
+ admin_limits.append(query_duration)
+
+ # Set the new attribute value.
+ msg = ldb.Message(policy_dn)
+ msg['lDAPAdminLimits'] = ldb.MessageElement(admin_limits,
+ ldb.FLAG_MOD_REPLACE,
+ 'lDAPAdminLimits')
+ self.ldb.modify(msg)
+
+ # Use a new connection so that the limits are reloaded.
+ samdb = SamDB(url, credentials=creds,
+ session_info=system_session(lp),
+ lp=lp)
+
+ # Create a large search expression that will take a long time to
+ # evaluate.
+ expression = '(jpegPhoto=*X*)' * 2000
+ expression = f'(|{expression})'
+
+ # Perform the LDAP search.
+ prev = time.time()
+ with self.assertRaises(ldb.LdbError) as err:
+ samdb.search(base=self.ou_dn,
+ scope=ldb.SCOPE_SUBTREE,
+ expression=expression,
+ attrs=['objectGUID'])
+ now = time.time()
+ duration = now - prev
+
+ # Ensure that we timed out.
+ enum, _ = err.exception.args
+ self.assertEqual(ldb.ERR_TIME_LIMIT_EXCEEDED, enum)
+
+ # Ensure that the time spent searching is within the limit we
+ # set. We allow a marginal amount over as the Samba timeout
+ # handling is not very accurate (and does not need to be)
+ self.assertLess(timeout - 1, duration)
+ self.assertLess(duration, timeout * 4)
+
+
+if "://" not in url:
+ if os.path.isfile(url):
+ url = "tdb://%s" % url
+ else:
+ url = "ldap://%s" % url
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ldap.py b/source4/dsdb/tests/python/ldap.py
new file mode 100755
index 0000000..54219ee
--- /dev/null
+++ b/source4/dsdb/tests/python/ldap.py
@@ -0,0 +1,3332 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This is a port of the original in testprogs/ejs/ldap.js
+
+# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008-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/>.
+
+import optparse
+import sys
+import time
+import base64
+import os
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+import samba.getopt as options
+
+from samba.auth import system_session
+from ldb import SCOPE_SUBTREE, SCOPE_ONELEVEL, SCOPE_BASE, LdbError
+from ldb import ERR_NO_SUCH_OBJECT, ERR_ATTRIBUTE_OR_VALUE_EXISTS
+from ldb import ERR_ENTRY_ALREADY_EXISTS, ERR_UNWILLING_TO_PERFORM
+from ldb import ERR_NOT_ALLOWED_ON_NON_LEAF, ERR_OTHER, ERR_INVALID_DN_SYNTAX
+from ldb import ERR_NO_SUCH_ATTRIBUTE, ERR_INVALID_ATTRIBUTE_SYNTAX
+from ldb import ERR_OBJECT_CLASS_VIOLATION, ERR_NOT_ALLOWED_ON_RDN
+from ldb import ERR_NAMING_VIOLATION, ERR_CONSTRAINT_VIOLATION
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+from ldb import timestring
+from samba import Ldb
+from samba.samdb import SamDB
+from samba.dsdb import (UF_NORMAL_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT,
+ UF_PASSWD_NOTREQD, UF_ACCOUNTDISABLE, ATYPE_NORMAL_ACCOUNT,
+ ATYPE_WORKSTATION_TRUST, SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE,
+ SYSTEM_FLAG_CONFIG_ALLOW_RENAME, SYSTEM_FLAG_CONFIG_ALLOW_MOVE,
+ SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE)
+from samba.dcerpc.security import DOMAIN_RID_DOMAIN_MEMBERS
+
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.dcerpc import security, lsa
+from samba.tests import delete_force
+from samba.common import get_string
+
+parser = optparse.OptionParser("ldap.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+class BasicTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(BasicTests, self).setUp()
+ self.ldb = ldb
+ self.gc_ldb = gc_ldb
+ self.base_dn = ldb.domain_dn()
+ self.configuration_dn = ldb.get_config_basedn().get_linearized()
+ self.schema_dn = ldb.get_schema_basedn().get_linearized()
+ self.domain_sid = security.dom_sid(ldb.get_domain_sid())
+
+ delete_force(self.ldb, "cn=posixuser,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser3,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser4,cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser4,cn=ldaptestcontainer2," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser5,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptest2computer,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcomputer3,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestutf8user èùéìòà,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestutf8user2 èùéìòà,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcontainer2," + self.base_dn)
+ delete_force(self.ldb, "cn=parentguidtest,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=parentguidtest,cn=testotherusers," + self.base_dn)
+ delete_force(self.ldb, "cn=testotherusers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestobject," + self.base_dn)
+ delete_force(self.ldb, "description=xyz,cn=users," + self.base_dn)
+ delete_force(self.ldb, "ou=testou,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=Test Secret,cn=system," + self.base_dn)
+ delete_force(self.ldb, "cn=testtimevaluesuser1,cn=users," + self.base_dn)
+
+ def test_objectclasses(self):
+ """Test objectClass behaviour"""
+ # Invalid objectclass specified
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": []})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # Invalid objectclass specified
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "X"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ # Invalid objectCategory specified
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person",
+ "objectCategory": self.base_dn})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Multi-valued "systemFlags"
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person",
+ "systemFlags": ["0", str(SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE)]})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # We cannot instantiate from an abstract object class ("connectionPoint"
+ # or "leaf"). In the first case we use "connectionPoint" (subclass of
+ # "leaf") to prevent a naming violation - this returns us a
+ # "ERR_UNWILLING_TO_PERFORM" since it is not structural. In the second
+ # case however we get "ERR_OBJECT_CLASS_VIOLATION" since an abstract
+ # class is also not allowed to be auxiliary.
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "connectionPoint"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": ["person", "leaf"]})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Objects instantiated using "satisfied" abstract classes (concrete
+ # subclasses) are allowed
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": ["top", "leaf", "connectionPoint", "serviceConnectionPoint"]})
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ # Two disjoint top-most structural object classes aren't allowed
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": ["person", "container"]})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Test allowed system flags
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person",
+ "systemFlags": str(~(SYSTEM_FLAG_CONFIG_ALLOW_RENAME | SYSTEM_FLAG_CONFIG_ALLOW_MOVE | SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE))})
+
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["systemFlags"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["systemFlags"][0]), "0")
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person"})
+
+ # We can remove derivation classes of the structural objectclass
+ # but they're going to be re-added afterwards
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("top", FLAG_MOD_DELETE,
+ "objectClass")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["objectClass"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue(b"top" in res[0]["objectClass"])
+
+ # The top-most structural class cannot be deleted since there are
+ # attributes of it in use
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("person", FLAG_MOD_DELETE,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # We cannot delete classes which weren't specified
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("computer", FLAG_MOD_DELETE,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ # An invalid class cannot be added
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("X", FLAG_MOD_ADD,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ # We cannot add a new top-most structural class "user" here since
+ # we are missing at least one new mandatory attribute (in this case
+ # "sAMAccountName")
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("user", FLAG_MOD_ADD,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # An already specified objectclass cannot be added another time
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("person", FLAG_MOD_ADD,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ # Auxiliary classes can always be added
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("bootableDevice", FLAG_MOD_ADD,
+ "objectClass")
+ ldb.modify(m)
+
+ # This does not work since object class "leaf" is not auxiliary nor it
+ # stands in direct relation to "person" (and it is abstract too!)
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("leaf", FLAG_MOD_ADD,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Objectclass replace operations can be performed as well
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement(["top", "person", "bootableDevice"],
+ FLAG_MOD_REPLACE, "objectClass")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement(["person", "bootableDevice"],
+ FLAG_MOD_REPLACE, "objectClass")
+ ldb.modify(m)
+
+ # This does not work since object class "leaf" is not auxiliary nor it
+ # stands in direct relation to "person" (and it is abstract too!)
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement(["top", "person", "bootableDevice",
+ "leaf"], FLAG_MOD_REPLACE, "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # More than one change operation is allowed
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m.add(MessageElement("bootableDevice", FLAG_MOD_DELETE, "objectClass"))
+ m.add(MessageElement("bootableDevice", FLAG_MOD_ADD, "objectClass"))
+ ldb.modify(m)
+
+ # We cannot remove all object classes by an empty replace
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement([], FLAG_MOD_REPLACE, "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement(["top", "computer"], FLAG_MOD_REPLACE,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Classes can be removed unless attributes of them are used.
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("bootableDevice", FLAG_MOD_DELETE,
+ "objectClass")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["objectClass"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("bootableDevice" in res[0]["objectClass"])
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("bootableDevice", FLAG_MOD_ADD,
+ "objectClass")
+ ldb.modify(m)
+
+ # Add an attribute specific to the "bootableDevice" class
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["bootParameter"] = MessageElement("test", FLAG_MOD_ADD,
+ "bootParameter")
+ ldb.modify(m)
+
+ # Classes can be removed unless attributes of them are used. Now there
+ # exist such attributes on the entry.
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("bootableDevice", FLAG_MOD_DELETE,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Remove the previously specified attribute
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["bootParameter"] = MessageElement("test", FLAG_MOD_DELETE,
+ "bootParameter")
+ ldb.modify(m)
+
+ # Classes can be removed unless attributes of them are used.
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("bootableDevice", FLAG_MOD_DELETE,
+ "objectClass")
+ ldb.modify(m)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "user"})
+
+ # Add a new top-most structural class "container". This does not work
+ # since it stands in no direct relation to the current one.
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("container", FLAG_MOD_ADD,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Try to add a new top-most structural class "inetOrgPerson"
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("inetOrgPerson", FLAG_MOD_ADD,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Try to remove the structural class "user"
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("user", FLAG_MOD_DELETE,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Try to replace top-most structural class to "inetOrgPerson"
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("inetOrgPerson", FLAG_MOD_REPLACE,
+ "objectClass")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Add a new auxiliary object class "posixAccount" to "ldaptestuser"
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("posixAccount", FLAG_MOD_ADD,
+ "objectClass")
+ ldb.modify(m)
+
+ # Be sure that "top" is the first and the (most) structural object class
+ # the last value of the "objectClass" attribute - MS-ADTS 3.1.1.1.4
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["objectClass"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["objectClass"][0]), "top")
+ self.assertEqual(str(res[0]["objectClass"][len(res[0]["objectClass"]) - 1]), "user")
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_system_only(self):
+ """Test systemOnly objects"""
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestobject," + self.base_dn,
+ "objectclass": "configuration"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ self.ldb.add({
+ "dn": "cn=Test Secret,cn=system," + self.base_dn,
+ "objectclass": "secret"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "cn=ldaptestobject," + self.base_dn)
+ delete_force(self.ldb, "cn=Test Secret,cn=system," + self.base_dn)
+
+ # Create secret over LSA and try to change it
+
+ lsa_conn = lsa.lsarpc("ncacn_np:%s" % args[0], lp, creds)
+ lsa_handle = lsa_conn.OpenPolicy2(system_name="\\",
+ attr=lsa.ObjectAttribute(),
+ access_mask=security.SEC_FLAG_MAXIMUM_ALLOWED)
+ secret_name = lsa.String()
+ secret_name.string = "G$Test"
+ sec_handle = lsa_conn.CreateSecret(handle=lsa_handle,
+ name=secret_name,
+ access_mask=security.SEC_FLAG_MAXIMUM_ALLOWED)
+ lsa_conn.Close(lsa_handle)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=Test Secret,cn=system," + self.base_dn)
+ m["description"] = MessageElement("desc", FLAG_MOD_REPLACE,
+ "description")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "cn=Test Secret,cn=system," + self.base_dn)
+
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectclass": "container",
+ "isCriticalSystemObject": "TRUE"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectclass": "container"})
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcontainer," + self.base_dn)
+ m["isCriticalSystemObject"] = MessageElement("TRUE", FLAG_MOD_REPLACE,
+ "isCriticalSystemObject")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+
+ # Proof if DC SAM object has "isCriticalSystemObject" set
+ res = self.ldb.search("", scope=SCOPE_BASE, attrs=["serverName"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("serverName" in res[0])
+ res = self.ldb.search(res[0]["serverName"][0], scope=SCOPE_BASE,
+ attrs=["serverReference"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("serverReference" in res[0])
+ res = self.ldb.search(res[0]["serverReference"][0], scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("isCriticalSystemObject" in res[0])
+ self.assertEqual(str(res[0]["isCriticalSystemObject"][0]), "TRUE")
+
+ def test_invalid_parent(self):
+ """Test adding an object with invalid parent"""
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=thisdoesnotexist123,"
+ + self.base_dn,
+ "objectclass": "group"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=thisdoesnotexist123,"
+ + self.base_dn)
+
+ try:
+ self.ldb.add({
+ "dn": "ou=testou,cn=users," + self.base_dn,
+ "objectclass": "organizationalUnit"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NAMING_VIOLATION)
+
+ delete_force(self.ldb, "ou=testou,cn=users," + self.base_dn)
+
+ def test_invalid_attribute(self):
+ """Test invalid attributes on schema/objectclasses"""
+ # attributes not in schema test
+
+ # add operation
+
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "thisdoesnotexist": "x"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ # modify operation
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["thisdoesnotexist"] = MessageElement("x", FLAG_MOD_REPLACE,
+ "thisdoesnotexist")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ #
+ # When searching the unknown attribute should be ignored
+ expr = "(|(cn=ldaptestgroup)(thisdoesnotexist=x))"
+ res = ldb.search(base=self.base_dn,
+ expression=expr,
+ scope=SCOPE_SUBTREE)
+ self.assertTrue(len(res) == 1,
+ "Search including unknown attribute failed")
+
+ # likewise, if we specifically request an unknown attribute
+ res = ldb.search(base=self.base_dn,
+ expression="(cn=ldaptestgroup)",
+ scope=SCOPE_SUBTREE,
+ attrs=["thisdoesnotexist"])
+ self.assertTrue(len(res) == 1,
+ "Search requesting unknown attribute failed")
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ # attributes not in objectclasses and mandatory attributes missing test
+ # Use here a non-SAM entry since it doesn't have special triggers
+ # associated which have an impact on the error results.
+
+ # add operations
+
+ # mandatory attribute missing
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestobject," + self.base_dn,
+ "objectclass": "ipProtocol"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # inadequate but schema-valid attribute specified
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestobject," + self.base_dn,
+ "objectclass": "ipProtocol",
+ "ipProtocolNumber": "1",
+ "uid": "0"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestobject," + self.base_dn,
+ "objectclass": "ipProtocol",
+ "ipProtocolNumber": "1"})
+
+ # modify operations
+
+ # inadequate but schema-valid attribute add trial
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestobject," + self.base_dn)
+ m["uid"] = MessageElement("0", FLAG_MOD_ADD, "uid")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # mandatory attribute delete trial
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestobject," + self.base_dn)
+ m["ipProtocolNumber"] = MessageElement([], FLAG_MOD_DELETE,
+ "ipProtocolNumber")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # mandatory attribute delete trial
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestobject," + self.base_dn)
+ m["ipProtocolNumber"] = MessageElement([], FLAG_MOD_REPLACE,
+ "ipProtocolNumber")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ delete_force(self.ldb, "cn=ldaptestobject," + self.base_dn)
+
+ def test_single_valued_attributes(self):
+ """Test single-valued attributes"""
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "sAMAccountName": ["nam1", "nam2"]})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["sAMAccountName"] = MessageElement(["nam1", "nam2"], FLAG_MOD_REPLACE,
+ "sAMAccountName")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("testgroupXX", FLAG_MOD_REPLACE,
+ "sAMAccountName")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("testgroupXX2", FLAG_MOD_ADD,
+ "sAMAccountName")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_single_valued_linked_attributes(self):
+ """Test managedBy, a single-valued linked attribute.
+
+ (The single-valuedness of this is enforced differently, in
+ repl_meta_data.c)
+ """
+ ou = 'OU=svla,%s' % (self.base_dn)
+
+ delete_force(self.ldb, ou, controls=['tree_delete:1'])
+
+ self.ldb.add({'objectclass': 'organizationalUnit',
+ 'dn': ou})
+
+ managers = []
+ for x in range(3):
+ m = "cn=manager%d,%s" % (x, ou)
+ self.ldb.add({
+ "dn": m,
+ "objectclass": "user"})
+ managers.append(m)
+
+ try:
+ self.ldb.add({
+ "dn": "cn=group1," + ou,
+ "objectclass": "group",
+ "managedBy": managers
+ })
+ self.fail("failed to fail to add multiple managedBy attributes")
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ managee = "cn=group2," + ou
+ self.ldb.add({
+ "dn": managee,
+ "objectclass": "group",
+ "managedBy": [managers[0]]})
+
+ m = Message()
+ m.dn = Dn(ldb, managee)
+ m["managedBy"] = MessageElement(managers, FLAG_MOD_REPLACE,
+ "managedBy")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, managee)
+ m["managedBy"] = MessageElement(managers[1], FLAG_MOD_REPLACE,
+ "managedBy")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, managee)
+ m["managedBy"] = MessageElement(managers[2], FLAG_MOD_ADD,
+ "managedBy")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ self.ldb.delete(ou, ['tree_delete:1'])
+
+ def test_multivalued_attributes(self):
+ """Test multi-valued attributes"""
+ ou = 'OU=mvattr,%s' % (self.base_dn)
+ delete_force(self.ldb, ou, controls=['tree_delete:1'])
+ self.ldb.add({'objectclass': 'organizationalUnit',
+ 'dn': ou})
+
+ # beyond 1210, Win2012r2 gives LDAP_ADMIN_LIMIT_EXCEEDED
+ ranges = (3, 30, 300, 1210)
+
+ for n in ranges:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser%d,%s" % (n, ou),
+ "objectclass": "user",
+ "carLicense": ["car%d" % x for x in range(n)]})
+
+ # add some more
+ for n in ranges:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser%d,%s" % (n, ou))
+ m["carLicense"] = MessageElement(["another"],
+ FLAG_MOD_ADD,
+ "carLicense")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser%d,%s" % (n, ou))
+ m["carLicense"] = MessageElement(["foo%d" % x for x in range(4)],
+ FLAG_MOD_ADD,
+ "carLicense")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser%d,%s" % (n, ou))
+ m["carLicense"] = MessageElement(["bar%d" % x for x in range(40)],
+ FLAG_MOD_ADD,
+ "carLicense")
+ ldb.modify(m)
+
+ for n in ranges:
+ m = Message()
+ dn = "cn=ldaptestuser%d,%s" % (n, ou)
+ m.dn = Dn(ldb, dn)
+ m["carLicense"] = MessageElement(["replacement"],
+ FLAG_MOD_REPLACE,
+ "carLicense")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, dn)
+ m["carLicense"] = MessageElement(["replacement%d" % x for x in range(n)],
+ FLAG_MOD_REPLACE,
+ "carLicense")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, dn)
+ m["carLicense"] = MessageElement(["again%d" % x for x in range(n)],
+ FLAG_MOD_REPLACE,
+ "carLicense")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, dn)
+ m["carLicense"] = MessageElement(["andagain%d" % x for x in range(n)],
+ FLAG_MOD_REPLACE,
+ "carLicense")
+ ldb.modify(m)
+
+ self.ldb.delete(ou, ['tree_delete:1'])
+
+ def test_attribute_ranges(self):
+ """Test attribute ranges"""
+ # Too short (min. 1)
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person",
+ "sn": ""})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_ATTRIBUTE_SYNTAX)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person"})
+
+ # Too short (min. 1)
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["sn"] = MessageElement("", FLAG_MOD_REPLACE, "sn")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_ATTRIBUTE_SYNTAX)
+
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["sn"] = MessageElement("x", FLAG_MOD_REPLACE, "sn")
+ ldb.modify(m)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_attribute_ranges_too_long(self):
+ """Test attribute ranges"""
+ # This is knownfail with the wrong error
+ # (INVALID_ATTRIBUTE_SYNTAX vs CONSTRAINT_VIOLATION per Windows)
+
+ # Too long (max. 64)
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person",
+ "sn": "x" * 65 })
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectClass": "person"})
+
+ # Too long (max. 64)
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["sn"] = MessageElement("x" * 66, FLAG_MOD_REPLACE, "sn")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ self.assertEqual(e.args[0], ERR_CONSTRAINT_VIOLATION)
+
+ def test_empty_messages(self):
+ """Test empty messages"""
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ try:
+ ldb.add(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_empty_attributes(self):
+ """Test empty attributes"""
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("group", FLAG_MOD_ADD, "objectClass")
+ m["description"] = MessageElement([], FLAG_MOD_ADD, "description")
+
+ try:
+ ldb.add(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement([], FLAG_MOD_ADD, "description")
+
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement([], FLAG_MOD_REPLACE, "description")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement([], FLAG_MOD_DELETE, "description")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_instanceType(self):
+ """Tests the 'instanceType' attribute"""
+ # The instance type is single-valued
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "instanceType": ["0", "1"]})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # The head NC flag cannot be set without the write flag
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "instanceType": "1"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # We cannot manipulate NCs without the head NC flag
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "instanceType": "32"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["instanceType"] = MessageElement("0", FLAG_MOD_REPLACE,
+ "instanceType")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["instanceType"] = MessageElement([], FLAG_MOD_REPLACE,
+ "instanceType")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["instanceType"] = MessageElement([], FLAG_MOD_DELETE, "instanceType")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ # only write is allowed with NC_HEAD for originating updates
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "instanceType": "3"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ delete_force(self.ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
+
+ def test_distinguished_name(self):
+ """Tests the 'distinguishedName' attribute"""
+ # The "dn" shortcut isn't supported
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["objectClass"] = MessageElement("group", 0, "objectClass")
+ m["dn"] = MessageElement("cn=ldaptestgroup,cn=users," + self.base_dn, 0,
+ "dn")
+ try:
+ ldb.add(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ # a wrong "distinguishedName" attribute is obviously tolerated
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "distinguishedName": "cn=ldaptest,cn=users," + self.base_dn})
+
+ # proof if the DN has been set correctly
+ res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["distinguishedName"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("distinguishedName" in res[0])
+ self.assertTrue(Dn(ldb, str(res[0]["distinguishedName"][0]))
+ == Dn(ldb, "cn=ldaptestgroup, cn=users," + self.base_dn))
+
+ # The "dn" shortcut isn't supported
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["dn"] = MessageElement(
+ "cn=ldaptestgroup,cn=users," + self.base_dn, FLAG_MOD_REPLACE,
+ "dn")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["distinguishedName"] = MessageElement(
+ "cn=ldaptestuser,cn=users," + self.base_dn, FLAG_MOD_ADD,
+ "distinguishedName")
+
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["distinguishedName"] = MessageElement(
+ "cn=ldaptestuser,cn=users," + self.base_dn, FLAG_MOD_REPLACE,
+ "distinguishedName")
+
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["distinguishedName"] = MessageElement(
+ "cn=ldaptestuser,cn=users," + self.base_dn, FLAG_MOD_DELETE,
+ "distinguishedName")
+
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_rdn_name(self):
+ """Tests the RDN"""
+ # Search
+
+ # empty RDN
+ try:
+ self.ldb.search("=,cn=users," + self.base_dn, scope=SCOPE_BASE)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # empty RDN name
+ try:
+ self.ldb.search("cn=,cn=users," + self.base_dn, scope=SCOPE_BASE)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ try:
+ self.ldb.search("=ldaptestgroup,cn=users," + self.base_dn, scope=SCOPE_BASE)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # Add
+
+ # empty RDN
+ try:
+ self.ldb.add({
+ "dn": "=,cn=users," + self.base_dn,
+ "objectclass": "group"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # empty RDN name
+ try:
+ self.ldb.add({
+ "dn": "=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # empty RDN value
+ try:
+ self.ldb.add({
+ "dn": "cn=,cn=users," + self.base_dn,
+ "objectclass": "group"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # a wrong RDN candidate
+ try:
+ self.ldb.add({
+ "dn": "description=xyz,cn=users," + self.base_dn,
+ "objectclass": "group"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NAMING_VIOLATION)
+
+ delete_force(self.ldb, "description=xyz,cn=users," + self.base_dn)
+
+ # a wrong "name" attribute is obviously tolerated
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "name": "ldaptestgroupx"})
+
+ # proof if the name has been set correctly
+ res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["name"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("name" in res[0])
+ self.assertTrue(str(res[0]["name"][0]) == "ldaptestgroup")
+
+ # Modify
+
+ # empty RDN value
+ m = Message()
+ m.dn = Dn(ldb, "cn=,cn=users," + self.base_dn)
+ m["description"] = "test"
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # Delete
+
+ # empty RDN value
+ try:
+ self.ldb.delete("cn=,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # Rename
+
+ # new empty RDN
+ try:
+ self.ldb.rename("cn=ldaptestgroup,cn=users," + self.base_dn,
+ "=,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # new empty RDN name
+ try:
+ self.ldb.rename("cn=ldaptestgroup,cn=users," + self.base_dn,
+ "=ldaptestgroup,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # new empty RDN value
+ try:
+ self.ldb.rename("cn=ldaptestgroup,cn=users," + self.base_dn,
+ "cn=,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NAMING_VIOLATION)
+
+ # new wrong RDN candidate
+ try:
+ self.ldb.rename("cn=ldaptestgroup,cn=users," + self.base_dn,
+ "description=xyz,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "description=xyz,cn=users," + self.base_dn)
+
+ # old empty RDN value
+ try:
+ self.ldb.rename("cn=,cn=users," + self.base_dn,
+ "cn=ldaptestgroup,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ # names
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["name"] = MessageElement("cn=ldaptestuser", FLAG_MOD_REPLACE,
+ "name")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_RDN)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["cn"] = MessageElement("ldaptestuser",
+ FLAG_MOD_REPLACE, "cn")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_RDN)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ # this test needs to be disabled until we really understand
+ # what the rDN length constraints are
+
+ def DISABLED_test_largeRDN(self):
+ """Testing large rDN (limit 64 characters)"""
+ rdn = "CN=a012345678901234567890123456789012345678901234567890123456789012"
+ delete_force(self.ldb, "%s,%s" % (rdn, self.base_dn))
+ ldif = """
+dn: %s,%s""" % (rdn, self.base_dn) + """
+objectClass: container
+"""
+ self.ldb.add_ldif(ldif)
+ delete_force(self.ldb, "%s,%s" % (rdn, self.base_dn))
+
+ rdn = "CN=a0123456789012345678901234567890123456789012345678901234567890120"
+ delete_force(self.ldb, "%s,%s" % (rdn, self.base_dn))
+ try:
+ ldif = """
+dn: %s,%s""" % (rdn, self.base_dn) + """
+objectClass: container
+"""
+ self.ldb.add_ldif(ldif)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ delete_force(self.ldb, "%s,%s" % (rdn, self.base_dn))
+
+ def test_rename(self):
+ """Tests the rename operation"""
+ try:
+ # cannot rename to be a child of itself
+ ldb.rename(self.base_dn, "dc=test," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ # inexistent object
+ ldb.rename("cn=ldaptestuser2,cn=users," + self.base_dn, "cn=ldaptestuser2,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestuser2,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ ldb.rename("cn=ldaptestuser2,cn=users," + self.base_dn, "cn=ldaptestuser2,cn=users," + self.base_dn)
+ ldb.rename("cn=ldaptestuser2,cn=users," + self.base_dn, "cn=ldaptestuser3,cn=users," + self.base_dn)
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, "cn=ldaptestUSER3,cn=users," + self.base_dn)
+
+ try:
+ # containment problem: a user entry cannot contain user entries
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, "cn=ldaptestuser4,cn=ldaptestuser3,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NAMING_VIOLATION)
+
+ try:
+ # invalid parent
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, "cn=ldaptestuser3,cn=people,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OTHER)
+
+ try:
+ # invalid target DN syntax
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, ",cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ try:
+ # invalid RDN name
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, "ou=ldaptestuser3,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "cn=ldaptestuser3,cn=users," + self.base_dn)
+
+ # Performs some "systemFlags" testing
+
+ # Move failing since no "SYSTEM_FLAG_CONFIG_ALLOW_MOVE"
+ try:
+ ldb.rename("CN=DisplaySpecifiers," + self.configuration_dn, "CN=DisplaySpecifiers,CN=Services," + self.configuration_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Limited move failing since no "SYSTEM_FLAG_CONFIG_ALLOW_LIMITED_MOVE"
+ try:
+ ldb.rename("CN=Directory Service,CN=Windows NT,CN=Services," + self.configuration_dn, "CN=Directory Service,CN=RRAS,CN=Services," + self.configuration_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Rename failing since no "SYSTEM_FLAG_CONFIG_ALLOW_RENAME"
+ try:
+ ldb.rename("CN=DisplaySpecifiers," + self.configuration_dn, "CN=DisplaySpecifiers2," + self.configuration_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # It's not really possible to test moves on the schema partition since
+ # there don't exist subcontainers on it.
+
+ # Rename failing since "SYSTEM_FLAG_SCHEMA_BASE_OBJECT"
+ try:
+ ldb.rename("CN=Top," + self.schema_dn, "CN=Top2," + self.schema_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Move failing since "SYSTEM_FLAG_DOMAIN_DISALLOW_MOVE"
+ try:
+ ldb.rename("CN=Users," + self.base_dn, "CN=Users,CN=Computers," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Rename failing since "SYSTEM_FLAG_DOMAIN_DISALLOW_RENAME"
+ try:
+ ldb.rename("CN=Users," + self.base_dn, "CN=Users2," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Performs some other constraints testing
+
+ try:
+ ldb.rename("CN=Policies,CN=System," + self.base_dn, "CN=Users2," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_OTHER)
+
+ def test_rename_twice(self):
+ """Tests the rename operation twice - this corresponds to a past bug"""
+ self.ldb.add({
+ "dn": "cn=ldaptestuser5,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ ldb.rename("cn=ldaptestuser5,cn=users," + self.base_dn, "cn=ldaptestUSER5,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser5,cn=users," + self.base_dn)
+ self.ldb.add({
+ "dn": "cn=ldaptestuser5,cn=users," + self.base_dn,
+ "objectclass": "user"})
+ ldb.rename("cn=ldaptestuser5,cn=Users," + self.base_dn, "cn=ldaptestUSER5,cn=users," + self.base_dn)
+ res = ldb.search(expression="cn=ldaptestuser5")
+ self.assertEqual(len(res), 1, "Wrong number of hits for cn=ldaptestuser5")
+ res = ldb.search(expression="(&(cn=ldaptestuser5)(objectclass=user))")
+ self.assertEqual(len(res), 1, "Wrong number of hits for (&(cn=ldaptestuser5)(objectclass=user))")
+ delete_force(self.ldb, "cn=ldaptestuser5,cn=users," + self.base_dn)
+
+ def test_objectGUID(self):
+ """Test objectGUID behaviour"""
+ # The objectGUID cannot directly be set
+ try:
+ self.ldb.add_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+objectClass: container
+objectGUID: bd3480c9-58af-4cd8-92df-bc4a18b6e44d
+""")
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectClass": "container"})
+
+ # The objectGUID cannot directly be changed
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+replace: objectGUID
+objectGUID: bd3480c9-58af-4cd8-92df-bc4a18b6e44d
+""")
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+
+ def test_parentGUID(self):
+ """Test parentGUID behaviour"""
+ self.ldb.add({
+ "dn": "cn=parentguidtest,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "samaccountname": "parentguidtest"})
+ res1 = ldb.search(base="cn=parentguidtest,cn=users," + self.base_dn, scope=SCOPE_BASE,
+ attrs=["parentGUID", "samaccountname"])
+ res2 = ldb.search(base="cn=users," + self.base_dn, scope=SCOPE_BASE,
+ attrs=["objectGUID"])
+ res3 = ldb.search(base=self.base_dn, scope=SCOPE_BASE,
+ attrs=["parentGUID"])
+ res4 = ldb.search(base=self.configuration_dn, scope=SCOPE_BASE,
+ attrs=["parentGUID"])
+ res5 = ldb.search(base=self.schema_dn, scope=SCOPE_BASE,
+ attrs=["parentGUID"])
+
+ """Check if the parentGUID is valid """
+ self.assertEqual(res1[0]["parentGUID"], res2[0]["objectGUID"])
+
+ """Check if it returns nothing when there is no parent object - default NC"""
+ has_parentGUID = False
+ for key in res3[0].keys():
+ if key == "parentGUID":
+ has_parentGUID = True
+ break
+ self.assertFalse(has_parentGUID)
+
+ """Check if it returns nothing when there is no parent object - configuration NC"""
+ has_parentGUID = False
+ for key in res4[0].keys():
+ if key == "parentGUID":
+ has_parentGUID = True
+ break
+ self.assertFalse(has_parentGUID)
+
+ """Check if it returns nothing when there is no parent object - schema NC"""
+ has_parentGUID = False
+ for key in res5[0].keys():
+ if key == "parentGUID":
+ has_parentGUID = True
+ break
+ self.assertFalse(has_parentGUID)
+
+ """Ensures that if you look for another object attribute after the constructed
+ parentGUID, it will return correctly"""
+ has_another_attribute = False
+ for key in res1[0].keys():
+ if key == "sAMAccountName":
+ has_another_attribute = True
+ break
+ self.assertTrue(has_another_attribute)
+ self.assertTrue(len(res1[0]["samaccountname"]) == 1)
+ self.assertEqual(str(res1[0]["samaccountname"][0]), "parentguidtest")
+
+ # Testing parentGUID behaviour on rename\
+
+ self.ldb.add({
+ "dn": "cn=testotherusers," + self.base_dn,
+ "objectclass": "container"})
+ res1 = ldb.search(base="cn=testotherusers," + self.base_dn, scope=SCOPE_BASE,
+ attrs=["objectGUID"])
+ ldb.rename("cn=parentguidtest,cn=users," + self.base_dn,
+ "cn=parentguidtest,cn=testotherusers," + self.base_dn)
+ res2 = ldb.search(base="cn=parentguidtest,cn=testotherusers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["parentGUID"])
+ self.assertEqual(res1[0]["objectGUID"], res2[0]["parentGUID"])
+
+ delete_force(self.ldb, "cn=parentguidtest,cn=testotherusers," + self.base_dn)
+ delete_force(self.ldb, "cn=testotherusers," + self.base_dn)
+
+ def test_usnChanged(self):
+ """Test usnChanged behaviour"""
+
+ self.ldb.add({
+ "dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectClass": "container"})
+
+ res = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["objectGUID", "uSNCreated", "uSNChanged", "whenCreated", "whenChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("description" in res[0])
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("uSNCreated" in res[0])
+ self.assertTrue("uSNChanged" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertTrue("whenChanged" in res[0])
+
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+
+ # All these attributes are specifiable on add operations
+ self.ldb.add({
+ "dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectclass": "container",
+ "uSNCreated": "1",
+ "uSNChanged": "1",
+ "whenCreated": timestring(int(time.time())),
+ "whenChanged": timestring(int(time.time()))})
+
+ res = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["objectGUID", "uSNCreated", "uSNChanged", "whenCreated", "whenChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("description" in res[0])
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("uSNCreated" in res[0])
+ self.assertFalse(res[0]["uSNCreated"][0] == "1") # these are corrected
+ self.assertTrue("uSNChanged" in res[0])
+ self.assertFalse(res[0]["uSNChanged"][0] == "1") # these are corrected
+ self.assertTrue("whenCreated" in res[0])
+ self.assertTrue("whenChanged" in res[0])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+replace: description
+""")
+
+ res2 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("description" in res2[0])
+ self.assertEqual(res[0]["usnCreated"], res2[0]["usnCreated"])
+ self.assertEqual(res[0]["usnCreated"], res2[0]["usnChanged"])
+ self.assertEqual(res[0]["usnChanged"], res2[0]["usnChanged"])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+replace: description
+description: test
+""")
+
+ res3 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res3[0])
+ self.assertEqual("test", str(res3[0]["description"][0]))
+ self.assertEqual(res[0]["usnCreated"], res3[0]["usnCreated"])
+ self.assertNotEqual(res[0]["usnCreated"], res3[0]["usnChanged"])
+ self.assertNotEqual(res[0]["usnChanged"], res3[0]["usnChanged"])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+replace: description
+description: test
+""")
+
+ res4 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res4[0])
+ self.assertEqual("test", str(res4[0]["description"][0]))
+ self.assertEqual(res[0]["usnCreated"], res4[0]["usnCreated"])
+ self.assertNotEqual(res3[0]["usnCreated"], res4[0]["usnChanged"])
+ self.assertEqual(res3[0]["usnChanged"], res4[0]["usnChanged"])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+replace: description
+description: test2
+""")
+
+ res5 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res5[0])
+ self.assertEqual("test2", str(res5[0]["description"][0]))
+ self.assertEqual(res[0]["usnCreated"], res5[0]["usnCreated"])
+ self.assertNotEqual(res3[0]["usnChanged"], res5[0]["usnChanged"])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+delete: description
+description: test2
+""")
+
+ res6 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("description" in res6[0])
+ self.assertEqual(res[0]["usnCreated"], res6[0]["usnCreated"])
+ self.assertNotEqual(res5[0]["usnChanged"], res6[0]["usnChanged"])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+add: description
+description: test3
+""")
+
+ res7 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res7[0])
+ self.assertEqual("test3", str(res7[0]["description"][0]))
+ self.assertEqual(res[0]["usnCreated"], res7[0]["usnCreated"])
+ self.assertNotEqual(res6[0]["usnChanged"], res7[0]["usnChanged"])
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+delete: description
+""")
+
+ res8 = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["uSNCreated", "uSNChanged", "description"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("description" in res8[0])
+ self.assertEqual(res[0]["usnCreated"], res8[0]["usnCreated"])
+ self.assertNotEqual(res7[0]["usnChanged"], res8[0]["usnChanged"])
+
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+
+ def test_groupType_int32(self):
+ """Test groupType (int32) behaviour (should appear to be cast to a 32 bit signed integer before comparison)"""
+
+ res1 = ldb.search(base=self.base_dn, scope=SCOPE_SUBTREE,
+ attrs=["groupType"], expression="groupType=2147483653")
+
+ res2 = ldb.search(base=self.base_dn, scope=SCOPE_SUBTREE,
+ attrs=["groupType"], expression="groupType=-2147483643")
+
+ self.assertEqual(len(res1), len(res2))
+
+ self.assertTrue(res1.count > 0)
+
+ self.assertEqual(str(res1[0]["groupType"][0]), "-2147483643")
+
+ def test_linked_attributes(self):
+ """This tests the linked attribute behaviour"""
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ # This should not work since "memberOf" is linked to "member"
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "memberOf": "cn=ldaptestgroup,cn=users," + self.base_dn})
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["memberOf"] = MessageElement("cn=ldaptestgroup,cn=users," + self.base_dn,
+ FLAG_MOD_ADD, "memberOf")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
+ FLAG_MOD_ADD, "member")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["memberOf"] = MessageElement("cn=ldaptestgroup,cn=users," + self.base_dn,
+ FLAG_MOD_REPLACE, "memberOf")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["memberOf"] = MessageElement("cn=ldaptestgroup,cn=users," + self.base_dn,
+ FLAG_MOD_DELETE, "memberOf")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
+ FLAG_MOD_DELETE, "member")
+ ldb.modify(m)
+
+ # This should yield no results since the member attribute for
+ # "ldaptestuser" should have been deleted
+ res1 = ldb.search("cn=ldaptestgroup, cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ expression="(member=cn=ldaptestuser,cn=users," + self.base_dn + ")",
+ attrs=[])
+ self.assertTrue(len(res1) == 0)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "member": "cn=ldaptestuser,cn=users," + self.base_dn})
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ # Make sure that the "member" attribute for "ldaptestuser" has been
+ # removed
+ res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["member"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("member" in res[0])
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_wkguid(self):
+ """Test Well known GUID behaviours (including DN+Binary)"""
+
+ res = self.ldb.search(base=("<WKGUID=ab1d30f3768811d1aded00c04fd8d5cd,%s>" % self.base_dn), scope=SCOPE_BASE, attrs=[])
+ self.assertEqual(len(res), 1)
+
+ res2 = self.ldb.search(scope=SCOPE_BASE, attrs=["wellKnownObjects"], expression=("wellKnownObjects=B:32:ab1d30f3768811d1aded00c04fd8d5cd:%s" % res[0].dn))
+ self.assertEqual(len(res2), 1)
+
+ # Prove that the matching rule is over the whole DN+Binary
+ res2 = self.ldb.search(scope=SCOPE_BASE, attrs=["wellKnownObjects"], expression=("wellKnownObjects=B:32:ab1d30f3768811d1aded00c04fd8d5cd"))
+ self.assertEqual(len(res2), 0)
+ # Prove that the matching rule is over the whole DN+Binary
+ res2 = self.ldb.search(scope=SCOPE_BASE, attrs=["wellKnownObjects"], expression=("wellKnownObjects=%s") % res[0].dn)
+ self.assertEqual(len(res2), 0)
+
+ def test_subschemasubentry(self):
+ """Test subSchemaSubEntry appears when requested, but not when not requested"""
+
+ res = self.ldb.search(base=self.base_dn, scope=SCOPE_BASE, attrs=["subSchemaSubEntry"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["subSchemaSubEntry"][0]), "CN=Aggregate," + self.schema_dn)
+
+ res = self.ldb.search(base=self.base_dn, scope=SCOPE_BASE, attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertTrue("subScheamSubEntry" not in res[0])
+
+ def test_all(self):
+ """Basic tests"""
+
+ # Testing user add
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=uSers," + self.base_dn,
+ "objectclass": "user",
+ "cN": "LDAPtestUSER",
+ "givenname": "ldap",
+ "sn": "testy"})
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=uSers," + self.base_dn,
+ "objectclass": "group",
+ "member": "cn=ldaptestuser,cn=useRs," + self.base_dn})
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "cN": "LDAPtestCOMPUTER"})
+
+ ldb.add({"dn": "cn=ldaptest2computer,cn=computers," + self.base_dn,
+ "objectClass": "computer",
+ "cn": "LDAPtest2COMPUTER",
+ "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT),
+ "displayname": "ldap testy"})
+
+ try:
+ ldb.add({"dn": "cn=ldaptestcomputer3,cn=computers," + self.base_dn,
+ "objectClass": "computer",
+ "cn": "LDAPtest2COMPUTER"
+ })
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_DN_SYNTAX)
+
+ try:
+ ldb.add({"dn": "cn=ldaptestcomputer3,cn=computers," + self.base_dn,
+ "objectClass": "computer",
+ "cn": "ldaptestcomputer3",
+ "sAMAccountType": str(ATYPE_NORMAL_ACCOUNT)
+ })
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ ldb.add({"dn": "cn=ldaptestcomputer3,cn=computers," + self.base_dn,
+ "objectClass": "computer",
+ "cn": "LDAPtestCOMPUTER3"
+ })
+
+ # Testing ldb.search for (&(cn=ldaptestcomputer3)(objectClass=user))
+ res = ldb.search(self.base_dn, expression="(&(cn=ldaptestcomputer3)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Found only %d for (&(cn=ldaptestcomputer3)(objectClass=user))" % len(res))
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestcomputer3,CN=Computers," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"][0]), "ldaptestcomputer3")
+ self.assertEqual(str(res[0]["name"][0]), "ldaptestcomputer3")
+ self.assertEqual(str(res[0]["objectClass"][0]), "top")
+ self.assertEqual(str(res[0]["objectClass"][1]), "person")
+ self.assertEqual(str(res[0]["objectClass"][2]), "organizationalPerson")
+ self.assertEqual(str(res[0]["objectClass"][3]), "user")
+ self.assertEqual(str(res[0]["objectClass"][4]), "computer")
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertEqual(str(res[0]["objectCategory"][0]), ("CN=Computer,%s" % ldb.get_schema_basedn()))
+ self.assertEqual(int(res[0]["primaryGroupID"][0]), DOMAIN_RID_DOMAIN_MEMBERS)
+ self.assertEqual(int(res[0]["sAMAccountType"][0]), ATYPE_WORKSTATION_TRUST)
+ self.assertEqual(int(res[0]["userAccountControl"][0]), UF_WORKSTATION_TRUST_ACCOUNT | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE)
+
+ delete_force(self.ldb, "cn=ldaptestcomputer3,cn=computers," + self.base_dn)
+
+ # Testing attribute or value exists behaviour
+ try:
+ ldb.modify_ldif("""
+dn: cn=ldaptest2computer,cn=computers,""" + self.base_dn + """
+changetype: modify
+replace: servicePrincipalName
+servicePrincipalName: host/ldaptest2computer
+servicePrincipalName: host/ldaptest2computer
+servicePrincipalName: cifs/ldaptest2computer
+""")
+ self.fail()
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ ldb.modify_ldif("""
+dn: cn=ldaptest2computer,cn=computers,""" + self.base_dn + """
+changetype: modify
+replace: servicePrincipalName
+servicePrincipalName: host/ldaptest2computer
+servicePrincipalName: cifs/ldaptest2computer
+""")
+ try:
+ ldb.modify_ldif("""
+dn: cn=ldaptest2computer,cn=computers,""" + self.base_dn + """
+changetype: modify
+add: servicePrincipalName
+servicePrincipalName: host/ldaptest2computer
+""")
+ self.fail()
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ # Testing ranged results
+ ldb.modify_ldif("""
+dn: cn=ldaptest2computer,cn=computers,""" + self.base_dn + """
+changetype: modify
+replace: servicePrincipalName
+""")
+
+ ldb.modify_ldif("""
+dn: cn=ldaptest2computer,cn=computers,""" + self.base_dn + """
+changetype: modify
+add: servicePrincipalName
+servicePrincipalName: host/ldaptest2computer0
+servicePrincipalName: host/ldaptest2computer1
+servicePrincipalName: host/ldaptest2computer2
+servicePrincipalName: host/ldaptest2computer3
+servicePrincipalName: host/ldaptest2computer4
+servicePrincipalName: host/ldaptest2computer5
+servicePrincipalName: host/ldaptest2computer6
+servicePrincipalName: host/ldaptest2computer7
+servicePrincipalName: host/ldaptest2computer8
+servicePrincipalName: host/ldaptest2computer9
+servicePrincipalName: host/ldaptest2computer10
+servicePrincipalName: host/ldaptest2computer11
+servicePrincipalName: host/ldaptest2computer12
+servicePrincipalName: host/ldaptest2computer13
+servicePrincipalName: host/ldaptest2computer14
+servicePrincipalName: host/ldaptest2computer15
+servicePrincipalName: host/ldaptest2computer16
+servicePrincipalName: host/ldaptest2computer17
+servicePrincipalName: host/ldaptest2computer18
+servicePrincipalName: host/ldaptest2computer19
+servicePrincipalName: host/ldaptest2computer20
+servicePrincipalName: host/ldaptest2computer21
+servicePrincipalName: host/ldaptest2computer22
+servicePrincipalName: host/ldaptest2computer23
+servicePrincipalName: host/ldaptest2computer24
+servicePrincipalName: host/ldaptest2computer25
+servicePrincipalName: host/ldaptest2computer26
+servicePrincipalName: host/ldaptest2computer27
+servicePrincipalName: host/ldaptest2computer28
+servicePrincipalName: host/ldaptest2computer29
+""")
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE,
+ attrs=["servicePrincipalName;range=0-*"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=0-*"]), 30)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=0-19"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=0-19"]), 20)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=0-30"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=0-*"]), 30)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=0-40"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=0-*"]), 30)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=30-40"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=30-*"]), 0)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=10-40"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=10-*"]), 20)
+ # pos_11 = res[0]["servicePrincipalName;range=10-*"][18]
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=11-40"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=11-*"]), 19)
+ # self.assertEqual((res[0]["servicePrincipalName;range=11-*"][18]), pos_11)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName;range=11-15"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName;range=11-15"]), 5)
+ # self.assertEqual(res[0]["servicePrincipalName;range=11-15"][4], pos_11)
+
+ res = ldb.search(self.base_dn, expression="(cn=ldaptest2computer))", scope=SCOPE_SUBTREE, attrs=["servicePrincipalName"])
+ self.assertEqual(len(res), 1, "Could not find (cn=ldaptest2computer)")
+ self.assertEqual(len(res[0]["servicePrincipalName"]), 30)
+ # self.assertEqual(res[0]["servicePrincipalName"][18], pos_11)
+
+ delete_force(self.ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
+ ldb.add({
+ "dn": "cn=ldaptestuser2,cn=useRs," + self.base_dn,
+ "objectClass": "user",
+ "cn": "LDAPtestUSER2",
+ "givenname": "testy",
+ "sn": "ldap user2"})
+
+ # Testing Ambiguous Name Resolution
+ # Testing ldb.search for (&(anr=ldap testy)(objectClass=user))
+ res = ldb.search(expression="(&(anr=ldap testy)(objectClass=user))")
+ self.assertEqual(len(res), 3, "Found only %d of 3 for (&(anr=ldap testy)(objectClass=user))" % len(res))
+
+ # Testing ldb.search for (&(anr=testy ldap)(objectClass=user))
+ res = ldb.search(expression="(&(anr=testy ldap)(objectClass=user))")
+ self.assertEqual(len(res), 2, "Found only %d of 2 for (&(anr=testy ldap)(objectClass=user))" % len(res))
+
+ # Testing ldb.search for (&(anr=ldap)(objectClass=user))
+ res = ldb.search(expression="(&(anr=ldap)(objectClass=user))")
+ self.assertEqual(len(res), 4, "Found only %d of 4 for (&(anr=ldap)(objectClass=user))" % len(res))
+
+ # Testing ldb.search for (&(anr==ldap)(objectClass=user))
+ res = ldb.search(expression="(&(anr==ldap)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(anr==ldap)(objectClass=user)). Found only %d for (&(anr=ldap)(objectClass=user))" % len(res))
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestuser,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"][0]), "ldaptestuser")
+ self.assertEqual(str(res[0]["name"]), "ldaptestuser")
+
+ # Testing ldb.search for (&(anr=testy)(objectClass=user))
+ res = ldb.search(expression="(&(anr=testy)(objectClass=user))")
+ self.assertEqual(len(res), 2, "Found only %d for (&(anr=testy)(objectClass=user))" % len(res))
+
+ # Testing ldb.search for (&(anr=testy ldap)(objectClass=user))
+ res = ldb.search(expression="(&(anr=testy ldap)(objectClass=user))")
+ self.assertEqual(len(res), 2, "Found only %d for (&(anr=testy ldap)(objectClass=user))" % len(res))
+
+ # Testing ldb.search for (&(anr==testy ldap)(objectClass=user))
+# this test disabled for the moment, as anr with == tests are not understood
+# res = ldb.search(expression="(&(anr==testy ldap)(objectClass=user))")
+# self.assertEqual(len(res), 1, "Found only %d for (&(anr==testy ldap)(objectClass=user))" % len(res))
+
+# self.assertEqual(str(res[0].dn), ("CN=ldaptestuser,CN=Users," + self.base_dn))
+# self.assertEqual(res[0]["cn"][0], "ldaptestuser")
+# self.assertEqual(res[0]["name"][0], "ldaptestuser")
+
+ # Testing ldb.search for (&(anr==testy ldap)(objectClass=user))
+# res = ldb.search(expression="(&(anr==testy ldap)(objectClass=user))")
+# self.assertEqual(len(res), 1, "Could not find (&(anr==testy ldap)(objectClass=user))")
+
+# self.assertEqual(str(res[0].dn), ("CN=ldaptestuser,CN=Users," + self.base_dn))
+# self.assertEqual(res[0]["cn"][0], "ldaptestuser")
+# self.assertEqual(res[0]["name"][0], "ldaptestuser")
+
+ # Testing ldb.search for (&(anr=testy ldap user)(objectClass=user))
+ res = ldb.search(expression="(&(anr=testy ldap user)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(anr=testy ldap user)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestuser2,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestuser2")
+ self.assertEqual(str(res[0]["name"]), "ldaptestuser2")
+
+ # Testing ldb.search for (&(anr==testy ldap user2)(objectClass=user))
+# res = ldb.search(expression="(&(anr==testy ldap user2)(objectClass=user))")
+# self.assertEqual(len(res), 1, "Could not find (&(anr==testy ldap user2)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestuser2,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestuser2")
+ self.assertEqual(str(res[0]["name"]), "ldaptestuser2")
+
+ # Testing ldb.search for (&(anr==ldap user2)(objectClass=user))
+# res = ldb.search(expression="(&(anr==ldap user2)(objectClass=user))")
+# self.assertEqual(len(res), 1, "Could not find (&(anr==ldap user2)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestuser2,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestuser2")
+ self.assertEqual(str(res[0]["name"]), "ldaptestuser2")
+
+ # Testing ldb.search for (&(anr==not ldap user2)(objectClass=user))
+# res = ldb.search(expression="(&(anr==not ldap user2)(objectClass=user))")
+# self.assertEqual(len(res), 0, "Must not find (&(anr==not ldap user2)(objectClass=user))")
+
+ # Testing ldb.search for (&(anr=not ldap user2)(objectClass=user))
+ res = ldb.search(expression="(&(anr=not ldap user2)(objectClass=user))")
+ self.assertEqual(len(res), 0, "Must not find (&(anr=not ldap user2)(objectClass=user))")
+
+ # Testing ldb.search for (&(anr="testy ldap")(objectClass=user)) (ie, with quotes)
+# res = ldb.search(expression="(&(anr==\"testy ldap\")(objectClass=user))")
+# self.assertEqual(len(res), 0, "Found (&(anr==\"testy ldap\")(objectClass=user))")
+
+ # Testing Renames
+
+ attrs = ["objectGUID", "objectSid"]
+ # Testing ldb.search for (&(cn=ldaptestUSer2)(objectClass=user))
+ res_user = ldb.search(self.base_dn, expression="(&(cn=ldaptestUSer2)(objectClass=user))", scope=SCOPE_SUBTREE, attrs=attrs)
+ self.assertEqual(len(res_user), 1, "Could not find (&(cn=ldaptestUSer2)(objectClass=user))")
+
+ # Check rename works with extended/alternate DN forms
+ ldb.rename("<SID=" + get_string(ldb.schema_format_value("objectSID", res_user[0]["objectSID"][0])) + ">", "cn=ldaptestUSER3,cn=users," + self.base_dn)
+
+ # Testing ldb.search for (&(cn=ldaptestuser3)(objectClass=user))
+ res = ldb.search(expression="(&(cn=ldaptestuser3)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestuser3)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestUSER3,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestUSER3")
+ self.assertEqual(str(res[0]["name"]), "ldaptestUSER3")
+
+ #"Testing ldb.search for (&(&(cn=ldaptestuser3)(userAccountControl=*))(objectClass=user))"
+ res = ldb.search(expression="(&(&(cn=ldaptestuser3)(userAccountControl=*))(objectClass=user))")
+ self.assertEqual(len(res), 1, "(&(&(cn=ldaptestuser3)(userAccountControl=*))(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestUSER3,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestUSER3")
+ self.assertEqual(str(res[0]["name"]), "ldaptestUSER3")
+
+ #"Testing ldb.search for (&(&(cn=ldaptestuser3)(userAccountControl=546))(objectClass=user))"
+ res = ldb.search(expression="(&(&(cn=ldaptestuser3)(userAccountControl=546))(objectClass=user))")
+ self.assertEqual(len(res), 1, "(&(&(cn=ldaptestuser3)(userAccountControl=546))(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestUSER3,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestUSER3")
+ self.assertEqual(str(res[0]["name"]), "ldaptestUSER3")
+
+ #"Testing ldb.search for (&(&(cn=ldaptestuser3)(userAccountControl=547))(objectClass=user))"
+ res = ldb.search(expression="(&(&(cn=ldaptestuser3)(userAccountControl=547))(objectClass=user))")
+ self.assertEqual(len(res), 0, "(&(&(cn=ldaptestuser3)(userAccountControl=547))(objectClass=user))")
+
+ # Testing ldb.search for (dn=CN=ldaptestUSER3,CN=Users," + self.base_dn + ") - should not work
+ res = ldb.search(expression="(dn=CN=ldaptestUSER3,CN=Users," + self.base_dn + ")")
+ self.assertEqual(len(res), 0, "Could find (dn=CN=ldaptestUSER3,CN=Users," + self.base_dn + ")")
+
+ # Testing ldb.search for (distinguishedName=CN=ldaptestUSER3,CN=Users," + self.base_dn + ")
+ res = ldb.search(expression="(distinguishedName=CN=ldaptestUSER3,CN=Users," + self.base_dn + ")")
+ self.assertEqual(len(res), 1, "Could not find (distinguishedName=CN=ldaptestUSER3,CN=Users," + self.base_dn + ")")
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestUSER3,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestUSER3")
+ self.assertEqual(str(res[0]["name"]), "ldaptestUSER3")
+
+ # ensure we cannot add it again
+ try:
+ ldb.add({"dn": "cn=ldaptestuser3,cn=userS," + self.base_dn,
+ "objectClass": "user",
+ "cn": "LDAPtestUSER3"})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # rename back
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, "cn=ldaptestuser2,cn=users," + self.base_dn)
+
+ # ensure we cannot rename it twice
+ try:
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn,
+ "cn=ldaptestuser2,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ # ensure can now use that name
+ ldb.add({"dn": "cn=ldaptestuser3,cn=users," + self.base_dn,
+ "objectClass": "user",
+ "cn": "LDAPtestUSER3"})
+
+ # ensure we now cannot rename
+ try:
+ ldb.rename("cn=ldaptestuser2,cn=users," + self.base_dn, "cn=ldaptestuser3,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+ try:
+ ldb.rename("cn=ldaptestuser3,cn=users,%s" % self.base_dn, "cn=ldaptestuser3,%s" % ldb.get_config_basedn())
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertTrue(num in (71, 64))
+
+ ldb.rename("cn=ldaptestuser3,cn=users," + self.base_dn, "cn=ldaptestuser5,cn=users," + self.base_dn)
+
+ ldb.delete("cn=ldaptestuser5,cn=users," + self.base_dn)
+
+ delete_force(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+
+ ldb.rename("cn=ldaptestgroup,cn=users," + self.base_dn, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+
+ # Testing subtree renames
+
+ ldb.add({"dn": "cn=ldaptestcontainer," + self.base_dn,
+ "objectClass": "container"})
+
+ ldb.add({"dn": "CN=ldaptestuser4,CN=ldaptestcontainer," + self.base_dn,
+ "objectClass": "user",
+ "cn": "LDAPtestUSER4"})
+
+ # Here we don't enforce these hard "description" constraints
+ ldb.modify_ldif("""
+dn: cn=ldaptestcontainer,""" + self.base_dn + """
+changetype: modify
+replace: description
+description: desc1
+description: desc2
+""")
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestgroup2,cn=users,""" + self.base_dn + """
+changetype: modify
+add: member
+member: cn=ldaptestuser4,cn=ldaptestcontainer,""" + self.base_dn + """
+member: cn=ldaptestcomputer,cn=computers,""" + self.base_dn + """
+member: cn=ldaptestuser2,cn=users,""" + self.base_dn + """
+""")
+
+ # Testing ldb.rename of cn=ldaptestcontainer," + self.base_dn + " to cn=ldaptestcontainer2," + self.base_dn
+ ldb.rename("CN=ldaptestcontainer," + self.base_dn, "CN=ldaptestcontainer2," + self.base_dn)
+
+ # Testing ldb.search for (&(cn=ldaptestuser4)(objectClass=user))
+ res = ldb.search(expression="(&(cn=ldaptestuser4)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestuser4)(objectClass=user))")
+
+ # Testing subtree ldb.search for (&(cn=ldaptestuser4)(objectClass=user)) in (just renamed from) cn=ldaptestcontainer," + self.base_dn
+ try:
+ res = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ expression="(&(cn=ldaptestuser4)(objectClass=user))",
+ scope=SCOPE_SUBTREE)
+ self.fail(res)
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ # Testing one-level ldb.search for (&(cn=ldaptestuser4)(objectClass=user)) in (just renamed from) cn=ldaptestcontainer," + self.base_dn
+ try:
+ res = ldb.search("cn=ldaptestcontainer," + self.base_dn,
+ expression="(&(cn=ldaptestuser4)(objectClass=user))", scope=SCOPE_ONELEVEL)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ # Testing ldb.search for (&(cn=ldaptestuser4)(objectClass=user)) in renamed container"
+ res = ldb.search("cn=ldaptestcontainer2," + self.base_dn, expression="(&(cn=ldaptestuser4)(objectClass=user))", scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestuser4)(objectClass=user)) under cn=ldaptestcontainer2," + self.base_dn)
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn))
+ self.assertEqual(str(res[0]["memberOf"][0]).upper(), ("CN=ldaptestgroup2,CN=Users," + self.base_dn).upper())
+
+ time.sleep(4)
+
+ # Testing ldb.search for (&(member=CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn + ")(objectclass=group)) to check subtree renames and linked attributes"
+ res = ldb.search(self.base_dn, expression="(&(member=CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn + ")(objectclass=group))", scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1, "Could not find (&(member=CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn + ")(objectclass=group)), perhaps linked attributes are not consistent with subtree renames?")
+
+ # Testing ldb.rename (into itself) of cn=ldaptestcontainer2," + self.base_dn + " to cn=ldaptestcontainer,cn=ldaptestcontainer2," + self.base_dn
+ try:
+ ldb.rename("cn=ldaptestcontainer2," + self.base_dn, "cn=ldaptestcontainer,cn=ldaptestcontainer2," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Testing ldb.rename (into non-existent container) of cn=ldaptestcontainer2," + self.base_dn + " to cn=ldaptestcontainer,cn=ldaptestcontainer3," + self.base_dn
+ try:
+ ldb.rename("cn=ldaptestcontainer2," + self.base_dn, "cn=ldaptestcontainer,cn=ldaptestcontainer3," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertTrue(num in (ERR_UNWILLING_TO_PERFORM, ERR_OTHER))
+
+ # Testing delete (should fail, not a leaf node) of renamed cn=ldaptestcontainer2," + self.base_dn
+ try:
+ ldb.delete("cn=ldaptestcontainer2," + self.base_dn)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NOT_ALLOWED_ON_NON_LEAF)
+
+ # Testing base ldb.search for CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn
+ res = ldb.search(expression="(objectclass=*)", base=("CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn), scope=SCOPE_BASE)
+ self.assertEqual(len(res), 1)
+ res = ldb.search(expression="(cn=ldaptestuser40)", base=("CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn), scope=SCOPE_BASE)
+ self.assertEqual(len(res), 0)
+
+ # Testing one-level ldb.search for (&(cn=ldaptestuser4)(objectClass=user)) in cn=ldaptestcontainer2," + self.base_dn
+ res = ldb.search(expression="(&(cn=ldaptestuser4)(objectClass=user))", base=("cn=ldaptestcontainer2," + self.base_dn), scope=SCOPE_ONELEVEL)
+ self.assertEqual(len(res), 1)
+
+ # Testing one-level ldb.search for (&(cn=ldaptestuser4)(objectClass=user)) in cn=ldaptestcontainer2," + self.base_dn
+ res = ldb.search(expression="(&(cn=ldaptestuser4)(objectClass=user))", base=("cn=ldaptestcontainer2," + self.base_dn), scope=SCOPE_SUBTREE)
+ self.assertEqual(len(res), 1)
+
+ # Testing delete of subtree renamed "+("CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn)
+ ldb.delete(("CN=ldaptestuser4,CN=ldaptestcontainer2," + self.base_dn))
+ # Testing delete of renamed cn=ldaptestcontainer2," + self.base_dn
+ ldb.delete("cn=ldaptestcontainer2," + self.base_dn)
+
+ ldb.add({"dn": "cn=ldaptestutf8user èùéìòà,cn=users," + self.base_dn, "objectClass": "user"})
+
+ ldb.add({"dn": "cn=ldaptestutf8user2 èùéìòà,cn=users," + self.base_dn, "objectClass": "user"})
+
+ # Testing ldb.search for (&(cn=ldaptestuser)(objectClass=user))"
+ res = ldb.search(expression="(&(cn=ldaptestuser)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestuser)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestuser,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestuser")
+ self.assertEqual(str(res[0]["name"]), "ldaptestuser")
+ self.assertEqual(set(res[0]["objectClass"]), set([b"top", b"person", b"organizationalPerson", b"user"]))
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertEqual(str(res[0]["objectCategory"]), ("CN=Person,%s" % ldb.get_schema_basedn()))
+ self.assertEqual(int(res[0]["sAMAccountType"][0]), ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE)
+ self.assertEqual(str(res[0]["memberOf"][0]).upper(), ("CN=ldaptestgroup2,CN=Users," + self.base_dn).upper())
+ self.assertEqual(len(res[0]["memberOf"]), 1)
+
+ # Testing ldb.search for (&(cn=ldaptestuser)(objectCategory=cn=person,%s))" % ldb.get_schema_basedn()
+ res2 = ldb.search(expression="(&(cn=ldaptestuser)(objectCategory=cn=person,%s))" % ldb.get_schema_basedn())
+ self.assertEqual(len(res2), 1, "Could not find (&(cn=ldaptestuser)(objectCategory=cn=person,%s))" % ldb.get_schema_basedn())
+
+ self.assertEqual(res[0].dn, res2[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestuser)(objectCategory=PerSon))"
+ res3 = ldb.search(expression="(&(cn=ldaptestuser)(objectCategory=PerSon))")
+ self.assertEqual(len(res3), 1, "Could not find (&(cn=ldaptestuser)(objectCategory=PerSon)): matched %d" % len(res3))
+
+ self.assertEqual(res[0].dn, res3[0].dn)
+
+ if gc_ldb is not None:
+ # Testing ldb.search for (&(cn=ldaptestuser)(objectCategory=PerSon)) in Global Catalog"
+ res3gc = gc_ldb.search(expression="(&(cn=ldaptestuser)(objectCategory=PerSon))")
+ self.assertEqual(len(res3gc), 1)
+
+ self.assertEqual(res[0].dn, res3gc[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestuser)(objectCategory=PerSon)) in with 'phantom root' control"
+
+ if gc_ldb is not None:
+ res3control = gc_ldb.search(self.base_dn, expression="(&(cn=ldaptestuser)(objectCategory=PerSon))", scope=SCOPE_SUBTREE, attrs=["cn"], controls=["search_options:1:2"])
+ self.assertEqual(len(res3control), 1, "Could not find (&(cn=ldaptestuser)(objectCategory=PerSon)) in Global Catalog")
+
+ self.assertEqual(res[0].dn, res3control[0].dn)
+
+ ldb.delete(res[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestcomputer)(objectClass=user))"
+ res = ldb.search(expression="(&(cn=ldaptestcomputer)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestuser)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestcomputer,CN=Computers," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestcomputer")
+ self.assertEqual(str(res[0]["name"]), "ldaptestcomputer")
+ self.assertEqual(set(res[0]["objectClass"]), set([b"top", b"person", b"organizationalPerson", b"user", b"computer"]))
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertEqual(str(res[0]["objectCategory"]), ("CN=Computer,%s" % ldb.get_schema_basedn()))
+ self.assertEqual(int(res[0]["primaryGroupID"][0]), DOMAIN_RID_DOMAIN_MEMBERS)
+ self.assertEqual(int(res[0]["sAMAccountType"][0]), ATYPE_WORKSTATION_TRUST)
+ self.assertEqual(int(res[0]["userAccountControl"][0]), UF_WORKSTATION_TRUST_ACCOUNT | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE)
+ self.assertEqual(str(res[0]["memberOf"][0]).upper(), ("CN=ldaptestgroup2,CN=Users," + self.base_dn).upper())
+ self.assertEqual(len(res[0]["memberOf"]), 1)
+
+ # Testing ldb.search for (&(cn=ldaptestcomputer)(objectCategory=cn=computer,%s))" % ldb.get_schema_basedn()
+ res2 = ldb.search(expression="(&(cn=ldaptestcomputer)(objectCategory=cn=computer,%s))" % ldb.get_schema_basedn())
+ self.assertEqual(len(res2), 1, "Could not find (&(cn=ldaptestcomputer)(objectCategory=cn=computer,%s))" % ldb.get_schema_basedn())
+
+ self.assertEqual(res[0].dn, res2[0].dn)
+
+ if gc_ldb is not None:
+ # Testing ldb.search for (&(cn=ldaptestcomputer)(objectCategory=cn=computer,%s)) in Global Catalog" % gc_ldb.get_schema_basedn()
+ res2gc = gc_ldb.search(expression="(&(cn=ldaptestcomputer)(objectCategory=cn=computer,%s))" % gc_ldb.get_schema_basedn())
+ self.assertEqual(len(res2gc), 1, "Could not find (&(cn=ldaptestcomputer)(objectCategory=cn=computer,%s)) In Global Catalog" % gc_ldb.get_schema_basedn())
+
+ self.assertEqual(res[0].dn, res2gc[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestcomputer)(objectCategory=compuTER))"
+ res3 = ldb.search(expression="(&(cn=ldaptestcomputer)(objectCategory=compuTER))")
+ self.assertEqual(len(res3), 1, "Could not find (&(cn=ldaptestcomputer)(objectCategory=compuTER))")
+
+ self.assertEqual(res[0].dn, res3[0].dn)
+
+ if gc_ldb is not None:
+ # Testing ldb.search for (&(cn=ldaptestcomputer)(objectCategory=compuTER)) in Global Catalog"
+ res3gc = gc_ldb.search(expression="(&(cn=ldaptestcomputer)(objectCategory=compuTER))")
+ self.assertEqual(len(res3gc), 1, "Could not find (&(cn=ldaptestcomputer)(objectCategory=compuTER)) in Global Catalog")
+
+ self.assertEqual(res[0].dn, res3gc[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestcomp*r)(objectCategory=compuTER))"
+ res4 = ldb.search(expression="(&(cn=ldaptestcomp*r)(objectCategory=compuTER))")
+ self.assertEqual(len(res4), 1, "Could not find (&(cn=ldaptestcomp*r)(objectCategory=compuTER))")
+
+ self.assertEqual(res[0].dn, res4[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestcomput*)(objectCategory=compuTER))"
+ res5 = ldb.search(expression="(&(cn=ldaptestcomput*)(objectCategory=compuTER))")
+ self.assertEqual(len(res5), 1, "Could not find (&(cn=ldaptestcomput*)(objectCategory=compuTER))")
+
+ self.assertEqual(res[0].dn, res5[0].dn)
+
+ # Testing ldb.search for (&(cn=*daptestcomputer)(objectCategory=compuTER))"
+ res6 = ldb.search(expression="(&(cn=*daptestcomputer)(objectCategory=compuTER))")
+ self.assertEqual(len(res6), 1, "Could not find (&(cn=*daptestcomputer)(objectCategory=compuTER))")
+
+ self.assertEqual(res[0].dn, res6[0].dn)
+
+ ldb.delete("<GUID=" + get_string(ldb.schema_format_value("objectGUID", res[0]["objectGUID"][0])) + ">")
+
+ # Testing ldb.search for (&(cn=ldaptest2computer)(objectClass=user))"
+ res = ldb.search(expression="(&(cn=ldaptest2computer)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptest2computer)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), "CN=ldaptest2computer,CN=Computers," + self.base_dn)
+ self.assertEqual(str(res[0]["cn"]), "ldaptest2computer")
+ self.assertEqual(str(res[0]["name"]), "ldaptest2computer")
+ self.assertEqual(list(res[0]["objectClass"]), [b"top", b"person", b"organizationalPerson", b"user", b"computer"])
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertEqual(str(res[0]["objectCategory"][0]), "CN=Computer,%s" % ldb.get_schema_basedn())
+ self.assertEqual(int(res[0]["sAMAccountType"][0]), ATYPE_WORKSTATION_TRUST)
+ self.assertEqual(int(res[0]["userAccountControl"][0]), UF_WORKSTATION_TRUST_ACCOUNT)
+
+ ldb.delete("<SID=" + get_string(ldb.schema_format_value("objectSID", res[0]["objectSID"][0])) + ">")
+
+ attrs = ["cn", "name", "objectClass", "objectGUID", "objectSID", "whenCreated", "nTSecurityDescriptor", "memberOf", "allowedAttributes", "allowedAttributesEffective"]
+ # Testing ldb.search for (&(cn=ldaptestUSer2)(objectClass=user))"
+ res_user = ldb.search(self.base_dn, expression="(&(cn=ldaptestUSer2)(objectClass=user))", scope=SCOPE_SUBTREE, attrs=attrs)
+ self.assertEqual(len(res_user), 1, "Could not find (&(cn=ldaptestUSer2)(objectClass=user))")
+
+ self.assertEqual(str(res_user[0].dn), ("CN=ldaptestuser2,CN=Users," + self.base_dn))
+ self.assertEqual(str(res_user[0]["cn"]), "ldaptestuser2")
+ self.assertEqual(str(res_user[0]["name"]), "ldaptestuser2")
+ self.assertEqual(list(res_user[0]["objectClass"]), [b"top", b"person", b"organizationalPerson", b"user"])
+ self.assertTrue("objectSid" in res_user[0])
+ self.assertTrue("objectGUID" in res_user[0])
+ self.assertTrue("whenCreated" in res_user[0])
+ self.assertTrue("nTSecurityDescriptor" in res_user[0])
+ self.assertTrue("allowedAttributes" in res_user[0])
+ self.assertTrue("allowedAttributesEffective" in res_user[0])
+ self.assertEqual(str(res_user[0]["memberOf"][0]).upper(), ("CN=ldaptestgroup2,CN=Users," + self.base_dn).upper())
+
+ ldaptestuser2_sid = res_user[0]["objectSid"][0]
+ ldaptestuser2_guid = res_user[0]["objectGUID"][0]
+
+ attrs = ["cn", "name", "objectClass", "objectGUID", "objectSID", "whenCreated", "nTSecurityDescriptor", "member", "allowedAttributes", "allowedAttributesEffective"]
+ # Testing ldb.search for (&(cn=ldaptestgroup2)(objectClass=group))"
+ res = ldb.search(self.base_dn, expression="(&(cn=ldaptestgroup2)(objectClass=group))", scope=SCOPE_SUBTREE, attrs=attrs)
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestgroup2)(objectClass=group))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestgroup2,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestgroup2")
+ self.assertEqual(str(res[0]["name"]), "ldaptestgroup2")
+ self.assertEqual(list(res[0]["objectClass"]), [b"top", b"group"])
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("objectSid" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertTrue("nTSecurityDescriptor" in res[0])
+ self.assertTrue("allowedAttributes" in res[0])
+ self.assertTrue("allowedAttributesEffective" in res[0])
+ memberUP = []
+ for m in res[0]["member"]:
+ memberUP.append(str(m).upper())
+ self.assertTrue(("CN=ldaptestuser2,CN=Users," + self.base_dn).upper() in memberUP)
+
+ res = ldb.search(self.base_dn, expression="(&(cn=ldaptestgroup2)(objectClass=group))", scope=SCOPE_SUBTREE, attrs=attrs, controls=["extended_dn:1:1"])
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestgroup2)(objectClass=group))")
+
+ print(res[0]["member"])
+ memberUP = []
+ for m in res[0]["member"]:
+ memberUP.append(str(m).upper())
+ print(("<GUID=" + get_string(ldb.schema_format_value("objectGUID", ldaptestuser2_guid)) + ">;<SID=" + get_string(ldb.schema_format_value("objectSid", ldaptestuser2_sid)) + ">;CN=ldaptestuser2,CN=Users," + self.base_dn).upper())
+
+ self.assertTrue(("<GUID=" + get_string(ldb.schema_format_value("objectGUID", ldaptestuser2_guid)) + ">;<SID=" + get_string(ldb.schema_format_value("objectSid", ldaptestuser2_sid)) + ">;CN=ldaptestuser2,CN=Users," + self.base_dn).upper() in memberUP)
+
+ # Quicktest for linked attributes"
+ ldb.modify_ldif("""
+dn: cn=ldaptestgroup2,cn=users,""" + self.base_dn + """
+changetype: modify
+replace: member
+member: CN=ldaptestuser2,CN=Users,""" + self.base_dn + """
+member: CN=ldaptestutf8user èùéìòà,CN=Users,""" + self.base_dn + """
+""")
+
+ ldb.modify_ldif("""
+dn: <GUID=""" + get_string(ldb.schema_format_value("objectGUID", res[0]["objectGUID"][0])) + """>
+changetype: modify
+replace: member
+member: CN=ldaptestutf8user èùéìòà,CN=Users,""" + self.base_dn + """
+""")
+
+ ldb.modify_ldif("""
+dn: <SID=""" + get_string(ldb.schema_format_value("objectSid", res[0]["objectSid"][0])) + """>
+changetype: modify
+delete: member
+""")
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestgroup2,cn=users,""" + self.base_dn + """
+changetype: modify
+add: member
+member: <GUID=""" + get_string(ldb.schema_format_value("objectGUID", res[0]["objectGUID"][0])) + """>
+member: CN=ldaptestutf8user èùéìòà,CN=Users,""" + self.base_dn + """
+""")
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestgroup2,cn=users,""" + self.base_dn + """
+changetype: modify
+replace: member
+""")
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestgroup2,cn=users,""" + self.base_dn + """
+changetype: modify
+add: member
+member: <SID=""" + get_string(ldb.schema_format_value("objectSid", res_user[0]["objectSid"][0])) + """>
+member: CN=ldaptestutf8user èùéìòà,CN=Users,""" + self.base_dn + """
+""")
+
+ ldb.modify_ldif("""
+dn: cn=ldaptestgroup2,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: member
+member: CN=ldaptestutf8user èùéìòà,CN=Users,""" + self.base_dn + """
+""")
+
+ res = ldb.search(self.base_dn, expression="(&(cn=ldaptestgroup2)(objectClass=group))", scope=SCOPE_SUBTREE, attrs=attrs)
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestgroup2)(objectClass=group))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestgroup2,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["member"][0]), ("CN=ldaptestuser2,CN=Users," + self.base_dn))
+ self.assertEqual(len(res[0]["member"]), 1)
+
+ ldb.delete(("CN=ldaptestuser2,CN=Users," + self.base_dn))
+
+ time.sleep(4)
+
+ attrs = ["cn", "name", "objectClass", "objectGUID", "whenCreated", "nTSecurityDescriptor", "member"]
+ # Testing ldb.search for (&(cn=ldaptestgroup2)(objectClass=group)) to check linked delete"
+ res = ldb.search(self.base_dn, expression="(&(cn=ldaptestgroup2)(objectClass=group))", scope=SCOPE_SUBTREE, attrs=attrs)
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestgroup2)(objectClass=group)) to check linked delete")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestgroup2,CN=Users," + self.base_dn))
+ self.assertTrue("member" not in res[0])
+
+ # Testing ldb.search for (&(cn=ldaptestutf8user ÈÙÉÌÒÀ)(objectClass=user))"
+ res = ldb.search(expression="(&(cn=ldaptestutf8user ÈÙÉÌÒÀ)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestutf8user ÈÙÉÌÒÀ)(objectClass=user))")
+ res = ldb.search(expression="(&(cn=ldaptestutf8user èùéìòà)(objectclass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestutf8user ÈÙÉÌÒÀ)(objectClass=user))")
+
+ self.assertEqual(str(res[0].dn), ("CN=ldaptestutf8user èùéìòà,CN=Users," + self.base_dn))
+ self.assertEqual(str(res[0]["cn"]), "ldaptestutf8user èùéìòà")
+ self.assertEqual(str(res[0]["name"]), "ldaptestutf8user èùéìòà")
+ self.assertEqual(list(res[0]["objectClass"]), [b"top", b"person", b"organizationalPerson", b"user"])
+ self.assertTrue("objectGUID" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+
+ # delete "ldaptestutf8user"
+ ldb.delete(res[0].dn)
+
+ # Testing ldb.search for (&(cn=ldaptestutf8user2*)(objectClass=user))"
+ res = ldb.search(expression="(&(cn=ldaptestutf8user2*)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestutf8user2*)(objectClass=user))")
+
+ # Testing ldb.search for (&(cn=ldaptestutf8user2 ÈÙÉÌÒÀ)(objectClass=user))"
+ res = ldb.search(expression="(&(cn=ldaptestutf8user2 ÈÙÉÌÒÀ)(objectClass=user))")
+ self.assertEqual(len(res), 1, "Could not find (&(cn=ldaptestutf8user2 ÈÙÉÌÒÀ)(objectClass=user))")
+
+ # delete "ldaptestutf8user2 "
+ ldb.delete(res[0].dn)
+
+ ldb.delete(("CN=ldaptestgroup2,CN=Users," + self.base_dn))
+
+ # Testing that we can't get at the configuration DN from the main search base"
+ res = ldb.search(self.base_dn, expression="objectClass=crossRef", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertEqual(len(res), 0)
+
+ # Testing that we can get at the configuration DN from the main search base on the LDAP port with the 'phantom root' search_options control"
+ res = ldb.search(self.base_dn, expression="objectClass=crossRef", scope=SCOPE_SUBTREE, attrs=["cn"], controls=["search_options:1:2"])
+ self.assertTrue(len(res) > 0)
+
+ if gc_ldb is not None:
+ # Testing that we can get at the configuration DN from the main search base on the GC port with the search_options control == 0"
+
+ res = gc_ldb.search(self.base_dn, expression="objectClass=crossRef", scope=SCOPE_SUBTREE, attrs=["cn"], controls=["search_options:1:0"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing that we do find configuration elements in the global catlog"
+ res = gc_ldb.search(self.base_dn, expression="objectClass=crossRef", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing that we do find configuration elements and user elements at the same time"
+ res = gc_ldb.search(self.base_dn, expression="(|(objectClass=crossRef)(objectClass=person))", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing that we do find configuration elements in the global catlog, with the configuration basedn"
+ res = gc_ldb.search(self.configuration_dn, expression="objectClass=crossRef", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing that we can get at the configuration DN on the main LDAP port"
+ res = ldb.search(self.configuration_dn, expression="objectClass=crossRef", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing objectCategory canonicalisation"
+ res = ldb.search(self.configuration_dn, expression="objectCategory=ntDsDSA", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0, "Didn't find any records with objectCategory=ntDsDSA")
+ self.assertTrue(len(res) != 0)
+
+ res = ldb.search(self.configuration_dn, expression="objectCategory=CN=ntDs-DSA," + self.schema_dn, scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0, "Didn't find any records with objectCategory=CN=ntDs-DSA," + self.schema_dn)
+ self.assertTrue(len(res) != 0)
+
+ # Testing objectClass attribute order on "+ self.base_dn
+ res = ldb.search(expression="objectClass=domain", base=self.base_dn,
+ scope=SCOPE_BASE, attrs=["objectClass"])
+ self.assertEqual(len(res), 1)
+
+ self.assertEqual(list(res[0]["objectClass"]), [b"top", b"domain", b"domainDNS"])
+
+ # check enumeration
+
+ # Testing ldb.search for objectCategory=person"
+ res = ldb.search(self.base_dn, expression="objectCategory=person", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing ldb.search for objectCategory=person with domain scope control"
+ res = ldb.search(self.base_dn, expression="objectCategory=person", scope=SCOPE_SUBTREE, attrs=["cn"], controls=["domain_scope:1"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing ldb.search for objectCategory=user"
+ res = ldb.search(self.base_dn, expression="objectCategory=user", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing ldb.search for objectCategory=user with domain scope control"
+ res = ldb.search(self.base_dn, expression="objectCategory=user", scope=SCOPE_SUBTREE, attrs=["cn"], controls=["domain_scope:1"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing ldb.search for objectCategory=group"
+ res = ldb.search(self.base_dn, expression="objectCategory=group", scope=SCOPE_SUBTREE, attrs=["cn"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing ldb.search for objectCategory=group with domain scope control"
+ res = ldb.search(self.base_dn, expression="objectCategory=group", scope=SCOPE_SUBTREE, attrs=["cn"], controls=["domain_scope:1"])
+ self.assertTrue(len(res) > 0)
+
+ # Testing creating a user with the posixAccount objectClass"
+ self.ldb.add_ldif("""dn: cn=posixuser,CN=Users,%s
+objectClass: top
+objectClass: person
+objectClass: posixAccount
+objectClass: user
+objectClass: organizationalPerson
+cn: posixuser
+uid: posixuser
+sn: posixuser
+uidNumber: 10126
+gidNumber: 10126
+homeDirectory: /home/posixuser
+loginShell: /bin/bash
+gecos: Posix User;;;
+description: A POSIX user""" % (self.base_dn))
+
+ # Testing removing the posixAccount objectClass from an existing user"
+ self.ldb.modify_ldif("""dn: cn=posixuser,CN=Users,%s
+changetype: modify
+delete: objectClass
+objectClass: posixAccount""" % (self.base_dn))
+
+ # Testing adding the posixAccount objectClass to an existing user"
+ self.ldb.modify_ldif("""dn: cn=posixuser,CN=Users,%s
+changetype: modify
+add: objectClass
+objectClass: posixAccount""" % (self.base_dn))
+
+ delete_force(self.ldb, "cn=posixuser,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser3,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser4,cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser4,cn=ldaptestcontainer2," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser5,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptest2computer,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcomputer3,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestutf8user èùéìòà,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestutf8user2 èùéìòà,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcontainer," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcontainer2," + self.base_dn)
+
+ def test_security_descriptor_add(self):
+ """ Testing ldb.add_ldif() for nTSecurityDescriptor """
+ user_name = "testdescriptoruser1"
+ user_dn = "CN=%s,CN=Users,%s" % (user_name, self.base_dn)
+ #
+ # Test an empty security descriptor (naturally this shouldn't work)
+ #
+ delete_force(self.ldb, user_dn)
+ try:
+ self.ldb.add({"dn": user_dn,
+ "objectClass": "user",
+ "sAMAccountName": user_name,
+ "nTSecurityDescriptor": []})
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ finally:
+ delete_force(self.ldb, user_dn)
+ #
+ # Test add_ldif() with SDDL security descriptor input
+ #
+ try:
+ sddl = "O:DUG:DUD:PAI(A;;RPWP;;;AU)S:PAI"
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name + """
+nTSecurityDescriptor: """ + sddl)
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(desc_sddl, sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+ #
+ # Test add_ldif() with BASE64 security descriptor
+ #
+ try:
+ sddl = "O:DUG:DUD:PAI(A;;RPWP;;;AU)S:PAI"
+ desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ desc_binary = ndr_pack(desc)
+ desc_base64 = base64.b64encode(desc_binary).decode('utf8')
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name + """
+nTSecurityDescriptor:: """ + desc_base64)
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(desc_sddl, sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+
+ def test_security_descriptor_add_neg(self):
+ """Test add_ldif() with BASE64 security descriptor input using WRONG domain SID
+ Negative test
+ """
+ user_name = "testdescriptoruser1"
+ user_dn = "CN=%s,CN=Users,%s" % (user_name, self.base_dn)
+ delete_force(self.ldb, user_dn)
+ try:
+ sddl = "O:DUG:DUD:AI(A;;RPWP;;;AU)S:PAI"
+ desc = security.descriptor.from_sddl(sddl, security.dom_sid('S-1-5-21'))
+ desc_base64 = base64.b64encode(ndr_pack(desc)).decode('utf8')
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name + """
+nTSecurityDescriptor:: """ + desc_base64)
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ self.assertTrue("nTSecurityDescriptor" in res[0])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertTrue("O:S-1-5-21-513G:S-1-5-21-513D:AI(A;;RPWP;;;AU)" in desc_sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+
+ def test_security_descriptor_modify(self):
+ """ Testing ldb.modify_ldif() for nTSecurityDescriptor """
+ user_name = "testdescriptoruser2"
+ user_dn = "CN=%s,CN=Users,%s" % (user_name, self.base_dn)
+ #
+ # Test an empty security descriptor (naturally this shouldn't work)
+ #
+ delete_force(self.ldb, user_dn)
+ self.ldb.add({"dn": user_dn,
+ "objectClass": "user",
+ "sAMAccountName": user_name})
+
+ m = Message()
+ m.dn = Dn(ldb, user_dn)
+ m["nTSecurityDescriptor"] = MessageElement([], FLAG_MOD_ADD,
+ "nTSecurityDescriptor")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, user_dn)
+ m["nTSecurityDescriptor"] = MessageElement([], FLAG_MOD_REPLACE,
+ "nTSecurityDescriptor")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, user_dn)
+ m["nTSecurityDescriptor"] = MessageElement([], FLAG_MOD_DELETE,
+ "nTSecurityDescriptor")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, user_dn)
+ #
+ # Test modify_ldif() with SDDL security descriptor input
+ # Add ACE to the original descriptor test
+ #
+ try:
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name)
+ # Modify descriptor
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ sddl = desc_sddl[:desc_sddl.find("(")] + "(A;;RPWP;;;AU)" + desc_sddl[desc_sddl.find("("):]
+ mod = """
+dn: """ + user_dn + """
+changetype: modify
+replace: nTSecurityDescriptor
+nTSecurityDescriptor: """ + sddl
+ self.ldb.modify_ldif(mod)
+ # Read modified descriptor
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(desc_sddl, sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+ #
+ # Test modify_ldif() with SDDL security descriptor input
+ # New descriptor test
+ #
+ try:
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name)
+ # Modify descriptor
+ sddl = "O:DUG:DUD:PAI(A;;RPWP;;;AU)S:PAI"
+ mod = """
+dn: """ + user_dn + """
+changetype: modify
+replace: nTSecurityDescriptor
+nTSecurityDescriptor: """ + sddl
+ self.ldb.modify_ldif(mod)
+ # Read modified descriptor
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(desc_sddl, sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+ #
+ # Test modify_ldif() with BASE64 security descriptor input
+ # Add ACE to the original descriptor test
+ #
+ try:
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name)
+ # Modify descriptor
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ sddl = desc_sddl[:desc_sddl.find("(")] + "(A;;RPWP;;;AU)" + desc_sddl[desc_sddl.find("("):]
+ desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ desc_base64 = base64.b64encode(ndr_pack(desc)).decode('utf8')
+ mod = """
+dn: """ + user_dn + """
+changetype: modify
+replace: nTSecurityDescriptor
+nTSecurityDescriptor:: """ + desc_base64
+ self.ldb.modify_ldif(mod)
+ # Read modified descriptor
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(desc_sddl, sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+ #
+ # Test modify_ldif() with BASE64 security descriptor input
+ # New descriptor test
+ #
+ try:
+ delete_force(self.ldb, user_dn)
+ self.ldb.add_ldif("""
+dn: """ + user_dn + """
+objectclass: user
+sAMAccountName: """ + user_name)
+ # Modify descriptor
+ sddl = "O:DUG:DUD:PAI(A;;RPWP;;;AU)S:PAI"
+ desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ desc_base64 = base64.b64encode(ndr_pack(desc)).decode('utf8')
+ mod = """
+dn: """ + user_dn + """
+changetype: modify
+replace: nTSecurityDescriptor
+nTSecurityDescriptor:: """ + desc_base64
+ self.ldb.modify_ldif(mod)
+ # Read modified descriptor
+ res = self.ldb.search(base=user_dn, attrs=["nTSecurityDescriptor"])
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc)
+ desc_sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(desc_sddl, sddl)
+ finally:
+ delete_force(self.ldb, user_dn)
+
+ def test_dsheuristics(self):
+ """Tests the 'dSHeuristics' attribute"""
+ # Tests the 'dSHeuristics' attribute"
+
+ # Get the current value to restore it later
+ dsheuristics = self.ldb.get_dsheuristics()
+ # Perform the length checks: for each decade (except the 0th) we need
+ # the first index to be the number. This goes till the 9th one, beyond
+ # there does not seem to be another limitation.
+ try:
+ dshstr = ""
+ for i in range(1, 11):
+ # This is in the range
+ self.ldb.set_dsheuristics(dshstr + "x")
+ self.ldb.set_dsheuristics(dshstr + "xxxxx")
+ dshstr = dshstr + "xxxxxxxxx"
+ if i < 10:
+ # Not anymore in the range, new decade specifier needed
+ try:
+ self.ldb.set_dsheuristics(dshstr + "x")
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ dshstr = dshstr + str(i)
+ else:
+ # There does not seem to be an upper limit
+ self.ldb.set_dsheuristics(dshstr + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")
+ # apart from the above, all char values are accepted
+ self.ldb.set_dsheuristics("123ABC-+!1asdfg@#^")
+ self.assertEqual(self.ldb.get_dsheuristics(), b"123ABC-+!1asdfg@#^")
+ finally:
+ # restore old value
+ self.ldb.set_dsheuristics(dsheuristics)
+
+ def test_ldapControlReturn(self):
+ """Testing that if we request a control that return a control it
+ really return something"""
+ res = self.ldb.search(attrs=["cn"],
+ controls=["paged_results:1:10"])
+ self.assertEqual(len(res.controls), 1)
+ self.assertEqual(res.controls[0].oid, "1.2.840.113556.1.4.319")
+ s = str(res.controls[0])
+
+ def test_operational(self):
+ """Tests operational attributes"""
+ # Tests operational attributes"
+
+ res = self.ldb.search(self.base_dn, scope=SCOPE_BASE,
+ attrs=["createTimeStamp", "modifyTimeStamp",
+ "structuralObjectClass", "whenCreated",
+ "whenChanged"])
+ self.assertEqual(len(res), 1)
+ self.assertTrue("createTimeStamp" in res[0])
+ self.assertTrue("modifyTimeStamp" in res[0])
+ self.assertTrue("structuralObjectClass" in res[0])
+ self.assertTrue("whenCreated" in res[0])
+ self.assertTrue("whenChanged" in res[0])
+
+ def test_timevalues1(self):
+ """Tests possible syntax of time attributes"""
+
+ user_name = "testtimevaluesuser1"
+ user_dn = "CN=%s,CN=Users,%s" % (user_name, self.base_dn)
+
+ delete_force(self.ldb, user_dn)
+ self.ldb.add({"dn": user_dn,
+ "objectClass": "user",
+ "sAMAccountName": user_name})
+
+ #
+ # We check the following values:
+ #
+ # 370101000000Z => 20370101000000.0Z
+ # 20370102000000.*Z => 20370102000000.0Z
+ #
+ ext = ["Z", ".0Z", ".Z", ".000Z", ".RandomIgnoredCharacters...987654321Z"]
+ for i in range(0, len(ext)):
+ v_raw = "203701%02d000000" % (i + 1)
+ if ext[i] == "Z":
+ v_set = v_raw[2:] + ext[i]
+ else:
+ v_set = v_raw + ext[i]
+ v_get = v_raw + ".0Z"
+
+ m = Message()
+ m.dn = Dn(ldb, user_dn)
+ m["msTSExpireDate"] = MessageElement([v_set],
+ FLAG_MOD_REPLACE,
+ "msTSExpireDate")
+ self.ldb.modify(m)
+
+ res = self.ldb.search(base=user_dn, scope=SCOPE_BASE, attrs=["msTSExpireDate"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("msTSExpireDate" in res[0])
+ self.assertTrue(len(res[0]["msTSExpireDate"]) == 1)
+ self.assertEqual(str(res[0]["msTSExpireDate"][0]), v_get)
+
+ def test_ldapSearchNoAttributes(self):
+ """Testing ldap search with no attributes"""
+
+ user_name = "testemptyattributesuser"
+ user_dn = "CN=%s,%s" % (user_name, self.base_dn)
+ delete_force(self.ldb, user_dn)
+
+ self.ldb.add({"dn": user_dn,
+ "objectClass": "user",
+ "sAMAccountName": user_name})
+
+ res = self.ldb.search(user_dn, scope=SCOPE_BASE, attrs=[])
+ delete_force(self.ldb, user_dn)
+
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]), 0)
+
+
+class BaseDnTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(BaseDnTests, self).setUp()
+ self.ldb = ldb
+
+ def test_rootdse_attrs(self):
+ """Testing for all rootDSE attributes"""
+ res = self.ldb.search("", scope=SCOPE_BASE, attrs=[])
+ self.assertEqual(len(res), 1)
+
+ def test_highestcommittedusn(self):
+ """Testing for highestCommittedUSN"""
+ res = self.ldb.search("", scope=SCOPE_BASE, attrs=["highestCommittedUSN"])
+ self.assertEqual(len(res), 1)
+ self.assertTrue(int(res[0]["highestCommittedUSN"][0]) != 0)
+
+ def test_netlogon(self):
+ """Testing for netlogon via LDAP"""
+ res = self.ldb.search("", scope=SCOPE_BASE, attrs=["netlogon"])
+ self.assertEqual(len(res), 0)
+
+ def test_netlogon_highestcommitted_usn(self):
+ """Testing for netlogon and highestCommittedUSN via LDAP"""
+ res = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["netlogon", "highestCommittedUSN"])
+ self.assertEqual(len(res), 0)
+
+ def test_namingContexts(self):
+ """Testing for namingContexts in rootDSE"""
+ res = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["namingContexts", "defaultNamingContext", "schemaNamingContext", "configurationNamingContext"])
+ self.assertEqual(len(res), 1)
+
+ ncs = set([])
+ for nc in res[0]["namingContexts"]:
+ self.assertTrue(nc not in ncs)
+ ncs.add(nc)
+
+ self.assertTrue(res[0]["defaultNamingContext"][0] in ncs)
+ self.assertTrue(res[0]["configurationNamingContext"][0] in ncs)
+ self.assertTrue(res[0]["schemaNamingContext"][0] in ncs)
+
+ def test_serverPath(self):
+ """Testing the server paths in rootDSE"""
+ res = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["dsServiceName", "serverName"])
+ self.assertEqual(len(res), 1)
+
+ self.assertTrue("CN=Servers" in str(res[0]["dsServiceName"][0]))
+ self.assertTrue("CN=Sites" in str(res[0]["dsServiceName"][0]))
+ self.assertTrue("CN=NTDS Settings" in str(res[0]["dsServiceName"][0]))
+ self.assertTrue("CN=Servers" in str(res[0]["serverName"][0]))
+ self.assertTrue("CN=Sites" in str(res[0]["serverName"][0]))
+ self.assertFalse("CN=NTDS Settings" in str(res[0]["serverName"][0]))
+
+ def test_functionality(self):
+ """Testing the server paths in rootDSE"""
+ res = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["forestFunctionality", "domainFunctionality", "domainControllerFunctionality"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]["forestFunctionality"]), 1)
+ self.assertEqual(len(res[0]["domainFunctionality"]), 1)
+ self.assertEqual(len(res[0]["domainControllerFunctionality"]), 1)
+
+ self.assertTrue(int(res[0]["forestFunctionality"][0]) <= int(res[0]["domainFunctionality"][0]))
+ self.assertTrue(int(res[0]["domainControllerFunctionality"][0]) >= int(res[0]["domainFunctionality"][0]))
+
+ res2 = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["dsServiceName", "serverName"])
+ self.assertEqual(len(res2), 1)
+ self.assertEqual(len(res2[0]["dsServiceName"]), 1)
+
+ res3 = self.ldb.search(res2[0]["dsServiceName"][0], scope=SCOPE_BASE, attrs=["msDS-Behavior-Version"])
+ self.assertEqual(len(res3), 1)
+ self.assertEqual(len(res3[0]["msDS-Behavior-Version"]), 1)
+ self.assertEqual(int(res[0]["domainControllerFunctionality"][0]), int(res3[0]["msDS-Behavior-Version"][0]))
+
+ res4 = self.ldb.search(ldb.domain_dn(), scope=SCOPE_BASE, attrs=["msDS-Behavior-Version"])
+ self.assertEqual(len(res4), 1)
+ self.assertEqual(len(res4[0]["msDS-Behavior-Version"]), 1)
+ self.assertEqual(int(res[0]["domainFunctionality"][0]), int(res4[0]["msDS-Behavior-Version"][0]))
+
+ res5 = self.ldb.search("cn=partitions,%s" % ldb.get_config_basedn(), scope=SCOPE_BASE, attrs=["msDS-Behavior-Version"])
+ self.assertEqual(len(res5), 1)
+ self.assertEqual(len(res5[0]["msDS-Behavior-Version"]), 1)
+ self.assertEqual(int(res[0]["forestFunctionality"][0]), int(res5[0]["msDS-Behavior-Version"][0]))
+
+ def test_dnsHostname(self):
+ """Testing the DNS hostname in rootDSE"""
+ res = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["dnsHostName", "serverName"])
+ self.assertEqual(len(res), 1)
+
+ res2 = self.ldb.search(res[0]["serverName"][0], scope=SCOPE_BASE,
+ attrs=["dNSHostName"])
+ self.assertEqual(len(res2), 1)
+
+ self.assertEqual(res[0]["dnsHostName"][0], res2[0]["dNSHostName"][0])
+
+ def test_ldapServiceName(self):
+ """Testing the ldap service name in rootDSE"""
+ res = self.ldb.search("", scope=SCOPE_BASE,
+ attrs=["ldapServiceName", "dnsHostName"])
+ self.assertEqual(len(res), 1)
+ self.assertTrue("ldapServiceName" in res[0])
+ self.assertTrue("dnsHostName" in res[0])
+
+ (hostname, _, dns_domainname) = str(res[0]["dnsHostName"][0]).partition(".")
+
+ given = str(res[0]["ldapServiceName"][0])
+ expected = "%s:%s$@%s" % (dns_domainname.lower(), hostname.lower(), dns_domainname.upper())
+ self.assertEqual(given, expected)
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+ldb = SamDB(host, credentials=creds, session_info=system_session(lp), lp=lp)
+if "tdb://" not in host:
+ gc_ldb = Ldb("%s:3268" % host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+else:
+ gc_ldb = None
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ldap_modify_order.py b/source4/dsdb/tests/python/ldap_modify_order.py
new file mode 100644
index 0000000..80c4a3a
--- /dev/null
+++ b/source4/dsdb/tests/python/ldap_modify_order.py
@@ -0,0 +1,371 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008-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/>.
+
+import optparse
+import sys
+import os
+from itertools import permutations
+import traceback
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+import samba.getopt as options
+
+from samba.auth import system_session
+from ldb import SCOPE_BASE, LdbError
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+from samba.samdb import SamDB
+
+from samba.tests import delete_force
+
+TEST_DATA_DIR = os.path.join(
+ os.path.dirname(__file__),
+ 'testdata')
+
+LDB_STRERR = {}
+def _build_ldb_strerr():
+ import ldb
+ for k, v in vars(ldb).items():
+ if k.startswith('ERR_') and isinstance(v, int):
+ LDB_STRERR[v] = k
+
+_build_ldb_strerr()
+
+
+class ModifyOrderTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super().setUp()
+ self.admin_dsdb = get_dsdb(admin_creds)
+ self.base_dn = self.admin_dsdb.domain_dn()
+
+ def delete_object(self, dn):
+ delete_force(self.admin_dsdb, dn)
+
+ def get_user_dn(self, name):
+ return "CN=%s,CN=Users,%s" % (name, self.base_dn)
+
+ def _test_modify_order(self,
+ start_attrs,
+ mod_attrs,
+ extra_search_attrs=(),
+ name=None):
+ if name is None:
+ name = traceback.extract_stack()[-2][2][5:]
+
+ if opts.normal_user:
+ name += '-non-admin'
+ username = "user123"
+ password = "pass123@#$@#"
+ self.admin_dsdb.newuser(username, password)
+ self.addCleanup(self.delete_object, self.get_user_dn(username))
+ mod_creds = self.insta_creds(template=admin_creds,
+ username=username,
+ userpass=password)
+ else:
+ mod_creds = admin_creds
+
+ mod_dsdb = get_dsdb(mod_creds)
+ sig = []
+ op_lut = ['', 'add', 'replace', 'delete']
+
+ search_attrs = set(extra_search_attrs)
+ lines = [name, "initial attrs:"]
+ for k, v in start_attrs:
+ lines.append("%20s: %r" % (k, v))
+ search_attrs.add(k)
+
+ for k, v, op in mod_attrs:
+ search_attrs.add(k)
+
+ search_attrs = sorted(search_attrs)
+ header = "\n".join(lines)
+ sig.append(header)
+
+ clusters = {}
+ for i, attrs in enumerate(permutations(mod_attrs)):
+ # for each permutation we construct a string describing the
+ # requested operations, and a string describing the result
+ # (which may be an exception). The we cluster the
+ # attribute strings by their results.
+ dn = "cn=ldaptest_%s_%d,cn=users,%s" % (name, i, self.base_dn)
+ m = Message()
+ m.dn = Dn(self.admin_dsdb, dn)
+
+ # We are using Message objects here for add (rather than the
+ # more convenient dict) because we maybe care about the order
+ # in which attributes are added.
+
+ for k, v in start_attrs:
+ m[k] = MessageElement(v, 0, k)
+
+ self.admin_dsdb.add(m)
+ self.addCleanup(self.delete_object, dn)
+
+ m = Message()
+ m.dn = Dn(mod_dsdb, dn)
+
+ attr_lines = []
+ for k, v, op in attrs:
+ if v is None:
+ v = dn
+ m[k] = MessageElement(v, op, k)
+ attr_lines.append("%16s %-8s %s" % (k, op_lut[op], v))
+
+ attr_str = '\n'.join(attr_lines)
+
+ try:
+ mod_dsdb.modify(m)
+ except LdbError as e:
+ err, _ = e.args
+ s = LDB_STRERR.get(err, "unknown error")
+ result_str = "%s (%d)" % (s, err)
+ else:
+ res = self.admin_dsdb.search(base=dn, scope=SCOPE_BASE,
+ attrs=search_attrs)
+
+ lines = []
+ for k, v in sorted(dict(res[0]).items()):
+ if k != "dn" or k in extra_search_attrs:
+ lines.append("%20s: %r" % (k, sorted(v)))
+
+ result_str = '\n'.join(lines)
+
+ clusters.setdefault(result_str, []).append(attr_str)
+
+ for s, attrs in sorted(clusters.items()):
+ sig.extend([
+ "== result ===[%3d]=======================" % len(attrs),
+ s,
+ "-- operations ---------------------------"])
+ for a in attrs:
+ sig.append(a)
+ sig.append("-" * 34)
+
+ sig = '\n'.join(sig).replace(self.base_dn, "{base dn}")
+
+ if opts.verbose:
+ print(sig)
+
+ if opts.rewrite_ground_truth:
+ f = open(os.path.join(TEST_DATA_DIR, name + '.expected'), 'w')
+ f.write(sig)
+ f.close()
+ f = open(os.path.join(TEST_DATA_DIR, name + '.expected'))
+ expected = f.read()
+ f.close()
+
+ self.assertStringsEqual(sig, expected)
+
+ def test_modify_order_mixed(self):
+ start_attrs = [("objectclass", "user"),
+ ("carLicense", ["1", "2", "3"]),
+ ("otherTelephone", "123")]
+
+ mod_attrs = [("carLicense", "3", FLAG_MOD_DELETE),
+ ("carLicense", "4", FLAG_MOD_ADD),
+ ("otherTelephone", "4", FLAG_MOD_REPLACE),
+ ("otherTelephone", "123", FLAG_MOD_DELETE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_mixed2(self):
+ start_attrs = [("objectclass", "user"),
+ ("carLicense", ["1", "2", "3"]),
+ ("ipPhone", "123")]
+
+ mod_attrs = [("carLicense", "3", FLAG_MOD_DELETE),
+ ("carLicense", "4", FLAG_MOD_ADD),
+ ("ipPhone", "4", FLAG_MOD_REPLACE),
+ ("ipPhone", "123", FLAG_MOD_DELETE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_telephone(self):
+ start_attrs = [("objectclass", "user"),
+ ("otherTelephone", "123")]
+
+ mod_attrs = [("carLicense", "3", FLAG_MOD_REPLACE),
+ ("carLicense", "4", FLAG_MOD_ADD),
+ ("otherTelephone", "4", FLAG_MOD_REPLACE),
+ ("otherTelephone", "4", FLAG_MOD_ADD),
+ ("otherTelephone", "123", FLAG_MOD_DELETE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_telephone_delete_delete(self):
+ start_attrs = [("objectclass", "user"),
+ ("otherTelephone", "123")]
+
+ mod_attrs = [("carLicense", "3", FLAG_MOD_REPLACE),
+ ("carLicense", "4", FLAG_MOD_DELETE),
+ ("otherTelephone", "4", FLAG_MOD_REPLACE),
+ ("otherTelephone", "4", FLAG_MOD_DELETE),
+ ("otherTelephone", "123", FLAG_MOD_DELETE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_objectclass(self):
+ start_attrs = [("objectclass", "user"),
+ ("otherTelephone", "123")]
+
+ mod_attrs = [("objectclass", "computer", FLAG_MOD_REPLACE),
+ ("objectclass", "user", FLAG_MOD_DELETE),
+ ("objectclass", "person", FLAG_MOD_DELETE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_objectclass2(self):
+ start_attrs = [("objectclass", "user")]
+
+ mod_attrs = [("objectclass", "computer", FLAG_MOD_REPLACE),
+ ("objectclass", "user", FLAG_MOD_ADD),
+ ("objectclass", "attributeSchema", FLAG_MOD_REPLACE),
+ ("objectclass", "inetOrgPerson", FLAG_MOD_ADD),
+ ("objectclass", "person", FLAG_MOD_DELETE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_singlevalue(self):
+ start_attrs = [("objectclass", "user"),
+ ("givenName", "a")]
+
+ mod_attrs = [("givenName", "a", FLAG_MOD_REPLACE),
+ ("givenName", ["b", "a"], FLAG_MOD_REPLACE),
+ ("givenName", "b", FLAG_MOD_DELETE),
+ ("givenName", "a", FLAG_MOD_DELETE),
+ ("givenName", "c", FLAG_MOD_ADD)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_inapplicable(self):
+ #attributes that don't go on a user
+ start_attrs = [("objectclass", "user"),
+ ("givenName", "a")]
+
+ mod_attrs = [("dhcpSites", "b", FLAG_MOD_REPLACE),
+ ("dhcpSites", "b", FLAG_MOD_DELETE),
+ ("dhcpSites", "c", FLAG_MOD_ADD)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_sometimes_inapplicable(self):
+ # attributes that don't go on a user, but do on a computer,
+ # which we sometimes change into.
+ start_attrs = [("objectclass", "user"),
+ ("givenName", "a")]
+
+ mod_attrs = [("objectclass", "computer", FLAG_MOD_REPLACE),
+ ("objectclass", "person", FLAG_MOD_DELETE),
+ ("dnsHostName", "b", FLAG_MOD_ADD),
+ ("dnsHostName", "c", FLAG_MOD_REPLACE)]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_account_locality_device(self):
+ # account, locality, and device all take l (locality name) but
+ # only device takes owner. We shouldn't be able to change
+ # objectclass at all.
+ start_attrs = [("objectclass", "account"),
+ ("l", "a")]
+
+ mod_attrs = [("objectclass", ["device", "top"], FLAG_MOD_REPLACE),
+ ("l", "a", FLAG_MOD_DELETE),
+ ("owner", "c", FLAG_MOD_ADD)
+ ]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_container_flags_multivalue(self):
+ # account, locality, and device all take l (locality name)
+ # but only device takes owner
+ start_attrs = [("objectclass", "container"),
+ ("wWWHomePage", "a")]
+
+ mod_attrs = [("flags", ["0", "1"], FLAG_MOD_ADD),
+ ("flags", "65355", FLAG_MOD_ADD),
+ ("flags", "65355", FLAG_MOD_DELETE),
+ ("flags", ["2", "101"], FLAG_MOD_REPLACE),
+ ]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_container_flags(self):
+ #flags should be an integer
+ start_attrs = [("objectclass", "container")]
+
+ mod_attrs = [("flags", "0x6", FLAG_MOD_ADD),
+ ("flags", "5", FLAG_MOD_ADD),
+ ("flags", "101", FLAG_MOD_REPLACE),
+ ("flags", "c", FLAG_MOD_DELETE),
+ ]
+ self._test_modify_order(start_attrs, mod_attrs)
+
+ def test_modify_order_member(self):
+ name = "modify_order_member_other_group"
+
+ dn2 = "cn=%s,%s" % (name, self.base_dn)
+ m = Message()
+ m.dn = Dn(self.admin_dsdb, dn2)
+ self.admin_dsdb.add({"dn": dn2, "objectclass": "group"})
+ self.addCleanup(self.delete_object, dn2)
+
+ start_attrs = [("objectclass", "group"),
+ ("member", dn2)]
+
+ mod_attrs = [("member", None, FLAG_MOD_DELETE),
+ ("member", None, FLAG_MOD_REPLACE),
+ ("member", dn2, FLAG_MOD_DELETE),
+ ("member", None, FLAG_MOD_ADD),
+ ]
+ self._test_modify_order(start_attrs, mod_attrs, ["memberOf"])
+
+
+def get_dsdb(creds=None):
+ if creds is None:
+ creds = admin_creds
+ dsdb = SamDB(host,
+ credentials=creds,
+ session_info=system_session(lp),
+ lp=lp)
+ return dsdb
+
+
+parser = optparse.OptionParser("ldap_modify_order.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+parser.add_option("--rewrite-ground-truth", action="store_true",
+ help="write expected values")
+parser.add_option("-v", "--verbose", action="store_true")
+parser.add_option("--normal-user", action="store_true")
+
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+admin_creds = credopts.get_credentials(lp)
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ldap_schema.py b/source4/dsdb/tests/python/ldap_schema.py
new file mode 100755
index 0000000..e2fceb7
--- /dev/null
+++ b/source4/dsdb/tests/python/ldap_schema.py
@@ -0,0 +1,1597 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This is a port of the original in testprogs/ejs/ldap.js
+
+# Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2008-2011
+# 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/>.
+
+
+import optparse
+import sys
+import time
+import random
+import os
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from ldb import SCOPE_ONELEVEL, SCOPE_BASE, LdbError
+from ldb import ERR_NO_SUCH_OBJECT
+from ldb import ERR_UNWILLING_TO_PERFORM
+from ldb import ERR_ENTRY_ALREADY_EXISTS
+from ldb import ERR_CONSTRAINT_VIOLATION
+from ldb import ERR_OBJECT_CLASS_VIOLATION
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_REPLACE
+from samba.samdb import SamDB
+from samba.dsdb import DS_DOMAIN_FUNCTION_2003
+from samba.tests import delete_force
+from samba.ndr import ndr_unpack
+from samba.dcerpc import drsblobs
+
+parser = optparse.OptionParser("ldap_schema.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+class SchemaTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(SchemaTests, self).setUp()
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp, options=ldb_options)
+ self.base_dn = self.ldb.domain_dn()
+ self.schema_dn = self.ldb.get_schema_basedn().get_linearized()
+
+ def test_generated_schema(self):
+ """Testing we can read the generated schema via LDAP"""
+ res = self.ldb.search("cn=aggregate," + self.schema_dn, scope=SCOPE_BASE,
+ attrs=["objectClasses", "attributeTypes", "dITContentRules"])
+ self.assertEqual(len(res), 1)
+ self.assertTrue("dITContentRules" in res[0])
+ self.assertTrue("objectClasses" in res[0])
+ self.assertTrue("attributeTypes" in res[0])
+
+ def test_generated_schema_is_operational(self):
+ """Testing we don't get the generated schema via LDAP by default"""
+ # Must keep the "*" form
+ res = self.ldb.search("cn=aggregate," + self.schema_dn, scope=SCOPE_BASE,
+ attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertFalse("dITContentRules" in res[0])
+ self.assertFalse("objectClasses" in res[0])
+ self.assertFalse("attributeTypes" in res[0])
+
+ def test_schemaUpdateNow(self):
+ """Testing schemaUpdateNow"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: 1.3.6.1.4.1.7165.4.6.1.6.1.""" + rand + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+ # We must do a schemaUpdateNow otherwise it's not 100% sure that the schema
+ # will contain the new attribute
+ ldif = """
+dn:
+changetype: modify
+add: schemaUpdateNow
+schemaUpdateNow: 1
+"""
+ self.ldb.modify_ldif(ldif)
+
+ # Search for created attribute
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (attr_name, self.schema_dn), scope=SCOPE_BASE,
+ attrs=["lDAPDisplayName", "schemaIDGUID", "msDS-IntID"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["lDAPDisplayName"][0]), attr_ldap_display_name)
+ self.assertTrue("schemaIDGUID" in res[0])
+ if "msDS-IntId" in res[0]:
+ msDS_IntId = int(res[0]["msDS-IntId"][0])
+ if msDS_IntId < 0:
+ msDS_IntId += (1 << 32)
+ else:
+ msDS_IntId = None
+
+ class_name = "test-Class" + time.strftime("%s", time.gmtime())
+ class_ldap_display_name = class_name.replace("-", "")
+
+ # First try to create a class with a wrong "defaultObjectCategory"
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+defaultObjectCategory: CN=_
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.6.1.""" + str(random.randint(1, 100000)) + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+systemFlags: 16
+rDNAttID: cn
+systemMustContain: cn
+systemMustContain: """ + attr_ldap_display_name + """
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail()
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.6.2.""" + str(random.randint(1, 100000)) + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+systemFlags: 16
+rDNAttID: cn
+systemMustContain: cn
+systemMustContain: """ + attr_ldap_display_name + """
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ # Search for created objectclass
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (class_name, self.schema_dn), scope=SCOPE_BASE,
+ attrs=["lDAPDisplayName", "defaultObjectCategory", "schemaIDGUID", "distinguishedName"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["lDAPDisplayName"][0]), class_ldap_display_name)
+ self.assertEqual(res[0]["defaultObjectCategory"][0], res[0]["distinguishedName"][0])
+ self.assertTrue("schemaIDGUID" in res[0])
+
+ ldif = """
+dn:
+changetype: modify
+add: schemaUpdateNow
+schemaUpdateNow: 1
+"""
+ self.ldb.modify_ldif(ldif)
+
+ object_name = "obj" + time.strftime("%s", time.gmtime())
+
+ ldif = """
+dn: CN=%s,CN=Users,%s""" % (object_name, self.base_dn) + """
+objectClass: organizationalPerson
+objectClass: person
+objectClass: """ + class_ldap_display_name + """
+objectClass: top
+cn: """ + object_name + """
+instanceType: 4
+objectCategory: CN=%s,%s""" % (class_name, self.schema_dn) + """
+distinguishedName: CN=%s,CN=Users,%s""" % (object_name, self.base_dn) + """
+name: """ + object_name + """
+""" + attr_ldap_display_name + """: test
+"""
+ self.ldb.add_ldif(ldif)
+
+ # Search for created object
+ obj_res = self.ldb.search("cn=%s,cn=Users,%s" % (object_name, self.base_dn), scope=SCOPE_BASE, attrs=["replPropertyMetaData"])
+
+ self.assertEqual(len(obj_res), 1)
+ self.assertTrue("replPropertyMetaData" in obj_res[0])
+ val = obj_res[0]["replPropertyMetaData"][0]
+ repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, val)
+
+ # Windows 2000 functional level won't have this. It is too
+ # hard to work it out from the prefixmap however, so we skip
+ # this test in that case.
+ if msDS_IntId is not None:
+ found = False
+ for o in repl.ctr.array:
+ if o.attid == msDS_IntId:
+ found = True
+ break
+ self.assertTrue(found, "Did not find 0x%08x in replPropertyMetaData" % msDS_IntId)
+ # Delete the object
+ delete_force(self.ldb, "cn=%s,cn=Users,%s" % (object_name, self.base_dn))
+
+ def test_subClassOf(self):
+ """ Testing usage of custom child classSchema
+ """
+
+ class_name = "my-Class" + time.strftime("%s", time.gmtime())
+ class_ldap_display_name = class_name.replace("-", "")
+
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.6.3.""" + str(random.randint(1, 100000)) + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalUnit
+systemFlags: 16
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ # Search for created objectclass
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (class_name, self.schema_dn), scope=SCOPE_BASE,
+ attrs=["lDAPDisplayName", "defaultObjectCategory",
+ "schemaIDGUID", "distinguishedName"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["lDAPDisplayName"][0]), class_ldap_display_name)
+ self.assertEqual(res[0]["defaultObjectCategory"][0], res[0]["distinguishedName"][0])
+ self.assertTrue("schemaIDGUID" in res[0])
+
+ ldif = """
+dn:
+changetype: modify
+add: schemaUpdateNow
+schemaUpdateNow: 1
+"""
+ self.ldb.modify_ldif(ldif)
+
+ object_name = "org" + time.strftime("%s", time.gmtime())
+
+ ldif = """
+dn: OU=%s,%s""" % (object_name, self.base_dn) + """
+objectClass: """ + class_ldap_display_name + """
+ou: """ + object_name + """
+instanceType: 4
+"""
+ self.ldb.add_ldif(ldif)
+
+ # Search for created object
+ res = []
+ res = self.ldb.search("ou=%s,%s" % (object_name, self.base_dn), scope=SCOPE_BASE, attrs=["dn"])
+ self.assertEqual(len(res), 1)
+ # Delete the object
+ delete_force(self.ldb, "ou=%s,%s" % (object_name, self.base_dn))
+
+ def test_duplicate_attributeID(self):
+ """Testing creating a duplicate attribute"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.2." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add duplicate attributeID value")
+ except LdbError as e2:
+ (enum, estr) = e2.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_duplicate_attributeID_governsID(self):
+ """Testing creating a duplicate attribute and class"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.3." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+governsId: """ + attributeID + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add duplicate governsID conflicting with attributeID value")
+ except LdbError as e3:
+ (enum, estr) = e3.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_duplicate_cn(self):
+ """Testing creating a duplicate attribute"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.4." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """.1
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add attribute with duplicate CN")
+ except LdbError as e4:
+ (enum, estr) = e4.args
+ self.assertEqual(enum, ERR_ENTRY_ALREADY_EXISTS)
+
+ def test_duplicate_implicit_ldapdisplayname(self):
+ """Testing creating a duplicate attribute ldapdisplayname"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.5." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+ldapDisplayName: """ + attr_ldap_display_name + """
+attributeId: """ + attributeID + """.1
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add attribute with duplicate of the implicit ldapDisplayName")
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_duplicate_explicit_ldapdisplayname(self):
+ """Testing creating a duplicate attribute ldapdisplayname"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.6." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+ldapDisplayName: """ + attr_ldap_display_name + """
+attributeId: """ + attributeID + """.1
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add attribute with duplicate ldapDisplayName")
+ except LdbError as e6:
+ (enum, estr) = e6.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_duplicate_explicit_ldapdisplayname_with_class(self):
+ """Testing creating a duplicate attribute ldapdisplayname between
+ and attribute and a class"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.7." + rand
+ governsID = "1.3.6.1.4.1.7165.4.6.2.6.4." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+ldapDisplayName: """ + attr_ldap_display_name + """
+governsID: """ + governsID + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add class with duplicate ldapDisplayName")
+ except LdbError as e7:
+ (enum, estr) = e7.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_duplicate_via_rename_ldapdisplayname(self):
+ """Testing creating a duplicate attribute ldapdisplayname"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.8." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+ldapDisplayName: """ + attr_ldap_display_name + """dup
+attributeId: """ + attributeID + """.1
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+changetype: modify
+replace: ldapDisplayName
+ldapDisplayName: """ + attr_ldap_display_name + """
+-
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to modify schema to have attribute with duplicate ldapDisplayName")
+ except LdbError as e8:
+ (enum, estr) = e8.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_duplicate_via_rename_attributeID(self):
+ """Testing creating a duplicate attributeID"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.9." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """dup
+adminDisplayName: """ + attr_name + """dup
+cn: """ + attr_name + """-dup
+ldapDisplayName: """ + attr_ldap_display_name + """dup
+attributeId: """ + attributeID + """.1
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s-dup,%s""" % (attr_name, self.schema_dn) + """
+changetype: modify
+replace: attributeId
+attributeId: """ + attributeID + """
+-
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to modify schema to have attribute with duplicate attributeID")
+ except LdbError as e9:
+ (enum, estr) = e9.args
+ self.assertEqual(enum, ERR_CONSTRAINT_VIOLATION)
+
+ def test_remove_ldapdisplayname(self):
+ """Testing removing the ldapdisplayname"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.10." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+changetype: modify
+replace: ldapDisplayName
+-
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to remove the ldapdisplayname")
+ except LdbError as e10:
+ (enum, estr) = e10.args
+ self.assertEqual(enum, ERR_OBJECT_CLASS_VIOLATION)
+
+ def test_rename_ldapdisplayname(self):
+ """Testing renaming ldapdisplayname"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.11." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+changetype: modify
+replace: ldapDisplayName
+ldapDisplayName: """ + attr_ldap_display_name + """2
+-
+"""
+ self.ldb.modify_ldif(ldif)
+
+ def test_change_attributeID(self):
+ """Testing change the attributeID"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.12." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+changetype: modify
+replace: attributeID
+attributeId: """ + attributeID + """.1
+
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to modify schema to have different attributeID")
+ except LdbError as e11:
+ (enum, estr) = e11.args
+ self.assertEqual(enum, ERR_CONSTRAINT_VIOLATION)
+
+ def test_change_attributeID_same(self):
+ """Testing change the attributeID to the same value"""
+ rand = str(random.randint(1, 100000))
+ attr_name = "test-Attr" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.13." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+attributeSyntax: 2.5.5.12
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+changetype: modify
+replace: attributeID
+attributeId: """ + attributeID + """
+
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to modify schema to have the same attributeID")
+ except LdbError as e12:
+ (enum, estr) = e12.args
+ self.assertEqual(enum, ERR_CONSTRAINT_VIOLATION)
+
+ def test_generated_linkID(self):
+ """
+ Test that we automatically generate a linkID if the
+ OID "1.2.840.113556.1.2.50" is given as the linkID
+ of a new attribute, and that we don't get/can't add
+ duplicate linkIDs. Also test that we can add a backlink
+ by providing the attributeID or ldapDisplayName of
+ a forwards link in the linkID attribute.
+ """
+
+ # linkID generation isn't available before 2003
+ res = self.ldb.search(base="", expression="", scope=SCOPE_BASE,
+ attrs=["domainControllerFunctionality"])
+ self.assertEqual(len(res), 1)
+ dc_level = int(res[0]["domainControllerFunctionality"][0])
+ if dc_level < DS_DOMAIN_FUNCTION_2003:
+ return
+
+ rand = str(random.randint(1, 100000))
+
+ attr_name_1 = "test-generated-linkID" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name_1 = attr_name_1.replace("-", "")
+ attributeID_1 = "1.3.6.1.4.1.7165.4.6.1.6.16." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name_1, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name_1 + """
+adminDisplayName: """ + attr_name_1 + """
+cn: """ + attr_name_1 + """
+attributeId: """ + attributeID_1 + """
+linkID: 1.2.840.113556.1.2.50
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name_1 + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e13:
+ (enum, estr) = e13.args
+ self.fail(estr)
+
+ attr_name_2 = "test-generated-linkID-2" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name_2 = attr_name_2.replace("-", "")
+ attributeID_2 = "1.3.6.1.4.1.7165.4.6.1.6.17." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name_2, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name_2 + """
+adminDisplayName: """ + attr_name_2 + """
+cn: """ + attr_name_2 + """
+attributeId: """ + attributeID_2 + """
+linkID: 1.2.840.113556.1.2.50
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name_2 + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e14:
+ (enum, estr) = e14.args
+ self.fail(estr)
+
+ res = self.ldb.search("CN=%s,%s" % (attr_name_1, self.schema_dn),
+ scope=SCOPE_BASE,
+ attrs=["linkID"])
+ self.assertEqual(len(res), 1)
+ linkID_1 = int(res[0]["linkID"][0])
+
+ res = self.ldb.search("CN=%s,%s" % (attr_name_2, self.schema_dn),
+ scope=SCOPE_BASE,
+ attrs=["linkID"])
+ self.assertEqual(len(res), 1)
+ linkID_2 = int(res[0]["linkID"][0])
+
+ # 0 should never be generated as a linkID
+ self.assertFalse(linkID_1 == 0)
+ self.assertFalse(linkID_2 == 0)
+
+ # The generated linkID should always be even, because
+ # it should assume we're adding a forward link.
+ self.assertTrue(linkID_1 % 2 == 0)
+ self.assertTrue(linkID_2 % 2 == 0)
+
+ self.assertFalse(linkID_1 == linkID_2)
+
+ # This is only necessary against Windows, since we depend
+ # on the previously added links in the next ones and Windows
+ # won't refresh the schema as we add them.
+ ldif = """
+dn:
+changetype: modify
+replace: schemaupdatenow
+schemaupdatenow: 1
+"""
+ self.ldb.modify_ldif(ldif)
+
+ # If we add a new link with the same linkID, it should fail
+ attr_name = "test-generated-linkID-duplicate" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.18." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+linkID: """ + str(linkID_1) + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add duplicate linkID value")
+ except LdbError as e15:
+ (enum, estr) = e15.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ # If we add another attribute with the attributeID or lDAPDisplayName
+ # of a forward link in its linkID field, it should add as a backlink
+
+ attr_name_3 = "test-generated-linkID-backlink" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name_3 = attr_name_3.replace("-", "")
+ attributeID_3 = "1.3.6.1.4.1.7165.4.6.1.6.19." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name_3, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name_3 + """
+adminDisplayName: """ + attr_name_3 + """
+cn: """ + attr_name_3 + """
+attributeId: """ + attributeID_3 + """
+linkID: """ + str(linkID_1 + 1) + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name_3 + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e16:
+ (enum, estr) = e16.args
+ self.fail(estr)
+
+ res = self.ldb.search("CN=%s,%s" % (attr_name_3, self.schema_dn),
+ scope=SCOPE_BASE,
+ attrs=["linkID"])
+ self.assertEqual(len(res), 1)
+ linkID = int(res[0]["linkID"][0])
+ self.assertEqual(linkID, linkID_1 + 1)
+
+ attr_name_4 = "test-generated-linkID-backlink-2" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name_4 = attr_name_4.replace("-", "")
+ attributeID_4 = "1.3.6.1.4.1.7165.4.6.1.6.20." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name_4, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name_4 + """
+adminDisplayName: """ + attr_name_4 + """
+cn: """ + attr_name_4 + """
+attributeId: """ + attributeID_4 + """
+linkID: """ + attr_ldap_display_name_2 + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name_4 + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e17:
+ (enum, estr) = e17.args
+ self.fail(estr)
+
+ res = self.ldb.search("CN=%s,%s" % (attr_name_4, self.schema_dn),
+ scope=SCOPE_BASE,
+ attrs=["linkID"])
+ self.assertEqual(len(res), 1)
+ linkID = int(res[0]["linkID"][0])
+ self.assertEqual(linkID, linkID_2 + 1)
+
+ # If we then try to add another backlink in the same way
+ # for the same forwards link, we should fail.
+
+ attr_name = "test-generated-linkID-backlink-duplicate" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.21." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+linkID: """ + attributeID_1 + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add duplicate backlink")
+ except LdbError as e18:
+ (enum, estr) = e18.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ # If we try to supply the attributeID or ldapDisplayName
+ # of an existing backlink in the linkID field of a new link,
+ # it should fail.
+
+ attr_name = "test-generated-linkID-backlink-invalid" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.22." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+linkID: """ + attributeID_3 + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add backlink of backlink")
+ except LdbError as e19:
+ (enum, estr) = e19.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ attr_name = "test-generated-linkID-backlink-invalid-2" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.23." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+linkID: """ + attr_ldap_display_name_4 + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add backlink of backlink")
+ except LdbError as e20:
+ (enum, estr) = e20.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_generated_mAPIID(self):
+ """
+ Test that we automatically generate a mAPIID if the
+ OID "1.2.840.113556.1.2.49" is given as the mAPIID
+ of a new attribute, and that we don't get/can't add
+ duplicate mAPIIDs.
+ """
+
+ rand = str(random.randint(1, 100000))
+
+ attr_name_1 = "test-generated-mAPIID" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name_1 = attr_name_1.replace("-", "")
+ attributeID_1 = "1.3.6.1.4.1.7165.4.6.1.6.24." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name_1, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name_1 + """
+adminDisplayName: """ + attr_name_1 + """
+cn: """ + attr_name_1 + """
+attributeId: """ + attributeID_1 + """
+mAPIID: 1.2.840.113556.1.2.49
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name_1 + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e21:
+ (enum, estr) = e21.args
+ self.fail(estr)
+
+ res = self.ldb.search("CN=%s,%s" % (attr_name_1, self.schema_dn),
+ scope=SCOPE_BASE,
+ attrs=["mAPIID"])
+ self.assertEqual(len(res), 1)
+ mAPIID_1 = int(res[0]["mAPIID"][0])
+
+ ldif = """
+dn:
+changetype: modify
+replace: schemaupdatenow
+schemaupdatenow: 1
+"""
+ self.ldb.modify_ldif(ldif)
+
+ # If we add a new attribute with the same mAPIID, it should fail
+ attr_name = "test-generated-mAPIID-duplicate" + time.strftime("%s", time.gmtime()) + "-" + rand
+ attr_ldap_display_name = attr_name.replace("-", "")
+ attributeID = "1.3.6.1.4.1.7165.4.6.1.6.25." + rand
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: """ + attributeID + """
+mAPIID: """ + str(mAPIID_1) + """
+attributeSyntax: 2.5.5.1
+ldapDisplayName: """ + attr_ldap_display_name + """
+omSyntax: 127
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+
+ try:
+ self.ldb.add_ldif(ldif)
+ self.fail("Should have failed to add duplicate mAPIID value")
+ except LdbError as e22:
+ (enum, estr) = e22.args
+ self.assertEqual(enum, ERR_UNWILLING_TO_PERFORM)
+
+ def test_change_governsID(self):
+ """Testing change the governsID"""
+ rand = str(random.randint(1, 100000))
+ class_name = "test-Class" + time.strftime("%s", time.gmtime()) + "-" + rand
+ class_ldap_display_name = class_name.replace("-", "")
+ governsID = "1.3.6.1.4.1.7165.4.6.2.6.5." + rand
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: """ + governsID + """
+ldapDisplayName: """ + class_ldap_display_name + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+changetype: modify
+replace: governsID
+governsId: """ + governsID + """.1
+
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to modify schema to have different governsID")
+ except LdbError as e23:
+ (enum, estr) = e23.args
+ self.assertEqual(enum, ERR_CONSTRAINT_VIOLATION)
+
+ def test_change_governsID_same(self):
+ """Testing change the governsID"""
+ rand = str(random.randint(1, 100000))
+ class_name = "test-Class" + time.strftime("%s", time.gmtime()) + "-" + rand
+ class_ldap_display_name = class_name.replace("-", "")
+ governsID = "1.3.6.1.4.1.7165.4.6.2.6.6." + rand
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: """ + governsID + """
+ldapDisplayName: """ + class_ldap_display_name + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+changetype: modify
+replace: governsID
+governsId: """ + governsID + """.1
+
+"""
+ try:
+ self.ldb.modify_ldif(ldif)
+ self.fail("Should have failed to modify schema to have the same governsID")
+ except LdbError as e24:
+ (enum, estr) = e24.args
+ self.assertEqual(enum, ERR_CONSTRAINT_VIOLATION)
+
+
+class SchemaTests_msDS_IntId(samba.tests.TestCase):
+
+ def setUp(self):
+ super(SchemaTests_msDS_IntId, self).setUp()
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp, options=ldb_options)
+ res = self.ldb.search(base="", expression="", scope=SCOPE_BASE,
+ attrs=["schemaNamingContext", "defaultNamingContext",
+ "forestFunctionality"])
+ self.assertEqual(len(res), 1)
+ self.schema_dn = res[0]["schemaNamingContext"][0]
+ self.base_dn = res[0]["defaultNamingContext"][0]
+ self.forest_level = int(res[0]["forestFunctionality"][0])
+
+ def _ldap_schemaUpdateNow(self):
+ ldif = """
+dn:
+changetype: modify
+add: schemaUpdateNow
+schemaUpdateNow: 1
+"""
+ self.ldb.modify_ldif(ldif)
+
+ def _make_obj_names(self, prefix):
+ class_name = prefix + time.strftime("%s", time.gmtime())
+ class_ldap_name = class_name.replace("-", "")
+ class_dn = "CN=%s,%s" % (class_name, self.schema_dn)
+ return (class_name, class_ldap_name, class_dn)
+
+ def _is_schema_base_object(self, ldb_msg):
+ """Test systemFlags for SYSTEM_FLAG_SCHEMA_BASE_OBJECT (16)"""
+ systemFlags = 0
+ if "systemFlags" in ldb_msg:
+ systemFlags = int(ldb_msg["systemFlags"][0])
+ return (systemFlags & 16) != 0
+
+ def _make_attr_ldif(self, attr_name, attr_dn):
+ ldif = """
+dn: """ + attr_dn + """
+objectClass: top
+objectClass: attributeSchema
+adminDescription: """ + attr_name + """
+adminDisplayName: """ + attr_name + """
+cn: """ + attr_name + """
+attributeId: 1.3.6.1.4.1.7165.4.6.1.6.14.""" + str(random.randint(1, 100000)) + """
+attributeSyntax: 2.5.5.12
+omSyntax: 64
+instanceType: 4
+isSingleValued: TRUE
+systemOnly: FALSE
+"""
+ return ldif
+
+ def test_msDS_IntId_on_attr(self):
+ """Testing msDs-IntId creation for Attributes.
+ See MS-ADTS - 3.1.1.Attributes
+
+ This test should verify that:
+ - Creating attribute with 'msDS-IntId' fails with ERR_UNWILLING_TO_PERFORM
+ - Adding 'msDS-IntId' on existing attribute fails with ERR_CONSTRAINT_VIOLATION
+ - Creating attribute with 'msDS-IntId' set and FLAG_SCHEMA_BASE_OBJECT flag
+ set fails with ERR_UNWILLING_TO_PERFORM
+ - Attributes created with FLAG_SCHEMA_BASE_OBJECT not set have
+ 'msDS-IntId' attribute added internally
+ """
+
+ # 1. Create attribute without systemFlags
+ # msDS-IntId should be created if forest functional
+ # level is >= DS_DOMAIN_FUNCTION_2003
+ # and missing otherwise
+ (attr_name, attr_ldap_name, attr_dn) = self._make_obj_names("msDS-IntId-Attr-1-")
+ ldif = self._make_attr_ldif(attr_name, attr_dn)
+
+ # try to add msDS-IntId during Attribute creation
+ ldif_fail = ldif + "msDS-IntId: -1993108831\n"
+ try:
+ self.ldb.add_ldif(ldif_fail)
+ self.fail("Adding attribute with preset msDS-IntId should fail")
+ except LdbError as e25:
+ (num, _) = e25.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # add the new attribute and update schema
+ self.ldb.add_ldif(ldif)
+ self._ldap_schemaUpdateNow()
+
+ # Search for created attribute
+ res = []
+ res = self.ldb.search(attr_dn, scope=SCOPE_BASE,
+ attrs=["lDAPDisplayName", "msDS-IntId", "systemFlags"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["lDAPDisplayName"][0]), attr_ldap_name)
+ if self.forest_level >= DS_DOMAIN_FUNCTION_2003:
+ if self._is_schema_base_object(res[0]):
+ self.assertTrue("msDS-IntId" not in res[0])
+ else:
+ self.assertTrue("msDS-IntId" in res[0])
+ else:
+ self.assertTrue("msDS-IntId" not in res[0])
+
+ msg = Message()
+ msg.dn = Dn(self.ldb, attr_dn)
+ msg["msDS-IntId"] = MessageElement("-1993108831", FLAG_MOD_REPLACE, "msDS-IntId")
+ try:
+ self.ldb.modify(msg)
+ self.fail("Modifying msDS-IntId should return error")
+ except LdbError as e26:
+ (num, _) = e26.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # 2. Create attribute with systemFlags = FLAG_SCHEMA_BASE_OBJECT
+ # msDS-IntId should be created if forest functional
+ # level is >= DS_DOMAIN_FUNCTION_2003
+ # and missing otherwise
+ (attr_name, attr_ldap_name, attr_dn) = self._make_obj_names("msDS-IntId-Attr-2-")
+ ldif = self._make_attr_ldif(attr_name, attr_dn)
+ ldif += "systemFlags: 16\n"
+
+ # try to add msDS-IntId during Attribute creation
+ ldif_fail = ldif + "msDS-IntId: -1993108831\n"
+ try:
+ self.ldb.add_ldif(ldif_fail)
+ self.fail("Adding attribute with preset msDS-IntId should fail")
+ except LdbError as e27:
+ (num, _) = e27.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # add the new attribute and update schema
+ self.ldb.add_ldif(ldif)
+ self._ldap_schemaUpdateNow()
+
+ # Search for created attribute
+ res = []
+ res = self.ldb.search(attr_dn, scope=SCOPE_BASE,
+ attrs=["lDAPDisplayName", "msDS-IntId"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["lDAPDisplayName"][0]), attr_ldap_name)
+ if self.forest_level >= DS_DOMAIN_FUNCTION_2003:
+ if self._is_schema_base_object(res[0]):
+ self.assertTrue("msDS-IntId" not in res[0])
+ else:
+ self.assertTrue("msDS-IntId" in res[0])
+ else:
+ self.assertTrue("msDS-IntId" not in res[0])
+
+ msg = Message()
+ msg.dn = Dn(self.ldb, attr_dn)
+ msg["msDS-IntId"] = MessageElement("-1993108831", FLAG_MOD_REPLACE, "msDS-IntId")
+ try:
+ self.ldb.modify(msg)
+ self.fail("Modifying msDS-IntId should return error")
+ except LdbError as e28:
+ (num, _) = e28.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ def _make_class_ldif(self, class_dn, class_name, sub_oid):
+ ldif = """
+dn: """ + class_dn + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.6.%d.""" % sub_oid + str(random.randint(1, 100000)) + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ return ldif
+
+ def test_msDS_IntId_on_class(self):
+ """Testing msDs-IntId creation for Class
+ Reference: MS-ADTS - 3.1.1.2.4.8 Class classSchema"""
+
+ # 1. Create Class without systemFlags
+ # msDS-IntId should be created if forest functional
+ # level is >= DS_DOMAIN_FUNCTION_2003
+ # and missing otherwise
+ (class_name, class_ldap_name, class_dn) = self._make_obj_names("msDS-IntId-Class-1-")
+ ldif = self._make_class_ldif(class_dn, class_name, 8)
+
+ # try to add msDS-IntId during Class creation
+ ldif_add = ldif + "msDS-IntId: -1993108831\n"
+ self.ldb.add_ldif(ldif_add)
+ self._ldap_schemaUpdateNow()
+
+ res = self.ldb.search(class_dn, scope=SCOPE_BASE, attrs=["msDS-IntId"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["msDS-IntId"][0]), "-1993108831")
+
+ # add a new Class and update schema
+ (class_name, class_ldap_name, class_dn) = self._make_obj_names("msDS-IntId-Class-2-")
+ ldif = self._make_class_ldif(class_dn, class_name, 9)
+
+ self.ldb.add_ldif(ldif)
+ self._ldap_schemaUpdateNow()
+
+ # Search for created Class
+ res = self.ldb.search(class_dn, scope=SCOPE_BASE, attrs=["msDS-IntId"])
+ self.assertEqual(len(res), 1)
+ self.assertFalse("msDS-IntId" in res[0])
+
+ msg = Message()
+ msg.dn = Dn(self.ldb, class_dn)
+ msg["msDS-IntId"] = MessageElement("-1993108831", FLAG_MOD_REPLACE, "msDS-IntId")
+ try:
+ self.ldb.modify(msg)
+ self.fail("Modifying msDS-IntId should return error")
+ except LdbError as e29:
+ (num, _) = e29.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # 2. Create Class with systemFlags = FLAG_SCHEMA_BASE_OBJECT
+ # msDS-IntId should be created if forest functional
+ # level is >= DS_DOMAIN_FUNCTION_2003
+ # and missing otherwise
+ (class_name, class_ldap_name, class_dn) = self._make_obj_names("msDS-IntId-Class-3-")
+ ldif = self._make_class_ldif(class_dn, class_name, 10)
+ ldif += "systemFlags: 16\n"
+
+ # try to add msDS-IntId during Class creation
+ ldif_add = ldif + "msDS-IntId: -1993108831\n"
+ self.ldb.add_ldif(ldif_add)
+
+ res = self.ldb.search(class_dn, scope=SCOPE_BASE, attrs=["msDS-IntId"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["msDS-IntId"][0]), "-1993108831")
+
+ # add the new Class and update schema
+ (class_name, class_ldap_name, class_dn) = self._make_obj_names("msDS-IntId-Class-4-")
+ ldif = self._make_class_ldif(class_dn, class_name, 11)
+ ldif += "systemFlags: 16\n"
+
+ self.ldb.add_ldif(ldif)
+ self._ldap_schemaUpdateNow()
+
+ # Search for created Class
+ res = self.ldb.search(class_dn, scope=SCOPE_BASE, attrs=["msDS-IntId"])
+ self.assertEqual(len(res), 1)
+ self.assertFalse("msDS-IntId" in res[0])
+
+ msg = Message()
+ msg.dn = Dn(self.ldb, class_dn)
+ msg["msDS-IntId"] = MessageElement("-1993108831", FLAG_MOD_REPLACE, "msDS-IntId")
+ try:
+ self.ldb.modify(msg)
+ self.fail("Modifying msDS-IntId should return error")
+ except LdbError as e30:
+ (num, _) = e30.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ res = self.ldb.search(class_dn, scope=SCOPE_BASE, attrs=["msDS-IntId"])
+ self.assertEqual(len(res), 1)
+ self.assertFalse("msDS-IntId" in res[0])
+
+ def test_verify_msDS_IntId(self):
+ """Verify msDS-IntId exists only on attributes without FLAG_SCHEMA_BASE_OBJECT flag set"""
+ count = 0
+ res = self.ldb.search(self.schema_dn, scope=SCOPE_ONELEVEL,
+ expression="objectClass=attributeSchema",
+ attrs=["systemFlags", "msDS-IntId", "attributeID", "cn"])
+ self.assertTrue(len(res) > 1)
+ for ldb_msg in res:
+ if self.forest_level >= DS_DOMAIN_FUNCTION_2003:
+ if self._is_schema_base_object(ldb_msg):
+ self.assertTrue("msDS-IntId" not in ldb_msg)
+ else:
+ # don't assert here as there are plenty of
+ # attributes under w2k8 that are not part of
+ # Base Schema (SYSTEM_FLAG_SCHEMA_BASE_OBJECT flag not set)
+ # has not msDS-IntId attribute set
+ #self.assertTrue("msDS-IntId" in ldb_msg, "msDS-IntId expected on: %s" % ldb_msg.dn)
+ if "msDS-IntId" not in ldb_msg:
+ count = count + 1
+ print("%3d warning: msDS-IntId expected on: %-30s %s" % (count, ldb_msg["attributeID"], ldb_msg["cn"]))
+ else:
+ self.assertTrue("msDS-IntId" not in ldb_msg)
+
+
+class SchemaTests_msDS_isRODC(samba.tests.TestCase):
+
+ def setUp(self):
+ super(SchemaTests_msDS_isRODC, self).setUp()
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp, options=ldb_options)
+ res = self.ldb.search(base="", expression="", scope=SCOPE_BASE, attrs=["defaultNamingContext"])
+ self.assertEqual(len(res), 1)
+ self.base_dn = res[0]["defaultNamingContext"][0]
+
+ def test_objectClass_ntdsdsa(self):
+ res = self.ldb.search(self.base_dn, expression="objectClass=nTDSDSA",
+ attrs=["msDS-isRODC"], controls=["search_options:1:2"])
+ for ldb_msg in res:
+ self.assertTrue("msDS-isRODC" in ldb_msg)
+
+ def test_objectClass_server(self):
+ res = self.ldb.search(self.base_dn, expression="objectClass=server",
+ attrs=["msDS-isRODC"], controls=["search_options:1:2"])
+ for ldb_msg in res:
+ ntds_search_dn = "CN=NTDS Settings,%s" % ldb_msg['dn']
+ try:
+ res_check = self.ldb.search(ntds_search_dn, attrs=["objectCategory"])
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+ print("Server entry %s doesn't have a NTDS settings object" % res[0]['dn'])
+ else:
+ self.assertTrue("objectCategory" in res_check[0])
+ self.assertTrue("msDS-isRODC" in ldb_msg)
+
+ def test_objectClass_computer(self):
+ res = self.ldb.search(self.base_dn, expression="objectClass=computer",
+ attrs=["serverReferenceBL", "msDS-isRODC"], controls=["search_options:1:2"])
+ for ldb_msg in res:
+ if "serverReferenceBL" not in ldb_msg:
+ print("Computer entry %s doesn't have a serverReferenceBL attribute" % ldb_msg['dn'])
+ else:
+ self.assertTrue("msDS-isRODC" in ldb_msg)
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+ldb_options = []
+if host.startswith("ldap://"):
+ # user 'paged_search' module when connecting remotely
+ ldb_options = ["modules:paged_searches"]
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ldap_syntaxes.py b/source4/dsdb/tests/python/ldap_syntaxes.py
new file mode 100755
index 0000000..081c280
--- /dev/null
+++ b/source4/dsdb/tests/python/ldap_syntaxes.py
@@ -0,0 +1,388 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Tests for LDAP syntaxes
+
+import optparse
+import sys
+import time
+import random
+import uuid
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from ldb import SCOPE_BASE, SCOPE_SUBTREE, LdbError
+from ldb import ERR_CONSTRAINT_VIOLATION
+from ldb import ERR_INVALID_ATTRIBUTE_SYNTAX
+from ldb import ERR_ENTRY_ALREADY_EXISTS
+
+import samba.tests
+
+parser = optparse.OptionParser("ldap_syntaxes.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+class SyntaxTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(SyntaxTests, self).setUp()
+ self.ldb = samba.tests.connect_samdb(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.domain_dn()
+ self.schema_dn = self.ldb.get_schema_basedn().get_linearized()
+ self._setup_dn_string_test()
+ self._setup_dn_binary_test()
+
+ def _setup_dn_string_test(self):
+ """Testing DN+String syntax"""
+ attr_name = "test-Attr-DN-String" + time.strftime("%s", time.gmtime())
+ attr_ldap_display_name = attr_name.replace("-", "")
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+ldapDisplayName: """ + attr_ldap_display_name + """
+objectClass: top
+objectClass: attributeSchema
+cn: """ + attr_name + """
+attributeId: 1.3.6.1.4.1.7165.4.6.1.1.""" + str(random.randint(1, 100000)) + """
+attributeSyntax: 2.5.5.14
+omSyntax: 127
+omObjectClass: \x2A\x86\x48\x86\xF7\x14\x01\x01\x01\x0C
+isSingleValued: FALSE
+schemaIdGuid: """ + str(uuid.uuid4()) + """
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ # search for created attribute
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (attr_name, self.schema_dn), scope=SCOPE_BASE, attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(res[0]["lDAPDisplayName"][0], attr_ldap_display_name)
+ self.assertTrue("schemaIDGUID" in res[0])
+
+ class_name = "test-Class-DN-String" + time.strftime("%s", time.gmtime())
+ class_ldap_display_name = class_name.replace("-", "")
+
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.1.""" + str(random.randint(1, 100000)) + """
+schemaIdGuid: """ + str(uuid.uuid4()) + """
+objectClassCategory: 1
+subClassOf: organizationalPerson
+systemMayContain: """ + attr_ldap_display_name + """
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ # search for created objectclass
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (class_name, self.schema_dn), scope=SCOPE_BASE, attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(res[0]["lDAPDisplayName"][0], class_ldap_display_name)
+ self.assertEqual(res[0]["defaultObjectCategory"][0], res[0]["distinguishedName"][0])
+ self.assertTrue("schemaIDGUID" in res[0])
+
+ # store the class and the attribute
+ self.dn_string_class_ldap_display_name = class_ldap_display_name
+ self.dn_string_attribute = attr_ldap_display_name
+ self.dn_string_class_name = class_name
+
+ def _setup_dn_binary_test(self):
+ """Testing DN+Binary syntaxes"""
+ attr_name = "test-Attr-DN-Binary" + time.strftime("%s", time.gmtime())
+ attr_ldap_display_name = attr_name.replace("-", "")
+
+ ldif = """
+dn: CN=%s,%s""" % (attr_name, self.schema_dn) + """
+ldapDisplayName: """ + attr_ldap_display_name + """
+objectClass: top
+objectClass: attributeSchema
+cn: """ + attr_name + """
+attributeId: 1.3.6.1.4.1.7165.4.6.1.2.""" + str(random.randint(1, 100000)) + """
+attributeSyntax: 2.5.5.7
+omSyntax: 127
+omObjectClass: \x2A\x86\x48\x86\xF7\x14\x01\x01\x01\x0B
+isSingleValued: FALSE
+schemaIdGuid: """ + str(uuid.uuid4()) + """
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ # search for created attribute
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (attr_name, self.schema_dn), scope=SCOPE_BASE, attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(res[0]["lDAPDisplayName"][0], attr_ldap_display_name)
+ self.assertTrue("schemaIDGUID" in res[0])
+
+ class_name = "test-Class-DN-Binary" + time.strftime("%s", time.gmtime())
+ class_ldap_display_name = class_name.replace("-", "")
+
+ ldif = """
+dn: CN=%s,%s""" % (class_name, self.schema_dn) + """
+objectClass: top
+objectClass: classSchema
+adminDescription: """ + class_name + """
+adminDisplayName: """ + class_name + """
+cn: """ + class_name + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.2.""" + str(random.randint(1, 100000)) + """
+schemaIdGuid: """ + str(uuid.uuid4()) + """
+objectClassCategory: 1
+subClassOf: organizationalPerson
+systemMayContain: """ + attr_ldap_display_name + """
+systemOnly: FALSE
+"""
+ self.ldb.add_ldif(ldif)
+
+ # search for created objectclass
+ res = []
+ res = self.ldb.search("cn=%s,%s" % (class_name, self.schema_dn), scope=SCOPE_BASE, attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(res[0]["lDAPDisplayName"][0], class_ldap_display_name)
+ self.assertEqual(res[0]["defaultObjectCategory"][0], res[0]["distinguishedName"][0])
+ self.assertTrue("schemaIDGUID" in res[0])
+
+ # store the class and the attribute
+ self.dn_binary_class_ldap_display_name = class_ldap_display_name
+ self.dn_binary_attribute = attr_ldap_display_name
+ self.dn_binary_class_name = class_name
+
+ def _get_object_ldif(self, object_name, class_name, class_ldap_display_name, attr_name, attr_value):
+ # add object with correct syntax
+ ldif = """
+dn: CN=%s,CN=Users,%s""" % (object_name, self.base_dn) + """
+objectClass: organizationalPerson
+objectClass: person
+objectClass: """ + class_ldap_display_name + """
+objectClass: top
+cn: """ + object_name + """
+instanceType: 4
+objectCategory: CN=%s,%s""" % (class_name, self.schema_dn) + """
+distinguishedName: CN=%s,CN=Users,%s""" % (object_name, self.base_dn) + """
+name: """ + object_name + """
+""" + attr_name + attr_value + """
+"""
+ return ldif
+
+ def test_dn_string(self):
+ # add object with correct value
+ object_name1 = "obj-DN-String1" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name1, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:ABCDE:" + self.base_dn)
+ self.ldb.add_ldif(ldif)
+
+ # search by specifying the DN part only
+ res = self.ldb.search(base=self.base_dn,
+ scope=SCOPE_SUBTREE,
+ expression="(%s=%s)" % (self.dn_string_attribute, self.base_dn))
+ self.assertEqual(len(res), 0)
+
+ # search by specifying the string part only
+ res = self.ldb.search(base=self.base_dn,
+ scope=SCOPE_SUBTREE,
+ expression="(%s=S:5:ABCDE)" % self.dn_string_attribute)
+ self.assertEqual(len(res), 0)
+
+ # search by DN+String
+ res = self.ldb.search(base=self.base_dn,
+ scope=SCOPE_SUBTREE,
+ expression="(%s=S:5:ABCDE:%s)" % (self.dn_string_attribute, self.base_dn))
+ self.assertEqual(len(res), 1)
+
+ # add object with wrong format
+ object_name2 = "obj-DN-String2" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name2, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:ABCD:" + self.base_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_INVALID_ATTRIBUTE_SYNTAX)
+
+ # add object with the same dn but with different string value in case
+ ldif = self._get_object_ldif(object_name1, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:abcde:" + self.base_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with the same dn but with different string value
+ ldif = self._get_object_ldif(object_name1, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:FGHIJ:" + self.base_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e2:
+ (num, _) = e2.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with the same dn but with different dn and string value
+ ldif = self._get_object_ldif(object_name1, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:FGHIJ:" + self.schema_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with the same dn but with different dn value
+ ldif = self._get_object_ldif(object_name1, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:ABCDE:" + self.schema_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e4:
+ (num, _) = e4.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with GUID instead of DN
+ object_name3 = "obj-DN-String3" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name3, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:ABCDE:<GUID=%s>" % str(uuid.uuid4()))
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e5:
+ (num, _) = e5.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # add object with SID instead of DN
+ object_name4 = "obj-DN-String4" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name4, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:ABCDE:<SID=%s>" % self.ldb.get_domain_sid())
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e6:
+ (num, _) = e6.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # add object with random string instead of DN
+ object_name5 = "obj-DN-String5" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name5, self.dn_string_class_name, self.dn_string_class_ldap_display_name,
+ self.dn_string_attribute, ": S:5:ABCDE:randomSTRING")
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e7:
+ (num, _) = e7.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ def test_dn_binary(self):
+ # add object with correct value
+ object_name1 = "obj-DN-Binary1" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name1, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:1234:" + self.base_dn)
+ self.ldb.add_ldif(ldif)
+
+ # search by specifyingthe DN part
+ res = self.ldb.search(base=self.base_dn,
+ scope=SCOPE_SUBTREE,
+ expression="(%s=%s)" % (self.dn_binary_attribute, self.base_dn))
+ self.assertEqual(len(res), 0)
+
+ # search by specifying the binary part
+ res = self.ldb.search(base=self.base_dn,
+ scope=SCOPE_SUBTREE,
+ expression="(%s=B:4:1234)" % self.dn_binary_attribute)
+ self.assertEqual(len(res), 0)
+
+ # search by DN+Binary
+ res = self.ldb.search(base=self.base_dn,
+ scope=SCOPE_SUBTREE,
+ expression="(%s=B:4:1234:%s)" % (self.dn_binary_attribute, self.base_dn))
+ self.assertEqual(len(res), 1)
+
+ # add object with wrong format - 5 bytes instead of 4, 8, 16, 32...
+ object_name2 = "obj-DN-Binary2" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name2, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:5:67890:" + self.base_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e8:
+ (num, _) = e8.args
+ self.assertEqual(num, ERR_INVALID_ATTRIBUTE_SYNTAX)
+
+ # add object with the same dn but with different binary value
+ ldif = self._get_object_ldif(object_name1, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:5678:" + self.base_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e9:
+ (num, _) = e9.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with the same dn but with different binary and dn value
+ ldif = self._get_object_ldif(object_name1, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:5678:" + self.schema_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e10:
+ (num, _) = e10.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with the same dn but with different dn value
+ ldif = self._get_object_ldif(object_name1, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:1234:" + self.schema_dn)
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e11:
+ (num, _) = e11.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # add object with GUID instead of DN
+ object_name3 = "obj-DN-Binary3" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name3, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:1234:<GUID=%s>" % str(uuid.uuid4()))
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e12:
+ (num, _) = e12.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # add object with SID instead of DN
+ object_name4 = "obj-DN-Binary4" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name4, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:1234:<SID=%s>" % self.ldb.get_domain_sid())
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e13:
+ (num, _) = e13.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # add object with random string instead of DN
+ object_name5 = "obj-DN-Binary5" + time.strftime("%s", time.gmtime())
+ ldif = self._get_object_ldif(object_name5, self.dn_binary_class_name, self.dn_binary_class_ldap_display_name,
+ self.dn_binary_attribute, ": B:4:1234:randomSTRING")
+ try:
+ self.ldb.add_ldif(ldif)
+ except LdbError as e14:
+ (num, _) = e14.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/linked_attributes.py b/source4/dsdb/tests/python/linked_attributes.py
new file mode 100644
index 0000000..24ad0c4
--- /dev/null
+++ b/source4/dsdb/tests/python/linked_attributes.py
@@ -0,0 +1,839 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Originally based on ./sam.py
+import optparse
+import sys
+import os
+import itertools
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+import ldb
+from samba.samdb import SamDB
+from samba.dcerpc import misc
+
+parser = optparse.OptionParser("linked_attributes.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+
+parser.add_option('--delete-in-setup', action='store_true',
+ help="cleanup in setup")
+
+parser.add_option('--no-cleanup', action='store_true',
+ help="don't cleanup in teardown")
+
+parser.add_option('--no-reveal-internals', action='store_true',
+ help="Only use windows compatible ldap controls")
+
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+class LATestException(Exception):
+ pass
+
+
+class LATests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(LATests, self).setUp()
+ self.samdb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+
+ self.base_dn = self.samdb.domain_dn()
+ self.testbase = "CN=LATests,%s" % self.base_dn
+ if opts.delete_in_setup:
+ try:
+ self.samdb.delete(self.testbase, ['tree_delete:1'])
+ except ldb.LdbError as e:
+ print("tried deleting %s, got error %s" % (self.testbase, e))
+ self.samdb.add({'objectclass': 'container',
+ 'dn': self.testbase})
+
+ def tearDown(self):
+ super(LATests, self).tearDown()
+ if not opts.no_cleanup:
+ self.samdb.delete(self.testbase, ['tree_delete:1'])
+
+ def add_object(self, cn, objectclass, more_attrs=None):
+ if more_attrs is None:
+ more_attrs = {}
+
+ dn = "CN=%s,%s" % (cn, self.testbase)
+ attrs = {'cn': cn,
+ 'objectclass': objectclass,
+ 'dn': dn}
+ attrs.update(more_attrs)
+ self.samdb.add(attrs)
+
+ return dn
+
+ def add_objects(self, n, objectclass, prefix=None, more_attrs=None):
+ if more_attrs is None:
+ more_attrs = {}
+ if prefix is None:
+ prefix = objectclass
+ dns = []
+ for i in range(n):
+ dns.append(self.add_object("%s%d" % (prefix, i + 1),
+ objectclass,
+ more_attrs=more_attrs))
+ return dns
+
+ def add_linked_attribute(self, src, dest, attr='member',
+ controls=None):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, src)
+ m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_ADD, attr)
+ self.samdb.modify(m, controls=controls)
+
+ def remove_linked_attribute(self, src, dest, attr='member',
+ controls=None):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, src)
+ m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_DELETE, attr)
+ self.samdb.modify(m, controls=controls)
+
+ def replace_linked_attribute(self, src, dest, attr='member',
+ controls=None):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, src)
+ m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_REPLACE, attr)
+ self.samdb.modify(m, controls=controls)
+
+ def attr_search(self, obj, attr, scope=ldb.SCOPE_BASE, **controls):
+ if opts.no_reveal_internals:
+ if 'reveal_internals' in controls:
+ del controls['reveal_internals']
+
+ controls = ['%s:%d' % (k, int(v)) for k, v in controls.items()]
+
+ res = self.samdb.search(obj,
+ scope=scope,
+ attrs=[attr],
+ controls=controls)
+ return res
+
+ def assert_links(self, obj, expected, attr, msg='', **kwargs):
+ res = self.attr_search(obj, attr, **kwargs)
+
+ if len(expected) == 0:
+ if attr in res[0]:
+ self.fail("found attr '%s' in %s" % (attr, res[0]))
+ return
+
+ try:
+ results = [str(x) for x in res[0][attr]]
+ except KeyError:
+ self.fail("missing attr '%s' on %s" % (attr, obj))
+
+ expected = sorted(expected)
+ results = sorted(results)
+
+ if expected != results:
+ print(msg)
+ print("expected %s" % expected)
+ print("received %s" % results)
+
+ self.assertEqual(results, expected)
+
+ def assert_back_links(self, obj, expected, attr='memberOf', **kwargs):
+ self.assert_links(obj, expected, attr=attr,
+ msg='back links do not match', **kwargs)
+
+ def assert_forward_links(self, obj, expected, attr='member', **kwargs):
+ self.assert_links(obj, expected, attr=attr,
+ msg='forward links do not match', **kwargs)
+
+ def get_object_guid(self, dn):
+ res = self.samdb.search(dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['objectGUID'])
+ return str(misc.GUID(res[0]['objectGUID'][0]))
+
+ def _test_la_backlinks(self, reveal=False):
+ tag = 'backlinks'
+ kwargs = {}
+ if reveal:
+ tag += '_reveal'
+ kwargs = {'reveal_internals': 0}
+
+ u1, u2 = self.add_objects(2, 'user', 'u_%s' % tag)
+ g1, g2 = self.add_objects(2, 'group', 'g_%s' % tag)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ self.assert_back_links(u1, [g1, g2], **kwargs)
+ self.assert_back_links(u2, [g2], **kwargs)
+
+ def test_la_backlinks(self):
+ self._test_la_backlinks()
+
+ def test_la_backlinks_reveal(self):
+ if opts.no_reveal_internals:
+ print('skipping because --no-reveal-internals')
+ return
+ self._test_la_backlinks(True)
+
+ def _test_la_backlinks_delete_group(self, reveal=False):
+ tag = 'del_group'
+ kwargs = {}
+ if reveal:
+ tag += '_reveal'
+ kwargs = {'reveal_internals': 0}
+
+ u1, u2 = self.add_objects(2, 'user', 'u_' + tag)
+ g1, g2 = self.add_objects(2, 'group', 'g_' + tag)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ self.samdb.delete(g2, ['tree_delete:1'])
+
+ self.assert_back_links(u1, [g1], **kwargs)
+ self.assert_back_links(u2, set(), **kwargs)
+
+ def test_la_backlinks_delete_group(self):
+ self._test_la_backlinks_delete_group()
+
+ def test_la_backlinks_delete_group_reveal(self):
+ if opts.no_reveal_internals:
+ print('skipping because --no-reveal-internals')
+ return
+ self._test_la_backlinks_delete_group(True)
+
+ def test_links_all_delete_group(self):
+ u1, u2 = self.add_objects(2, 'user', 'u_all_del_group')
+ g1, g2 = self.add_objects(2, 'group', 'g_all_del_group')
+ g2guid = self.get_object_guid(g2)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ self.samdb.delete(g2)
+ self.assert_back_links(u1, [g1], show_deleted=1, show_recycled=1,
+ show_deactivated_link=0)
+ self.assert_back_links(u2, set(), show_deleted=1, show_recycled=1,
+ show_deactivated_link=0)
+ self.assert_forward_links(g1, [u1], show_deleted=1, show_recycled=1,
+ show_deactivated_link=0)
+ self.assert_forward_links('<GUID=%s>' % g2guid,
+ [], show_deleted=1, show_recycled=1,
+ show_deactivated_link=0)
+
+ def test_links_all_delete_group_reveal(self):
+ u1, u2 = self.add_objects(2, 'user', 'u_all_del_group_reveal')
+ g1, g2 = self.add_objects(2, 'group', 'g_all_del_group_reveal')
+ g2guid = self.get_object_guid(g2)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ self.samdb.delete(g2)
+ self.assert_back_links(u1, [g1], show_deleted=1, show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0)
+ self.assert_back_links(u2, set(), show_deleted=1, show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0)
+ self.assert_forward_links(g1, [u1], show_deleted=1, show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0)
+ self.assert_forward_links('<GUID=%s>' % g2guid,
+ [], show_deleted=1, show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0)
+
+ def test_la_links_delete_link(self):
+ u1, u2 = self.add_objects(2, 'user', 'u_del_link')
+ g1, g2 = self.add_objects(2, 'group', 'g_del_link')
+
+ res = self.samdb.search(g1, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ old_usn1 = int(res[0]['uSNChanged'][0])
+
+ self.add_linked_attribute(g1, u1)
+
+ res = self.samdb.search(g1, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ new_usn1 = int(res[0]['uSNChanged'][0])
+
+ self.assertNotEqual(old_usn1, new_usn1, "USN should have incremented")
+
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ res = self.samdb.search(g2, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ old_usn2 = int(res[0]['uSNChanged'][0])
+
+ self.remove_linked_attribute(g2, u1)
+
+ res = self.samdb.search(g2, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ new_usn2 = int(res[0]['uSNChanged'][0])
+
+ self.assertNotEqual(old_usn2, new_usn2, "USN should have incremented")
+
+ self.assert_forward_links(g1, [u1])
+ self.assert_forward_links(g2, [u2])
+
+ self.add_linked_attribute(g2, u1)
+ self.assert_forward_links(g2, [u1, u2])
+ self.remove_linked_attribute(g2, u2)
+ self.assert_forward_links(g2, [u1])
+ self.remove_linked_attribute(g2, u1)
+ self.assert_forward_links(g2, [])
+ self.remove_linked_attribute(g1, [])
+ self.assert_forward_links(g1, [])
+
+ # removing a duplicate link in the same message should fail
+ self.add_linked_attribute(g2, [u1, u2])
+ self.assertRaises(ldb.LdbError,
+ self.remove_linked_attribute, g2, [u1, u1])
+
+ def _test_la_links_delete_link_reveal(self):
+ u1, u2 = self.add_objects(2, 'user', 'u_del_link_reveal')
+ g1, g2 = self.add_objects(2, 'group', 'g_del_link_reveal')
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ self.remove_linked_attribute(g2, u1)
+
+ self.assert_forward_links(g2, [u1, u2], show_deleted=1,
+ show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0
+ )
+
+ def test_la_links_delete_link_reveal(self):
+ if opts.no_reveal_internals:
+ print('skipping because --no-reveal-internals')
+ return
+ self._test_la_links_delete_link_reveal()
+
+ def test_la_links_delete_user(self):
+ u1, u2 = self.add_objects(2, 'user', 'u_del_user')
+ g1, g2 = self.add_objects(2, 'group', 'g_del_user')
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ res = self.samdb.search(g1, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ old_usn1 = int(res[0]['uSNChanged'][0])
+
+ res = self.samdb.search(g2, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ old_usn2 = int(res[0]['uSNChanged'][0])
+
+ self.samdb.delete(u1)
+
+ self.assert_forward_links(g1, [])
+ self.assert_forward_links(g2, [u2])
+
+ res = self.samdb.search(g1, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ new_usn1 = int(res[0]['uSNChanged'][0])
+
+ res = self.samdb.search(g2, scope=ldb.SCOPE_BASE,
+ attrs=['uSNChanged'])
+ new_usn2 = int(res[0]['uSNChanged'][0])
+
+ # Assert the USN on the alternate object is unchanged
+ self.assertEqual(old_usn1, new_usn1)
+ self.assertEqual(old_usn2, new_usn2)
+
+ def test_la_links_delete_user_reveal(self):
+ u1, u2 = self.add_objects(2, 'user', 'u_del_user_reveal')
+ g1, g2 = self.add_objects(2, 'group', 'g_del_user_reveal')
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+
+ self.samdb.delete(u1)
+
+ self.assert_forward_links(g2, [u2],
+ show_deleted=1, show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0)
+ self.assert_forward_links(g1, [],
+ show_deleted=1, show_recycled=1,
+ show_deactivated_link=0,
+ reveal_internals=0)
+
+ def test_multiple_links(self):
+ u1, u2, u3, u4 = self.add_objects(4, 'user', 'u_multiple_links')
+ g1, g2, g3, g4 = self.add_objects(4, 'group', 'g_multiple_links')
+
+ self.add_linked_attribute(g1, [u1, u2, u3, u4])
+ self.add_linked_attribute(g2, [u3, u1])
+ self.add_linked_attribute(g3, u2)
+
+ self.assertRaisesLdbError(ldb.ERR_ENTRY_ALREADY_EXISTS,
+ "adding duplicate values",
+ self.add_linked_attribute, g2,
+ [u1, u2, u3, u2])
+
+ self.assert_forward_links(g1, [u1, u2, u3, u4])
+ self.assert_forward_links(g2, [u3, u1])
+ self.assert_forward_links(g3, [u2])
+ self.assert_back_links(u1, [g2, g1])
+ self.assert_back_links(u2, [g3, g1])
+ self.assert_back_links(u3, [g2, g1])
+ self.assert_back_links(u4, [g1])
+
+ self.remove_linked_attribute(g2, [u1, u3])
+ self.remove_linked_attribute(g1, [u1, u3])
+
+ self.assert_forward_links(g1, [u2, u4])
+ self.assert_forward_links(g2, [])
+ self.assert_forward_links(g3, [u2])
+ self.assert_back_links(u1, [])
+ self.assert_back_links(u2, [g3, g1])
+ self.assert_back_links(u3, [])
+ self.assert_back_links(u4, [g1])
+
+ self.add_linked_attribute(g1, [u1, u3])
+ self.add_linked_attribute(g2, [u3, u1])
+ self.add_linked_attribute(g3, [u1, u3])
+
+ self.assert_forward_links(g1, [u1, u2, u3, u4])
+ self.assert_forward_links(g2, [u1, u3])
+ self.assert_forward_links(g3, [u1, u2, u3])
+ self.assert_back_links(u1, [g1, g2, g3])
+ self.assert_back_links(u2, [g3, g1])
+ self.assert_back_links(u3, [g3, g2, g1])
+ self.assert_back_links(u4, [g1])
+
+ def test_la_links_replace(self):
+ u1, u2, u3, u4 = self.add_objects(4, 'user', 'u_replace')
+ g1, g2, g3, g4 = self.add_objects(4, 'group', 'g_replace')
+
+ self.add_linked_attribute(g1, [u1, u2])
+ self.add_linked_attribute(g2, [u1, u3])
+ self.add_linked_attribute(g3, u1)
+
+ self.replace_linked_attribute(g1, [u2])
+ self.replace_linked_attribute(g2, [u2, u3])
+ self.replace_linked_attribute(g3, [u1, u3])
+ self.replace_linked_attribute(g4, [u4])
+
+ self.assert_forward_links(g1, [u2])
+ self.assert_forward_links(g2, [u3, u2])
+ self.assert_forward_links(g3, [u3, u1])
+ self.assert_forward_links(g4, [u4])
+ self.assert_back_links(u1, [g3])
+ self.assert_back_links(u2, [g1, g2])
+ self.assert_back_links(u3, [g2, g3])
+ self.assert_back_links(u4, [g4])
+
+ self.replace_linked_attribute(g1, [u1, u2, u3])
+ self.replace_linked_attribute(g2, [u1])
+ self.replace_linked_attribute(g3, [u2])
+ self.replace_linked_attribute(g4, [])
+
+ self.assert_forward_links(g1, [u1, u2, u3])
+ self.assert_forward_links(g2, [u1])
+ self.assert_forward_links(g3, [u2])
+ self.assert_forward_links(g4, [])
+ self.assert_back_links(u1, [g1, g2])
+ self.assert_back_links(u2, [g1, g3])
+ self.assert_back_links(u3, [g1])
+ self.assert_back_links(u4, [])
+
+ self.assertRaisesLdbError(ldb.ERR_ENTRY_ALREADY_EXISTS,
+ "replacing duplicate values",
+ self.replace_linked_attribute, g2,
+ [u1, u2, u3, u2])
+
+ def test_la_links_replace2(self):
+ users = self.add_objects(12, 'user', 'u_replace2')
+ g1, = self.add_objects(1, 'group', 'g_replace2')
+
+ self.add_linked_attribute(g1, users[:6])
+ self.assert_forward_links(g1, users[:6])
+ self.replace_linked_attribute(g1, users)
+ self.assert_forward_links(g1, users)
+ self.replace_linked_attribute(g1, users[6:])
+ self.assert_forward_links(g1, users[6:])
+ self.remove_linked_attribute(g1, users[6:9])
+ self.assert_forward_links(g1, users[9:])
+ self.remove_linked_attribute(g1, users[9:])
+ self.assert_forward_links(g1, [])
+
+ def test_la_links_permutations(self):
+ """Make sure the order in which we add links doesn't matter."""
+ users = self.add_objects(3, 'user', 'u_permutations')
+ groups = self.add_objects(6, 'group', 'g_permutations')
+
+ for g, p in zip(groups, itertools.permutations(users)):
+ self.add_linked_attribute(g, p)
+
+ # everyone should be in every group
+ for g in groups:
+ self.assert_forward_links(g, users)
+
+ for u in users:
+ self.assert_back_links(u, groups)
+
+ for g, p in zip(groups[::-1], itertools.permutations(users)):
+ self.replace_linked_attribute(g, p)
+
+ for g in groups:
+ self.assert_forward_links(g, users)
+
+ for u in users:
+ self.assert_back_links(u, groups)
+
+ for g, p in zip(groups, itertools.permutations(users)):
+ self.remove_linked_attribute(g, p)
+
+ for g in groups:
+ self.assert_forward_links(g, [])
+
+ for u in users:
+ self.assert_back_links(u, [])
+
+ def test_la_links_relaxed(self):
+ """Check that the relax control doesn't mess with linked attributes."""
+ relax_control = ['relax:0']
+
+ users = self.add_objects(10, 'user', 'u_relax')
+ groups = self.add_objects(3, 'group', 'g_relax',
+ more_attrs={'member': users[:2]})
+ g_relax1, g_relax2, g_uptight = groups
+
+ # g_relax1 has all users added at once
+ # g_relax2 gets them one at a time in reverse order
+ # g_uptight never relaxes
+
+ self.add_linked_attribute(g_relax1, users[2:5], controls=relax_control)
+
+ for u in reversed(users[2:5]):
+ self.add_linked_attribute(g_relax2, u, controls=relax_control)
+ self.add_linked_attribute(g_uptight, u)
+
+ for g in groups:
+ self.assert_forward_links(g, users[:5])
+
+ self.add_linked_attribute(g, users[5:7])
+ self.assert_forward_links(g, users[:7])
+
+ for u in users[7:]:
+ self.add_linked_attribute(g, u)
+
+ self.assert_forward_links(g, users)
+
+ for u in users:
+ self.assert_back_links(u, groups)
+
+ # try some replacement permutations
+ import random
+ random.seed(1)
+ users2 = users[:]
+ for i in range(5):
+ random.shuffle(users2)
+ self.replace_linked_attribute(g_relax1, users2,
+ controls=relax_control)
+
+ self.assert_forward_links(g_relax1, users)
+
+ for i in range(5):
+ random.shuffle(users2)
+ self.remove_linked_attribute(g_relax2, users2,
+ controls=relax_control)
+ self.remove_linked_attribute(g_uptight, users2)
+
+ self.replace_linked_attribute(g_relax1, [], controls=relax_control)
+
+ random.shuffle(users2)
+ self.add_linked_attribute(g_relax2, users2,
+ controls=relax_control)
+ self.add_linked_attribute(g_uptight, users2)
+ self.replace_linked_attribute(g_relax1, users2,
+ controls=relax_control)
+
+ self.assert_forward_links(g_relax1, users)
+ self.assert_forward_links(g_relax2, users)
+ self.assert_forward_links(g_uptight, users)
+
+ for u in users:
+ self.assert_back_links(u, groups)
+
+ def test_add_all_at_once(self):
+ """All these other tests are creating linked attributes after the
+ objects are there. We want to test creating them all at once
+ using LDIF.
+ """
+ users = self.add_objects(7, 'user', 'u_all_at_once')
+ g1, g3 = self.add_objects(2, 'group', 'g_all_at_once',
+ more_attrs={'member': users})
+ (g2,) = self.add_objects(1, 'group', 'g_all_at_once2',
+ more_attrs={'member': users[:5]})
+
+ self.assertRaisesLdbError(ldb.ERR_ENTRY_ALREADY_EXISTS,
+ "adding multiple duplicate values",
+ self.add_objects, 1, 'group',
+ 'g_with_duplicate_links',
+ more_attrs={'member': users[:5] + users[1:2]})
+
+ self.assert_forward_links(g1, users)
+ self.assert_forward_links(g2, users[:5])
+ self.assert_forward_links(g3, users)
+ for u in users[:5]:
+ self.assert_back_links(u, [g1, g2, g3])
+ for u in users[5:]:
+ self.assert_back_links(u, [g1, g3])
+
+ self.remove_linked_attribute(g2, users[0])
+ self.remove_linked_attribute(g2, users[1])
+ self.add_linked_attribute(g2, users[1])
+ self.add_linked_attribute(g2, users[5])
+ self.add_linked_attribute(g2, users[6])
+
+ self.assert_forward_links(g1, users)
+ self.assert_forward_links(g2, users[1:])
+
+ for u in users[1:]:
+ self.remove_linked_attribute(g2, u)
+ self.remove_linked_attribute(g1, users)
+
+ for u in users:
+ self.samdb.delete(u)
+
+ self.assert_forward_links(g1, [])
+ self.assert_forward_links(g2, [])
+ self.assert_forward_links(g3, [])
+
+ def test_one_way_attributes(self):
+ e1, e2 = self.add_objects(2, 'msExchConfigurationContainer',
+ 'e_one_way')
+ guid = self.get_object_guid(e2)
+
+ self.add_linked_attribute(e1, e2, attr="addressBookRoots")
+ self.assert_forward_links(e1, [e2], attr='addressBookRoots')
+
+ self.samdb.delete(e2)
+
+ res = self.samdb.search("<GUID=%s>" % guid,
+ scope=ldb.SCOPE_BASE,
+ controls=['show_deleted:1',
+ 'show_recycled:1'])
+
+ new_dn = str(res[0].dn)
+ self.assert_forward_links(e1, [new_dn], attr='addressBookRoots')
+ self.assert_forward_links(e1, [new_dn],
+ attr='addressBookRoots',
+ show_deactivated_link=0)
+
+ def test_one_way_attributes_delete_link(self):
+ e1, e2 = self.add_objects(2, 'msExchConfigurationContainer',
+ 'e_one_way')
+ guid = self.get_object_guid(e2)
+
+ self.add_linked_attribute(e1, e2, attr="addressBookRoots")
+ self.assert_forward_links(e1, [e2], attr='addressBookRoots')
+
+ self.remove_linked_attribute(e1, e2, attr="addressBookRoots")
+
+ self.assert_forward_links(e1, [], attr='addressBookRoots')
+ self.assert_forward_links(e1, [], attr='addressBookRoots',
+ show_deactivated_link=0)
+
+ def test_pretend_one_way_attributes(self):
+ e1, e2 = self.add_objects(2, 'msExchConfigurationContainer',
+ 'e_one_way')
+ guid = self.get_object_guid(e2)
+
+ self.add_linked_attribute(e1, e2, attr="addressBookRoots2")
+ self.assert_forward_links(e1, [e2], attr='addressBookRoots2')
+
+ self.samdb.delete(e2)
+ res = self.samdb.search("<GUID=%s>" % guid,
+ scope=ldb.SCOPE_BASE,
+ controls=['show_deleted:1',
+ 'show_recycled:1'])
+
+ new_dn = str(res[0].dn)
+
+ self.assert_forward_links(e1, [], attr='addressBookRoots2')
+ self.assert_forward_links(e1, [], attr='addressBookRoots2',
+ show_deactivated_link=0)
+
+ def test_pretend_one_way_attributes_delete_link(self):
+ e1, e2 = self.add_objects(2, 'msExchConfigurationContainer',
+ 'e_one_way')
+ guid = self.get_object_guid(e2)
+
+ self.add_linked_attribute(e1, e2, attr="addressBookRoots2")
+ self.assert_forward_links(e1, [e2], attr='addressBookRoots2')
+
+ self.remove_linked_attribute(e1, e2, attr="addressBookRoots2")
+
+ self.assert_forward_links(e1, [], attr='addressBookRoots2')
+ self.assert_forward_links(e1, [], attr='addressBookRoots2',
+ show_deactivated_link=0)
+
+
+ def test_self_link(self):
+ e1, = self.add_objects(1, 'group',
+ 'e_self_link')
+
+ guid = self.get_object_guid(e1)
+ self.add_linked_attribute(e1, e1, attr="member")
+ self.assert_forward_links(e1, [e1], attr='member')
+ self.assert_back_links(e1, [e1], attr='memberOf')
+
+ try:
+ self.samdb.delete(e1)
+ except ldb.LdbError:
+ # Cope with the current bug to make this a failure
+ self.remove_linked_attribute(e1, e1, attr="member")
+ self.samdb.delete(e1)
+ self.fail("could not delete object with link to itself")
+
+ self.assert_forward_links('<GUID=%s>' % guid, [], attr='member',
+ show_deleted=1)
+ self.assert_forward_links('<GUID=%s>' % guid, [], attr='member',
+ show_deactivated_link=0,
+ show_deleted=1)
+ self.assert_back_links('<GUID=%s>' % guid, [], attr='memberOf',
+ show_deleted=1)
+
+ def test_la_invisible_backlink(self):
+ u1, = self.add_objects(1, 'user', 'u_invisible_bl')
+ k1, = self.add_objects(1, 'msDS-KeyCredential', 'k1_invisible_bl',
+ more_attrs={'msDS-KeyId': 'KeyId1', })
+ c2, = self.add_objects(1, 'container', 'c_invisible_bl')
+ k2, = self.add_objects(1, 'msDS-KeyCredential', 'k2_invisible_bl',
+ more_attrs={'msDS-KeyId': 'KeyId2', })
+
+ # msDS-KeyPrincipalBL is allowed on objectClass 'user'
+ # so the msDS-KeyPrincipalBL attribute is visible by
+ # default (asking for '*')
+ self.add_linked_attribute(k1, u1, attr="msDS-KeyPrincipal")
+ self.assert_forward_links(k1, [u1], attr="msDS-KeyPrincipal")
+ self.assert_back_links(u1, [k1], attr="msDS-KeyPrincipalBL")
+ res = self.samdb.search(u1, scope=ldb.SCOPE_BASE, attrs=["*"])
+ self.assertIn("msDS-KeyPrincipalBL", res[0])
+ res = self.samdb.search(u1, scope=ldb.SCOPE_BASE,
+ expression='(msDS-KeyPrincipalBL=*)',
+ attrs=["*"])
+ self.assertIn("msDS-KeyPrincipalBL", res[0])
+ expression = '(msDS-KeyPrincipalBL=%s)' % ldb.binary_encode(str(k1))
+ res = self.samdb.search(self.testbase, scope=ldb.SCOPE_SUBTREE,
+ expression=expression, attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0].dn), u1)
+ self.assertIn("msDS-KeyPrincipalBL", res[0])
+
+ # msDS-KeyPrincipalBL is allowed on objectClass 'msDS-KeyPrincipal'
+ # so the msDS-KeyPrincipalBL attribute is not visible by
+ # default (asking for '*'), it is only visible if
+ # explicitly requested
+ self.add_linked_attribute(k2, c2, attr="msDS-KeyPrincipal")
+ self.assert_forward_links(k2, [c2], attr="msDS-KeyPrincipal")
+ self.assert_back_links(c2, [k2], attr="msDS-KeyPrincipalBL")
+ res = self.samdb.search(c2, scope=ldb.SCOPE_BASE, attrs=["*"])
+ self.assertNotIn("msDS-KeyPrincipalBL", res[0])
+ res = self.samdb.search(c2, scope=ldb.SCOPE_BASE,
+ expression='(msDS-KeyPrincipalBL=*)',
+ attrs=["*"])
+ self.assertNotIn("msDS-KeyPrincipalBL", res[0])
+ res = self.samdb.search(c2, scope=ldb.SCOPE_BASE,
+ attrs=["*", "msDS-KeyPrincipalBL"])
+ self.assertIn("msDS-KeyPrincipalBL", res[0])
+ res = self.samdb.search(c2, scope=ldb.SCOPE_BASE,
+ expression='(msDS-KeyPrincipalBL=*)',
+ attrs=["*", "msDS-KeyPrincipalBL"])
+ self.assertIn("msDS-KeyPrincipalBL", res[0])
+ expression = '(msDS-KeyPrincipalBL=%s)' % ldb.binary_encode(str(k2))
+ res = self.samdb.search(self.testbase, scope=ldb.SCOPE_SUBTREE,
+ expression=expression,
+ attrs=["*"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0].dn), c2)
+ self.assertNotIn("msDS-KeyPrincipalBL", res[0])
+ res = self.samdb.search(self.testbase, scope=ldb.SCOPE_SUBTREE,
+ expression=expression,
+ attrs=["*", "msDS-KeyPrincipalBL"])
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0].dn), c2)
+ self.assertIn("msDS-KeyPrincipalBL", res[0])
+
+ # msDS-KeyCredentialLink-BL is allowed on any objectClass at all
+ # so the msDS-KeyCredentialLink-BL attribute is not visible by
+ # default (asking for '*'), it is only visible if
+ # explicitly requested...
+
+ cl1a = "B:4:AAAA:%s" % u1
+ self.add_linked_attribute(u1, cl1a, attr="msDS-KeyCredentialLink")
+ self.assert_forward_links(u1, [cl1a], attr="msDS-KeyCredentialLink")
+ self.assert_back_links(u1, [u1], attr="msDS-KeyCredentialLink-BL")
+ res = self.samdb.search(u1, scope=ldb.SCOPE_BASE, attrs=["*"])
+ self.assertNotIn("msDS-KeyCredentialLink-BL", res[0])
+ res = self.samdb.search(u1, scope=ldb.SCOPE_BASE,
+ attrs=["*", "msDS-KeyCredentialLink-BL"])
+ self.assertIn("msDS-KeyCredentialLink-BL", res[0])
+ self.assertEqual(1, len(res[0]["msDS-KeyCredentialLink-BL"]))
+
+ cl1b = "B:4:BBBB:%s" % u1
+ self.add_linked_attribute(u1, cl1b, attr="msDS-KeyCredentialLink")
+ self.assert_forward_links(u1, [cl1a,cl1b], attr="msDS-KeyCredentialLink")
+ self.assert_back_links(u1, [u1,u1], attr="msDS-KeyCredentialLink-BL")
+ res = self.samdb.search(u1, scope=ldb.SCOPE_BASE, attrs=["*"])
+ self.assertNotIn("msDS-KeyCredentialLink-BL", res[0])
+ res = self.samdb.search(u1, scope=ldb.SCOPE_BASE,
+ attrs=["*", "msDS-KeyCredentialLink-BL"])
+ self.assertIn("msDS-KeyCredentialLink-BL", res[0])
+ self.assertEqual(2, len(res[0]["msDS-KeyCredentialLink-BL"]))
+
+ cl1c = "B:4:CCCC:%s" % k1
+ self.add_linked_attribute(u1, cl1c, attr="msDS-KeyCredentialLink")
+ self.assert_forward_links(u1, [cl1a,cl1b,cl1c], attr="msDS-KeyCredentialLink")
+ self.assert_back_links(u1, [u1,u1], attr="msDS-KeyCredentialLink-BL")
+ self.assert_back_links(k1, [u1], attr="msDS-KeyCredentialLink-BL")
+ res = self.samdb.search(k1, scope=ldb.SCOPE_BASE, attrs=["*"])
+ self.assertNotIn("msDS-KeyCredentialLink-BL", res[0])
+ res = self.samdb.search(k1, scope=ldb.SCOPE_BASE,
+ attrs=["*", "msDS-KeyCredentialLink-BL"])
+ self.assertIn("msDS-KeyCredentialLink-BL", res[0])
+ self.assertEqual(1, len(res[0]["msDS-KeyCredentialLink-BL"]))
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/login_basics.py b/source4/dsdb/tests/python/login_basics.py
new file mode 100755
index 0000000..46983e8
--- /dev/null
+++ b/source4/dsdb/tests/python/login_basics.py
@@ -0,0 +1,273 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Basic sanity-checks of user login. This sanity-checks that a user can login
+# over both NTLM and Kerberos, that incorrect passwords are rejected, and that
+# the user can change their password successfully.
+#
+# Copyright Andrew Bartlett 2018
+#
+import optparse
+import sys
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+import samba.getopt as options
+from samba.auth import system_session
+from samba.credentials import MUST_USE_KERBEROS
+from samba.dsdb import UF_NORMAL_ACCOUNT
+from samba.samdb import SamDB
+from password_lockout_base import BasePasswordTestCase
+
+sys.path.insert(0, "bin/python")
+
+parser = optparse.OptionParser("login_basics.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+global_creds = credopts.get_credentials(lp)
+
+
+#
+# Tests start here
+#
+class BasicUserAuthTests(BasePasswordTestCase):
+
+ def setUp(self):
+ self.host = host
+ self.host_url = "ldap://%s" % host
+ self.host_url_ldaps = "ldaps://%s" % host
+ self.lp = lp
+ self.global_creds = global_creds
+ self.ldb = SamDB(url=self.host_url, credentials=self.global_creds,
+ session_info=system_session(self.lp), lp=self.lp)
+ super(BasicUserAuthTests, self).setUp()
+
+ def _test_login_basics(self, creds, simple=False):
+ username = creds.get_username()
+ userpass = creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+ if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+ logoncount_relation = 'greater'
+ lastlogon_relation = 'greater'
+ ldap_url = self.host_url
+ print("Performs a lockout attempt against LDAP using Kerberos")
+ elif simple:
+ logoncount_relation = 'equal'
+ lastlogon_relation = 'equal'
+ ldap_url = self.host_url_ldaps
+ print("Performs a lockout attempt against LDAP using Simple")
+ else:
+ logoncount_relation = 'equal'
+ lastlogon_relation = 'equal'
+ ldap_url = self.host_url
+ print("Performs a lockout attempt against LDAP using NTLM")
+
+ # get the initial logon values for this user
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=("greater", 0),
+ logonCount=(logoncount_relation, 0),
+ lastLogon=("greater", 0),
+ lastLogonTimestamp=("greater", 0),
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='Initial test setup...')
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+
+ test_creds = self.insta_creds(creds)
+
+ # check logging in with the wrong password fails
+ test_creds.set_password("thatsAcomplPASS1xBAD")
+ self.assertLoginFailure(ldap_url, test_creds, self.lp)
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='Test login with wrong password')
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # check logging in with the correct password succeeds
+ test_creds.set_password(userpass)
+ user_ldb = self.assertLoginSuccess(ldap_url, test_creds, self.lp)
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=('greater', lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='Test login with correct password')
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+
+ # check that the user can change its password
+ too_old_password = "thatsAcomplTooOldPass1!"
+ user_ldb.modify_ldif("""
+dn: %s
+changetype: modify
+delete: userPassword
+userPassword: %s
+add: userPassword
+userPassword: %s
+""" % (userdn, userpass, too_old_password))
+
+ # change the password again
+ older_password = "thatsAcomplOlderPass1!"
+ user_ldb.modify_ldif("""
+dn: %s
+changetype: modify
+delete: userPassword
+userPassword: %s
+add: userPassword
+userPassword: %s
+""" % (userdn, too_old_password, older_password))
+
+ # change the password again
+ old_password = "thatsAcomplOldPass1!"
+ user_ldb.modify_ldif("""
+dn: %s
+changetype: modify
+delete: userPassword
+userPassword: %s
+add: userPassword
+userPassword: %s
+""" % (userdn, older_password, old_password))
+
+ # change the password once more
+ new_password = "thatsAcomplNewPass1!"
+ user_ldb.modify_ldif("""
+dn: %s
+changetype: modify
+delete: userPassword
+userPassword: %s
+add: userPassword
+userPassword: %s
+""" % (userdn, old_password, new_password))
+
+ # discard the old creds (i.e. get rid of our valid Kerberos ticket)
+ del test_creds
+ test_creds = self.insta_creds(creds)
+ test_creds.set_password(older_password)
+
+ self.assertLoginFailure(ldap_url, test_creds, self.lp)
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='Test with older password fails (but badPwdCount=0)')
+
+ del test_creds
+ test_creds = self.insta_creds(creds)
+ test_creds.set_password(old_password)
+
+ # for Kerberos, logging in with the old password fails
+ if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+ self.assertLoginFailure(ldap_url, test_creds, self.lp)
+ info_msg = 'Test Kerberos login with old password fails (but badPwdCount=0)'
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg=info_msg)
+ else:
+ # for NTLM, logging in with the old password succeeds
+ user_ldb = self.assertLoginSuccess(ldap_url, test_creds, self.lp)
+ if simple:
+ info_msg = 'Test simple-bind login with old password succeeds'
+ else:
+ info_msg = 'Test NTLM login with old password succeeds'
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg=info_msg)
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+
+ # check logging in with the correct password succeeds
+ test_creds.set_password(new_password)
+ user_ldb = self.assertLoginSuccess(ldap_url, test_creds, self.lp)
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=(lastlogon_relation, lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='Test login with new password succeeds')
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+
+ del test_creds
+ test_creds = self.insta_creds(creds)
+ test_creds.set_password(too_old_password)
+
+ self.assertLoginFailure(ldap_url, test_creds, self.lp)
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='Test login with too old password fails')
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # check logging in with the correct password succeeds
+ test_creds.set_password(new_password)
+ user_ldb = self.assertLoginSuccess(ldap_url, test_creds, self.lp)
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=('greater', lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='Test login with new password succeeds again')
+
+ def test_login_basics_krb5(self):
+ self._test_login_basics(self.lockout1krb5_creds)
+
+ def test_login_basics_ntlm(self):
+ self._test_login_basics(self.lockout1ntlm_creds)
+
+ def test_login_basics_simple(self):
+ self._test_login_basics(self.lockout1simple_creds, simple=True)
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/ndr_pack_performance.py b/source4/dsdb/tests/python/ndr_pack_performance.py
new file mode 100644
index 0000000..45c1816
--- /dev/null
+++ b/source4/dsdb/tests/python/ndr_pack_performance.py
@@ -0,0 +1,215 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import optparse
+import sys
+sys.path.insert(0, 'bin/python')
+
+import samba
+import gzip
+
+# We try to use the test infrastructure of Samba 4.3+, but if it
+# doesn't work, we are probably in a back-ported patch and trying to
+# run on 4.1 or something.
+#
+# Don't copy this horror into ordinary tests -- it is special for
+# performance tests that want to apply to old versions.
+
+from samba.tests.subunitrun import TestProgram
+
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.dcerpc import security
+from samba.dcerpc import drsuapi
+
+
+BIG_SD_SDDL = ''.join(
+ """O:S-1-5-21-3328325300-3937145445-4190589019-512G:S-1-5-2
+1-3328325300-3937145445-4190589019-512D:AI(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;S-
+1-5-21-3328325300-3937145445-4190589019-512)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;
+SY)(A;;RPLCLORC;;;AU)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;AO)(A;;RPLCLORC;;;PS)(O
+A;;CR;ab721a55-1e2f-11d0-9819-00aa0040529b;;AU)(OA;;RP;46a9b11d-60ae-405a-b7e
+8-ff8a58d456d2;;S-1-5-32-560)(OA;CIIOID;RP;4c164200-20c0-11d0-a768-00aa006e05
+29;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;4c164200-20c0-11d0-a
+768-00aa006e0529;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP;5f2020
+10-79a5-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CI
+IOID;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa0030
+49e2;RU)(OA;CIIOID;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc
+-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;bf96
+7aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP;59ba2f42-79a2-11d0-9020-00c
+04fc2d3cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;59ba2f42-79a2
+-11d0-9020-00c04fc2d3cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP
+;037088f8-0ae1-11d2-b422-00a0c968f939;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU
+)(OA;CIIOID;RP;037088f8-0ae1-11d2-b422-00a0c968f939;bf967aba-0de6-11d0-a285-0
+0aa003049e2;RU)(OA;CIIOID;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a86-0d
+e6-11d0-a285-00aa003049e2;ED)(OA;CIID;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608
+;bf967a9c-0de6-11d0-a285-00aa003049e2;ED)(OA;CIIOID;RP;b7c69e6d-2cc7-11d2-854
+e-00a0c983f608;bf967aba-0de6-11d0-a285-00aa003049e2;ED)(OA;CIIOID;RPLCLORC;;4
+828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIID;RPLCLORC;;bf967a9c-0de6-11d0-
+a285-00aa003049e2;RU)(OA;CIIOID;RPLCLORC;;bf967aba-0de6-11d0-a285-00aa003049e
+2;RU)(OA;CIID;RPWPCR;91e647de-d96f-4b70-9557-d63ff4f3ccd8;;PS)(A;CIID;RPWPCRC
+CDCLCLORCWOWDSDDTSW;;;S-1-5-21-3328325300-3937145445-4190589019-519)(A;CIID;L
+C;;;RU)(A;CIID;RPWPCRCCLCLORCWOWDSDSW;;;BA)(OA;CIIOID;RP;4c164200-20c0-11d0-a
+768-00aa006e0529;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;4c1642
+00-20c0-11d0-a768-00aa006e0529;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CI
+IOID;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e
+5f28;RU)(OA;CIIOID;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0
+-a285-00aa003049e2;RU)(OA;CIIOID;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;4828
+cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;bc0ac240-79a9-11d0-9020-00c
+04fc2d4cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP;59ba2f42-79a2
+-11d0-9020-00c04fc2d3cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP
+;59ba2f42-79a2-11d0-9020-00c04fc2d3cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU
+)(OA;CIIOID;RP;037088f8-0ae1-11d2-b422-00a0c968f939;4828cc14-1437-45bc-9b07-a
+d6f015e5f28;RU)(OA;CIIOID;RP;037088f8-0ae1-11d2-b422-00a0c968f939;bf967aba-0d
+e6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f6
+08;bf967a86-0de6-11d0-a285-00aa003049e2;ED)(OA;CIID;RP;b7c69e6d-2cc7-11d2-854
+e-00a0c983f608;bf967a9c-0de6-11d0-a285-00aa003049e2;ED)(OA;CIIOID;RP;b7c69e6d
+-2cc7-11d2-854e-00a0c983f608;bf967aba-0de6-11d0-a285-00aa003049e2;ED)(OA;CIIO
+ID;RPLCLORC;;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIID;RPLCLORC;;bf967
+a9c-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RPLCLORC;;bf967aba-0de6-11d0-a2
+85-00aa003049e2;RU)(OA;CIID;RPWPCR;91e647de-d96f-4b70-9557-d63ff4f3ccd8;;PS)(
+A;CIID;RPWPCRCCDCLCLORCWOWDSDDTSW;;;S-1-5-21-3328325300-3937145445-4190589019
+-519)(A;CIID;LC;;;RU)(A;CIID;RPWPCRCCLCLORCWOWDSDSW;;;BA)S:AI(OU;CIIOIDSA;WP;
+f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)
+(OU;CIIOIDSA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-
+00aa003049e2;WD)(OU;CIIOIDSA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5
+-0de6-11d0-a285-00aa003049e2;WD)(OU;CIIOIDSA;WP;f30e3bbf-9ff0-11d1-b603-0000f
+80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)""".split())
+
+LITTLE_SD_SDDL = ''.join(
+ """O:S-1-5-21-3328325300-3937145445-4190589019-512G:S-1-5-2
+1-3328325300-3937145445-4190589019-512D:AI(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;S-
+1-5-21-3328325300-3937145445-4190589019-512)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;
+SY)(A;;RPLCLORC;;;AU)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;AO)(A;;RPLCLORC;;;PS)(O
+A;;CR;ab721a55-1e2f-11d0-9819-00aa0040529b;;AU)(OA;;RP;46a9b11d-60ae-405a-b7e
+8-ff8a58d456d2;;S-1-5-32-560)(OA;CIIOID;RP;4c164200-20c0-11d0-a768-00aa006e05
+29;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;4c164200-20c0-11d0-a
+768-00aa006e0529;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP;5f2020
+10-79a5-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CI
+IOID;RP;5f202010-79a5-11d0-9020-00c04fc2d4cf;bf967aba-0de6-11d0-a285-00aa0030
+49e2;RU)(OA;CIIOID;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;4828cc14-1437-45bc
+-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;bc0ac240-79a9-11d0-9020-00c04fc2d4cf;bf96
+7aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP;59ba2f42-79a2-11d0-9020-00c
+04fc2d3cf;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU)(OA;CIIOID;RP;59ba2f42-79a2
+-11d0-9020-00c04fc2d3cf;bf967aba-0de6-11d0-a285-00aa003049e2;RU)(OA;CIIOID;RP
+;037088f8-0ae1-11d2-b422-00a0c968f939;4828cc14-1437-45bc-9b07-ad6f015e5f28;RU
+)(OA;CIIOID;RP;037088f8-0ae1-11d2-b422-00a0c968f939;bf967aba-0de6-11d0-a285-0
+0aa003049e2;RU)(OA;CIIOID;RP;b7c69e6d-2cc7-11d2-854e-00a0c983f608;bf967a86-0d
+e6-11d0-a285-00aa003049e2;ED)""".split())
+
+
+CONDITIONAL_ACE_SDDL = ('O:SYG:SYD:(XA;OICI;CR;;;WD;'
+ '(@USER.ad://ext/AuthenticationSilo == "siloname"))')
+
+NON_OBJECT_SDDL = (
+ "O:S-1-5-21-2212615479-2695158682-2101375468-512"
+ "G:S-1-5-21-2212615479-2695158682-2101375468-513"
+ "D:P(A;OICI;FA;;;S-1-5-21-2212615479-2695158682-2101375468-512)"
+ "(A;OICI;FA;;;S-1-5-21-2212615479-2695158682-2101375468-519)"
+ "(A;OICIIO;FA;;;CO)"
+ "(A;OICI;FA;;;S-1-5-21-2212615479-2695158682-2101375468-512)"
+ "(A;OICI;FA;;;SY)"
+ "(A;OICI;0x1200a9;;;AU)"
+ "(A;OICI;0x1200a9;;;ED)")
+
+
+
+# set SCALE = 100 for normal test, or 1 for testing the test.
+SCALE = 100
+
+
+class UserTests(samba.tests.TestCase):
+
+ def get_file_blob(self, filename):
+ if filename.endswith('.gz'):
+ f = gzip.open(filename)
+ else:
+ f = open(filename)
+ return f.read()
+
+ def get_desc(self, sddl):
+ dummy_sid = security.dom_sid("S-1-2-3")
+ return security.descriptor.from_sddl(sddl, dummy_sid)
+
+ def get_blob(self, sddl):
+ return ndr_pack(self.get_desc(sddl))
+
+ def test_00_00_do_nothing(self, cycles=10000):
+ # this gives us an idea of the overhead
+ for i in range(SCALE * cycles):
+ pass
+
+ def _test_pack(self, unpacked, cycles=10000):
+ pack = unpacked.__ndr_pack__
+ for i in range(SCALE * cycles):
+ pack()
+
+ def _test_unpack(self, blob, cycles=10000, cls=security.descriptor):
+ for i in range(SCALE * cycles):
+ cls().__ndr_unpack__(blob)
+
+ def _test_pack_unpack(self, desc, cycles=5000, cls=security.descriptor):
+ blob2 = ndr_pack(desc)
+ for i in range(SCALE * cycles):
+ blob = ndr_pack(desc)
+ desc = ndr_unpack(cls, blob)
+
+ self.assertEqual(blob, blob2)
+
+ def test_pack_big_sd_with_object_aces(self):
+ unpacked = self.get_desc(BIG_SD_SDDL)
+ self._test_pack(unpacked)
+
+ def test_unpack_big_sd_with_object_aces(self):
+ blob = self.get_blob(BIG_SD_SDDL)
+ self._test_unpack(blob)
+
+ def test_pack_unpack_big_sd_with_object_aces(self):
+ unpacked = self.get_desc(BIG_SD_SDDL)
+ self._test_pack_unpack(unpacked)
+
+ def test_pack_little_sd_with_object_aces(self):
+ unpacked = self.get_desc(LITTLE_SD_SDDL)
+ self._test_pack(unpacked)
+
+ def test_unpack_little_sd_with_object_aces(self):
+ blob = self.get_blob(LITTLE_SD_SDDL)
+ self._test_unpack(blob)
+
+ def test_pack_unpack_little_sd_with_object_aces(self):
+ unpacked = self.get_desc(LITTLE_SD_SDDL)
+ self._test_pack_unpack(unpacked)
+
+ def test_pack_conditional_ace_sd(self):
+ unpacked = self.get_desc(CONDITIONAL_ACE_SDDL)
+ self._test_pack(unpacked)
+
+ def test_unpack_conditional_ace_sd(self):
+ blob = self.get_blob(CONDITIONAL_ACE_SDDL)
+ self._test_unpack(blob)
+
+ def test_pack_unpack_conditional_ace_sd(self):
+ unpacked = self.get_desc(CONDITIONAL_ACE_SDDL)
+ self._test_pack_unpack(unpacked)
+
+ def test_pack_non_object_sd(self):
+ unpacked = self.get_desc(NON_OBJECT_SDDL)
+ self._test_pack(unpacked)
+
+ def test_unpack_non_object_sd(self):
+ blob = self.get_blob(NON_OBJECT_SDDL)
+ self._test_unpack(blob)
+
+ def test_pack_unpack_non_object_sd(self):
+ unpacked = self.get_desc(NON_OBJECT_SDDL)
+ self._test_pack_unpack(unpacked)
+
+ def test_unpack_repl_sample(self):
+ blob = self.get_file_blob('testdata/replication-ndrpack-example.gz')
+ self._test_unpack(blob, cycles=20, cls=drsuapi.DsGetNCChangesCtr6)
+
+ def test_pack_repl_sample(self):
+ blob = self.get_file_blob('testdata/replication-ndrpack-example.gz')
+ desc = ndr_unpack(drsuapi.DsGetNCChangesCtr6, blob)
+ self._test_pack(desc, cycles=20)
+
+
+TestProgram(module=__name__)
diff --git a/source4/dsdb/tests/python/notification.py b/source4/dsdb/tests/python/notification.py
new file mode 100755
index 0000000..1124af1
--- /dev/null
+++ b/source4/dsdb/tests/python/notification.py
@@ -0,0 +1,367 @@
+#!/usr/bin/env python3
+#
+# Unit tests for the notification control
+# Copyright (C) Stefan Metzmacher 2016
+#
+# 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/>.
+
+import optparse
+import sys
+import os
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba import ldb
+from samba.samdb import SamDB
+from samba.ndr import ndr_unpack
+import samba.tests
+
+from ldb import LdbError
+from ldb import ERR_TIME_LIMIT_EXCEEDED, ERR_ADMIN_LIMIT_EXCEEDED, ERR_UNWILLING_TO_PERFORM
+
+parser = optparse.OptionParser("notification.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+url = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+class LDAPNotificationTest(samba.tests.TestCase):
+
+ def setUp(self):
+ super(LDAPNotificationTest, self).setUp()
+ self.ldb = SamDB(url, credentials=creds, session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.domain_dn()
+
+ res = self.ldb.search("", scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ self.user_sid_dn = "<SID=%s>" % str(ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["tokenGroups"][0]))
+
+ def test_simple_search(self):
+ """Testing a notification with an modify and a timeout"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ msg1 = None
+ search1 = self.ldb.search_iterator(base=self.user_sid_dn,
+ expression="(objectClass=*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name", "objectGUID", "displayName"])
+ for reply in search1:
+ self.assertIsInstance(reply, ldb.Message)
+ self.assertIsNone(msg1)
+ msg1 = reply
+ res1 = search1.result()
+
+ search2 = self.ldb.search_iterator(base=self.base_dn,
+ expression="(objectClass=*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name", "objectGUID", "displayName"])
+ refs2 = 0
+ msg2 = None
+ for reply in search2:
+ if isinstance(reply, str):
+ refs2 += 1
+ continue
+ self.assertIsInstance(reply, ldb.Message)
+ if reply["objectGUID"][0] == msg1["objectGUID"][0]:
+ self.assertIsNone(msg2)
+ msg2 = reply
+ self.assertEqual(msg1.dn, msg2.dn)
+ self.assertEqual(len(msg1), len(msg2))
+ self.assertEqual(msg1["name"], msg2["name"])
+ #self.assertEqual(msg1["displayName"], msg2["displayName"])
+ res2 = search2.result()
+
+ self.ldb.modify_ldif("""
+dn: """ + self.user_sid_dn + """
+changetype: modify
+replace: otherLoginWorkstations
+otherLoginWorkstations: BEFORE"
+""")
+ notify1 = self.ldb.search_iterator(base=self.base_dn,
+ expression="(objectClass=*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name", "objectGUID", "displayName"],
+ controls=["notification:1"],
+ timeout=1)
+
+ self.ldb.modify_ldif("""
+dn: """ + self.user_sid_dn + """
+changetype: modify
+replace: otherLoginWorkstations
+otherLoginWorkstations: AFTER"
+""")
+
+ msg3 = None
+ for reply in notify1:
+ self.assertIsInstance(reply, ldb.Message)
+ if reply["objectGUID"][0] == msg1["objectGUID"][0]:
+ self.assertIsNone(msg3)
+ msg3 = reply
+ self.assertEqual(msg1.dn, msg3.dn)
+ self.assertEqual(len(msg1), len(msg3))
+ self.assertEqual(msg1["name"], msg3["name"])
+ #self.assertEqual(msg1["displayName"], msg3["displayName"])
+ try:
+ res = notify1.result()
+ self.fail()
+ except LdbError as e10:
+ (num, _) = e10.args
+ self.assertEqual(num, ERR_TIME_LIMIT_EXCEEDED)
+ self.assertIsNotNone(msg3)
+
+ self.ldb.modify_ldif("""
+dn: """ + self.user_sid_dn + """
+changetype: modify
+delete: otherLoginWorkstations
+""")
+
+ def test_max_search(self):
+ """Testing the max allowed notifications"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ max_notifications = 5
+
+ notifies = [None] * (max_notifications + 1)
+ for i in range(0, max_notifications + 1):
+ notifies[i] = self.ldb.search_iterator(base=self.base_dn,
+ expression="(objectClass=*)",
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=1)
+ num_admin_limit = 0
+ num_time_limit = 0
+ for i in range(0, max_notifications + 1):
+ try:
+ for msg in notifies[i]:
+ continue
+ res = notifies[i].result()
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ if num == ERR_ADMIN_LIMIT_EXCEEDED:
+ num_admin_limit += 1
+ continue
+ if num == ERR_TIME_LIMIT_EXCEEDED:
+ num_time_limit += 1
+ continue
+ raise
+ self.assertEqual(num_admin_limit, 1)
+ self.assertEqual(num_time_limit, max_notifications)
+
+ def test_invalid_filter(self):
+ """Testing invalid filters for notifications"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ valid_attrs = ["objectClass", "objectGUID", "distinguishedName", "name"]
+
+ for va in valid_attrs:
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s=*)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=1)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_TIME_LIMIT_EXCEEDED)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(|(%s=*)(%s=value))" % (va, va),
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=1)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e2:
+ (num, _) = e2.args
+ self.assertEqual(num, ERR_TIME_LIMIT_EXCEEDED)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(&(%s=*)(%s=value))" % (va, va),
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s=value)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e4:
+ (num, _) = e4.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s>=value)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e5:
+ (num, _) = e5.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s<=value)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e6:
+ (num, _) = e6.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s=*value*)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e7:
+ (num, _) = e7.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(!(%s=*))" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e8:
+ (num, _) = e8.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ res = self.ldb.search(base=self.ldb.get_schema_basedn(),
+ expression="(objectClass=attributeSchema)",
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=["lDAPDisplayName"],
+ controls=["paged_results:1:2500"])
+ for msg in res:
+ va = str(msg["lDAPDisplayName"][0])
+ if va in valid_attrs:
+ continue
+
+ try:
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s=*)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e9:
+ (num, _) = e9.args
+ if num != ERR_UNWILLING_TO_PERFORM:
+ print("va[%s]" % va)
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ va = "noneAttributeName"
+ hnd = self.ldb.search_iterator(base=self.base_dn,
+ expression="(%s=*)" % va,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["name"],
+ controls=["notification:1"],
+ timeout=0)
+ for reply in hnd:
+ self.fail()
+ res = hnd.result()
+ self.fail()
+ except LdbError as e11:
+ (num, _) = e11.args
+ if num != ERR_UNWILLING_TO_PERFORM:
+ print("va[%s]" % va)
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+
+if "://" not in url:
+ if os.path.isfile(url):
+ url = "tdb://%s" % url
+ else:
+ url = "ldap://%s" % url
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/password_lockout.py b/source4/dsdb/tests/python/password_lockout.py
new file mode 100755
index 0000000..78edcce
--- /dev/null
+++ b/source4/dsdb/tests/python/password_lockout.py
@@ -0,0 +1,1704 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This tests the password lockout behavior for AD implementations
+#
+# Copyright Matthias Dieter Wallnoefer 2010
+# Copyright Andrew Bartlett 2013
+# Copyright Stefan Metzmacher 2014
+#
+
+import optparse
+import sys
+import base64
+import time
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+from samba.netcmd.main import samba_tool
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba.credentials import DONT_USE_KERBEROS, MUST_USE_KERBEROS
+from ldb import SCOPE_BASE, LdbError
+from ldb import ERR_CONSTRAINT_VIOLATION
+from ldb import ERR_INVALID_CREDENTIALS
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_REPLACE
+from samba import dsdb
+from samba.samdb import SamDB
+from samba.tests import delete_force
+from samba.dcerpc import security, samr
+from samba.tests.pso import PasswordSettings
+from samba.net import Net
+from samba import NTSTATUSError, ntstatus
+import ctypes
+
+parser = optparse.OptionParser("password_lockout.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+global_creds = credopts.get_credentials(lp)
+
+import password_lockout_base
+
+#
+# Tests start here
+#
+
+
+class PasswordTests(password_lockout_base.BasePasswordTestCase):
+ def setUp(self):
+ self.host = host
+ self.host_url = "ldap://%s" % host
+ self.host_url_ldaps = "ldaps://%s" % host
+ self.lp = lp
+ self.global_creds = global_creds
+ self.ldb = SamDB(url=self.host_url, session_info=system_session(self.lp),
+ credentials=self.global_creds, lp=self.lp)
+ super(PasswordTests, self).setUp()
+
+ self.lockout2krb5_creds = self.insta_creds(self.template_creds,
+ username="lockout2krb5",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=MUST_USE_KERBEROS)
+ self._readd_user(self.lockout2krb5_creds,
+ lockOutObservationWindow=self.lockout_observation_window)
+ self.lockout2krb5_ldb = SamDB(url=self.host_url,
+ credentials=self.lockout2krb5_creds,
+ lp=lp)
+
+ self.lockout2ntlm_creds = self.insta_creds(self.template_creds,
+ username="lockout2ntlm",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=DONT_USE_KERBEROS)
+ self._readd_user(self.lockout2ntlm_creds,
+ lockOutObservationWindow=self.lockout_observation_window)
+ self.lockout2ntlm_ldb = SamDB(url=self.host_url,
+ credentials=self.lockout2ntlm_creds,
+ lp=lp)
+
+
+ def use_pso_lockout_settings(self, creds):
+
+ # create a PSO with the lockout settings the test cases normally expect
+ #
+ # Some test cases sleep() for self.account_lockout_duration
+ pso = PasswordSettings("lockout-PSO", self.ldb, lockout_attempts=3,
+ lockout_duration=self.account_lockout_duration)
+ self.addCleanup(self.ldb.delete, pso.dn)
+
+ userdn = "cn=%s,cn=users,%s" % (creds.get_username(), self.base_dn)
+ pso.apply_to(userdn)
+
+ # update the global lockout settings to be wildly different to what
+ # the test cases normally expect
+ self.update_lockout_settings(threshold=10, duration=600,
+ observation_window=600)
+
+ def _reset_samr(self, res):
+
+ # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
+ samr_user = self._open_samr_user(res)
+ acb_info = self.samr.QueryUserInfo(samr_user, 16)
+ acb_info.acct_flags &= ~samr.ACB_AUTOLOCK
+ self.samr.SetUserInfo(samr_user, 16, acb_info)
+ self.samr.Close(samr_user)
+
+
+class PasswordTestsWithoutSleep(PasswordTests):
+ def setUp(self):
+ # The tests in this class do not sleep, so we can have a
+ # longer window and not flap on slower hosts
+ self.account_lockout_duration = 30
+ self.lockout_observation_window = 30
+ super(PasswordTestsWithoutSleep, self).setUp()
+
+ def _reset_ldap_lockoutTime(self, res):
+ self.ldb.modify_ldif("""
+dn: """ + str(res[0].dn) + """
+changetype: modify
+replace: lockoutTime
+lockoutTime: 0
+""")
+
+ def _reset_samba_tool(self, res):
+ username = res[0]["sAMAccountName"][0]
+
+ result = samba_tool('user', 'unlock', username,
+ "-H%s" % self.host_url,
+ "-U%s%%%s" % (global_creds.get_username(),
+ global_creds.get_password()))
+ self.assertEqual(result, None)
+
+ def _reset_ldap_userAccountControl(self, res):
+ self.assertIn("userAccountControl", res[0])
+ self.assertIn("msDS-User-Account-Control-Computed", res[0])
+
+ uac = int(res[0]["userAccountControl"][0])
+ uacc = int(res[0]["msDS-User-Account-Control-Computed"][0])
+
+ uac |= uacc
+ uac = uac & ~dsdb.UF_LOCKOUT
+
+ self.ldb.modify_ldif("""
+dn: """ + str(res[0].dn) + """
+changetype: modify
+replace: userAccountControl
+userAccountControl: %d
+""" % uac)
+
+ def _reset_by_method(self, res, method):
+ if method == "ldap_userAccountControl":
+ self._reset_ldap_userAccountControl(res)
+ elif method == "ldap_lockoutTime":
+ self._reset_ldap_lockoutTime(res)
+ elif method == "samr":
+ self._reset_samr(res)
+ elif method == "samba-tool":
+ self._reset_samba_tool(res)
+ else:
+ self.fail("Invalid reset method[%s]" % method)
+
+ def _test_userPassword_lockout_with_clear_change(self, creds, other_ldb, method,
+ initial_lastlogon_relation=None):
+ """
+ Tests user lockout behaviour when we try to change the user's password
+ but specify an incorrect old-password. The method parameter specifies
+ how to reset the locked out account (e.g. by resetting lockoutTime)
+ """
+ # Notice: This works only against Windows if "dSHeuristics" has been set
+ # properly
+ username = creds.get_username()
+ userpass = creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ use_kerberos = creds.get_kerberos_state()
+ if use_kerberos == MUST_USE_KERBEROS:
+ logoncount_relation = 'greater'
+ lastlogon_relation = 'greater'
+ self.debug("Performs a password cleartext change operation on 'userPassword' using Kerberos")
+ else:
+ logoncount_relation = 'equal'
+ lastlogon_relation = 'equal'
+ self.debug("Performs a password cleartext change operation on 'userPassword' using NTLMSSP")
+
+ if initial_lastlogon_relation is not None:
+ lastlogon_relation = initial_lastlogon_relation
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=("greater", 0),
+ logonCount=(logoncount_relation, 0),
+ lastLogon=(lastlogon_relation, 0),
+ lastLogonTimestamp=('greater', 0),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+ if lastlogon_relation == 'greater':
+ self.assertGreater(lastLogon, badPasswordTime)
+ self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
+
+ # Change password on a connection as another user
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1x
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000056', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Correct old password
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: """ + userpass + """
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1x
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e1:
+ (num, msg) = e1.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000056', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ self.debug("two failed password change")
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1x
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e2:
+ (num, msg) = e2.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000056', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=("greater", badPasswordTime),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ lockoutTime = int(res[0]["lockoutTime"][0])
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1x
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e3:
+ (num, msg) = e3.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000775', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1x
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e4:
+ (num, msg) = e4.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000775', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ try:
+ # Correct old password
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS2
+add: userPassword
+userPassword: thatsAcomplPASS2x
+""")
+ self.fail()
+ except LdbError as e5:
+ (num, msg) = e5.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000775', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # Now reset the password, which does NOT change the lockout!
+ self.ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS2
+""")
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ try:
+ # Correct old password
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS2
+add: userPassword
+userPassword: thatsAcomplPASS2x
+""")
+ self.fail()
+ except LdbError as e6:
+ (num, msg) = e6.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000775', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ m = Message()
+ m.dn = Dn(self.ldb, userdn)
+ m["userAccountControl"] = MessageElement(
+ str(dsdb.UF_LOCKOUT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+
+ self.ldb.modify(m)
+
+ # This shows that setting the UF_LOCKOUT flag alone makes no difference
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # This shows that setting the UF_LOCKOUT flag makes no difference
+ try:
+ # Correct old password
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2x\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e7:
+ (num, msg) = e7.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000775', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ self._reset_by_method(res, method)
+
+ # Here bad password counts are reset without logon success.
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The correct password after doing the unlock
+
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2x\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ userpass = "thatsAcomplPASS2x"
+ creds.set_password(userpass)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1xyz
+add: userPassword
+userPassword: thatsAcomplPASS2XYZ
+""")
+ self.fail()
+ except LdbError as e8:
+ (num, msg) = e8.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000056', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1xyz
+add: userPassword
+userPassword: thatsAcomplPASS2XYZ
+""")
+ self.fail()
+ except LdbError as e9:
+ (num, msg) = e9.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000056', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ self._reset_ldap_lockoutTime(res)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The following test lockout behaviour when modifying a user's password
+ # and specifying an invalid old password. There are variants for both
+ # NTLM and kerberos user authentication. As well as that, there are 3 ways
+ # to reset the locked out account: by clearing the lockout bit for
+ # userAccountControl (via LDAP), resetting it via SAMR, and by resetting
+ # the lockoutTime.
+ def test_userPassword_lockout_with_clear_change_krb5_ldap_userAccountControl(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
+ self.lockout2krb5_ldb,
+ "ldap_userAccountControl")
+
+ def test_userPassword_lockout_with_clear_change_krb5_ldap_lockoutTime(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
+ self.lockout2krb5_ldb,
+ "ldap_lockoutTime")
+
+ def test_userPassword_lockout_with_clear_change_krb5_samr(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
+ self.lockout2krb5_ldb,
+ "samr")
+
+ def test_userPassword_lockout_with_clear_change_ntlm_ldap_userAccountControl(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
+ self.lockout2ntlm_ldb,
+ "ldap_userAccountControl",
+ initial_lastlogon_relation='greater')
+
+ def test_userPassword_lockout_with_clear_change_ntlm_ldap_lockoutTime(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
+ self.lockout2ntlm_ldb,
+ "ldap_lockoutTime",
+ initial_lastlogon_relation='greater')
+
+ def test_userPassword_lockout_with_clear_change_ntlm_samr(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
+ self.lockout2ntlm_ldb,
+ "samr",
+ initial_lastlogon_relation='greater')
+
+ # For PSOs, just test a selection of the above combinations
+ def test_pso_userPassword_lockout_with_clear_change_krb5_ldap_userAccountControl(self):
+ self.use_pso_lockout_settings(self.lockout1krb5_creds)
+ self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
+ self.lockout2krb5_ldb,
+ "ldap_userAccountControl")
+
+ def test_pso_userPassword_lockout_with_clear_change_ntlm_ldap_lockoutTime(self):
+ self.use_pso_lockout_settings(self.lockout1ntlm_creds)
+ self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
+ self.lockout2ntlm_ldb,
+ "ldap_lockoutTime",
+ initial_lastlogon_relation='greater')
+
+ def test_pso_userPassword_lockout_with_clear_change_ntlm_samr(self):
+ self.use_pso_lockout_settings(self.lockout1ntlm_creds)
+ self._test_userPassword_lockout_with_clear_change(self.lockout1ntlm_creds,
+ self.lockout2ntlm_ldb,
+ "samr",
+ initial_lastlogon_relation='greater')
+
+ # just test "samba-tool user unlock" command once
+ def test_userPassword_lockout_with_clear_change_krb5_ldap_samba_tool(self):
+ self._test_userPassword_lockout_with_clear_change(self.lockout1krb5_creds,
+ self.lockout2krb5_ldb,
+ "samba-tool")
+
+ def test_multiple_logon_krb5(self):
+ self._test_multiple_logon(self.lockout1krb5_creds)
+
+ def test_multiple_logon_ntlm(self):
+ self._test_multiple_logon(self.lockout1ntlm_creds)
+
+ def _test_samr_password_change(self, creds, other_creds, lockout_threshold=3):
+ """Tests user lockout by using bad password in SAMR password_change"""
+
+ # create a connection for SAMR using another user's credentials
+ lp = self.get_loadparm()
+ net = Net(other_creds, lp, server=self.host)
+
+ # work out the initial account values for this user
+ username = creds.get_username()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=("greater", 0),
+ badPwdCountOnly=True)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+
+ # prove we can change the user password (using the correct password)
+ new_password = "thatsAcomplPASS2"
+ net.change_password(newpassword=new_password,
+ username=username,
+ oldpassword=creds.get_password())
+ creds.set_password(new_password)
+
+ # try entering 'x' many bad passwords in a row to lock the user out
+ new_password = "thatsAcomplPASS3"
+ for i in range(lockout_threshold):
+ badPwdCount = i + 1
+ try:
+ self.debug("Trying bad password, attempt #%u" % badPwdCount)
+ net.change_password(newpassword=new_password,
+ username=creds.get_username(),
+ oldpassword="bad-password")
+ self.fail("Invalid SAMR change_password accepted")
+ except NTSTATUSError as e:
+ enum = ctypes.c_uint32(e.args[0]).value
+ self.assertEqual(enum, ntstatus.NT_STATUS_WRONG_PASSWORD)
+
+ # check the status of the account is updated after each bad attempt
+ account_flags = 0
+ lockoutTime = None
+ if badPwdCount >= lockout_threshold:
+ account_flags = dsdb.UF_LOCKOUT
+ lockoutTime = ("greater", badPasswordTime)
+
+ res = self._check_account(userdn,
+ badPwdCount=badPwdCount,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=account_flags)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # the user is now locked out
+ lockoutTime = int(res[0]["lockoutTime"][0])
+
+ # check the user remains locked out regardless of whether they use a
+ # good or a bad password now
+ for password in (creds.get_password(), "bad-password"):
+ try:
+ self.debug("Trying password %s" % password)
+ net.change_password(newpassword=new_password,
+ username=creds.get_username(),
+ oldpassword=password)
+ self.fail("Invalid SAMR change_password accepted")
+ except NTSTATUSError as e:
+ enum = ctypes.c_uint32(e.args[0]).value
+ self.assertEqual(enum, ntstatus.NT_STATUS_ACCOUNT_LOCKED_OUT)
+
+ res = self._check_account(userdn,
+ badPwdCount=lockout_threshold,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # reset the user account lockout
+ self._reset_samr(res)
+
+ # check bad password counts are reset
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # check we can change the user password successfully now
+ net.change_password(newpassword=new_password,
+ username=username,
+ oldpassword=creds.get_password())
+ creds.set_password(new_password)
+
+ def test_samr_change_password(self):
+ self._test_samr_password_change(self.lockout1ntlm_creds,
+ other_creds=self.lockout2ntlm_creds)
+
+ # same as above, but use a PSO to enforce the lockout
+ def test_pso_samr_change_password(self):
+ self.use_pso_lockout_settings(self.lockout1ntlm_creds)
+ self._test_samr_password_change(self.lockout1ntlm_creds,
+ other_creds=self.lockout2ntlm_creds)
+
+ def test_ntlm_lockout_protected(self):
+ creds = self.lockout1ntlm_creds
+ self.assertEqual(DONT_USE_KERBEROS, creds.get_kerberos_state())
+
+ # Work out the initial account values for this user.
+ username = creds.get_username()
+ userdn = f'cn={username},cn=users,{self.base_dn}'
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=('greater', 0),
+ badPwdCountOnly=True)
+ badPasswordTime = int(res[0]['badPasswordTime'][0])
+ logonCount = int(res[0]['logonCount'][0])
+ lastLogon = int(res[0]['lastLogon'][0])
+ lastLogonTimestamp = int(res[0]['lastLogonTimestamp'][0])
+
+ # Add the user to the Protected Users group.
+
+ # Search for the Protected Users group.
+ group_dn = Dn(self.ldb,
+ f'<SID={self.ldb.get_domain_sid()}-'
+ f'{security.DOMAIN_RID_PROTECTED_USERS}>')
+ try:
+ group_res = self.ldb.search(base=group_dn,
+ scope=SCOPE_BASE,
+ attrs=['member'])
+ except LdbError as err:
+ self.fail(err)
+
+ orig_msg = group_res[0]
+
+ # Add the user to the list of members.
+ members = list(orig_msg.get('member', ()))
+ self.assertNotIn(userdn, members, 'account already in Protected Users')
+ members.append(userdn)
+
+ m = Message(group_dn)
+ m['member'] = MessageElement(members,
+ FLAG_MOD_REPLACE,
+ 'member')
+ cleanup = self.ldb.msg_diff(m, orig_msg)
+ self.ldb.modify(m)
+
+ password = creds.get_password()
+ creds.set_password('wrong_password')
+
+ lockout_threshold = 5
+
+ lp = self.get_loadparm()
+ server = f'ldap://{self.ldb.host_dns_name()}'
+
+ for _ in range(lockout_threshold):
+ with self.assertRaises(LdbError) as err:
+ SamDB(url=server,
+ credentials=creds,
+ lp=lp)
+
+ num, _ = err.exception.args
+ self.assertEqual(ERR_INVALID_CREDENTIALS, num)
+
+ res = self._check_account(
+ userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=None,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The user should not be locked out.
+ self.assertNotIn('lockoutTime', res[0],
+ 'account unexpectedly locked out')
+
+ # Move the account out of 'Protected Users'.
+ self.ldb.modify(cleanup)
+
+ # The account should not be locked out.
+ creds.set_password(password)
+
+ try:
+ SamDB(url=server,
+ credentials=creds,
+ lp=lp)
+ except LdbError:
+ self.fail('account unexpectedly locked out')
+
+ def test_samr_change_password_protected(self):
+ """Tests the SAMR password change method for Protected Users"""
+
+ creds = self.lockout1ntlm_creds
+ other_creds = self.lockout2ntlm_creds
+ lockout_threshold = 5
+
+ # Create a connection for SAMR using another user's credentials.
+ lp = self.get_loadparm()
+ net = Net(other_creds, lp, server=self.host)
+
+ # Work out the initial account values for this user.
+ username = creds.get_username()
+ userdn = f'cn={username},cn=users,{self.base_dn}'
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=('greater', 0),
+ badPwdCountOnly=True)
+ badPasswordTime = int(res[0]['badPasswordTime'][0])
+ logonCount = int(res[0]['logonCount'][0])
+ lastLogon = int(res[0]['lastLogon'][0])
+ lastLogonTimestamp = int(res[0]['lastLogonTimestamp'][0])
+
+ # prove we can change the user password (using the correct password)
+ new_password = 'thatsAcomplPASS1'
+ net.change_password(newpassword=new_password,
+ username=username,
+ oldpassword=creds.get_password())
+ creds.set_password(new_password)
+
+ # Add the user to the Protected Users group.
+
+ # Search for the Protected Users group.
+ group_dn = Dn(self.ldb,
+ f'<SID={self.ldb.get_domain_sid()}-'
+ f'{security.DOMAIN_RID_PROTECTED_USERS}>')
+ try:
+ group_res = self.ldb.search(base=group_dn,
+ scope=SCOPE_BASE,
+ attrs=['member'])
+ except LdbError as err:
+ self.fail(err)
+
+ orig_msg = group_res[0]
+
+ # Add the user to the list of members.
+ members = list(orig_msg.get('member', ()))
+ self.assertNotIn(userdn, members, 'account already in Protected Users')
+ members.append(userdn)
+
+ m = Message(group_dn)
+ m['member'] = MessageElement(members,
+ FLAG_MOD_REPLACE,
+ 'member')
+ self.ldb.modify(m)
+
+ # Try entering the correct password 'x' times in a row, which should
+ # fail, but not lock the user out.
+ new_password = 'thatsAcomplPASS2'
+ for i in range(lockout_threshold):
+ with self.assertRaises(
+ NTSTATUSError,
+ msg='Invalid SAMR change_password accepted') as err:
+ self.debug(f'Trying correct password, attempt #{i}')
+ net.change_password(newpassword=new_password,
+ username=username,
+ oldpassword=creds.get_password())
+
+ enum = ctypes.c_uint32(err.exception.args[0]).value
+ self.assertEqual(enum, ntstatus.NT_STATUS_ACCOUNT_RESTRICTION)
+
+ res = self._check_account(
+ userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=None,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The user should not be locked out.
+ self.assertNotIn('lockoutTime', res[0])
+
+ # Ensure that the password can still be changed via LDAP.
+ self.ldb.modify_ldif(f'''
+dn: {userdn}
+changetype: modify
+delete: userPassword
+userPassword: {creds.get_password()}
+add: userPassword
+userPassword: {new_password}
+''')
+
+ def test_samr_set_password_protected(self):
+ """Tests the SAMR password set method for Protected Users"""
+
+ creds = self.lockout1ntlm_creds
+ lockout_threshold = 5
+
+ # create a connection for SAMR using another user's credentials
+ lp = self.get_loadparm()
+ net = Net(self.global_creds, lp, server=self.host)
+
+ # work out the initial account values for this user
+ username = creds.get_username()
+ userdn = f'cn={username},cn=users,{self.base_dn}'
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=('greater', 0),
+ badPwdCountOnly=True)
+ badPasswordTime = int(res[0]['badPasswordTime'][0])
+ logonCount = int(res[0]['logonCount'][0])
+ lastLogon = int(res[0]['lastLogon'][0])
+ lastLogonTimestamp = int(res[0]['lastLogonTimestamp'][0])
+
+ # prove we can change the user password (using the correct password)
+ new_password = 'thatsAcomplPASS1'
+ net.set_password(newpassword=new_password,
+ account_name=username,
+ domain_name=creds.get_domain())
+ creds.set_password(new_password)
+
+ # Add the user to the Protected Users group.
+
+ # Search for the Protected Users group.
+ group_dn = Dn(self.ldb,
+ f'<SID={self.ldb.get_domain_sid()}-'
+ f'{security.DOMAIN_RID_PROTECTED_USERS}>')
+ try:
+ group_res = self.ldb.search(base=group_dn,
+ scope=SCOPE_BASE,
+ attrs=['member'])
+ except LdbError as err:
+ self.fail(err)
+
+ orig_msg = group_res[0]
+
+ # Add the user to the list of members.
+ members = list(orig_msg.get('member', ()))
+ self.assertNotIn(userdn, members, 'account already in Protected Users')
+ members.append(userdn)
+
+ m = Message(group_dn)
+ m['member'] = MessageElement(members,
+ FLAG_MOD_REPLACE,
+ 'member')
+ self.ldb.modify(m)
+
+ # Try entering the correct password 'x' times in a row, which should
+ # fail, but not lock the user out.
+ new_password = 'thatsAcomplPASS2'
+ for i in range(lockout_threshold):
+ with self.assertRaises(
+ NTSTATUSError,
+ msg='Invalid SAMR set_password accepted') as err:
+ self.debug(f'Trying correct password, attempt #{i}')
+ net.set_password(newpassword=new_password,
+ account_name=username,
+ domain_name=creds.get_domain())
+
+ enum = ctypes.c_uint32(err.exception.args[0]).value
+ self.assertEqual(enum, ntstatus.NT_STATUS_ACCOUNT_RESTRICTION)
+
+ res = self._check_account(
+ userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=None,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The user should not be locked out.
+ self.assertNotIn('lockoutTime', res[0])
+
+ # Ensure that the password can still be changed via LDAP.
+ self.ldb.modify_ldif(f'''
+dn: {userdn}
+changetype: modify
+delete: userPassword
+userPassword: {creds.get_password()}
+add: userPassword
+userPassword: {new_password}
+''')
+
+
+class PasswordTestsWithSleep(PasswordTests):
+ def setUp(self):
+ super(PasswordTestsWithSleep, self).setUp()
+
+ def _test_unicodePwd_lockout_with_clear_change(self, creds, other_ldb,
+ initial_logoncount_relation=None):
+ self.debug("Performs a password cleartext change operation on 'unicodePwd'")
+ username = creds.get_username()
+ userpass = creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+ if initial_logoncount_relation is not None:
+ logoncount_relation = initial_logoncount_relation
+ else:
+ logoncount_relation = "greater"
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=("greater", 0),
+ logonCount=(logoncount_relation, 0),
+ lastLogon=("greater", 0),
+ lastLogonTimestamp=("greater", 0),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+ self.assertGreater(lastLogonTimestamp, badPasswordTime)
+ self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
+
+ # Change password on a connection as another user
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1x\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e10:
+ (num, msg) = e10.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000056', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Correct old password
+ old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
+ invalid_utf16 = "\"thatsAcomplPASSX\"".encode('utf-16-le')
+ userpass = "thatsAcomplPASS2"
+ creds.set_password(userpass)
+ new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
+
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e11:
+ (num, msg) = e11.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000056', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
+ # It doesn't create "lockoutTime" = 0 and doesn't
+ # reset "badPwdCount" = 0.
+ self._reset_samr(res)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ self.debug("two failed password change")
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e12:
+ (num, msg) = e12.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000056', msg)
+
+ # this is strange, why do we have lockoutTime=badPasswordTime here?
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=("greater", badPasswordTime),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ lockoutTime = int(res[0]["lockoutTime"][0])
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e13:
+ (num, msg) = e13.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000775', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e14:
+ (num, msg) = e14.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000775', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ try:
+ # Correct old password
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e15:
+ (num, msg) = e15.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000775', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # Now reset the lockout, by removing ACB_AUTOLOCK (which removes the lock, despite being a generated attribute)
+ self._reset_samr(res)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # Correct old password
+ old_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
+ invalid_utf16 = "\"thatsAcomplPASSiX\"".encode('utf-16-le')
+ userpass = "thatsAcomplPASS2x"
+ creds.set_password(userpass)
+ new_utf16 = ("\"%s\"" % userpass).encode('utf-16-le')
+
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(old_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e16:
+ (num, msg) = e16.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000056', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e17:
+ (num, msg) = e17.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000056', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
+ # It doesn't reset "badPwdCount" = 0.
+ self._reset_samr(res)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # Wrong old password
+ try:
+ other_ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode(invalid_utf16).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode(new_utf16).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e18:
+ (num, msg) = e18.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertIn('00000056', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=("greater", badPasswordTime),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ lockoutTime = int(res[0]["lockoutTime"][0])
+
+ time.sleep(self.account_lockout_duration + 1)
+
+ res = self._check_account(userdn,
+ badPwdCount=3, effective_bad_password_count=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
+ # It doesn't reset "lockoutTime" = 0 and doesn't
+ # reset "badPwdCount" = 0.
+ self._reset_samr(res)
+
+ res = self._check_account(userdn,
+ badPwdCount=3, effective_bad_password_count=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ def test_unicodePwd_lockout_with_clear_change_krb5(self):
+ self._test_unicodePwd_lockout_with_clear_change(self.lockout1krb5_creds,
+ self.lockout2krb5_ldb)
+
+ def test_unicodePwd_lockout_with_clear_change_ntlm(self):
+ self._test_unicodePwd_lockout_with_clear_change(self.lockout1ntlm_creds,
+ self.lockout2ntlm_ldb,
+ initial_logoncount_relation="equal")
+
+ def test_login_lockout_krb5(self):
+ self._test_login_lockout(self.lockout1krb5_creds)
+
+ def test_login_lockout_ntlm(self):
+ self._test_login_lockout(self.lockout1ntlm_creds)
+
+ # Repeat the login lockout tests using PSOs
+ def test_pso_login_lockout_krb5(self):
+ """Check the PSO lockout settings get applied to the user correctly"""
+ self.use_pso_lockout_settings(self.lockout1krb5_creds)
+ self._test_login_lockout(self.lockout1krb5_creds)
+
+ def test_pso_login_lockout_ntlm(self):
+ """Check the PSO lockout settings get applied to the user correctly"""
+ self.use_pso_lockout_settings(self.lockout1ntlm_creds)
+ self._test_login_lockout(self.lockout1ntlm_creds)
+
+ def _testing_add_user(self, creds, lockOutObservationWindow=0):
+ username = creds.get_username()
+ userpass = creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ use_kerberos = creds.get_kerberos_state()
+ if use_kerberos == MUST_USE_KERBEROS:
+ logoncount_relation = 'greater'
+ lastlogon_relation = 'greater'
+ else:
+ logoncount_relation = 'equal'
+ if lockOutObservationWindow == 0:
+ lastlogon_relation = 'greater'
+ else:
+ lastlogon_relation = 'equal'
+
+ delete_force(self.ldb, userdn)
+ self.ldb.add({
+ "dn": userdn,
+ "objectclass": "user",
+ "sAMAccountName": username})
+
+ self.addCleanup(delete_force, self.ldb, userdn)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=0,
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=('absent', None),
+ userAccountControl=(dsdb.UF_NORMAL_ACCOUNT |
+ dsdb.UF_ACCOUNTDISABLE |
+ dsdb.UF_PASSWD_NOTREQD),
+ msDSUserAccountControlComputed=dsdb.UF_PASSWORD_EXPIRED)
+
+ # SAMR doesn't have any impact if dsdb.UF_LOCKOUT isn't present.
+ # It doesn't create "lockoutTime" = 0.
+ self._reset_samr(res)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=0,
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=('absent', None),
+ userAccountControl=(dsdb.UF_NORMAL_ACCOUNT |
+ dsdb.UF_ACCOUNTDISABLE |
+ dsdb.UF_PASSWD_NOTREQD),
+ msDSUserAccountControlComputed=dsdb.UF_PASSWORD_EXPIRED)
+
+ # Tests a password change when we don't have any password yet with a
+ # wrong old password
+ try:
+ self.ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+userPassword: noPassword
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e19:
+ (num, msg) = e19.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ # Windows (2008 at least) seems to have some small bug here: it
+ # returns "0000056A" on longer (always wrong) previous passwords.
+ self.assertIn('00000056', msg)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", 0),
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=('absent', None),
+ userAccountControl=(dsdb.UF_NORMAL_ACCOUNT |
+ dsdb.UF_ACCOUNTDISABLE |
+ dsdb.UF_PASSWD_NOTREQD),
+ msDSUserAccountControlComputed=dsdb.UF_PASSWORD_EXPIRED)
+ badPwdCount = int(res[0]["badPwdCount"][0])
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Sets the initial user password with a "special" password change
+ # I think that this internally is a password set operation and it can
+ # only be performed by someone which has password set privileges on the
+ # account (at least in s4 we do handle it like that).
+ self.ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+add: userPassword
+userPassword: """ + userpass + """
+""")
+
+ res = self._check_account(userdn,
+ badPwdCount=badPwdCount,
+ badPasswordTime=badPasswordTime,
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=('absent', None),
+ userAccountControl=(dsdb.UF_NORMAL_ACCOUNT |
+ dsdb.UF_ACCOUNTDISABLE |
+ dsdb.UF_PASSWD_NOTREQD),
+ msDSUserAccountControlComputed=0)
+
+ # Enables the user account
+ self.ldb.enable_account("(sAMAccountName=%s)" % username)
+
+ res = self._check_account(userdn,
+ badPwdCount=badPwdCount,
+ badPasswordTime=badPasswordTime,
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=('absent', None),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ if lockOutObservationWindow != 0:
+ time.sleep(lockOutObservationWindow + 1)
+ effective_bad_password_count = 0
+ else:
+ effective_bad_password_count = badPwdCount
+
+ res = self._check_account(userdn,
+ badPwdCount=badPwdCount,
+ effective_bad_password_count=effective_bad_password_count,
+ badPasswordTime=badPasswordTime,
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=('absent', None),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ ldb = SamDB(url=self.host_url, credentials=creds, lp=self.lp)
+
+ if lockOutObservationWindow == 0:
+ badPwdCount = 0
+ effective_bad_password_count = 0
+ if use_kerberos == MUST_USE_KERBEROS:
+ badPwdCount = 0
+ effective_bad_password_count = 0
+
+ res = self._check_account(userdn,
+ badPwdCount=badPwdCount,
+ effective_bad_password_count=effective_bad_password_count,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, 0),
+ lastLogon=(lastlogon_relation, 0),
+ lastLogonTimestamp=('greater', badPasswordTime),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+ if lastlogon_relation == 'greater':
+ self.assertGreater(lastLogon, badPasswordTime)
+ self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
+
+ res = self._check_account(userdn,
+ badPwdCount=badPwdCount,
+ effective_bad_password_count=effective_bad_password_count,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ return ldb
+
+ def test_lockout_observation_window(self):
+ lockout3krb5_creds = self.insta_creds(self.template_creds,
+ username="lockout3krb5",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=MUST_USE_KERBEROS)
+ self._testing_add_user(lockout3krb5_creds)
+
+ lockout4krb5_creds = self.insta_creds(self.template_creds,
+ username="lockout4krb5",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=MUST_USE_KERBEROS)
+ self._testing_add_user(lockout4krb5_creds,
+ lockOutObservationWindow=self.lockout_observation_window)
+
+ lockout3ntlm_creds = self.insta_creds(self.template_creds,
+ username="lockout3ntlm",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=DONT_USE_KERBEROS)
+ self._testing_add_user(lockout3ntlm_creds)
+ lockout4ntlm_creds = self.insta_creds(self.template_creds,
+ username="lockout4ntlm",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=DONT_USE_KERBEROS)
+ self._testing_add_user(lockout4ntlm_creds,
+ lockOutObservationWindow=self.lockout_observation_window)
+
+class PasswordTestsWithDefaults(PasswordTests):
+ def setUp(self):
+ # The tests in this class do not sleep, so we can use the default
+ # timeout windows here
+ self.account_lockout_duration = 30 * 60
+ self.lockout_observation_window = 30 * 60
+ super(PasswordTestsWithDefaults, self).setUp()
+
+ # sanity-check that user lockout works with the default settings (we just
+ # check the user is locked out - we don't wait for the lockout to expire)
+ def test_login_lockout_krb5(self):
+ self._test_login_lockout(self.lockout1krb5_creds,
+ wait_lockout_duration=False)
+
+ def test_login_lockout_ntlm(self):
+ self._test_login_lockout(self.lockout1ntlm_creds,
+ wait_lockout_duration=False)
+
+ # Repeat the login lockout tests using PSOs
+ def test_pso_login_lockout_krb5(self):
+ """Check the PSO lockout settings get applied to the user correctly"""
+ self.use_pso_lockout_settings(self.lockout1krb5_creds)
+ self._test_login_lockout(self.lockout1krb5_creds,
+ wait_lockout_duration=False)
+
+ def test_pso_login_lockout_ntlm(self):
+ """Check the PSO lockout settings get applied to the user correctly"""
+ self.use_pso_lockout_settings(self.lockout1ntlm_creds)
+ self._test_login_lockout(self.lockout1ntlm_creds,
+ wait_lockout_duration=False)
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/password_lockout_base.py b/source4/dsdb/tests/python/password_lockout_base.py
new file mode 100644
index 0000000..77529b7
--- /dev/null
+++ b/source4/dsdb/tests/python/password_lockout_base.py
@@ -0,0 +1,785 @@
+from samba.credentials import Credentials, DONT_USE_KERBEROS, MUST_USE_KERBEROS
+from ldb import SCOPE_BASE, LdbError
+from ldb import ERR_INVALID_CREDENTIALS
+from ldb import SUCCESS as LDB_SUCCESS
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_REPLACE
+from samba import gensec, dsdb
+from samba.samdb import SamDB
+from samba.tests import delete_force
+from samba.dcerpc import security, samr
+from samba.ndr import ndr_unpack
+from samba.tests.password_test import PasswordTestCase
+
+import time
+
+
+class BasePasswordTestCase(PasswordTestCase):
+ if False:
+ debug = print
+ else:
+ def debug(self, *args, **kwargs):
+ pass
+
+ def _open_samr_user(self, res):
+ self.assertIn("objectSid", res[0])
+
+ (domain_sid, rid) = ndr_unpack(security.dom_sid, res[0]["objectSid"][0]).split()
+ self.assertEqual(self.domain_sid, domain_sid)
+
+ return self.samr.OpenUser(self.samr_domain, security.SEC_FLAG_MAXIMUM_ALLOWED, rid)
+
+ def _check_attribute(self, res, name, value):
+ if value is None:
+ self.assertNotIn(name, res[0],
+ msg="attr[%s]=%r on dn[%s]" %
+ (name, res[0], res[0].dn))
+ return
+
+ if isinstance(value, tuple):
+ (mode, value) = value
+ else:
+ mode = "equal"
+
+ if mode == "ignore":
+ return
+
+ if mode == "absent":
+ self.assertNotIn(name, res[0],
+ msg="attr[%s] not missing on dn[%s]" %
+ (name, res[0].dn))
+ return
+
+ self.assertIn(name, res[0],
+ msg="attr[%s] missing on dn[%s]" %
+ (name, res[0].dn))
+ self.assertEqual(1, len(res[0][name]),
+ msg="attr[%s]=%r on dn[%s]" %
+ (name, res[0][name], res[0].dn))
+
+ self.debug("%s = '%s'" % (name, res[0][name][0]))
+
+ if mode == "present":
+ return
+
+ if mode == "equal":
+ v = int(res[0][name][0])
+ value = int(value)
+ msg = ("attr[%s]=[%s] != [%s] on dn[%s]\n"
+ "(diff %d; actual value is %s than expected)" %
+ (name, v, value, res[0].dn, v - value,
+ ('less' if v < value else 'greater')))
+
+ self.assertEqual(v, value, msg)
+ return
+
+ if mode == "greater":
+ v = int(res[0][name][0])
+ self.assertGreater(v, int(value),
+ msg="attr[%s]=[%s] <= [%s] on dn[%s] (diff %d)" %
+ (name, v, int(value), res[0].dn, v - int(value)))
+ return
+ if mode == "less":
+ v = int(res[0][name][0])
+ self.assertLess(v, int(value),
+ msg="attr[%s]=[%s] >= [%s] on dn[%s] (diff %d)" %
+ (name, v, int(value), res[0].dn, v - int(value)))
+ return
+ self.fail("Invalid Mode[%s]" % mode)
+
+ def _check_account_initial(self, userdn):
+ self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=0,
+ logonCount=0,
+ lastLogon=0,
+ lastLogonTimestamp=("absent", None),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ def _check_account(self, dn,
+ badPwdCount=None,
+ badPasswordTime=None,
+ logonCount=None,
+ lastLogon=None,
+ lastLogonTimestamp=None,
+ lockoutTime=None,
+ userAccountControl=None,
+ msDSUserAccountControlComputed=None,
+ effective_bad_password_count=None,
+ msg=None,
+ badPwdCountOnly=False):
+ self.debug('-=' * 36)
+ if msg is not None:
+ self.debug("\033[01;32m %s \033[00m\n" % msg)
+ attrs = [
+ "objectSid",
+ "sAMAccountName",
+ "badPwdCount",
+ "badPasswordTime",
+ "lastLogon",
+ "lastLogonTimestamp",
+ "logonCount",
+ "lockoutTime",
+ "userAccountControl",
+ "msDS-User-Account-Control-Computed"
+ ]
+
+ # in order to prevent some time resolution problems we sleep for
+ # 10 micro second
+ time.sleep(0.01)
+
+ res = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs)
+ self.assertEqual(1, len(res))
+ self._check_attribute(res, "badPwdCount", badPwdCount)
+ self._check_attribute(res, "lockoutTime", lockoutTime)
+ self._check_attribute(res, "badPasswordTime", badPasswordTime)
+ if not badPwdCountOnly:
+ self._check_attribute(res, "logonCount", logonCount)
+ self._check_attribute(res, "lastLogon", lastLogon)
+ self._check_attribute(res, "lastLogonTimestamp", lastLogonTimestamp)
+ self._check_attribute(res, "userAccountControl", userAccountControl)
+ self._check_attribute(res, "msDS-User-Account-Control-Computed",
+ msDSUserAccountControlComputed)
+
+ lastLogon = int(res[0]["lastLogon"][0])
+ logonCount = int(res[0]["logonCount"][0])
+
+ samr_user = self._open_samr_user(res)
+ uinfo3 = self.samr.QueryUserInfo(samr_user, 3)
+ uinfo5 = self.samr.QueryUserInfo(samr_user, 5)
+ uinfo16 = self.samr.QueryUserInfo(samr_user, 16)
+ uinfo21 = self.samr.QueryUserInfo(samr_user, 21)
+ self.samr.Close(samr_user)
+
+ expected_acb_info = 0
+ if not badPwdCountOnly:
+ if userAccountControl & dsdb.UF_NORMAL_ACCOUNT:
+ expected_acb_info |= samr.ACB_NORMAL
+ if userAccountControl & dsdb.UF_ACCOUNTDISABLE:
+ expected_acb_info |= samr.ACB_DISABLED
+ if userAccountControl & dsdb.UF_PASSWD_NOTREQD:
+ expected_acb_info |= samr.ACB_PWNOTREQ
+ if msDSUserAccountControlComputed & dsdb.UF_LOCKOUT:
+ expected_acb_info |= samr.ACB_AUTOLOCK
+ if msDSUserAccountControlComputed & dsdb.UF_PASSWORD_EXPIRED:
+ expected_acb_info |= samr.ACB_PW_EXPIRED
+
+ self.assertEqual(uinfo3.acct_flags, expected_acb_info)
+ self.assertEqual(uinfo3.last_logon, lastLogon)
+ self.assertEqual(uinfo3.logon_count, logonCount)
+
+ expected_bad_password_count = 0
+ if badPwdCount is not None:
+ expected_bad_password_count = badPwdCount
+ if effective_bad_password_count is None:
+ effective_bad_password_count = expected_bad_password_count
+
+ self.assertEqual(uinfo3.bad_password_count, expected_bad_password_count)
+
+ if not badPwdCountOnly:
+ self.assertEqual(uinfo5.acct_flags, expected_acb_info)
+ self.assertEqual(uinfo5.bad_password_count, effective_bad_password_count)
+ self.assertEqual(uinfo5.last_logon, lastLogon)
+ self.assertEqual(uinfo5.logon_count, logonCount)
+
+ self.assertEqual(uinfo16.acct_flags, expected_acb_info)
+
+ self.assertEqual(uinfo21.acct_flags, expected_acb_info)
+ self.assertEqual(uinfo21.bad_password_count, effective_bad_password_count)
+ self.assertEqual(uinfo21.last_logon, lastLogon)
+ self.assertEqual(uinfo21.logon_count, logonCount)
+
+ # check LDAP again and make sure the samr.QueryUserInfo
+ # doesn't have any impact.
+ res2 = self.ldb.search(dn, scope=SCOPE_BASE, attrs=attrs)
+ self.assertEqual(res[0], res2[0])
+
+ # in order to prevent some time resolution problems we sleep for
+ # 10 micro second
+ time.sleep(0.01)
+ return res
+
+ def update_lockout_settings(self, threshold, duration, observation_window):
+ """Updates the global user lockout settings"""
+ m = Message()
+ m.dn = Dn(self.ldb, self.base_dn)
+ account_lockout_duration_ticks = -int(duration * (1e7))
+ m["lockoutDuration"] = MessageElement(str(account_lockout_duration_ticks),
+ FLAG_MOD_REPLACE, "lockoutDuration")
+ m["lockoutThreshold"] = MessageElement(str(threshold),
+ FLAG_MOD_REPLACE, "lockoutThreshold")
+ lockout_observation_window_ticks = -int(observation_window * (1e7))
+ m["lockOutObservationWindow"] = MessageElement(str(lockout_observation_window_ticks),
+ FLAG_MOD_REPLACE, "lockOutObservationWindow")
+ self.ldb.modify(m)
+
+ def _readd_user(self, creds, lockOutObservationWindow=0, simple=False):
+ username = creds.get_username()
+ userpass = creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ if simple:
+ creds.set_bind_dn(userdn)
+ ldap_url = self.host_url_ldaps
+ else:
+ ldap_url = self.host_url
+
+ delete_force(self.ldb, userdn)
+ self.ldb.add({
+ "dn": userdn,
+ "objectclass": "user",
+ "sAMAccountName": username})
+
+ self.addCleanup(delete_force, self.ldb, userdn)
+
+ # Sets the initial user password with a "special" password change
+ # I think that this internally is a password set operation and it can
+ # only be performed by someone which has password set privileges on the
+ # account (at least in s4 we do handle it like that).
+ self.ldb.modify_ldif("""
+dn: """ + userdn + """
+changetype: modify
+delete: userPassword
+add: userPassword
+userPassword: """ + userpass + """
+""")
+ # Enables the user account
+ self.ldb.enable_account("(sAMAccountName=%s)" % username)
+
+ use_kerberos = creds.get_kerberos_state()
+ fail_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass + "X",
+ kerberos_state=use_kerberos)
+ if simple:
+ fail_creds.set_bind_dn(userdn)
+
+ self._check_account_initial(userdn)
+
+ # Fail once to get a badPasswordTime
+ self.assertLoginFailure(ldap_url, fail_creds, self.lp)
+
+ # Always reset with Simple bind or Kerberos, allows testing without NTLM
+ if simple or use_kerberos == MUST_USE_KERBEROS:
+ success_creds = creds
+ else:
+ success_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass)
+ success_creds.set_bind_dn(userdn)
+ ldap_url = self.host_url_ldaps
+
+ # Succeed to reset everything to 0
+ self.assertLoginSuccess(ldap_url, success_creds, self.lp)
+
+ def assertLoginFailure(self, url, creds, lp, errno=ERR_INVALID_CREDENTIALS):
+ try:
+ SamDB(url=url, credentials=creds, lp=lp)
+ self.fail("Login unexpectedly succeeded")
+ except LdbError as e1:
+ (num, msg) = e1.args
+ if errno is not None:
+ self.assertEqual(num, errno, ("Login failed in the wrong way"
+ "(got err %d, expected %d)" %
+ (num, errno)))
+
+ def assertLoginSuccess(self, url, creds, lp):
+ try:
+ ldb = SamDB(url=url, credentials=creds, lp=lp)
+ return ldb
+ except LdbError as e1:
+ (num, msg) = e1.args
+ self.assertEqual(num, LDB_SUCCESS,
+ ("Login failed - %d - %s" % (
+ num, msg)))
+
+
+ def setUp(self):
+ super(BasePasswordTestCase, self).setUp()
+
+ self.global_creds.set_gensec_features(self.global_creds.get_gensec_features() |
+ gensec.FEATURE_SEAL)
+
+ self.template_creds = Credentials()
+ self.template_creds.set_username("testuser")
+ self.template_creds.set_password("thatsAcomplPASS1")
+ self.template_creds.set_domain(self.global_creds.get_domain())
+ self.template_creds.set_realm(self.global_creds.get_realm())
+ self.template_creds.set_workstation(self.global_creds.get_workstation())
+ self.template_creds.set_gensec_features(self.global_creds.get_gensec_features())
+ self.template_creds.set_kerberos_state(self.global_creds.get_kerberos_state())
+
+ # Gets back the basedn
+ base_dn = self.ldb.domain_dn()
+
+ res = self.ldb.search(base_dn,
+ scope=SCOPE_BASE, attrs=["lockoutDuration", "lockOutObservationWindow", "lockoutThreshold"])
+
+ if "lockoutDuration" in res[0]:
+ lockoutDuration = res[0]["lockoutDuration"][0]
+ else:
+ lockoutDuration = 0
+
+ if "lockoutObservationWindow" in res[0]:
+ lockoutObservationWindow = res[0]["lockoutObservationWindow"][0]
+ else:
+ lockoutObservationWindow = 0
+
+ if "lockoutThreshold" in res[0]:
+ lockoutThreshold = res[0]["lockoutThreshold"][0]
+ else:
+ lockoutThreshold = 0
+
+ self.addCleanup(self.ldb.modify_ldif, """
+dn: """ + base_dn + """
+changetype: modify
+replace: lockoutDuration
+lockoutDuration: """ + str(lockoutDuration) + """
+replace: lockoutObservationWindow
+lockoutObservationWindow: """ + str(lockoutObservationWindow) + """
+replace: lockoutThreshold
+lockoutThreshold: """ + str(lockoutThreshold) + """
+""")
+
+ self.base_dn = self.ldb.domain_dn()
+
+ #
+ # Some test cases sleep() for self.account_lockout_duration
+ # so allow it to be controlled via the subclass
+ #
+ if not hasattr(self, 'account_lockout_duration'):
+ self.account_lockout_duration = 3
+ if not hasattr(self, 'lockout_observation_window'):
+ self.lockout_observation_window = 3
+ self.update_lockout_settings(threshold=3,
+ duration=self.account_lockout_duration,
+ observation_window=self.lockout_observation_window)
+
+ # update DC to allow password changes for the duration of this test
+ self.allow_password_changes()
+
+ self.domain_sid = security.dom_sid(self.ldb.get_domain_sid())
+ self.samr = samr.samr("ncacn_ip_tcp:%s[seal]" % self.host, self.lp, self.global_creds)
+ self.samr_handle = self.samr.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
+ self.samr_domain = self.samr.OpenDomain(self.samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED, self.domain_sid)
+
+ self.addCleanup(self.delete_ldb_connections)
+
+ # (Re)adds the test user accounts
+ self.lockout1krb5_creds = self.insta_creds(self.template_creds,
+ username="lockout1krb5",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=MUST_USE_KERBEROS)
+ self._readd_user(self.lockout1krb5_creds)
+ self.lockout1ntlm_creds = self.insta_creds(self.template_creds,
+ username="lockout1ntlm",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=DONT_USE_KERBEROS)
+ self._readd_user(self.lockout1ntlm_creds)
+ self.lockout1simple_creds = self.insta_creds(self.template_creds,
+ username="lockout1simple",
+ userpass="thatsAcomplPASS0",
+ kerberos_state=DONT_USE_KERBEROS)
+ self._readd_user(self.lockout1simple_creds,
+ simple=True)
+
+ def delete_ldb_connections(self):
+ del self.ldb
+
+ def tearDown(self):
+ super(BasePasswordTestCase, self).tearDown()
+
+ def _test_login_lockout(self, creds, wait_lockout_duration=True):
+ username = creds.get_username()
+ userpass = creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ use_kerberos = creds.get_kerberos_state()
+ # This unlocks by waiting for account_lockout_duration
+ if use_kerberos == MUST_USE_KERBEROS:
+ logoncount_relation = 'greater'
+ lastlogon_relation = 'greater'
+ self.debug("Performs a lockout attempt against LDAP using Kerberos")
+ else:
+ logoncount_relation = 'equal'
+ lastlogon_relation = 'equal'
+ self.debug("Performs a lockout attempt against LDAP using NTLM")
+
+ # Change password on a connection as another user
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=("greater", 0),
+ logonCount=(logoncount_relation, 0),
+ lastLogon=("greater", 0),
+ lastLogonTimestamp=("greater", 0),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ firstLogon = lastLogon
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+ self.debug(firstLogon)
+ self.debug(lastLogonTimestamp)
+
+ self.assertGreater(lastLogon, badPasswordTime)
+ self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
+
+ # Open a second LDB connection with the user credentials. Use the
+ # command line credentials for information like the domain, the realm
+ # and the workstation.
+ creds_lockout = self.insta_creds(creds)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ self.assertLoginFailure(self.host_url, creds_lockout, self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='lastlogontimestamp with wrong password')
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Correct old password
+ creds_lockout.set_password(userpass)
+
+ SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+
+ # lastLogonTimestamp should not change
+ # lastLogon increases if badPwdCount is non-zero (!)
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=('greater', lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='LLTimestamp is updated to lastlogon')
+
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ self.assertGreater(lastLogon, badPasswordTime)
+ self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ self.assertLoginFailure(self.host_url, creds_lockout, self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ try:
+ SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+
+ except LdbError as e2:
+ (num, msg) = e2.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ self.debug("two failed password change")
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ try:
+ SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+
+ except LdbError as e3:
+ (num, msg) = e3.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=("greater", badPasswordTime),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ lockoutTime = int(res[0]["lockoutTime"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e4:
+ (num, msg) = e4.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e5:
+ (num, msg) = e5.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # The correct password, but we are locked out
+ creds_lockout.set_password(userpass)
+ try:
+ SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e6:
+ (num, msg) = e6.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # if we're just checking the user gets locked out, we can stop here
+ if not wait_lockout_duration:
+ return
+
+ # wait for the lockout to end
+ time.sleep(self.account_lockout_duration + 1)
+ self.debug(self.account_lockout_duration + 1)
+
+ res = self._check_account(userdn,
+ badPwdCount=3, effective_bad_password_count=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The correct password after letting the timeout expire
+
+ creds_lockout.set_password(userpass)
+
+ creds_lockout2 = self.insta_creds(creds_lockout)
+
+ SamDB(url=self.host_url, credentials=creds_lockout2, lp=self.lp)
+ time.sleep(3)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=(lastlogon_relation, lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=0,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg="lastLogon is way off")
+
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e7:
+ (num, msg) = e7.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e8:
+ (num, msg) = e8.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ time.sleep(self.lockout_observation_window + 1)
+
+ res = self._check_account(userdn,
+ badPwdCount=2, effective_bad_password_count=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e9:
+ (num, msg) = e9.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=0,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # The correct password without letting the timeout expire
+ creds_lockout.set_password(userpass)
+ SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lockoutTime=0,
+ lastLogon=("greater", lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ def _test_multiple_logon(self, creds):
+ # Test the happy case in which a user logs on correctly, then
+ # logs on correctly again, so that the bad password and
+ # lockout times are both zero the second time. The lastlogon
+ # time should increase.
+
+ # Open a second LDB connection with the user credentials. Use the
+ # command line credentials for information like the domain, the realm
+ # and the workstation.
+ username = creds.get_username()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ use_kerberos = creds.get_kerberos_state()
+ if use_kerberos == MUST_USE_KERBEROS:
+ self.debug("Testing multiple logon with Kerberos")
+ logoncount_relation = 'greater'
+ lastlogon_relation = 'greater'
+ else:
+ self.debug("Testing multiple logon with NTLM")
+ logoncount_relation = 'equal'
+ lastlogon_relation = 'equal'
+
+ SamDB(url=self.host_url, credentials=self.insta_creds(creds), lp=self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=("greater", 0),
+ logonCount=(logoncount_relation, 0),
+ lastLogon=("greater", 0),
+ lastLogonTimestamp=("greater", 0),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+ lastLogonTimestamp = int(res[0]["lastLogonTimestamp"][0])
+ firstLogon = lastLogon
+ self.debug("last logon is %d" % lastLogon)
+ self.assertGreater(lastLogon, badPasswordTime)
+ self.assertGreaterEqual(lastLogon, lastLogonTimestamp)
+
+ time.sleep(1)
+ SamDB(url=self.host_url, credentials=self.insta_creds(creds), lp=self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=(lastlogon_relation, lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg=("second logon, firstlogon was %s" %
+ firstLogon))
+
+ lastLogon = int(res[0]["lastLogon"][0])
+
+ time.sleep(1)
+
+ SamDB(url=self.host_url, credentials=self.insta_creds(creds), lp=self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=(lastlogon_relation, lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
diff --git a/source4/dsdb/tests/python/password_settings.py b/source4/dsdb/tests/python/password_settings.py
new file mode 100644
index 0000000..cdaad05
--- /dev/null
+++ b/source4/dsdb/tests/python/password_settings.py
@@ -0,0 +1,876 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Tests for Password Settings Objects.
+#
+# This also tests the default password complexity (i.e. pwdProperties),
+# minPwdLength, pwdHistoryLength settings as a side-effect.
+#
+# 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/>.
+#
+
+#
+# Usage:
+# export SERVER_IP=target_dc
+# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun
+# PYTHONPATH="$PYTHONPATH:$samba4srcdir/dsdb/tests/python" $SUBUNITRUN \
+# password_settings -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD"
+#
+
+import samba.tests
+import ldb
+from ldb import FLAG_MOD_DELETE, FLAG_MOD_ADD, FLAG_MOD_REPLACE
+from samba import dsdb
+import time
+from samba.tests.password_test import PasswordTestCase
+from samba.tests.pso import TestUser
+from samba.tests.pso import PasswordSettings
+from samba.tests import env_get_var_value
+from samba.credentials import Credentials
+from samba import gensec
+import base64
+
+
+class PasswordSettingsTestCase(PasswordTestCase):
+ def setUp(self):
+ super(PasswordSettingsTestCase, self).setUp()
+
+ self.host_url = "ldap://%s" % env_get_var_value("SERVER_IP")
+ self.ldb = samba.tests.connect_samdb(self.host_url)
+
+ # create a temp OU to put this test's users into
+ self.ou = samba.tests.create_test_ou(self.ldb, "password_settings")
+
+ # update DC to allow password changes for the duration of this test
+ self.allow_password_changes()
+
+ # store the current password-settings for the domain
+ self.pwd_defaults = PasswordSettings(None, self.ldb)
+ self.test_objs = []
+
+ def tearDown(self):
+ super(PasswordSettingsTestCase, self).tearDown()
+
+ # remove all objects under the top-level OU
+ self.ldb.delete(self.ou, ["tree_delete:1"])
+
+ # PSOs can't reside within an OU so they get cleaned up separately
+ for obj in self.test_objs:
+ self.ldb.delete(obj)
+
+ def add_obj_cleanup(self, dn_list):
+ """Handles cleanup of objects outside of the test OU in the tearDown"""
+ self.test_objs.extend(dn_list)
+
+ def add_group(self, group_name):
+ """Creates a new group"""
+ dn = "CN=%s,%s" % (group_name, self.ou)
+ self.ldb.add({"dn": dn, "objectclass": "group"})
+ return dn
+
+ def set_attribute(self, dn, attr, value, operation=FLAG_MOD_ADD,
+ samdb=None):
+ """Modifies an attribute for an object"""
+ if samdb is None:
+ samdb = self.ldb
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, dn)
+ m[attr] = ldb.MessageElement(value, operation, attr)
+ samdb.modify(m)
+
+ def add_user(self, username):
+ # add a new user to the DB under our top-level OU
+ userou = "ou=%s" % self.ou.get_component_value(0)
+ return TestUser(username, self.ldb, userou=userou)
+
+ def assert_password_invalid(self, user, password):
+ """
+ Check we can't set a password that violates complexity or length
+ constraints
+ """
+ try:
+ user.set_password(password)
+ # fail the test if no exception was encountered
+ self.fail("Password '%s' should have been rejected" % password)
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_CONSTRAINT_VIOLATION, msg)
+ self.assertTrue('0000052D' in msg, msg)
+
+ def assert_password_valid(self, user, password):
+ """Checks that we can set a password successfully"""
+ try:
+ user.set_password(password)
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ # fail the test (rather than throw an error)
+ self.fail("Password '%s' unexpectedly rejected: %s" % (password,
+ msg))
+
+ def assert_PSO_applied(self, user, pso):
+ """
+ Asserts the expected PSO is applied by checking the msDS-ResultantPSO
+ attribute, as well as checking the corresponding password-length,
+ complexity, and history are enforced correctly
+ """
+ resultant_pso = user.get_resultant_PSO()
+ self.assertTrue(resultant_pso == pso.dn,
+ "Expected PSO %s, not %s" % (pso.name,
+ str(resultant_pso)))
+
+ # we're mirroring the pwd_history for the user, so make sure this is
+ # up-to-date, before we start making password changes
+ if user.last_pso:
+ user.pwd_history_change(user.last_pso.history_len, pso.history_len)
+ user.last_pso = pso
+
+ # check if we can set a sufficiently long, but non-complex, password.
+ # (We use the history-size to generate a unique password for each
+ # assertion - otherwise, if the password is already in the history,
+ # then it'll be rejected)
+ unique_char = chr(ord('a') + len(user.all_old_passwords))
+ noncomplex_pwd = "%cabcdefghijklmnopqrst" % unique_char
+
+ if pso.complexity:
+ self.assert_password_invalid(user, noncomplex_pwd)
+ else:
+ self.assert_password_valid(user, noncomplex_pwd)
+
+ # use a unique and sufficiently complex base-string to check pwd-length
+ pass_phrase = "%d#AaBbCcDdEeFfGgHhIi" % len(user.all_old_passwords)
+
+ # check that passwords less than the specified length are rejected
+ for i in range(3, pso.password_len):
+ self.assert_password_invalid(user, pass_phrase[:i])
+
+ # check we can set a password that's exactly the minimum length
+ self.assert_password_valid(user, pass_phrase[:pso.password_len])
+
+ # check the password history is enforced correctly.
+ # first, check the last n items in the password history are invalid
+ invalid_passwords = user.old_invalid_passwords(pso.history_len)
+ for pwd in invalid_passwords:
+ self.assert_password_invalid(user, pwd)
+
+ # next, check any passwords older than the history-len can be re-used
+ valid_passwords = user.old_valid_passwords(pso.history_len)
+ for pwd in valid_passwords:
+ self.assert_set_old_password(user, pwd, pso)
+
+ def password_is_complex(self, password):
+ # non-complex passwords used in the tests are all lower-case letters
+ # If it's got a number in the password, assume it's complex
+ return any(c.isdigit() for c in password)
+
+ def assert_set_old_password(self, user, password, pso):
+ """
+ Checks a user password can be set (if the password conforms to the PSO
+ settings). Used to check an old password that falls outside the history
+ length, but might still be invalid for other reasons.
+ """
+ if self.password_is_complex(password):
+ # check password conforms to length requirements
+ if len(password) < pso.password_len:
+ self.assert_password_invalid(user, password)
+ else:
+ self.assert_password_valid(user, password)
+ else:
+ # password is not complex, check PSO handles it appropriately
+ if pso.complexity:
+ self.assert_password_invalid(user, password)
+ else:
+ self.assert_password_valid(user, password)
+
+ def test_pso_basics(self):
+ """Simple tests that a PSO takes effect when applied to a group/user"""
+
+ # create some PSOs that vary in priority and basic password-len
+ best_pso = PasswordSettings("highest-priority-PSO", self.ldb,
+ precedence=5, password_len=16,
+ history_len=6)
+ medium_pso = PasswordSettings("med-priority-PSO", self.ldb,
+ precedence=15, password_len=10,
+ history_len=4)
+ worst_pso = PasswordSettings("lowest-priority-PSO", self.ldb,
+ precedence=100, complexity=False,
+ password_len=4, history_len=2)
+
+ # handle PSO clean-up (as they're outside the top-level test OU)
+ self.add_obj_cleanup([worst_pso.dn, medium_pso.dn, best_pso.dn])
+
+ # create some groups and apply the PSOs to the groups
+ group1 = self.add_group("Group-1")
+ group2 = self.add_group("Group-2")
+ group3 = self.add_group("Group-3")
+ group4 = self.add_group("Group-4")
+ worst_pso.apply_to(group1)
+ medium_pso.apply_to(group2)
+ best_pso.apply_to(group3)
+ worst_pso.apply_to(group4)
+
+ # create a user and check the default settings apply to it
+ user = self.add_user("testuser")
+ self.assert_PSO_applied(user, self.pwd_defaults)
+
+ # add user to a group. Check that the group's PSO applies to the user
+ self.set_attribute(group1, "member", user.dn)
+ self.assert_PSO_applied(user, worst_pso)
+
+ # add the user to a group with a higher precedence PSO and and check
+ # that now trumps the previous PSO
+ self.set_attribute(group2, "member", user.dn)
+ self.assert_PSO_applied(user, medium_pso)
+
+ # add the user to the remaining groups. The highest precedence PSO
+ # should now take effect
+ self.set_attribute(group3, "member", user.dn)
+ self.set_attribute(group4, "member", user.dn)
+ self.assert_PSO_applied(user, best_pso)
+
+ # delete a group membership and check the PSO changes
+ self.set_attribute(group3, "member", user.dn,
+ operation=FLAG_MOD_DELETE)
+ self.assert_PSO_applied(user, medium_pso)
+
+ # apply the low-precedence PSO directly to the user
+ # (directly applied PSOs should trump higher precedence group PSOs)
+ worst_pso.apply_to(user.dn)
+ self.assert_PSO_applied(user, worst_pso)
+
+ # remove applying the PSO directly to the user and check PSO changes
+ worst_pso.unapply(user.dn)
+ self.assert_PSO_applied(user, medium_pso)
+
+ # remove all appliesTo and check we have the default settings again
+ worst_pso.unapply(group1)
+ medium_pso.unapply(group2)
+ worst_pso.unapply(group4)
+ self.assert_PSO_applied(user, self.pwd_defaults)
+
+ def test_pso_nested_groups(self):
+ """PSOs operate correctly when applied to nested groups"""
+
+ # create some PSOs that vary in priority and basic password-len
+ group1_pso = PasswordSettings("group1-PSO", self.ldb, precedence=50,
+ password_len=12, history_len=3)
+ group2_pso = PasswordSettings("group2-PSO", self.ldb, precedence=25,
+ password_len=10, history_len=5,
+ complexity=False)
+ group3_pso = PasswordSettings("group3-PSO", self.ldb, precedence=10,
+ password_len=6, history_len=2)
+
+ # create some groups and apply the PSOs to the groups
+ group1 = self.add_group("Group-1")
+ group2 = self.add_group("Group-2")
+ group3 = self.add_group("Group-3")
+ group4 = self.add_group("Group-4")
+ group1_pso.apply_to(group1)
+ group2_pso.apply_to(group2)
+ group3_pso.apply_to(group3)
+
+ # create a PSO and apply it to a group that the user is not a member
+ # of - it should not have any effect on the user
+ unused_pso = PasswordSettings("unused-PSO", self.ldb, precedence=1,
+ password_len=20)
+ unused_pso.apply_to(group4)
+
+ # handle PSO clean-up (as they're outside the top-level test OU)
+ self.add_obj_cleanup([group1_pso.dn, group2_pso.dn, group3_pso.dn,
+ unused_pso.dn])
+
+ # create a user and check the default settings apply to it
+ user = self.add_user("testuser")
+ self.assert_PSO_applied(user, self.pwd_defaults)
+
+ # add user to a group. Check that the group's PSO applies to the user
+ self.set_attribute(group1, "member", user.dn)
+ self.set_attribute(group2, "member", group1)
+ self.assert_PSO_applied(user, group2_pso)
+
+ # add another level to the group hierarchy & check this PSO takes effect
+ self.set_attribute(group3, "member", group2)
+ self.assert_PSO_applied(user, group3_pso)
+
+ # invert the PSO precedence and check the new lowest value takes effect
+ group1_pso.set_precedence(3)
+ group2_pso.set_precedence(13)
+ group3_pso.set_precedence(33)
+ self.assert_PSO_applied(user, group1_pso)
+
+ # delete a PSO and check it no longer applies
+ self.ldb.delete(group1_pso.dn)
+ self.test_objs.remove(group1_pso.dn)
+ self.assert_PSO_applied(user, group2_pso)
+
+ def get_guid(self, dn):
+ res = self.ldb.search(base=dn, attrs=["objectGUID"],
+ scope=ldb.SCOPE_BASE)
+ return res[0]['objectGUID'][0]
+
+ def guid_string(self, guid):
+ return self.ldb.schema_format_value("objectGUID", guid)
+
+ def PSO_with_lowest_GUID(self, pso_list):
+ """Returns the PSO object in the list with the lowest GUID"""
+ # go through each PSO and fetch its GUID
+ guid_list = []
+ mapping = {}
+ for pso in pso_list:
+ guid = self.get_guid(pso.dn)
+ guid_list.append(guid)
+ # remember which GUID maps to what PSO
+ mapping[guid] = pso
+
+ # sort the GUID list to work out the lowest/best GUID
+ guid_list.sort()
+ best_guid = guid_list[0]
+
+ # sanity-check the mapping between GUID and DN is correct
+ best_pso_dn = mapping[best_guid].dn
+ self.assertEqual(self.guid_string(self.get_guid(best_pso_dn)),
+ self.guid_string(best_guid))
+
+ # return the PSO that this GUID corresponds to
+ return mapping[best_guid]
+
+ def test_pso_equal_precedence(self):
+ """Tests expected PSO wins when several have the same precedence"""
+
+ # create some PSOs that vary in priority and basic password-len
+ pso1 = PasswordSettings("PSO-1", self.ldb, precedence=5, history_len=1,
+ password_len=11)
+ pso2 = PasswordSettings("PSO-2", self.ldb, precedence=5, history_len=2,
+ password_len=8)
+ pso3 = PasswordSettings("PSO-3", self.ldb, precedence=5, history_len=3,
+ password_len=5, complexity=False)
+ pso4 = PasswordSettings("PSO-4", self.ldb, precedence=5, history_len=4,
+ password_len=13, complexity=False)
+
+ # handle PSO clean-up (as they're outside the top-level test OU)
+ self.add_obj_cleanup([pso1.dn, pso2.dn, pso3.dn, pso4.dn])
+
+ # create some groups and apply the PSOs to the groups
+ group1 = self.add_group("Group-1")
+ group2 = self.add_group("Group-2")
+ group3 = self.add_group("Group-3")
+ group4 = self.add_group("Group-4")
+ pso1.apply_to(group1)
+ pso2.apply_to(group2)
+ pso3.apply_to(group3)
+ pso4.apply_to(group4)
+
+ # create a user and check the default settings apply to it
+ user = self.add_user("testuser")
+ self.assert_PSO_applied(user, self.pwd_defaults)
+
+ # add the user to all the groups
+ self.set_attribute(group1, "member", user.dn)
+ self.set_attribute(group2, "member", user.dn)
+ self.set_attribute(group3, "member", user.dn)
+ self.set_attribute(group4, "member", user.dn)
+
+ # precedence is equal, so the PSO with lowest GUID gets applied
+ pso_list = [pso1, pso2, pso3, pso4]
+ best_pso = self.PSO_with_lowest_GUID(pso_list)
+ self.assert_PSO_applied(user, best_pso)
+
+ # excluding the winning PSO, apply the other PSOs directly to the user
+ pso_list.remove(best_pso)
+ for pso in pso_list:
+ pso.apply_to(user.dn)
+
+ # we should now have a different PSO applied (the 2nd lowest GUID)
+ next_best_pso = self.PSO_with_lowest_GUID(pso_list)
+ self.assertTrue(next_best_pso is not best_pso)
+ self.assert_PSO_applied(user, next_best_pso)
+
+ # bump the precedence of another PSO and it should now win
+ pso_list.remove(next_best_pso)
+ best_pso = pso_list[0]
+ best_pso.set_precedence(4)
+ self.assert_PSO_applied(user, best_pso)
+
+ def test_pso_invalid_location(self):
+ """Tests that PSOs in an invalid location have no effect"""
+
+ # PSOs should only be able to be created within a Password Settings
+ # Container object. Trying to create one under an OU should fail
+ try:
+ rogue_pso = PasswordSettings("rogue-PSO", self.ldb, precedence=1,
+ complexity=False, password_len=20,
+ container=self.ou)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_NAMING_VIOLATION, msg)
+ # Windows returns 2099 (Illegal superior), Samba returns 2037
+ # (Naming violation - "not a valid child class")
+ self.assertTrue('00002099' in msg or '00002037' in msg, msg)
+
+ # we can't create Password Settings Containers under an OU either
+ try:
+ rogue_psc = "CN=Rogue-PSO-container,%s" % self.ou
+ self.ldb.add({"dn": rogue_psc,
+ "objectclass": "msDS-PasswordSettingsContainer"})
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_NAMING_VIOLATION, msg)
+ self.assertTrue('00002099' in msg or '00002037' in msg, msg)
+
+ base_dn = self.ldb.get_default_basedn()
+ rogue_psc = "CN=Rogue-PSO-container,CN=Computers,%s" % base_dn
+ self.ldb.add({"dn": rogue_psc,
+ "objectclass": "msDS-PasswordSettingsContainer"})
+
+ rogue_pso = PasswordSettings("rogue-PSO", self.ldb, precedence=1,
+ container=rogue_psc, password_len=20)
+ self.add_obj_cleanup([rogue_pso.dn, rogue_psc])
+
+ # apply the PSO to a group and check it has no effect on the user
+ user = self.add_user("testuser")
+ group = self.add_group("Group-1")
+ rogue_pso.apply_to(group)
+ self.set_attribute(group, "member", user.dn)
+ self.assert_PSO_applied(user, self.pwd_defaults)
+
+ # apply the PSO directly to the user and check it has no effect
+ rogue_pso.apply_to(user.dn)
+ self.assert_PSO_applied(user, self.pwd_defaults)
+
+ # the PSOs created in these test-cases all use a default min-age of zero.
+ # This is the only test case that checks the PSO's min-age is enforced
+ def test_pso_min_age(self):
+ """Tests that a PSO's min-age is enforced"""
+ pso = PasswordSettings("min-age-PSO", self.ldb, password_len=10,
+ password_age_min=2, complexity=False)
+ self.add_obj_cleanup([pso.dn])
+
+ # create a user and apply the PSO
+ user = self.add_user("testuser")
+ pso.apply_to(user.dn)
+ self.assertTrue(user.get_resultant_PSO() == pso.dn)
+
+ # changing the password immediately should fail, even if the password
+ # is valid
+ valid_password = "min-age-passwd"
+ self.assert_password_invalid(user, valid_password)
+ # then trying the same password later should succeed
+ time.sleep(pso.password_age_min + 0.5)
+ self.assert_password_valid(user, valid_password)
+
+ def test_pso_max_age(self):
+ """Tests that a PSO's max-age is used"""
+
+ # create PSOs that use the domain's max-age +/- 1 day
+ domain_max_age = self.pwd_defaults.password_age_max
+ day_in_secs = 60 * 60 * 24
+ higher_max_age = domain_max_age + day_in_secs
+ lower_max_age = domain_max_age - day_in_secs
+ longer_pso = PasswordSettings("longer-age-PSO", self.ldb, precedence=5,
+ password_age_max=higher_max_age)
+ shorter_pso = PasswordSettings("shorter-age-PSO", self.ldb,
+ precedence=1,
+ password_age_max=lower_max_age)
+ self.add_obj_cleanup([longer_pso.dn, shorter_pso.dn])
+
+ user = self.add_user("testuser")
+
+ # we can't wait around long enough for the max-age to expire, so
+ # instead just check the msDS-UserPasswordExpiryTimeComputed for
+ # the user
+ attrs = ['msDS-UserPasswordExpiryTimeComputed']
+ res = self.ldb.search(user.dn, attrs=attrs)
+ domain_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
+
+ # apply the longer PSO and check the expiry-time becomes longer
+ longer_pso.apply_to(user.dn)
+ self.assertTrue(user.get_resultant_PSO() == longer_pso.dn)
+ res = self.ldb.search(user.dn, attrs=attrs)
+ new_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
+
+ # use timestamp diff of 1 day - 1 minute. The new expiry should still
+ # be greater than this, without getting into nano-second granularity
+ approx_timestamp_diff = (day_in_secs - 60) * (1e7)
+ self.assertTrue(new_expiry > domain_expiry + approx_timestamp_diff)
+
+ # apply the shorter PSO and check the expiry-time is shorter
+ shorter_pso.apply_to(user.dn)
+ self.assertTrue(user.get_resultant_PSO() == shorter_pso.dn)
+ res = self.ldb.search(user.dn, attrs=attrs)
+ new_expiry = int(res[0]['msDS-UserPasswordExpiryTimeComputed'][0])
+ self.assertTrue(new_expiry < domain_expiry - approx_timestamp_diff)
+
+ def test_pso_special_groups(self):
+ """Checks applying a PSO to built-in AD groups takes effect"""
+
+ # create some PSOs that will apply to special groups
+ default_pso = PasswordSettings("default-PSO", self.ldb, precedence=20,
+ password_len=8, complexity=False)
+ guest_pso = PasswordSettings("guest-PSO", self.ldb, history_len=4,
+ precedence=5, password_len=5)
+ builtin_pso = PasswordSettings("builtin-PSO", self.ldb, history_len=9,
+ precedence=1, password_len=9)
+ admin_pso = PasswordSettings("admin-PSO", self.ldb, history_len=0,
+ precedence=2, password_len=10)
+ self.add_obj_cleanup([default_pso.dn, guest_pso.dn, admin_pso.dn,
+ builtin_pso.dn])
+ base_dn = self.ldb.domain_dn()
+ domain_users = "CN=Domain Users,CN=Users,%s" % base_dn
+ domain_guests = "CN=Domain Guests,CN=Users,%s" % base_dn
+ admin_users = "CN=Domain Admins,CN=Users,%s" % base_dn
+
+ # if we apply a PSO to Domain Users (which all users are a member of)
+ # then that PSO should take effect on a new user
+ default_pso.apply_to(domain_users)
+ user = self.add_user("testuser")
+ self.assert_PSO_applied(user, default_pso)
+
+ # Apply a PSO to a builtin group. 'Domain Users' should be a member of
+ # Builtin/Users, but builtin groups should be excluded from the PSO
+ # calculation, so this should have no effect
+ builtin_pso.apply_to("CN=Users,CN=Builtin,%s" % base_dn)
+ builtin_pso.apply_to("CN=Administrators,CN=Builtin,%s" % base_dn)
+ self.assert_PSO_applied(user, default_pso)
+
+ # change the user's primary group to another group (the primaryGroupID
+ # is a little odd in that there's no memberOf backlink for it)
+ self.set_attribute(domain_guests, "member", user.dn)
+ user.set_primary_group(domain_guests)
+ # No PSO is applied to the Domain Guests yet, so the default PSO should
+ # still apply
+ self.assert_PSO_applied(user, default_pso)
+
+ # now apply a PSO to the guests group, which should trump the default
+ # PSO (because the guest PSO has a better precedence)
+ guest_pso.apply_to(domain_guests)
+ self.assert_PSO_applied(user, guest_pso)
+
+ # create a new group that's a member of Admin Users
+ nested_group = self.add_group("nested-group")
+ self.set_attribute(admin_users, "member", nested_group)
+ # set the user's primary-group to be the new group
+ self.set_attribute(nested_group, "member", user.dn)
+ user.set_primary_group(nested_group)
+ # we've only changed group membership so far, not the PSO
+ self.assert_PSO_applied(user, guest_pso)
+
+ # now apply the best-precedence PSO to Admin Users and check it applies
+ # to the user (via the nested-group's membership)
+ admin_pso.apply_to(admin_users)
+ self.assert_PSO_applied(user, admin_pso)
+
+ # restore the default primaryGroupID so we can safely delete the group
+ user.set_primary_group(domain_users)
+
+ def test_pso_none_applied(self):
+ """Tests cases where no Resultant PSO should be returned"""
+
+ # create a PSO that we will check *doesn't* get returned
+ dummy_pso = PasswordSettings("dummy-PSO", self.ldb, password_len=20)
+ self.add_obj_cleanup([dummy_pso.dn])
+
+ # you can apply a PSO to other objects (like OUs), but the resultantPSO
+ # attribute should only be returned for users
+ dummy_pso.apply_to(str(self.ou))
+ res = self.ldb.search(self.ou, attrs=['msDS-ResultantPSO'])
+ self.assertFalse('msDS-ResultantPSO' in res[0])
+
+ # create a dummy user and apply the PSO
+ user = self.add_user("testuser")
+ dummy_pso.apply_to(user.dn)
+ self.assertTrue(user.get_resultant_PSO() == dummy_pso.dn)
+
+ try:
+ # now clear the ADS_UF_NORMAL_ACCOUNT flag for the user, which should
+ # mean a resultant PSO is no longer returned (we're essentially turning
+ # the user into a DC here, which is a little overkill but tests
+ # behaviour as per the Windows specification)
+ self.set_attribute(user.dn, "userAccountControl",
+ str(dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ operation=FLAG_MOD_REPLACE)
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.fail(f"Failed to change user into a workstation: {msg}")
+ self.assertIsNone(user.get_resultant_PSO())
+
+ try:
+ # reset it back to a normal user account
+ self.set_attribute(user.dn, "userAccountControl",
+ str(dsdb.UF_NORMAL_ACCOUNT),
+ operation=FLAG_MOD_REPLACE)
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.fail(f"Failed to change user back into a user: {msg}")
+ self.assertTrue(user.get_resultant_PSO() == dummy_pso.dn)
+
+ # no PSO should be returned if RID is equal to DOMAIN_USER_RID_KRBTGT
+ # (note this currently fails against Windows due to a Windows bug)
+ krbtgt_user = "CN=krbtgt,CN=Users,%s" % self.ldb.domain_dn()
+ dummy_pso.apply_to(krbtgt_user)
+ res = self.ldb.search(krbtgt_user, attrs=['msDS-ResultantPSO'])
+ self.assertFalse('msDS-ResultantPSO' in res[0])
+
+ def get_ldb_connection(self, username, password, ldaphost):
+ """Returns an LDB connection using the specified user's credentials"""
+ creds = self.get_credentials()
+ creds_tmp = Credentials()
+ creds_tmp.set_username(username)
+ creds_tmp.set_password(password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ features = creds_tmp.get_gensec_features() | gensec.FEATURE_SEAL
+ creds_tmp.set_gensec_features(features)
+ return samba.tests.connect_samdb(ldaphost, credentials=creds_tmp)
+
+ def test_pso_permissions(self):
+ """Checks that regular users can't modify/view PSO objects"""
+
+ user = self.add_user("testuser")
+
+ # get an ldb connection with the new user's privileges
+ user_ldb = self.get_ldb_connection("testuser", user.get_password(),
+ self.host_url)
+
+ # regular users should not be able to create a PSO (at least, not in
+ # the default Password Settings container)
+ try:
+ priv_pso = PasswordSettings("priv-PSO", user_ldb, password_len=20)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
+
+ # create a PSO as the admin user
+ priv_pso = PasswordSettings("priv-PSO", self.ldb, password_len=20)
+ self.add_obj_cleanup([priv_pso.dn])
+
+ # regular users should not be able to apply a PSO to a user
+ try:
+ self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
+ samdb=user_ldb)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
+ self.assertTrue('00002098' in msg, msg)
+
+ self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
+ samdb=self.ldb)
+
+ # regular users should not be able to change a PSO's precedence
+ try:
+ priv_pso.set_precedence(100, samdb=user_ldb)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
+ self.assertTrue('00002098' in msg, msg)
+
+ priv_pso.set_precedence(100, samdb=self.ldb)
+
+ # regular users should not be able to view a PSO's settings
+ pso_attrs = ["msDS-PSOAppliesTo", "msDS-PasswordSettingsPrecedence",
+ "msDS-PasswordHistoryLength", "msDS-LockoutThreshold",
+ "msDS-PasswordComplexityEnabled"]
+
+ # users can see the PSO object's DN, but not its attributes
+ res = user_ldb.search(priv_pso.dn, scope=ldb.SCOPE_BASE,
+ attrs=pso_attrs)
+ self.assertTrue(str(priv_pso.dn) == str(res[0].dn))
+ for attr in pso_attrs:
+ self.assertFalse(attr in res[0])
+
+ # whereas admin users can see everything
+ res = self.ldb.search(priv_pso.dn, scope=ldb.SCOPE_BASE,
+ attrs=pso_attrs)
+ for attr in pso_attrs:
+ self.assertTrue(attr in res[0])
+
+ # check replace/delete operations can't be performed by regular users
+ operations = [FLAG_MOD_REPLACE, FLAG_MOD_DELETE]
+
+ for oper in operations:
+ try:
+ self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
+ samdb=user_ldb, operation=oper)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, msg)
+ self.assertTrue('00002098' in msg, msg)
+
+ # ...but can be performed by the admin user
+ self.set_attribute(priv_pso.dn, "msDS-PSOAppliesTo", user.dn,
+ samdb=self.ldb, operation=oper)
+
+ def format_password_for_ldif(self, password):
+ """Encodes/decodes the password so that it's accepted in an LDIF"""
+ pwd = '"{0}"'.format(password)
+ return base64.b64encode(pwd.encode('utf-16-le')).decode('utf8')
+
+ # The 'user add' case is a bit more complicated as you can't really query
+ # the msDS-ResultantPSO attribute on a user that doesn't exist yet (it
+ # won't have any group membership or PSOs applied directly against it yet).
+ # In theory it's possible to still get an applicable PSO via the user's
+ # primaryGroupID (i.e. 'Domain Users' by default). However, testing against
+ # Windows shows that the PSO doesn't take effect during the user add
+ # operation. (However, the Windows GUI tools presumably adds the user in 2
+ # steps, because it does enforce the PSO for users added via the GUI).
+ def test_pso_add_user(self):
+ """Tests against a 'Domain Users' PSO taking effect on a new user"""
+
+ # create a PSO that will apply to users by default
+ default_pso = PasswordSettings("default-PSO", self.ldb, precedence=20,
+ password_len=12, complexity=False)
+ self.add_obj_cleanup([default_pso.dn])
+
+ # apply the PSO to Domain Users (which all users are a member of). In
+ # theory, this PSO *could* take effect on a new user (but it doesn't)
+ domain_users = "CN=Domain Users,CN=Users,%s" % self.ldb.domain_dn()
+ default_pso.apply_to(domain_users)
+
+ # first try to add a user with a password that doesn't meet the domain
+ # defaults, to prove that the DC will reject bad passwords during a
+ # user add
+ userdn = "CN=testuser,%s" % self.ou
+ password = self.format_password_for_ldif('abcdef')
+
+ # Note we use an LDIF operation to ensure that the password gets set
+ # as part of the 'add' operation (whereas self.add_user() adds the user
+ # first, then sets the password later in a 2nd step)
+ try:
+ ldif = """
+dn: %s
+objectClass: user
+sAMAccountName: testuser
+unicodePwd:: %s
+""" % (userdn, password)
+ self.ldb.add_ldif(ldif)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ # error codes differ between Samba and Windows
+ self.assertTrue(num == ldb.ERR_UNWILLING_TO_PERFORM or
+ num == ldb.ERR_CONSTRAINT_VIOLATION, msg)
+ self.assertTrue('0000052D' in msg, msg)
+
+ # now use a password that meets the domain defaults, but doesn't meet
+ # the PSO requirements. Note that Windows allows this, i.e. it doesn't
+ # honour the PSO during the add operation
+ password = self.format_password_for_ldif('abcde12#')
+ ldif = """
+dn: %s
+objectClass: user
+sAMAccountName: testuser
+unicodePwd:: %s
+""" % (userdn, password)
+ self.ldb.add_ldif(ldif)
+
+ # Now do essentially the same thing, but set the password in a 2nd step
+ # which proves that the same password doesn't meet the PSO requirements
+ userdn = "CN=testuser2,%s" % self.ou
+ ldif = """
+dn: %s
+objectClass: user
+sAMAccountName: testuser2
+""" % userdn
+ self.ldb.add_ldif(ldif)
+
+ # now that the user exists, assert that the PSO is honoured
+ try:
+ ldif = """
+dn: %s
+changetype: modify
+delete: unicodePwd
+add: unicodePwd
+unicodePwd:: %s
+""" % (userdn, password)
+ self.ldb.modify_ldif(ldif)
+ self.fail()
+ except ldb.LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ldb.ERR_CONSTRAINT_VIOLATION, msg)
+ self.assertTrue('0000052D' in msg, msg)
+
+ # check setting a password that meets the PSO settings works
+ password = self.format_password_for_ldif('abcdefghijkl')
+ ldif = """
+dn: %s
+changetype: modify
+delete: unicodePwd
+add: unicodePwd
+unicodePwd:: %s
+""" % (userdn, password)
+ self.ldb.modify_ldif(ldif)
+
+ def set_domain_pwdHistoryLength(self, value):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.ldb, self.ldb.domain_dn())
+ m["pwdHistoryLength"] = ldb.MessageElement(value,
+ ldb.FLAG_MOD_REPLACE,
+ "pwdHistoryLength")
+ self.ldb.modify(m)
+
+ def test_domain_pwd_history(self):
+ """Non-PSO test for domain's pwdHistoryLength setting"""
+
+ # restore the current pwdHistoryLength setting after the test completes
+ curr_hist_len = str(self.pwd_defaults.history_len)
+ self.addCleanup(self.set_domain_pwdHistoryLength, curr_hist_len)
+
+ self.set_domain_pwdHistoryLength("4")
+ user = self.add_user("testuser")
+
+ initial_pwd = user.get_password()
+ passwords = ["First12#", "Second12#", "Third12#", "Fourth12#"]
+
+ # we should be able to set the password to new values OK
+ for pwd in passwords:
+ self.assert_password_valid(user, pwd)
+
+ # the 2nd time round it should fail because they're in the history now
+ for pwd in passwords:
+ self.assert_password_invalid(user, pwd)
+
+ # but the initial password is now outside the history, so should be OK
+ self.assert_password_valid(user, initial_pwd)
+
+ # if we set the history to zero, all the old passwords should now be OK
+ self.set_domain_pwdHistoryLength("0")
+ for pwd in passwords:
+ self.assert_password_valid(user, pwd)
+
+ def test_domain_pwd_history_zero(self):
+ """Non-PSO test for pwdHistoryLength going from zero to non-zero"""
+
+ # restore the current pwdHistoryLength setting after the test completes
+ curr_hist_len = str(self.pwd_defaults.history_len)
+ self.addCleanup(self.set_domain_pwdHistoryLength, curr_hist_len)
+
+ self.set_domain_pwdHistoryLength("0")
+ user = self.add_user("testuser")
+
+ self.assert_password_valid(user, "NewPwd12#")
+ # we can set the exact same password again because there's no history
+ self.assert_password_valid(user, "NewPwd12#")
+
+ # When going from zero to non-zero password-history, Windows treats
+ # the current user's password as invalid (even though the password has
+ # not been altered since the setting changed).
+ self.set_domain_pwdHistoryLength("1")
+ self.assert_password_invalid(user, "NewPwd12#")
diff --git a/source4/dsdb/tests/python/passwords.py b/source4/dsdb/tests/python/passwords.py
new file mode 100755
index 0000000..d431486
--- /dev/null
+++ b/source4/dsdb/tests/python/passwords.py
@@ -0,0 +1,1451 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This tests the password changes over LDAP for AD implementations
+#
+# Copyright Matthias Dieter Wallnoefer 2010
+#
+# Notice: This tests will also work against Windows Server if the connection is
+# secured enough (SASL with a minimum of 128 Bit encryption) - consider
+# MS-ADTS 3.1.1.3.1.5
+
+import optparse
+import sys
+import base64
+import time
+import os
+
+sys.path.insert(0, "bin/python")
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+from samba.tests.password_test import PasswordTestCase
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba.credentials import Credentials
+from samba.dcerpc import security
+from samba.hresult import HRES_SEC_E_INVALID_TOKEN
+from ldb import SCOPE_BASE, LdbError
+from ldb import ERR_ATTRIBUTE_OR_VALUE_EXISTS
+from ldb import ERR_UNWILLING_TO_PERFORM, ERR_INSUFFICIENT_ACCESS_RIGHTS
+from ldb import ERR_NO_SUCH_ATTRIBUTE
+from ldb import ERR_CONSTRAINT_VIOLATION
+from ldb import ERR_INVALID_CREDENTIALS
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+from samba import gensec, werror
+from samba.samdb import SamDB
+from samba.tests import delete_force
+
+parser = optparse.OptionParser("passwords.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+# Force an encrypted connection
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+#
+# Tests start here
+#
+
+
+class PasswordTests(PasswordTestCase):
+
+ def setUp(self):
+ super(PasswordTests, self).setUp()
+ self.ldb = SamDB(url=host, session_info=system_session(lp), credentials=creds, lp=lp)
+
+ # permit password changes during this test
+ self.allow_password_changes()
+
+ self.base_dn = self.ldb.domain_dn()
+
+ # (Re)adds the test user "testuser" with no password atm
+ delete_force(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ self.ldb.add({
+ "dn": "cn=testuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "sAMAccountName": "testuser"})
+
+ # Tests a password change when we don't have any password yet with a
+ # wrong old password
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: noPassword
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ # Windows (2008 at least) seems to have some small bug here: it
+ # returns "0000056A" on longer (always wrong) previous passwords.
+ self.assertTrue('00000056' in msg)
+
+ # Sets the initial user password with a "special" password change
+ # I think that this internally is a password set operation and it can
+ # only be performed by someone which has password set privileges on the
+ # account (at least in s4 we do handle it like that).
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+add: userPassword
+userPassword: thatsAcomplPASS1
+""")
+
+ # But in the other way around this special syntax doesn't work
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+""")
+ self.fail()
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # Enables the user account
+ self.ldb.enable_account("(sAMAccountName=testuser)")
+
+ # Open a second LDB connection with the user credentials. Use the
+ # command line credentials for information like the domain, the realm
+ # and the workstation.
+ creds2 = Credentials()
+ creds2.set_username("testuser")
+ creds2.set_password("thatsAcomplPASS1")
+ creds2.set_domain(creds.get_domain())
+ creds2.set_realm(creds.get_realm())
+ creds2.set_workstation(creds.get_workstation())
+ creds2.set_gensec_features(creds2.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ self.ldb2 = SamDB(url=host, credentials=creds2, lp=lp)
+ self.creds = creds2
+
+ def test_unicodePwd_hash_set(self):
+ """Performs a password hash set operation on 'unicodePwd' which should be prevented"""
+ # Notice: Direct hash password sets should never work
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["unicodePwd"] = MessageElement("XXXXXXXXXXXXXXXX", FLAG_MOD_REPLACE,
+ "unicodePwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e2:
+ (num, _) = e2.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ def test_unicodePwd_hash_change(self):
+ """Performs a password hash change operation on 'unicodePwd' which should be prevented"""
+ # Notice: Direct hash password changes should never work
+
+ # Hash password changes should never work
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd: XXXXXXXXXXXXXXXX
+add: unicodePwd
+unicodePwd: YYYYYYYYYYYYYYYY
+""")
+ self.fail()
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ def test_unicodePwd_clear_set(self):
+ """Performs a password cleartext set operation on 'unicodePwd'"""
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["unicodePwd"] = MessageElement("\"thatsAcomplPASS2\"".encode('utf-16-le'),
+ FLAG_MOD_REPLACE, "unicodePwd")
+ self.ldb.modify(m)
+
+ def test_unicodePwd_clear_change(self):
+ """Performs a password cleartext change operation on 'unicodePwd'"""
+
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS1\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+
+ # Wrong old password
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS4\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e4:
+ (num, msg) = e4.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg)
+
+ # A change to the same password again will not work (password history)
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS2\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e5:
+ (num, msg) = e5.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('0000052D' in msg)
+
+ def test_old_password_simple_bind(self):
+ """Shows that we can log in with the immediate previous password, but not any earlier passwords."""
+
+ user_dn_str = f'CN=testuser,CN=Users,{self.base_dn}'
+ user_dn = Dn(self.ldb, user_dn_str)
+
+ # Change the account password.
+ m = Message(user_dn)
+ m['0'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement('Password#2',
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show we can still log in using the previous password.
+ self.creds.set_bind_dn(user_dn_str)
+ try:
+ SamDB(url=host_ldaps,
+ credentials=self.creds, lp=lp)
+ except LdbError:
+ self.fail('failed to login with previous password!')
+
+ # Change the account password a second time.
+ m = Message(user_dn)
+ m['0'] = MessageElement('Password#2',
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement('Password#3',
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show we can no longer log in using the original password.
+ try:
+ SamDB(url=host_ldaps,
+ credentials=self.creds, lp=lp)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_INVALID_CREDENTIALS, num)
+ self.assertIn(f"{HRES_SEC_E_INVALID_TOKEN:08X}", estr)
+ else:
+ self.fail('should have failed to login with previous password!')
+
+ def test_old_password_attempt_reuse(self):
+ """Shows that we cannot reuse the original password after changing the password twice."""
+ res = self.ldb.search(self.ldb.domain_dn(), scope=SCOPE_BASE,
+ attrs=['pwdHistoryLength'])
+
+ history_len = int(res[0].get('pwdHistoryLength', idx=0))
+ self.assertGreaterEqual(history_len, 3)
+
+ user_dn_str = f'CN=testuser,CN=Users,{self.base_dn}'
+ user_dn = Dn(self.ldb, user_dn_str)
+
+ first_pwd = self.creds.get_password()
+ previous_pwd = first_pwd
+
+ for new_pwd in ['Password#0', 'Password#1']:
+ # Change the account password.
+ m = Message(user_dn)
+ m['0'] = MessageElement(previous_pwd,
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement(new_pwd,
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show that the original password is in the history by trying to
+ # set it as our new password.
+ m = Message(user_dn)
+ m['0'] = MessageElement(new_pwd,
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement(first_pwd,
+ FLAG_MOD_ADD, 'userPassword')
+ try:
+ self.ldb.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ self.assertIn(f'{werror.WERR_PASSWORD_RESTRICTION:08X}', estr)
+ else:
+ self.fail('should not have been able to reuse password!')
+
+ previous_pwd = new_pwd
+
+ def test_old_password_rename_simple_bind(self):
+ """Shows that we can log in with the previous password after renaming the account."""
+ user_dn_str = f'CN=testuser,CN=Users,{self.base_dn}'
+ user_dn = Dn(self.ldb, user_dn_str)
+
+ # Change the account password.
+ m = Message(user_dn)
+ m['0'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement('Password#2',
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show we can still log in using the previous password.
+ self.creds.set_bind_dn(user_dn_str)
+ try:
+ SamDB(url=host_ldaps,
+ credentials=self.creds, lp=lp)
+ except LdbError:
+ self.fail('failed to login with previous password!')
+
+ # Rename the account, causing the salt to change.
+ m = Message(user_dn)
+ m['1'] = MessageElement('testuser_2',
+ FLAG_MOD_REPLACE, 'sAMAccountName')
+ self.ldb.modify(m)
+
+ # Show that a simple bind can still be performed using the previous
+ # password.
+ self.creds.set_username('testuser_2')
+ try:
+ SamDB(url=host_ldaps,
+ credentials=self.creds, lp=lp)
+ except LdbError:
+ self.fail('failed to login with previous password!')
+
+ def test_old_password_rename_simple_bind_2(self):
+ """Shows that we can rename the account, change the password and log in with the previous password."""
+ user_dn_str = f'CN=testuser,CN=Users,{self.base_dn}'
+ user_dn = Dn(self.ldb, user_dn_str)
+
+ # Rename the account, causing the salt to change.
+ m = Message(user_dn)
+ m['1'] = MessageElement('testuser_2',
+ FLAG_MOD_REPLACE, 'sAMAccountName')
+ self.ldb.modify(m)
+
+ # Change the account password, causing the new salt to be stored.
+ m = Message(user_dn)
+ m['0'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement('Password#2',
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show that a simple bind can still be performed using the previous
+ # password.
+ self.creds.set_bind_dn(user_dn_str)
+ self.creds.set_username('testuser_2')
+ try:
+ SamDB(url=host_ldaps,
+ credentials=self.creds, lp=lp)
+ except LdbError:
+ self.fail('failed to login with previous password!')
+
+ def test_old_password_rename_attempt_reuse(self):
+ """Shows that we cannot reuse the original password after renaming the account."""
+ user_dn_str = f'CN=testuser,CN=Users,{self.base_dn}'
+ user_dn = Dn(self.ldb, user_dn_str)
+
+ # Change the account password.
+ m = Message(user_dn)
+ m['0'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement('Password#2',
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show that the previous password is in the history by trying to set it
+ # as our new password.
+ m = Message(user_dn)
+ m['0'] = MessageElement('Password#2',
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_ADD, 'userPassword')
+ try:
+ self.ldb.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ self.assertIn(f'{werror.WERR_PASSWORD_RESTRICTION:08X}', estr)
+ else:
+ self.fail('should not have been able to reuse password!')
+
+ # Rename the account, causing the salt to change.
+ m = Message(user_dn)
+ m['1'] = MessageElement('testuser_2',
+ FLAG_MOD_REPLACE, 'sAMAccountName')
+ self.ldb.modify(m)
+
+ # Show that the previous password is still in the history by trying to
+ # set it as our new password.
+ m = Message(user_dn)
+ m['0'] = MessageElement('Password#2',
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_ADD, 'userPassword')
+ try:
+ self.ldb.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ self.assertIn(f'{werror.WERR_PASSWORD_RESTRICTION:08X}', estr)
+ else:
+ self.fail('should not have been able to reuse password!')
+
+ def test_old_password_rename_attempt_reuse_2(self):
+ """Shows that we cannot reuse the original password after renaming the account and changing the password."""
+ user_dn_str = f'CN=testuser,CN=Users,{self.base_dn}'
+ user_dn = Dn(self.ldb, user_dn_str)
+
+ # Rename the account, causing the salt to change.
+ m = Message(user_dn)
+ m['1'] = MessageElement('testuser_2',
+ FLAG_MOD_REPLACE, 'sAMAccountName')
+ self.ldb.modify(m)
+
+ # Change the account password, causing the new salt to be stored.
+ m = Message(user_dn)
+ m['0'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement('Password#2',
+ FLAG_MOD_ADD, 'userPassword')
+ self.ldb.modify(m)
+
+ # Show that the previous password is in the history by trying to set it
+ # as our new password.
+ m = Message(user_dn)
+ m['0'] = MessageElement('Password#2',
+ FLAG_MOD_DELETE, 'userPassword')
+ m['1'] = MessageElement(self.creds.get_password(),
+ FLAG_MOD_ADD, 'userPassword')
+ try:
+ self.ldb.modify(m)
+ except LdbError as err:
+ num, estr = err.args
+ self.assertEqual(ERR_CONSTRAINT_VIOLATION, num)
+ self.assertIn(f'{werror.WERR_PASSWORD_RESTRICTION:08X}', estr)
+ else:
+ self.fail('should not have been able to reuse password!')
+
+ def test_protected_unicodePwd_clear_set(self):
+ """Performs a password cleartext set operation on 'unicodePwd' with the user in
+the Protected Users group"""
+
+ user_dn = f'cn=testuser,cn=users,{self.base_dn}'
+
+ # Add the user to the Protected Users group.
+
+ # Search for the Protected Users group.
+ group_dn = Dn(self.ldb,
+ f'<SID={self.ldb.get_domain_sid()}-'
+ f'{security.DOMAIN_RID_PROTECTED_USERS}>')
+ try:
+ group_res = self.ldb.search(base=group_dn,
+ scope=SCOPE_BASE,
+ attrs=['member'])
+ except LdbError as err:
+ self.fail(err)
+
+ # Add the user to the list of members.
+ members = list(group_res[0].get('member', ()))
+ members.append(user_dn)
+
+ m = Message(group_dn)
+ m['member'] = MessageElement(members,
+ FLAG_MOD_REPLACE,
+ 'member')
+ self.ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(self.ldb, user_dn)
+ m['unicodePwd'] = MessageElement(
+ '"thatsAcomplPASS2"'.encode('utf-16-le'),
+ FLAG_MOD_REPLACE, 'unicodePwd')
+ self.ldb.modify(m)
+
+ def test_protected_unicodePwd_clear_change(self):
+ """Performs a password cleartext change operation on 'unicodePwd' with the user
+in the Protected Users group"""
+
+ user_dn = f'cn=testuser,cn=users,{self.base_dn}'
+
+ # Add the user to the Protected Users group.
+
+ # Search for the Protected Users group.
+ group_dn = Dn(self.ldb,
+ f'<SID={self.ldb.get_domain_sid()}-'
+ f'{security.DOMAIN_RID_PROTECTED_USERS}>')
+ try:
+ group_res = self.ldb.search(base=group_dn,
+ scope=SCOPE_BASE,
+ attrs=['member'])
+ except LdbError as err:
+ self.fail(err)
+
+ # Add the user to the list of members.
+ members = list(group_res[0].get('member', ()))
+ members.append(user_dn)
+
+ m = Message(group_dn)
+ m['member'] = MessageElement(members,
+ FLAG_MOD_REPLACE,
+ 'member')
+ self.ldb.modify(m)
+
+ self.ldb2.modify_ldif(f"""
+dn: cn=testuser,cn=users,{self.base_dn}
+changetype: modify
+delete: unicodePwd
+unicodePwd:: {base64.b64encode('"thatsAcomplPASS1"'.encode('utf-16-le'))
+ .decode('utf8')}
+add: unicodePwd
+unicodePwd:: {base64.b64encode('"thatsAcomplPASS2"'.encode('utf-16-le'))
+ .decode('utf8')}
+""")
+
+ def test_dBCSPwd_hash_set(self):
+ """Performs a password hash set operation on 'dBCSPwd' which should be prevented"""
+ # Notice: Direct hash password sets should never work
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["dBCSPwd"] = MessageElement("XXXXXXXXXXXXXXXX", FLAG_MOD_REPLACE,
+ "dBCSPwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e6:
+ (num, _) = e6.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ def test_dBCSPwd_hash_change(self):
+ """Performs a password hash change operation on 'dBCSPwd' which should be prevented"""
+ # Notice: Direct hash password changes should never work
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: dBCSPwd
+dBCSPwd: XXXXXXXXXXXXXXXX
+add: dBCSPwd
+dBCSPwd: YYYYYYYYYYYYYYYY
+""")
+ self.fail()
+ except LdbError as e7:
+ (num, _) = e7.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ def test_userPassword_clear_set(self):
+ """Performs a password cleartext set operation on 'userPassword'"""
+ # Notice: This works only against Windows if "dSHeuristics" has been set
+ # properly
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("thatsAcomplPASS2", FLAG_MOD_REPLACE,
+ "userPassword")
+ self.ldb.modify(m)
+
+ def test_userPassword_clear_change(self):
+ """Performs a password cleartext change operation on 'userPassword'"""
+ # Notice: This works only against Windows if "dSHeuristics" has been set
+ # properly
+
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+
+ # Wrong old password
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS3
+add: userPassword
+userPassword: thatsAcomplPASS4
+""")
+ self.fail()
+ except LdbError as e8:
+ (num, msg) = e8.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg)
+
+ # A change to the same password again will not work (password history)
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS2
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e9:
+ (num, msg) = e9.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('0000052D' in msg)
+
+ def test_clearTextPassword_clear_set(self):
+ """Performs a password cleartext set operation on 'clearTextPassword'"""
+ # Notice: This never works against Windows - only supported by us
+
+ try:
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["clearTextPassword"] = MessageElement("thatsAcomplPASS2".encode('utf-16-le'),
+ FLAG_MOD_REPLACE, "clearTextPassword")
+ self.ldb.modify(m)
+ # this passes against s4
+ except LdbError as e10:
+ (num, msg) = e10.args
+ # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
+ if num != ERR_NO_SUCH_ATTRIBUTE:
+ raise LdbError(num, msg)
+
+ def test_clearTextPassword_clear_change(self):
+ """Performs a password cleartext change operation on 'clearTextPassword'"""
+ # Notice: This never works against Windows - only supported by us
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: clearTextPassword
+clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS1".encode('utf-16-le')).decode('utf8') + """
+add: clearTextPassword
+clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')).decode('utf8') + """
+""")
+ # this passes against s4
+ except LdbError as e11:
+ (num, msg) = e11.args
+ # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
+ if num != ERR_NO_SUCH_ATTRIBUTE:
+ raise LdbError(num, msg)
+
+ # Wrong old password
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: clearTextPassword
+clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS3".encode('utf-16-le')).decode('utf8') + """
+add: clearTextPassword
+clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS4".encode('utf-16-le')).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e12:
+ (num, msg) = e12.args
+ # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
+ if num != ERR_NO_SUCH_ATTRIBUTE:
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('00000056' in msg)
+
+ # A change to the same password again will not work (password history)
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: clearTextPassword
+clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')).decode('utf8') + """
+add: clearTextPassword
+clearTextPassword:: """ + base64.b64encode("thatsAcomplPASS2".encode('utf-16-le')).decode('utf8') + """
+""")
+ self.fail()
+ except LdbError as e13:
+ (num, msg) = e13.args
+ # "NO_SUCH_ATTRIBUTE" is returned by Windows -> ignore it
+ if num != ERR_NO_SUCH_ATTRIBUTE:
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ self.assertTrue('0000052D' in msg)
+
+ def test_failures(self):
+ """Performs some failure testing"""
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ self.fail()
+ except LdbError as e14:
+ (num, _) = e14.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ self.fail()
+ except LdbError as e15:
+ (num, _) = e15.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+""")
+ self.fail()
+ except LdbError as e16:
+ (num, _) = e16.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+""")
+ self.fail()
+ except LdbError as e17:
+ (num, _) = e17.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+add: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ self.fail()
+ except LdbError as e18:
+ (num, _) = e18.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+add: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ self.fail()
+ except LdbError as e19:
+ (num, _) = e19.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e20:
+ (num, _) = e20.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e21:
+ (num, _) = e21.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e22:
+ (num, _) = e22.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e23:
+ (num, _) = e23.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e24:
+ (num, _) = e24.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e25:
+ (num, _) = e25.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e26:
+ (num, _) = e26.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+""")
+ self.fail()
+ except LdbError as e27:
+ (num, _) = e27.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ try:
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+replace: userPassword
+userPassword: thatsAcomplPASS3
+""")
+ self.fail()
+ except LdbError as e28:
+ (num, _) = e28.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS1
+add: userPassword
+userPassword: thatsAcomplPASS2
+replace: userPassword
+userPassword: thatsAcomplPASS3
+""")
+ self.fail()
+ except LdbError as e29:
+ (num, _) = e29.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ # Reverse order does work
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+add: userPassword
+userPassword: thatsAcomplPASS2
+delete: userPassword
+userPassword: thatsAcomplPASS1
+""")
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+userPassword: thatsAcomplPASS2
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ # this passes against s4
+ except LdbError as e30:
+ (num, _) = e30.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')).decode('utf8') + """
+add: userPassword
+userPassword: thatsAcomplPASS4
+""")
+ # this passes against s4
+ except LdbError as e31:
+ (num, _) = e31.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ # Several password changes at once are allowed
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS1
+userPassword: thatsAcomplPASS2
+""")
+
+ # Several password changes at once are allowed
+ self.ldb.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+replace: userPassword
+userPassword: thatsAcomplPASS1
+userPassword: thatsAcomplPASS2
+replace: userPassword
+userPassword: thatsAcomplPASS3
+replace: userPassword
+userPassword: thatsAcomplPASS4
+""")
+
+ # This surprisingly should work
+ delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
+ self.ldb.add({
+ "dn": "cn=testuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS2"]})
+
+ # This surprisingly should work
+ delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
+ self.ldb.add({
+ "dn": "cn=testuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userPassword": ["thatsAcomplPASS1", "thatsAcomplPASS1"]})
+
+ def test_empty_passwords(self):
+ print("Performs some empty passwords testing")
+
+ try:
+ self.ldb.add({
+ "dn": "cn=testuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "unicodePwd": []})
+ self.fail()
+ except LdbError as e32:
+ (num, _) = e32.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.add({
+ "dn": "cn=testuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "dBCSPwd": []})
+ self.fail()
+ except LdbError as e33:
+ (num, _) = e33.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.add({
+ "dn": "cn=testuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userPassword": []})
+ self.fail()
+ except LdbError as e34:
+ (num, _) = e34.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ try:
+ self.ldb.add({
+ "dn": "cn=testuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "clearTextPassword": []})
+ self.fail()
+ except LdbError as e35:
+ (num, _) = e35.args
+ self.assertTrue(num == ERR_CONSTRAINT_VIOLATION or
+ num == ERR_NO_SUCH_ATTRIBUTE) # for Windows
+
+ delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["unicodePwd"] = MessageElement([], FLAG_MOD_ADD, "unicodePwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e36:
+ (num, _) = e36.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["dBCSPwd"] = MessageElement([], FLAG_MOD_ADD, "dBCSPwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e37:
+ (num, _) = e37.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement([], FLAG_MOD_ADD, "userPassword")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e38:
+ (num, _) = e38.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["clearTextPassword"] = MessageElement([], FLAG_MOD_ADD, "clearTextPassword")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e39:
+ (num, _) = e39.args
+ self.assertTrue(num == ERR_CONSTRAINT_VIOLATION or
+ num == ERR_NO_SUCH_ATTRIBUTE) # for Windows
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["unicodePwd"] = MessageElement([], FLAG_MOD_REPLACE, "unicodePwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e40:
+ (num, _) = e40.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["dBCSPwd"] = MessageElement([], FLAG_MOD_REPLACE, "dBCSPwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e41:
+ (num, _) = e41.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement([], FLAG_MOD_REPLACE, "userPassword")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e42:
+ (num, _) = e42.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["clearTextPassword"] = MessageElement([], FLAG_MOD_REPLACE, "clearTextPassword")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e43:
+ (num, _) = e43.args
+ self.assertTrue(num == ERR_UNWILLING_TO_PERFORM or
+ num == ERR_NO_SUCH_ATTRIBUTE) # for Windows
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["unicodePwd"] = MessageElement([], FLAG_MOD_DELETE, "unicodePwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e44:
+ (num, _) = e44.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["dBCSPwd"] = MessageElement([], FLAG_MOD_DELETE, "dBCSPwd")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e45:
+ (num, _) = e45.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement([], FLAG_MOD_DELETE, "userPassword")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e46:
+ (num, _) = e46.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["clearTextPassword"] = MessageElement([], FLAG_MOD_DELETE, "clearTextPassword")
+ try:
+ self.ldb.modify(m)
+ self.fail()
+ except LdbError as e47:
+ (num, _) = e47.args
+ self.assertTrue(num == ERR_CONSTRAINT_VIOLATION or
+ num == ERR_NO_SUCH_ATTRIBUTE) # for Windows
+
+ def test_plain_userPassword(self):
+ print("Performs testing about the standard 'userPassword' behaviour")
+
+ # Delete the "dSHeuristics"
+ self.ldb.set_dsheuristics(None)
+
+ time.sleep(1) # This switching time is strictly needed!
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("myPassword", FLAG_MOD_ADD,
+ "userPassword")
+ self.ldb.modify(m)
+
+ res = self.ldb.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("userPassword" in res[0])
+ self.assertEqual(str(res[0]["userPassword"][0]), "myPassword")
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("myPassword2", FLAG_MOD_REPLACE,
+ "userPassword")
+ self.ldb.modify(m)
+
+ res = self.ldb.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("userPassword" in res[0])
+ self.assertEqual(str(res[0]["userPassword"][0]), "myPassword2")
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement([], FLAG_MOD_DELETE,
+ "userPassword")
+ self.ldb.modify(m)
+
+ res = self.ldb.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("userPassword" in res[0])
+
+ # Set the test "dSHeuristics" to deactivate "userPassword" pwd changes
+ self.ldb.set_dsheuristics("000000000")
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("myPassword3", FLAG_MOD_REPLACE,
+ "userPassword")
+ self.ldb.modify(m)
+
+ res = self.ldb.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("userPassword" in res[0])
+ self.assertEqual(str(res[0]["userPassword"][0]), "myPassword3")
+
+ # Set the test "dSHeuristics" to deactivate "userPassword" pwd changes
+ self.ldb.set_dsheuristics("000000002")
+
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("myPassword4", FLAG_MOD_REPLACE,
+ "userPassword")
+ self.ldb.modify(m)
+
+ res = self.ldb.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("userPassword" in res[0])
+ self.assertEqual(str(res[0]["userPassword"][0]), "myPassword4")
+
+ # Reset the test "dSHeuristics" (reactivate "userPassword" pwd changes)
+ self.ldb.set_dsheuristics("000000001")
+
+ def test_modify_dsheuristics_userPassword(self):
+ print("Performs testing about reading userPassword between dsHeuristic modifies")
+
+ # Make sure userPassword cannot be read
+ self.ldb.set_dsheuristics("000000000")
+
+ # Open a new connection (with dsHeuristic=000000000)
+ ldb1 = SamDB(url=host, session_info=system_session(lp),
+ credentials=creds, lp=lp)
+
+ # Set userPassword to be read
+ # This setting only affects newer connections (ldb2)
+ ldb1.set_dsheuristics("000000001")
+ time.sleep(1)
+
+ m = Message()
+ m.dn = Dn(ldb1, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("thatsAcomplPASS1", FLAG_MOD_REPLACE,
+ "userPassword")
+ ldb1.modify(m)
+
+ res = ldb1.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+
+ # userPassword cannot be read, it wasn't set, instead the
+ # password was
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("userPassword" in res[0])
+
+ # Open another new connection (with dsHeuristic=000000001)
+ ldb2 = SamDB(url=host, session_info=system_session(lp),
+ credentials=creds, lp=lp)
+
+ res = ldb2.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+
+ # Check on the new connection that userPassword was not stored
+ # from ldb1 or is not readable
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("userPassword" in res[0])
+
+ # Set userPassword to be readable
+ # This setting does not affect this connection
+ ldb2.set_dsheuristics("000000000")
+ time.sleep(1)
+
+ res = ldb2.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+
+ # Check that userPassword was not stored from ldb1
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("userPassword" in res[0])
+
+ m = Message()
+ m.dn = Dn(ldb2, "cn=testuser,cn=users," + self.base_dn)
+ m["userPassword"] = MessageElement("thatsAcomplPASS2", FLAG_MOD_REPLACE,
+ "userPassword")
+ ldb2.modify(m)
+
+ res = ldb2.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+
+ # Check despite setting it with userPassword support disabled
+ # on this connection it should still not be readable
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("userPassword" in res[0])
+
+ # Only password from ldb1 is the user's password
+ creds2 = Credentials()
+ creds2.set_username("testuser")
+ creds2.set_password("thatsAcomplPASS1")
+ creds2.set_domain(creds.get_domain())
+ creds2.set_realm(creds.get_realm())
+ creds2.set_workstation(creds.get_workstation())
+ creds2.set_gensec_features(creds2.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+
+ try:
+ SamDB(url=host, credentials=creds2, lp=lp)
+ except:
+ self.fail("testuser used the wrong password")
+
+ ldb3 = SamDB(url=host, session_info=system_session(lp),
+ credentials=creds, lp=lp)
+
+ # Check that userPassword was stored from ldb2
+ res = ldb3.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+
+ # userPassword can be read
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("userPassword" in res[0])
+ self.assertEqual(str(res[0]["userPassword"][0]), "thatsAcomplPASS2")
+
+ # Reset the test "dSHeuristics" (reactivate "userPassword" pwd changes)
+ self.ldb.set_dsheuristics("000000001")
+
+ ldb4 = SamDB(url=host, session_info=system_session(lp),
+ credentials=creds, lp=lp)
+
+ # Check that userPassword that was stored from ldb2
+ res = ldb4.search("cn=testuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["userPassword"])
+
+ # userPassword can be not be read
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("userPassword" in res[0])
+
+ def test_zero_length(self):
+ # Get the old "minPwdLength"
+ minPwdLength = self.ldb.get_minPwdLength()
+ # Set it temporarily to "0"
+ self.ldb.set_minPwdLength("0")
+
+ # Get the old "pwdProperties"
+ pwdProperties = self.ldb.get_pwdProperties()
+ # Set them temporarily to "0" (to deactivate eventually the complexity)
+ self.ldb.set_pwdProperties("0")
+
+ self.ldb.setpassword("(sAMAccountName=testuser)", "")
+
+ # Reset the "pwdProperties" as they were before
+ self.ldb.set_pwdProperties(pwdProperties)
+
+ # Reset the "minPwdLength" as it was before
+ self.ldb.set_minPwdLength(minPwdLength)
+
+ def test_pw_change_delete_no_value_userPassword(self):
+ """Test password change with userPassword where the delete attribute doesn't have a value"""
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: userPassword
+add: userPassword
+userPassword: thatsAcomplPASS1
+""")
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+
+ def test_pw_change_delete_no_value_clearTextPassword(self):
+ """Test password change with clearTextPassword where the delete attribute doesn't have a value"""
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: clearTextPassword
+add: clearTextPassword
+clearTextPassword: thatsAcomplPASS2
+""")
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertTrue(num == ERR_CONSTRAINT_VIOLATION or
+ num == ERR_NO_SUCH_ATTRIBUTE) # for Windows
+ else:
+ self.fail()
+
+ def test_pw_change_delete_no_value_unicodePwd(self):
+ """Test password change with unicodePwd where the delete attribute doesn't have a value"""
+
+ try:
+ self.ldb2.modify_ldif("""
+dn: cn=testuser,cn=users,""" + self.base_dn + """
+changetype: modify
+delete: unicodePwd
+add: unicodePwd
+unicodePwd:: """ + base64.b64encode("\"thatsAcomplPASS3\"".encode('utf-16-le')).decode('utf8') + """
+""")
+ except LdbError as e:
+ (num, msg) = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+
+ def tearDown(self):
+ super(PasswordTests, self).tearDown()
+ delete_force(self.ldb, "cn=testuser,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=testuser2,cn=users," + self.base_dn)
+ # Close the second LDB connection (with the user credentials)
+ self.ldb2 = None
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host_ldaps = None
+ host = "tdb://%s" % host
+ else:
+ host_ldaps = "ldaps://%s" % host
+ host = "ldap://%s" % host
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/priv_attrs.py b/source4/dsdb/tests/python/priv_attrs.py
new file mode 100644
index 0000000..5c9db73
--- /dev/null
+++ b/source4/dsdb/tests/python/priv_attrs.py
@@ -0,0 +1,392 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This tests the restrictions on userAccountControl that apply even if write access is permitted
+#
+# Copyright Samuel Cabrero 2014 <samuelcabrero@kernevil.me>
+# Copyright Andrew Bartlett 2014 <abartlet@samba.org>
+#
+# Licenced under the GPLv3
+#
+
+import optparse
+import sys
+import unittest
+import samba
+import samba.getopt as options
+import samba.tests
+import ldb
+
+sys.path.insert(0, "bin/python")
+from samba.tests import DynamicTestCase
+from samba.subunit.run import SubunitTestRunner
+from samba.samdb import SamDB
+from samba.dcerpc import security
+from samba.credentials import Credentials
+from samba.ndr import ndr_pack
+from samba.tests import delete_force
+from samba import gensec, sd_utils
+from samba.credentials import DONT_USE_KERBEROS
+from ldb import SCOPE_BASE, LdbError
+from samba.dsdb import (
+ UF_NORMAL_ACCOUNT,
+ UF_PARTIAL_SECRETS_ACCOUNT,
+ UF_PASSWD_NOTREQD,
+ UF_SERVER_TRUST_ACCOUNT,
+ UF_TRUSTED_FOR_DELEGATION,
+ UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
+ UF_WORKSTATION_TRUST_ACCOUNT,
+)
+
+
+parser = optparse.OptionParser("priv_attrs.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+host = args[0]
+
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+ start = host.rindex("://")
+ host = host.lstrip(start + 3)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+
+"""
+Check the combinations of:
+
+rodc kdc
+a2d2
+useraccountcontrol (trusted for delegation)
+sidHistory
+
+x
+
+add
+modify(replace)
+modify(add)
+
+x
+
+sd WP on add
+cc default perms
+admin created, WP to user
+
+x
+
+computer
+user
+"""
+
+attrs = {"sidHistory":
+ {"value": ndr_pack(security.dom_sid(security.SID_BUILTIN_ADMINISTRATORS)),
+ "priv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS},
+
+ "msDS-AllowedToDelegateTo":
+ {"value": f"host/{host}",
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS},
+
+ "userAccountControl-a2d-user":
+ {"attr": "userAccountControl",
+ "value": str(UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION|UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD),
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS},
+
+ "userAccountControl-a2d-computer":
+ {"attr": "userAccountControl",
+ "value": str(UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION|UF_WORKSTATION_TRUST_ACCOUNT),
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "only-1": "computer"},
+
+ # This flag makes many legitimate authenticated clients
+ # send a forwardable ticket-granting-ticket to the server
+ "userAccountControl-t4d-user":
+ {"attr": "userAccountControl",
+ "value": str(UF_TRUSTED_FOR_DELEGATION|UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD),
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS},
+
+ "userAccountControl-t4d-computer":
+ {"attr": "userAccountControl",
+ "value": str(UF_TRUSTED_FOR_DELEGATION|UF_WORKSTATION_TRUST_ACCOUNT),
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "only-1": "computer"},
+
+ "userAccountControl-DC":
+ {"attr": "userAccountControl",
+ "value": str(UF_SERVER_TRUST_ACCOUNT),
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "only-2": "computer"},
+
+ "userAccountControl-RODC":
+ {"attr": "userAccountControl",
+ "value": str(UF_PARTIAL_SECRETS_ACCOUNT|UF_WORKSTATION_TRUST_ACCOUNT),
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ "only-1": "computer"},
+
+ "msDS-SecondaryKrbTgtNumber":
+ {"value": "65536",
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS},
+ "primaryGroupID":
+ {"value": str(security.DOMAIN_RID_ADMINS),
+ "priv-error": ldb.ERR_UNWILLING_TO_PERFORM,
+ "unpriv-add-error": ldb.ERR_UNWILLING_TO_PERFORM,
+ "unpriv-error": ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS}
+ }
+
+
+
+@DynamicTestCase
+class PrivAttrsTests(samba.tests.TestCase):
+
+ def get_creds(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop
+ return creds_tmp
+
+ def assertGotLdbError(self, wanted, got):
+ if not self.strict_checking:
+ self.assertNotEqual(got, ldb.SUCCESS)
+ else:
+ self.assertEqual(got, wanted)
+
+ def setUp(self):
+ super().setUp()
+
+ strict_checking = samba.tests.env_get_var_value('STRICT_CHECKING', allow_missing=True)
+ if strict_checking is None:
+ strict_checking = '1'
+ self.strict_checking = bool(int(strict_checking))
+
+ self.admin_creds = creds
+ self.admin_samdb = SamDB(url=ldaphost, credentials=self.admin_creds, lp=lp)
+ self.domain_sid = security.dom_sid(self.admin_samdb.get_domain_sid())
+ self.base_dn = self.admin_samdb.domain_dn()
+
+ self.unpriv_user = "testuser1"
+ self.unpriv_user_pw = "samba123@"
+ self.unpriv_creds = self.get_creds(self.unpriv_user, self.unpriv_user_pw)
+
+ self.admin_sd_utils = sd_utils.SDUtils(self.admin_samdb)
+
+ self.test_ou_name = "OU=test_priv_attrs"
+ self.test_ou = self.test_ou_name + "," + self.base_dn
+
+ delete_force(self.admin_samdb, self.test_ou, controls=["tree_delete:0"])
+
+ self.admin_samdb.create_ou(self.test_ou)
+
+ expected_user_dn = f"CN={self.unpriv_user},{self.test_ou_name},{self.base_dn}"
+
+ self.admin_samdb.newuser(self.unpriv_user, self.unpriv_user_pw, userou=self.test_ou_name)
+ res = self.admin_samdb.search(expected_user_dn,
+ scope=SCOPE_BASE,
+ attrs=["objectSid"])
+
+ self.assertEqual(1, len(res))
+
+ self.unpriv_user_dn = res[0].dn
+ self.addCleanup(delete_force, self.admin_samdb, self.unpriv_user_dn, controls=["tree_delete:0"])
+
+ self.unpriv_user_sid = self.admin_sd_utils.get_object_sid(self.unpriv_user_dn)
+
+ self.unpriv_samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp)
+
+ @classmethod
+ def setUpDynamicTestCases(cls):
+ for test_name in attrs.keys():
+ for add_or_mod in ["add", "mod-del-add", "mod-replace"]:
+ for permission in ["admin-add", "CC"]:
+ for sd in ["default", "WP"]:
+ for objectclass in ["computer", "user"]:
+ tname = f"{test_name}_{add_or_mod}_{permission}_{sd}_{objectclass}"
+ targs = (test_name,
+ add_or_mod,
+ permission,
+ sd,
+ objectclass)
+ cls.generate_dynamic_test("test_priv_attr",
+ tname,
+ *targs)
+
+ def add_computer_ldap(self, computername, others=None, samdb=None):
+ dn = "CN=%s,%s" % (computername, self.test_ou)
+ samaccountname = "%s$" % computername
+ msg_dict = {
+ "dn": dn,
+ "objectclass": "computer"}
+ if others is not None:
+ msg_dict = dict(list(msg_dict.items()) + list(others.items()))
+
+ msg = ldb.Message.from_dict(samdb, msg_dict)
+ msg["sAMAccountName"] = samaccountname
+
+ print("Adding computer account %s" % computername)
+ try:
+ samdb.add(msg)
+ except ldb.LdbError:
+ print(msg)
+ raise
+ return msg.dn
+
+ def add_user_ldap(self, username, others=None, samdb=None):
+ dn = "CN=%s,%s" % (username, self.test_ou)
+ samaccountname = "%s$" % username
+ msg_dict = {
+ "dn": dn,
+ "objectclass": "user"}
+ if others is not None:
+ msg_dict = dict(list(msg_dict.items()) + list(others.items()))
+
+ msg = ldb.Message.from_dict(samdb, msg_dict)
+ msg["sAMAccountName"] = samaccountname
+
+ print("Adding user account %s" % username)
+ try:
+ samdb.add(msg)
+ except ldb.LdbError:
+ print(msg)
+ raise
+ return msg.dn
+
+ def add_thing_ldap(self, user, others, samdb, objectclass):
+ if objectclass == "user":
+ dn = self.add_user_ldap(user, others, samdb=samdb)
+ elif objectclass == "computer":
+ dn = self.add_computer_ldap(user, others, samdb=samdb)
+ return dn
+
+ def _test_priv_attr_with_args(self, test_name, add_or_mod, permission, sd, objectclass):
+ user="privattrs"
+ if "attr" in attrs[test_name]:
+ attr = attrs[test_name]["attr"]
+ else:
+ attr = test_name
+ if add_or_mod == "add":
+ others = {attr: attrs[test_name]["value"]}
+ else:
+ others = {}
+
+ if permission == "CC":
+ samdb = self.unpriv_samdb
+ # Set CC on container to allow user add
+ mod = "(OA;CI;CC;bf967aba-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.unpriv_user_sid)
+ self.admin_sd_utils.dacl_add_ace(self.test_ou, mod)
+ mod = "(OA;CI;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.unpriv_user_sid)
+ self.admin_sd_utils.dacl_add_ace(self.test_ou, mod)
+
+ else:
+ samdb = self.admin_samdb
+
+ if sd == "WP":
+ # Set SD to WP to the target user as part of add
+ sd = "O:%sG:DUD:(OA;CIID;RPWP;;;%s)(OA;;CR;00299570-246d-11d0-a768-00aa006e0529;;%s)" % (self.unpriv_user_sid, self.unpriv_user_sid, self.unpriv_user_sid)
+ tmp_desc = security.descriptor.from_sddl(sd, self.domain_sid)
+ others["ntSecurityDescriptor"] = ndr_pack(tmp_desc)
+
+ if add_or_mod == "add":
+
+ # only-1 and only-2 are due to windows behaviour
+
+ if "only-1" in attrs[test_name] and \
+ attrs[test_name]["only-1"] != objectclass:
+ try:
+ dn = self.add_thing_ldap(user, others, samdb, objectclass)
+ self.fail(f"{test_name}: Unexpectedly able to set {attr} on new {objectclass} as ADMIN (should fail LDAP_OBJECT_CLASS_VIOLATION)")
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.assertGotLdbError(ldb.ERR_OBJECT_CLASS_VIOLATION, enum)
+ elif permission == "CC":
+ try:
+ dn = self.add_thing_ldap(user, others, samdb, objectclass)
+ self.fail(f"{test_name}: Unexpectedly able to set {attr} on new {objectclass}")
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ if "unpriv-add-error" in attrs[test_name]:
+ self.assertGotLdbError(attrs[test_name]["unpriv-add-error"],
+ enum)
+ else:
+ self.assertGotLdbError(attrs[test_name]["unpriv-error"],
+ enum)
+ elif "only-2" in attrs[test_name] and \
+ attrs[test_name]["only-2"] != objectclass:
+ try:
+ dn = self.add_thing_ldap(user, others, samdb, objectclass)
+ self.fail(f"{test_name}: Unexpectedly able to set {attr} on new {objectclass} as ADMIN (should fail LDAP_OBJECT_CLASS_VIOLATION)")
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.assertGotLdbError(ldb.ERR_OBJECT_CLASS_VIOLATION, enum)
+ elif "priv-error" in attrs[test_name]:
+ try:
+ dn = self.add_thing_ldap(user, others, samdb, objectclass)
+ self.fail(f"{test_name}: Unexpectedly able to set {attr} on new {objectclass} as ADMIN")
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.assertGotLdbError(attrs[test_name]["priv-error"], enum)
+ else:
+ try:
+ dn = self.add_thing_ldap(user, others, samdb, objectclass)
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.fail(f"Failed to add account {user} as objectclass {objectclass}")
+ else:
+ try:
+ dn = self.add_thing_ldap(user, others, samdb, objectclass)
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.fail(f"Failed to add account {user} as objectclass {objectclass}")
+
+ if add_or_mod == "add":
+ return
+
+ m = ldb.Message()
+ m.dn = dn
+
+ # Do modify
+ if add_or_mod == "mod-del-add":
+ m["0"] = ldb.MessageElement([],
+ ldb.FLAG_MOD_DELETE,
+ attr)
+ m["1"] = ldb.MessageElement(attrs[test_name]["value"],
+ ldb.FLAG_MOD_ADD,
+ attr)
+ else:
+ m["0"] = ldb.MessageElement(attrs[test_name]["value"],
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+
+ try:
+ self.unpriv_samdb.modify(m)
+ self.fail(f"{test_name}: Unexpectedly able to set {attr} on {m.dn}")
+ except LdbError as e5:
+ (enum, estr) = e5.args
+ self.assertGotLdbError(attrs[test_name]["unpriv-error"], enum)
+
+
+
+
+runner = SubunitTestRunner()
+rc = 0
+if not runner.run(unittest.TestLoader().loadTestsFromTestCase(
+ PrivAttrsTests)).wasSuccessful():
+ rc = 1
+sys.exit(rc)
diff --git a/source4/dsdb/tests/python/rodc.py b/source4/dsdb/tests/python/rodc.py
new file mode 100755
index 0000000..b089a27
--- /dev/null
+++ b/source4/dsdb/tests/python/rodc.py
@@ -0,0 +1,254 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+import os
+import re
+import uuid
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+import ldb
+from samba.samdb import SamDB
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.dcerpc import drsblobs
+
+
+class RodcTestException(Exception):
+ pass
+
+
+class RodcTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(RodcTests, self).setUp()
+ self.samdb = SamDB(HOST, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+
+ self.base_dn = self.samdb.domain_dn()
+
+ root = self.samdb.search(base='', scope=ldb.SCOPE_BASE,
+ attrs=['dsServiceName'])
+ self.service = root[0]['dsServiceName'][0]
+ self.tag = uuid.uuid4().hex
+
+ def test_add_replicated_objects(self):
+ for o in (
+ {
+ 'dn': "ou=%s1,%s" % (self.tag, self.base_dn),
+ "objectclass": "organizationalUnit"
+ },
+ {
+ 'dn': "cn=%s2,%s" % (self.tag, self.base_dn),
+ "objectclass": "user"
+ },
+ {
+ 'dn': "cn=%s3,%s" % (self.tag, self.base_dn),
+ "objectclass": "group"
+ },
+ {
+ 'dn': "cn=%s4,%s" % (self.tag, self.service),
+ "objectclass": "NTDSConnection",
+ 'enabledConnection': 'TRUE',
+ 'fromServer': self.base_dn,
+ 'options': '0'
+ },
+ ):
+ try:
+ self.samdb.add(o)
+ self.fail("Failed to fail to add %s" % o['dn'])
+ except ldb.LdbError as e:
+ (ecode, emsg) = e.args
+ if ecode != ldb.ERR_REFERRAL:
+ print(emsg)
+ self.fail("Adding %s: ldb error: %s %s, wanted referral" %
+ (o['dn'], ecode, emsg))
+ else:
+ m = re.search(r'(ldap://[^>]+)>', emsg)
+ if m is None:
+ self.fail("referral seems not to refer to anything")
+ address = m.group(1)
+
+ try:
+ tmpdb = SamDB(address, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+ tmpdb.add(o)
+ tmpdb.delete(o['dn'])
+ except ldb.LdbError as e:
+ self.fail("couldn't modify referred location %s" %
+ address)
+
+ if address.lower().startswith(self.samdb.domain_dns_name()):
+ self.fail("referral address did not give a specific DC")
+
+ def test_modify_replicated_attributes(self):
+ # some timestamp ones
+ dn = 'CN=Guest,CN=Users,' + self.base_dn
+ value = 'hallooo'
+ for attr in ['carLicense', 'middleName']:
+ msg = ldb.Message()
+ msg.dn = ldb.Dn(self.samdb, dn)
+ msg[attr] = ldb.MessageElement(value,
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+ try:
+ self.samdb.modify(msg)
+ self.fail("Failed to fail to modify %s %s" % (dn, attr))
+ except ldb.LdbError as e1:
+ (ecode, emsg) = e1.args
+ if ecode != ldb.ERR_REFERRAL:
+ self.fail("Failed to REFER when trying to modify %s %s" %
+ (dn, attr))
+ else:
+ m = re.search(r'(ldap://[^>]+)>', emsg)
+ if m is None:
+ self.fail("referral seems not to refer to anything")
+ address = m.group(1)
+
+ try:
+ tmpdb = SamDB(address, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+ tmpdb.modify(msg)
+ except ldb.LdbError as e:
+ self.fail("couldn't modify referred location %s" %
+ address)
+
+ if address.lower().startswith(self.samdb.domain_dns_name()):
+ self.fail("referral address did not give a specific DC")
+
+ def test_modify_nonreplicated_attributes(self):
+ # some timestamp ones
+ dn = 'CN=Guest,CN=Users,' + self.base_dn
+ value = '123456789'
+ for attr in ['badPwdCount', 'lastLogon', 'lastLogoff']:
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, dn)
+ m[attr] = ldb.MessageElement(value,
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+ # Windows refers these ones even though they are non-replicated
+ try:
+ self.samdb.modify(m)
+ self.fail("Failed to fail to modify %s %s" % (dn, attr))
+ except ldb.LdbError as e2:
+ (ecode, emsg) = e2.args
+ if ecode != ldb.ERR_REFERRAL:
+ self.fail("Failed to REFER when trying to modify %s %s" %
+ (dn, attr))
+ else:
+ m = re.search(r'(ldap://[^>]+)>', emsg)
+ if m is None:
+ self.fail("referral seems not to refer to anything")
+ address = m.group(1)
+
+ if address.lower().startswith(self.samdb.domain_dns_name()):
+ self.fail("referral address did not give a specific DC")
+
+ def test_modify_nonreplicated_reps_attributes(self):
+ # some timestamp ones
+ dn = self.base_dn
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, dn)
+ attr = 'repsFrom'
+
+ res = self.samdb.search(dn, scope=ldb.SCOPE_BASE,
+ attrs=['repsFrom'])
+ rep = ndr_unpack(drsblobs.repsFromToBlob, res[0]['repsFrom'][0],
+ allow_remaining=True)
+ rep.ctr.result_last_attempt = -1
+ value = ndr_pack(rep)
+
+ m[attr] = ldb.MessageElement(value,
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+ try:
+ self.samdb.modify(m)
+ self.fail("Failed to fail to modify %s %s" % (dn, attr))
+ except ldb.LdbError as e3:
+ (ecode, emsg) = e3.args
+ if ecode != ldb.ERR_REFERRAL:
+ self.fail("Failed to REFER when trying to modify %s %s" %
+ (dn, attr))
+ else:
+ m = re.search(r'(ldap://[^>]+)>', emsg)
+ if m is None:
+ self.fail("referral seems not to refer to anything")
+ address = m.group(1)
+
+ if address.lower().startswith(self.samdb.domain_dns_name()):
+ self.fail("referral address did not give a specific DC")
+
+ def test_delete_special_objects(self):
+ dn = 'CN=Guest,CN=Users,' + self.base_dn
+ try:
+ self.samdb.delete(dn)
+ self.fail("Failed to fail to delete %s" % (dn))
+ except ldb.LdbError as e4:
+ (ecode, emsg) = e4.args
+ if ecode != ldb.ERR_REFERRAL:
+ print(ecode, emsg)
+ self.fail("Failed to REFER when trying to delete %s" % dn)
+ else:
+ m = re.search(r'(ldap://[^>]+)>', emsg)
+ if m is None:
+ self.fail("referral seems not to refer to anything")
+ address = m.group(1)
+
+ if address.lower().startswith(self.samdb.domain_dns_name()):
+ self.fail("referral address did not give a specific DC")
+
+ def test_no_delete_nonexistent_objects(self):
+ dn = 'CN=does-not-exist-%s,CN=Users,%s' % (self.tag, self.base_dn)
+ try:
+ self.samdb.delete(dn)
+ self.fail("Failed to fail to delete %s" % (dn))
+ except ldb.LdbError as e5:
+ (ecode, emsg) = e5.args
+ if ecode != ldb.ERR_NO_SUCH_OBJECT:
+ print(ecode, emsg)
+ self.fail("Failed to NO_SUCH_OBJECT when trying to delete "
+ "%s (which does not exist)" % dn)
+
+
+def main():
+ global HOST, CREDS, LP
+ parser = optparse.OptionParser("rodc.py [options] <host>")
+
+ sambaopts = options.SambaOptions(parser)
+ versionopts = options.VersionOptions(parser)
+ credopts = options.CredentialsOptions(parser)
+ subunitopts = SubunitOptions(parser)
+
+ parser.add_option_group(sambaopts)
+ parser.add_option_group(versionopts)
+ parser.add_option_group(credopts)
+ parser.add_option_group(subunitopts)
+
+ opts, args = parser.parse_args()
+
+ LP = sambaopts.get_loadparm()
+ CREDS = credopts.get_credentials(LP)
+
+ try:
+ HOST = args[0]
+ except IndexError:
+ parser.print_usage()
+ sys.exit(1)
+
+ if "://" not in HOST:
+ if os.path.isfile(HOST):
+ HOST = "tdb://%s" % HOST
+ else:
+ HOST = "ldap://%s" % HOST
+
+ TestProgram(module=__name__, opts=subunitopts)
+
+
+main()
diff --git a/source4/dsdb/tests/python/rodc_rwdc.py b/source4/dsdb/tests/python/rodc_rwdc.py
new file mode 100644
index 0000000..b2e8c73
--- /dev/null
+++ b/source4/dsdb/tests/python/rodc_rwdc.py
@@ -0,0 +1,1324 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+"""Test communication of credentials etc, between an RODC and a RWDC.
+
+How does it work when the password is changed on the RWDC?
+"""
+
+import optparse
+import sys
+import base64
+import uuid
+import subprocess
+import itertools
+import time
+
+sys.path.insert(0, "bin/python")
+import ldb
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba.samdb import SamDB
+from samba.credentials import Credentials, DONT_USE_KERBEROS, MUST_USE_KERBEROS
+from samba import gensec, dsdb
+from ldb import LdbError, ERR_INVALID_CREDENTIALS
+from samba.dcerpc import security, samr
+import os
+
+import password_lockout_base
+
+def adjust_cmd_for_py_version(parts):
+ if os.getenv("PYTHON", None):
+ parts.insert(0, os.environ["PYTHON"])
+ return parts
+
+def passwd_encode(pw):
+ return base64.b64encode(('"%s"' % pw).encode('utf-16-le')).decode('utf8')
+
+
+class RodcRwdcTestException(Exception):
+ pass
+
+
+def make_creds(username, password, kerberos_state=None, simple_dn=None):
+ # use the global CREDS as a template
+ c = Credentials()
+ c.set_username(username)
+ c.set_password(password)
+ c.set_domain(CREDS.get_domain())
+ c.set_realm(CREDS.get_realm())
+ c.set_workstation(CREDS.get_workstation())
+
+ if simple_dn is not None:
+ c.set_bind_dn(simple_dn)
+
+ if kerberos_state is None:
+ kerberos_state = CREDS.get_kerberos_state()
+ c.set_kerberos_state(kerberos_state)
+
+ print('-' * 73)
+ if kerberos_state == MUST_USE_KERBEROS:
+ print("we seem to be using kerberos for %s %s" % (username, password))
+ elif kerberos_state == DONT_USE_KERBEROS:
+ print("NOT using kerberos for %s %s" % (username, password))
+ else:
+ print("kerberos state is %s" % kerberos_state)
+
+ c.set_gensec_features(c.get_gensec_features() |
+ gensec.FEATURE_SEAL)
+ return c
+
+
+def set_auto_replication(dc, allow):
+ credstring = '-U%s%%%s' % (CREDS.get_username(),
+ CREDS.get_password())
+
+ on_or_off = '-' if allow else '+'
+
+ for opt in ['DISABLE_INBOUND_REPL',
+ 'DISABLE_OUTBOUND_REPL']:
+ cmd = adjust_cmd_for_py_version(['bin/samba-tool',
+ 'drs', 'options',
+ credstring, dc,
+ "--dsa-option=%s%s" % (on_or_off, opt)])
+
+ p = subprocess.Popen(cmd,
+ stderr=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ if p.returncode:
+ if b'LDAP_REFERRAL' not in stderr:
+ raise RodcRwdcTestException()
+ print("ignoring +%s REFERRAL error; assuming %s is RODC" %
+ (opt, dc))
+
+
+def preload_rodc_user(user_dn):
+ credstring = '-U%s%%%s' % (CREDS.get_username(),
+ CREDS.get_password())
+
+ set_auto_replication(RWDC, True)
+ cmd = adjust_cmd_for_py_version(['bin/samba-tool',
+ 'rodc', 'preload',
+ user_dn,
+ credstring,
+ '--server', RWDC, ])
+
+ print(' '.join(cmd))
+ subprocess.check_call(cmd)
+ set_auto_replication(RWDC, False)
+
+
+def get_server_ref_from_samdb(samdb):
+ server_name = samdb.get_serverName()
+ res = samdb.search(server_name,
+ scope=ldb.SCOPE_BASE,
+ attrs=['serverReference'])
+
+ return res[0]['serverReference'][0]
+
+
+class RodcRwdcCachedTests(password_lockout_base.BasePasswordTestCase):
+
+ def _check_account_initial(self, dn):
+ self.force_replication()
+ return super(RodcRwdcCachedTests, self)._check_account_initial(dn)
+
+ def _check_account(self, dn,
+ badPwdCount=None,
+ badPasswordTime=None,
+ logonCount=None,
+ lastLogon=None,
+ lastLogonTimestamp=None,
+ lockoutTime=None,
+ userAccountControl=None,
+ msDSUserAccountControlComputed=None,
+ effective_bad_password_count=None,
+ msg=None,
+ badPwdCountOnly=False):
+ # Wait for the RWDC to get any delayed messages
+ # e.g. SendToSam or KRB5 bad passwords via winbindd
+ if (self.kerberos and isinstance(badPasswordTime, tuple) or
+ badPwdCount == 0):
+ time.sleep(5)
+
+ return super(RodcRwdcCachedTests,
+ self)._check_account(dn, badPwdCount, badPasswordTime,
+ logonCount, lastLogon,
+ lastLogonTimestamp, lockoutTime,
+ userAccountControl,
+ msDSUserAccountControlComputed,
+ effective_bad_password_count, msg,
+ True)
+
+ def force_replication(self, base=None):
+ if base is None:
+ base = self.base_dn
+
+ # XXX feels like a horrendous way to do it.
+ credstring = '-U%s%%%s' % (CREDS.get_username(),
+ CREDS.get_password())
+ cmd = adjust_cmd_for_py_version(['bin/samba-tool',
+ 'drs', 'replicate',
+ RODC, RWDC, base,
+ credstring,
+ '--sync-forced'])
+
+ p = subprocess.Popen(cmd,
+ stderr=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ if p.returncode:
+ print("failed with code %s" % p.returncode)
+ print(' '.join(cmd))
+ print("stdout")
+ print(stdout)
+ print("stderr")
+ print(stderr)
+ raise RodcRwdcTestException()
+
+ def _change_password(self, user_dn, old_password, new_password):
+ self.rwdc_db.modify_ldif(
+ "dn: %s\n"
+ "changetype: modify\n"
+ "delete: userPassword\n"
+ "userPassword: %s\n"
+ "add: userPassword\n"
+ "userPassword: %s\n" % (user_dn, old_password, new_password))
+
+ def tearDown(self):
+ super(RodcRwdcCachedTests, self).tearDown()
+ set_auto_replication(RWDC, True)
+
+ def setUp(self):
+ self.kerberos = False # To be set later
+
+ self.rodc_db = SamDB('ldap://%s' % RODC, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+
+ self.rwdc_db = SamDB('ldap://%s' % RWDC, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+
+ # Define variables for BasePasswordTestCase
+ self.lp = LP
+ self.global_creds = CREDS
+ self.host = RWDC
+ self.host_url = 'ldap://%s' % RWDC
+ self.host_url_ldaps = 'ldaps://%s' % RWDC
+ self.ldb = SamDB(url='ldap://%s' % RWDC, session_info=system_session(self.lp),
+ credentials=self.global_creds, lp=self.lp)
+
+ super(RodcRwdcCachedTests, self).setUp()
+ self.host_url = 'ldap://%s' % RODC
+ self.host_url_ldaps = 'ldaps://%s' % RODC
+
+ self.samr = samr.samr("ncacn_ip_tcp:%s[seal]" % self.host, self.lp, self.global_creds)
+ self.samr_handle = self.samr.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
+ self.samr_domain = self.samr.OpenDomain(self.samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED, self.domain_sid)
+
+ self.base_dn = self.rwdc_db.domain_dn()
+
+ root = self.rodc_db.search(base='', scope=ldb.SCOPE_BASE,
+ attrs=['dsServiceName'])
+ self.service = root[0]['dsServiceName'][0]
+ self.tag = uuid.uuid4().hex
+
+ self.rwdc_dsheuristics = self.rwdc_db.get_dsheuristics()
+ self.rwdc_db.set_dsheuristics("000000001")
+
+ set_auto_replication(RWDC, False)
+
+ # make sure DCs are synchronized before the test
+ self.force_replication()
+
+ def delete_ldb_connections(self):
+ super(RodcRwdcCachedTests, self).delete_ldb_connections()
+ del self.rwdc_db
+ del self.rodc_db
+
+ def test_cache_and_flush_password(self):
+ username = self.lockout1krb5_creds.get_username()
+ userpass = self.lockout1krb5_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ ldb_system = SamDB(session_info=system_session(self.lp),
+ credentials=self.global_creds, lp=self.lp)
+
+ res = ldb_system.search(userdn, attrs=['unicodePwd'])
+ self.assertFalse('unicodePwd' in res[0])
+
+ preload_rodc_user(userdn)
+
+ res = ldb_system.search(userdn, attrs=['unicodePwd'])
+ self.assertTrue('unicodePwd' in res[0])
+
+ # force replication here to flush any pending preloads (this
+ # was a racy test).
+ self.force_replication()
+
+ newpass = userpass + '!'
+
+ # Forcing replication should blank out password (when changed)
+ self._change_password(userdn, userpass, newpass)
+ self.force_replication()
+
+ res = ldb_system.search(userdn, attrs=['unicodePwd'])
+ self.assertFalse('unicodePwd' in res[0])
+
+ def test_login_lockout_krb5(self):
+ username = self.lockout1krb5_creds.get_username()
+ userpass = self.lockout1krb5_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ preload_rodc_user(userdn)
+
+ self.kerberos = True
+
+ self.rodc_dn = get_server_ref_from_samdb(self.rodc_db)
+
+ res = self.rodc_db.search(self.rodc_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['msDS-RevealOnDemandGroup'])
+
+ group = res[0]['msDS-RevealOnDemandGroup'][0].decode('utf8')
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.rwdc_db, group)
+ m['member'] = ldb.MessageElement(userdn, ldb.FLAG_MOD_ADD, 'member')
+ self.rwdc_db.modify(m)
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.ldb, self.base_dn)
+
+ self.account_lockout_duration = 15
+ account_lockout_duration_ticks = -int(self.account_lockout_duration * (1e7))
+
+ m["lockoutDuration"] = ldb.MessageElement(str(account_lockout_duration_ticks),
+ ldb.FLAG_MOD_REPLACE,
+ "lockoutDuration")
+
+ self.lockout_observation_window = 15
+ lockout_observation_window_ticks = -int(self.lockout_observation_window * (1e7))
+
+ m["lockOutObservationWindow"] = ldb.MessageElement(str(lockout_observation_window_ticks),
+ ldb.FLAG_MOD_REPLACE,
+ "lockOutObservationWindow")
+
+ self.rwdc_db.modify(m)
+ self.force_replication()
+
+ self._test_login_lockout_rodc_rwdc(self.lockout1krb5_creds, userdn)
+
+ def test_login_lockout_ntlm(self):
+ username = self.lockout1ntlm_creds.get_username()
+ userpass = self.lockout1ntlm_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ preload_rodc_user(userdn)
+
+ self.kerberos = False
+
+ self.rodc_dn = get_server_ref_from_samdb(self.rodc_db)
+
+ res = self.rodc_db.search(self.rodc_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['msDS-RevealOnDemandGroup'])
+
+ group = res[0]['msDS-RevealOnDemandGroup'][0].decode('utf8')
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.rwdc_db, group)
+ m['member'] = ldb.MessageElement(userdn, ldb.FLAG_MOD_ADD, 'member')
+ self.rwdc_db.modify(m)
+
+ self._test_login_lockout_rodc_rwdc(self.lockout1ntlm_creds, userdn)
+
+ def test_login_lockout_not_revealed(self):
+ '''Test that SendToSam is restricted by preloaded users/groups'''
+
+ username = self.lockout1ntlm_creds.get_username()
+ userpass = self.lockout1ntlm_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ # Preload but do not add to revealed group
+ preload_rodc_user(userdn)
+
+ self.kerberos = False
+
+ creds = self.lockout1ntlm_creds
+
+ # Open a second LDB connection with the user credentials. Use the
+ # command line credentials for information like the domain, the realm
+ # and the workstation.
+ creds_lockout = self.insta_creds(creds)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ self.assertLoginFailure(self.host_url, creds_lockout, self.lp)
+
+ badPasswordTime = 0
+ logonCount = 0
+ lastLogon = 0
+ lastLogonTimestamp = 0
+ logoncount_relation = ''
+ lastlogon_relation = ''
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='lastlogontimestamp with wrong password')
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # BadPwdCount on RODC increases alongside RWDC
+ res = self.rodc_db.search(userdn, attrs=['badPwdCount'])
+ self.assertTrue('badPwdCount' in res[0])
+ self.assertEqual(int(res[0]['badPwdCount'][0]), 1)
+
+ # Correct old password
+ creds_lockout.set_password(userpass)
+
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+
+ # Wait for potential SendToSam...
+ time.sleep(5)
+
+ # BadPwdCount on RODC decreases, but not the RWDC
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=('greater', lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='badPwdCount not reset on RWDC')
+
+ res = self.rodc_db.search(userdn, attrs=['badPwdCount'])
+ self.assertTrue('badPwdCount' in res[0])
+ self.assertEqual(int(res[0]['badPwdCount'][0]), 0)
+
+ def _test_login_lockout_rodc_rwdc(self, creds, userdn):
+ username = creds.get_username()
+ userpass = creds.get_password()
+
+ # Open a second LDB connection with the user credentials. Use the
+ # command line credentials for information like the domain, the realm
+ # and the workstation.
+ creds_lockout = self.insta_creds(creds)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ self.assertLoginFailure(self.host_url, creds_lockout, self.lp)
+
+ badPasswordTime = 0
+ logonCount = 0
+ lastLogon = 0
+ lastLogonTimestamp = 0
+ logoncount_relation = ''
+ lastlogon_relation = ''
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='lastlogontimestamp with wrong password')
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # Correct old password
+ creds_lockout.set_password(userpass)
+
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+
+ # lastLogonTimestamp should not change
+ # lastLogon increases if badPwdCount is non-zero (!)
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=('greater', lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg='LLTimestamp is updated to lastlogon')
+
+ logonCount = int(res[0]["logonCount"][0])
+ lastLogon = int(res[0]["lastLogon"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ self.assertLoginFailure(self.host_url, creds_lockout, self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+
+ except LdbError as e1:
+ (num, msg) = e1.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ print("two failed password change")
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+
+ except LdbError as e2:
+ (num, msg) = e2.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=("greater", badPasswordTime),
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+ lockoutTime = int(res[0]["lockoutTime"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e3:
+ (num, msg) = e3.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e4:
+ (num, msg) = e4.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # The correct password, but we are locked out
+ creds_lockout.set_password(userpass)
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e5:
+ (num, msg) = e5.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=3,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=dsdb.UF_LOCKOUT)
+
+ # wait for the lockout to end
+ time.sleep(self.account_lockout_duration + 1)
+ print(self.account_lockout_duration + 1)
+
+ res = self._check_account(userdn,
+ badPwdCount=3, effective_bad_password_count=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The correct password after letting the timeout expire
+
+ creds_lockout.set_password(userpass)
+
+ creds_lockout2 = self.insta_creds(creds_lockout)
+
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout2, lp=self.lp)
+ time.sleep(3)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lastLogon=(lastlogon_relation, lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ lockoutTime=lockoutTime,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0,
+ msg="lastLogon is way off")
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e6:
+ (num, msg) = e6.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e7:
+ (num, msg) = e7.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=2,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ time.sleep(self.lockout_observation_window + 1)
+
+ res = self._check_account(userdn,
+ badPwdCount=2, effective_bad_password_count=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+ # The wrong password
+ creds_lockout.set_password("thatsAcomplPASS1x")
+ try:
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+ self.fail()
+ except LdbError as e8:
+ (num, msg) = e8.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ res = self._check_account(userdn,
+ badPwdCount=1,
+ badPasswordTime=("greater", badPasswordTime),
+ logonCount=logonCount,
+ lockoutTime=lockoutTime,
+ lastLogon=lastLogon,
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+ badPasswordTime = int(res[0]["badPasswordTime"][0])
+
+ # The correct password without letting the timeout expire
+ creds_lockout.set_password(userpass)
+ ldb_lockout = SamDB(url=self.host_url, credentials=creds_lockout, lp=self.lp)
+
+ res = self._check_account(userdn,
+ badPwdCount=0,
+ badPasswordTime=badPasswordTime,
+ logonCount=(logoncount_relation, logonCount),
+ lockoutTime=lockoutTime,
+ lastLogon=("greater", lastLogon),
+ lastLogonTimestamp=lastLogonTimestamp,
+ userAccountControl=dsdb.UF_NORMAL_ACCOUNT,
+ msDSUserAccountControlComputed=0)
+
+
+class RodcRwdcTests(password_lockout_base.BasePasswordTestCase):
+ counter = itertools.count(1, 1)
+
+ def force_replication(self, base=None):
+ if base is None:
+ base = self.base_dn
+
+ # XXX feels like a horrendous way to do it.
+ credstring = '-U%s%%%s' % (CREDS.get_username(),
+ CREDS.get_password())
+ cmd = adjust_cmd_for_py_version(['bin/samba-tool',
+ 'drs', 'replicate',
+ RODC, RWDC, base,
+ credstring,
+ '--sync-forced'])
+
+ p = subprocess.Popen(cmd,
+ stderr=subprocess.PIPE,
+ stdout=subprocess.PIPE)
+ stdout, stderr = p.communicate()
+ if p.returncode:
+ print("failed with code %s" % p.returncode)
+ print(' '.join(cmd))
+ print("stdout")
+ print(stdout)
+ print("stderr")
+ print(stderr)
+ raise RodcRwdcTestException()
+
+ def _check_account_initial(self, dn):
+ self.force_replication()
+ return super(RodcRwdcTests, self)._check_account_initial(dn)
+
+ def tearDown(self):
+ super(RodcRwdcTests, self).tearDown()
+ self.rwdc_db.set_dsheuristics(self.rwdc_dsheuristics)
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ set_auto_replication(RWDC, True)
+
+ def setUp(self):
+ self.rodc_db = SamDB('ldap://%s' % RODC, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+
+ self.rwdc_db = SamDB('ldap://%s' % RWDC, credentials=CREDS,
+ session_info=system_session(LP), lp=LP)
+
+ # Define variables for BasePasswordTestCase
+ self.lp = LP
+ self.global_creds = CREDS
+ self.host = RWDC
+ self.host_url = 'ldap://%s' % RWDC
+ self.host_url_ldaps = 'ldaps://%s' % RWDC
+ self.ldb = SamDB(url='ldap://%s' % RWDC, session_info=system_session(self.lp),
+ credentials=self.global_creds, lp=self.lp)
+
+ super(RodcRwdcTests, self).setUp()
+ self.host = RODC
+ self.host_url = 'ldap://%s' % RODC
+ self.host_url_ldaps = 'ldaps://%s' % RODC
+ self.ldb = SamDB(url='ldap://%s' % RODC, session_info=system_session(self.lp),
+ credentials=self.global_creds, lp=self.lp)
+
+ self.samr = samr.samr("ncacn_ip_tcp:%s[seal]" % self.host, self.lp, self.global_creds)
+ self.samr_handle = self.samr.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
+ self.samr_domain = self.samr.OpenDomain(self.samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED, self.domain_sid)
+
+ self.base_dn = self.rwdc_db.domain_dn()
+
+ root = self.rodc_db.search(base='', scope=ldb.SCOPE_BASE,
+ attrs=['dsServiceName'])
+ self.service = root[0]['dsServiceName'][0]
+ self.tag = uuid.uuid4().hex
+
+ self.rwdc_dsheuristics = self.rwdc_db.get_dsheuristics()
+ self.rwdc_db.set_dsheuristics("000000001")
+
+ set_auto_replication(RWDC, False)
+
+ # make sure DCs are synchronized before the test
+ self.force_replication()
+ self.rwdc_dn = get_server_ref_from_samdb(self.rwdc_db)
+ self.rodc_dn = get_server_ref_from_samdb(self.rodc_db)
+
+ def delete_ldb_connections(self):
+ super(RodcRwdcTests, self).delete_ldb_connections()
+ del self.rwdc_db
+ del self.rodc_db
+
+ def assertReferral(self, fn, *args, **kwargs):
+ try:
+ fn(*args, **kwargs)
+ self.fail("failed to raise ldap referral")
+ except ldb.LdbError as e9:
+ (code, msg) = e9.args
+ self.assertEqual(code, ldb.ERR_REFERRAL,
+ "expected referral, got %s %s" % (code, msg))
+
+ def _test_rodc_dsheuristics(self):
+ d = self.rodc_db.get_dsheuristics()
+ self.assertReferral(self.rodc_db.set_dsheuristics, "000000001")
+ self.assertReferral(self.rodc_db.set_dsheuristics, d)
+
+ def TEST_rodc_heuristics_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_rodc_dsheuristics()
+
+ def TEST_rodc_heuristics_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_rodc_dsheuristics()
+
+ def _test_add(self, objects, cross_ncs=False):
+ for o in objects:
+ dn = o['dn']
+ if cross_ncs:
+ base = str(self.rwdc_db.get_config_basedn())
+ controls = ["search_options:1:2"]
+ cn = dn.split(',', 1)[0]
+ expression = '(%s)' % cn
+ else:
+ base = dn
+ controls = []
+ expression = None
+
+ try:
+ res = self.rodc_db.search(base,
+ expression=expression,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=['dn'],
+ controls=controls)
+ self.assertEqual(len(res), 0)
+ except ldb.LdbError as e:
+ if e.args[0] != ldb.ERR_NO_SUCH_OBJECT:
+ raise
+
+ try:
+ self.rwdc_db.add(o)
+ except ldb.LdbError as e:
+ (ecode, emsg) = e.args
+ self.fail("Failed to add %s to rwdc: ldb error: %s %s" %
+ (o, ecode, emsg))
+
+ if cross_ncs:
+ self.force_replication(base=base)
+ else:
+ self.force_replication()
+
+ try:
+ res = self.rodc_db.search(base,
+ expression=expression,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=['dn'],
+ controls=controls)
+ self.assertEqual(len(res), 1)
+ except ldb.LdbError as e:
+ self.assertNotEqual(e.args[0], ldb.ERR_NO_SUCH_OBJECT,
+ "replication seems to have failed")
+
+ def _test_add_replicated_objects(self, mode):
+ tag = "%s%s" % (self.tag, mode)
+ self._test_add([
+ {
+ 'dn': "ou=%s1,%s" % (tag, self.base_dn),
+ "objectclass": "organizationalUnit"
+ },
+ {
+ 'dn': "cn=%s2,%s" % (tag, self.base_dn),
+ "objectclass": "user"
+ },
+ {
+ 'dn': "cn=%s3,%s" % (tag, self.base_dn),
+ "objectclass": "group"
+ },
+ ])
+ self.rwdc_db.delete("ou=%s1,%s" % (tag, self.base_dn))
+ self.rwdc_db.delete("cn=%s2,%s" % (tag, self.base_dn))
+ self.rwdc_db.delete("cn=%s3,%s" % (tag, self.base_dn))
+
+ def test_add_replicated_objects_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_add_replicated_objects('kerberos')
+
+ def test_add_replicated_objects_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_add_replicated_objects('ntlm')
+
+ def _test_add_replicated_connections(self, mode):
+ tag = "%s%s" % (self.tag, mode)
+ self._test_add([
+ {
+ 'dn': "cn=%sfoofoofoo,%s" % (tag, self.service),
+ "objectclass": "NTDSConnection",
+ 'enabledConnection': 'TRUE',
+ 'fromServer': self.base_dn,
+ 'options': '0'
+ },
+ ], cross_ncs=True)
+ self.rwdc_db.delete("cn=%sfoofoofoo,%s" % (tag, self.service))
+
+ def test_add_replicated_connections_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_add_replicated_connections('kerberos')
+
+ def test_add_replicated_connections_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_add_replicated_connections('ntlm')
+
+ def _test_modify_replicated_attributes(self):
+ dn = 'CN=Guest,CN=Users,' + self.base_dn
+ value = self.tag
+ for attr in ['carLicense', 'middleName']:
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.rwdc_db, dn)
+ m[attr] = ldb.MessageElement(value,
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+ try:
+ self.rwdc_db.modify(m)
+ except ldb.LdbError as e:
+ self.fail("Failed to modify %s %s on RWDC %s with %s" %
+ (dn, attr, RWDC, e))
+
+ self.force_replication()
+
+ try:
+ res = self.rodc_db.search(dn,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=[attr])
+ results = [str(x[attr][0]) for x in res]
+ self.assertEqual(results, [value])
+ except ldb.LdbError as e:
+ self.assertNotEqual(e.args[0], ldb.ERR_NO_SUCH_OBJECT,
+ "replication seems to have failed")
+
+ def test_modify_replicated_attributes_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_modify_replicated_attributes()
+
+ def test_modify_replicated_attributes_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_modify_replicated_attributes()
+
+ def _test_add_modify_delete(self):
+ dn = "cn=%s_add_modify,%s" % (self.tag, self.base_dn)
+ values = ["%s%s" % (i, self.tag) for i in range(3)]
+ attr = "carLicense"
+ self._test_add([
+ {
+ 'dn': dn,
+ "objectclass": "user",
+ attr: values[0]
+ },
+ ])
+ self.force_replication()
+ for value in values[1:]:
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.rwdc_db, dn)
+ m[attr] = ldb.MessageElement(value,
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+ try:
+ self.rwdc_db.modify(m)
+ except ldb.LdbError as e:
+ self.fail("Failed to modify %s %s on RWDC %s with %s" %
+ (dn, attr, RWDC, e))
+
+ self.force_replication()
+
+ try:
+ res = self.rodc_db.search(dn,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=[attr])
+ results = [str(x[attr][0]) for x in res]
+ self.assertEqual(results, [value])
+ except ldb.LdbError as e:
+ self.assertNotEqual(e.args[0], ldb.ERR_NO_SUCH_OBJECT,
+ "replication seems to have failed")
+
+ self.rwdc_db.delete(dn)
+ self.force_replication()
+ try:
+ res = self.rodc_db.search(dn,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=[attr])
+ if len(res) > 0:
+ self.fail("Failed to delete %s" % (dn))
+ except ldb.LdbError as e:
+ self.assertEqual(e.args[0], ldb.ERR_NO_SUCH_OBJECT,
+ "Failed to delete %s" % (dn))
+
+ def test_add_modify_delete_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_add_modify_delete()
+
+ def test_add_modify_delete_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_add_modify_delete()
+
+ def _new_user(self):
+ username = "u%sX%s" % (self.tag[:12], next(self.counter))
+ password = 'password#1'
+ dn = 'CN=%s,CN=Users,%s' % (username, self.base_dn)
+ o = {
+ 'dn': dn,
+ "objectclass": "user",
+ 'sAMAccountName': username,
+ }
+ try:
+ self.rwdc_db.add(o)
+ except ldb.LdbError as e:
+ self.fail("Failed to add %s to rwdc: ldb error: %s" % (o, e))
+
+ self.rwdc_db.modify_ldif("dn: %s\n"
+ "changetype: modify\n"
+ "delete: userPassword\n"
+ "add: userPassword\n"
+ "userPassword: %s\n" % (dn, password))
+ self.rwdc_db.enable_account("(sAMAccountName=%s)" % username)
+ return (dn, username, password)
+
+ def _change_password(self, user_dn, old_password, new_password):
+ self.rwdc_db.modify_ldif(
+ "dn: %s\n"
+ "changetype: modify\n"
+ "delete: userPassword\n"
+ "userPassword: %s\n"
+ "add: userPassword\n"
+ "userPassword: %s\n" % (user_dn, old_password, new_password))
+
+ def try_ldap_logon(self, server, creds, errno=None, simple=False):
+ try:
+ if simple:
+ tmpdb = SamDB('ldaps://%s' % server, credentials=creds,
+ session_info=system_session(LP), lp=LP)
+ else:
+ tmpdb = SamDB('ldap://%s' % server, credentials=creds,
+ session_info=system_session(LP), lp=LP)
+ if errno is not None:
+ self.fail("logon failed to fail with ldb error %s" % errno)
+ except ldb.LdbError as e10:
+ (code, msg) = e10.args
+ if code != errno:
+ if errno is None:
+ self.fail("logon incorrectly raised ldb error (code=%s)" %
+ code)
+ else:
+ self.fail("logon failed to raise correct ldb error"
+ "Expected: %s Got: %s" %
+ (errno, code))
+
+ def zero_min_password_age(self):
+ min_pwd_age = int(self.rwdc_db.get_minPwdAge())
+ if min_pwd_age != 0:
+ self.rwdc_db.set_minPwdAge('0')
+
+ def _test_ldap_change_password(self, errno=None, simple=False):
+ self.zero_min_password_age()
+
+ dn, username, password = self._new_user()
+
+ simple_dn = dn if simple else None
+
+ creds1 = make_creds(username, password, simple_dn=simple_dn)
+
+ # With NTLM, this should fail on RODC before replication,
+ # because the user isn't known.
+ self.try_ldap_logon(RODC, creds1, ldb.ERR_INVALID_CREDENTIALS,
+ simple=simple)
+ self.force_replication()
+
+ # Now the user is replicated to RODC, so logon should work
+ self.try_ldap_logon(RODC, creds1, simple=simple)
+
+ passwords = ['password#%s' % i for i in range(1, 6)]
+ for prev, password in zip(passwords[:-1], passwords[1:]):
+ self._change_password(dn, prev, password)
+
+ # The password has changed enough times to make the old
+ # password invalid (though with kerberos that doesn't matter).
+ # For NTLM, the old creds should always fail
+ self.try_ldap_logon(RODC, creds1, errno, simple=simple)
+ self.try_ldap_logon(RWDC, creds1, errno, simple=simple)
+
+ creds2 = make_creds(username, password, simple_dn=simple_dn)
+
+ # new creds work straight away with NTLM, because although it
+ # doesn't have the password, it knows the user and forwards
+ # the query.
+ self.try_ldap_logon(RODC, creds2, simple=simple)
+ self.try_ldap_logon(RWDC, creds2, simple=simple)
+
+ self.force_replication()
+
+ # After another replication check RODC still works and fails,
+ # as appropriate to various creds
+ self.try_ldap_logon(RODC, creds2, simple=simple)
+ self.try_ldap_logon(RODC, creds1, errno, simple=simple)
+
+ prev = password
+ password = 'password#6'
+ self._change_password(dn, prev, password)
+ creds3 = make_creds(username, password, simple_dn=simple_dn)
+
+ # previous password should still work.
+ self.try_ldap_logon(RWDC, creds2, simple=simple)
+ self.try_ldap_logon(RODC, creds2, simple=simple)
+
+ # new password should still work.
+ self.try_ldap_logon(RWDC, creds3, simple=simple)
+ self.try_ldap_logon(RODC, creds3, simple=simple)
+
+ # old password should still fail (but not on kerberos).
+ self.try_ldap_logon(RWDC, creds1, errno, simple=simple)
+ self.try_ldap_logon(RODC, creds1, errno, simple=simple)
+
+ def test_ldap_change_password_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_ldap_change_password()
+
+ def test_ldap_change_password_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_ldap_change_password(ldb.ERR_INVALID_CREDENTIALS)
+
+ def test_ldap_change_password_simple_bind(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_ldap_change_password(ldb.ERR_INVALID_CREDENTIALS, simple=True)
+
+ def _test_ldap_change_password_reveal_on_demand(self, errno=None):
+ self.zero_min_password_age()
+
+ res = self.rodc_db.search(self.rodc_dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['msDS-RevealOnDemandGroup'])
+
+ group = res[0]['msDS-RevealOnDemandGroup'][0].decode('utf8')
+
+ user_dn, username, password = self._new_user()
+ creds1 = make_creds(username, password)
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.rwdc_db, group)
+ m['member'] = ldb.MessageElement(user_dn, ldb.FLAG_MOD_ADD, 'member')
+ self.rwdc_db.modify(m)
+
+ # Against Windows, this will just forward if no account exists on the KDC
+ # Therefore, this does not error on Windows.
+ self.try_ldap_logon(RODC, creds1, ldb.ERR_INVALID_CREDENTIALS)
+
+ self.force_replication()
+
+ # The proxy case
+ self.try_ldap_logon(RODC, creds1)
+ preload_rodc_user(user_dn)
+
+ # Now the user AND password are replicated to RODC, so logon should work (not proxy case)
+ self.try_ldap_logon(RODC, creds1)
+
+ passwords = ['password#%s' % i for i in range(1, 6)]
+ for prev, password in zip(passwords[:-1], passwords[1:]):
+ self._change_password(user_dn, prev, password)
+
+ # The password has changed enough times to make the old
+ # password invalid, but the RODC shouldn't know that.
+ self.try_ldap_logon(RODC, creds1)
+ self.try_ldap_logon(RWDC, creds1, errno)
+
+ creds2 = make_creds(username, password)
+ self.try_ldap_logon(RWDC, creds2)
+ # The RODC forward WRONG_PASSWORD to the RWDC
+ self.try_ldap_logon(RODC, creds2)
+
+ def test_change_password_reveal_on_demand_ntlm(self):
+ CREDS.set_kerberos_state(DONT_USE_KERBEROS)
+ self._test_ldap_change_password_reveal_on_demand(ldb.ERR_INVALID_CREDENTIALS)
+
+ def test_change_password_reveal_on_demand_kerberos(self):
+ CREDS.set_kerberos_state(MUST_USE_KERBEROS)
+ self._test_ldap_change_password_reveal_on_demand()
+
+ def test_login_lockout_krb5(self):
+ username = self.lockout1krb5_creds.get_username()
+ userpass = self.lockout1krb5_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ preload_rodc_user(userdn)
+
+ use_kerberos = self.lockout1krb5_creds.get_kerberos_state()
+ fail_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass + "X",
+ kerberos_state=use_kerberos)
+
+ try:
+ ldb = SamDB(url=self.host_url, credentials=fail_creds, lp=self.lp)
+ self.fail()
+ except LdbError as e11:
+ (num, msg) = e11.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ # Succeed to reset everything to 0
+ success_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass,
+ kerberos_state=use_kerberos)
+
+ ldb = SamDB(url=self.host_url, credentials=success_creds, lp=self.lp)
+
+ self._test_login_lockout(self.lockout1krb5_creds)
+
+ def test_login_lockout_ntlm(self):
+ username = self.lockout1ntlm_creds.get_username()
+ userpass = self.lockout1ntlm_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ preload_rodc_user(userdn)
+
+ use_kerberos = self.lockout1ntlm_creds.get_kerberos_state()
+ fail_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass + "X",
+ kerberos_state=use_kerberos)
+
+ try:
+ ldb = SamDB(url=self.host_url, credentials=fail_creds, lp=self.lp)
+ self.fail()
+ except LdbError as e12:
+ (num, msg) = e12.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ # Succeed to reset everything to 0
+ ldb = SamDB(url=self.host_url, credentials=self.lockout1ntlm_creds, lp=self.lp)
+
+ self._test_login_lockout(self.lockout1ntlm_creds)
+
+ def test_multiple_logon_krb5(self):
+ username = self.lockout1krb5_creds.get_username()
+ userpass = self.lockout1krb5_creds.get_password()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+
+ preload_rodc_user(userdn)
+
+ use_kerberos = self.lockout1krb5_creds.get_kerberos_state()
+ fail_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass + "X",
+ kerberos_state=use_kerberos)
+
+ try:
+ ldb = SamDB(url=self.host_url, credentials=fail_creds, lp=self.lp)
+ self.fail()
+ except LdbError as e13:
+ (num, msg) = e13.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ # Succeed to reset everything to 0
+ success_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass,
+ kerberos_state=use_kerberos)
+
+ ldb = SamDB(url=self.host_url, credentials=success_creds, lp=self.lp)
+
+ self._test_multiple_logon(self.lockout1krb5_creds)
+
+ def test_multiple_logon_ntlm(self):
+ username = self.lockout1ntlm_creds.get_username()
+ userdn = "cn=%s,cn=users,%s" % (username, self.base_dn)
+ userpass = self.lockout1ntlm_creds.get_password()
+
+ preload_rodc_user(userdn)
+
+ use_kerberos = self.lockout1ntlm_creds.get_kerberos_state()
+ fail_creds = self.insta_creds(self.template_creds,
+ username=username,
+ userpass=userpass + "X",
+ kerberos_state=use_kerberos)
+
+ try:
+ ldb = SamDB(url=self.host_url, credentials=fail_creds, lp=self.lp)
+ self.fail()
+ except LdbError as e14:
+ (num, msg) = e14.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+
+ # Succeed to reset everything to 0
+ ldb = SamDB(url=self.host_url, credentials=self.lockout1ntlm_creds, lp=self.lp)
+
+ self._test_multiple_logon(self.lockout1ntlm_creds)
+
+
+def main():
+ global RODC, RWDC, CREDS, LP
+ parser = optparse.OptionParser(
+ "rodc_rwdc.py [options] <rodc host> <rwdc host>")
+
+ sambaopts = options.SambaOptions(parser)
+ versionopts = options.VersionOptions(parser)
+ credopts = options.CredentialsOptions(parser)
+ subunitopts = SubunitOptions(parser)
+
+ parser.add_option_group(sambaopts)
+ parser.add_option_group(versionopts)
+ parser.add_option_group(credopts)
+ parser.add_option_group(subunitopts)
+
+ opts, args = parser.parse_args()
+
+ LP = sambaopts.get_loadparm()
+ CREDS = credopts.get_credentials(LP)
+ CREDS.set_gensec_features(CREDS.get_gensec_features() |
+ gensec.FEATURE_SEAL)
+
+ try:
+ RODC, RWDC = args
+ except ValueError:
+ parser.print_usage()
+ sys.exit(1)
+
+ set_auto_replication(RWDC, True)
+ try:
+ TestProgram(module=__name__, opts=subunitopts)
+ finally:
+ set_auto_replication(RWDC, True)
+
+
+main()
diff --git a/source4/dsdb/tests/python/sam.py b/source4/dsdb/tests/python/sam.py
new file mode 100755
index 0000000..f1c1c0f
--- /dev/null
+++ b/source4/dsdb/tests/python/sam.py
@@ -0,0 +1,3874 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This is a port of the original in testprogs/ejs/ldap.js
+
+import optparse
+import sys
+import os
+import time
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.hresult import HRES_SEC_E_INVALID_TOKEN, HRES_SEC_E_LOGON_DENIED
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.credentials import Credentials, DONT_USE_KERBEROS
+from samba.auth import system_session
+from samba.common import get_string
+
+from ldb import SCOPE_BASE, LdbError
+from ldb import ERR_NO_SUCH_OBJECT, ERR_ATTRIBUTE_OR_VALUE_EXISTS
+from ldb import ERR_ENTRY_ALREADY_EXISTS, ERR_UNWILLING_TO_PERFORM
+from ldb import ERR_OTHER, ERR_NO_SUCH_ATTRIBUTE
+from ldb import ERR_OBJECT_CLASS_VIOLATION
+from ldb import ERR_CONSTRAINT_VIOLATION
+from ldb import ERR_UNDEFINED_ATTRIBUTE_TYPE
+from ldb import ERR_INSUFFICIENT_ACCESS_RIGHTS
+from ldb import ERR_INVALID_CREDENTIALS
+from ldb import ERR_STRONG_AUTH_REQUIRED
+from ldb import Message, MessageElement, Dn
+from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, FLAG_MOD_DELETE
+from samba.samdb import SamDB
+from samba.dsdb import (UF_NORMAL_ACCOUNT, UF_ACCOUNTDISABLE,
+ UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT,
+ UF_PARTIAL_SECRETS_ACCOUNT, UF_TEMP_DUPLICATE_ACCOUNT,
+ UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SMARTCARD_REQUIRED,
+ UF_PASSWD_NOTREQD, UF_LOCKOUT, UF_PASSWORD_EXPIRED, ATYPE_NORMAL_ACCOUNT,
+ GTYPE_SECURITY_BUILTIN_LOCAL_GROUP, GTYPE_SECURITY_DOMAIN_LOCAL_GROUP,
+ GTYPE_SECURITY_GLOBAL_GROUP, GTYPE_SECURITY_UNIVERSAL_GROUP,
+ GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP, GTYPE_DISTRIBUTION_GLOBAL_GROUP,
+ GTYPE_DISTRIBUTION_UNIVERSAL_GROUP,
+ ATYPE_SECURITY_GLOBAL_GROUP, ATYPE_SECURITY_UNIVERSAL_GROUP,
+ ATYPE_SECURITY_LOCAL_GROUP, ATYPE_DISTRIBUTION_GLOBAL_GROUP,
+ ATYPE_DISTRIBUTION_UNIVERSAL_GROUP, ATYPE_DISTRIBUTION_LOCAL_GROUP,
+ ATYPE_WORKSTATION_TRUST)
+from samba.dcerpc.security import (DOMAIN_RID_USERS, DOMAIN_RID_ADMINS,
+ DOMAIN_RID_DOMAIN_MEMBERS, DOMAIN_RID_DCS, DOMAIN_RID_READONLY_DCS)
+
+from samba.ndr import ndr_unpack
+from samba.dcerpc import drsblobs
+from samba.dcerpc import drsuapi
+from samba.dcerpc import security
+from samba.tests import delete_force
+from samba import gensec
+from samba import werror
+
+parser = optparse.OptionParser("sam.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+
+class SamTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(SamTests, self).setUp()
+ self.ldb = ldb
+ self.base_dn = ldb.domain_dn()
+
+ print("baseDN: %s\n" % self.base_dn)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
+ delete_force(self.ldb, r"cn=ldaptest\,specialuser,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcomputer2,cn=computers," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+
+ def test_users_groups(self):
+ """This tests the SAM users and groups behaviour"""
+ print("Testing users and groups behaviour\n")
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup2,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["objectSID"])
+ self.assertTrue(len(res1) == 1)
+ obj_sid = get_string(ldb.schema_format_value("objectSID",
+ res1[0]["objectSID"][0]))
+ group_rid_1 = security.dom_sid(obj_sid).split()[1]
+
+ res1 = ldb.search("cn=ldaptestgroup2,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["objectSID"])
+ self.assertTrue(len(res1) == 1)
+ obj_sid = get_string(ldb.schema_format_value("objectSID",
+ res1[0]["objectSID"][0]))
+ group_rid_2 = security.dom_sid(obj_sid).split()[1]
+
+ # Try to create a user with an invalid account name
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "sAMAccountName": "administrator"})
+ self.fail()
+ except LdbError as e9:
+ (num, _) = e9.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ # Try to create a user with an invalid account name
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "sAMAccountName": []})
+ self.fail()
+ except LdbError as e10:
+ (num, _) = e10.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ # Try to create a user with an invalid primary group
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "primaryGroupID": "0"})
+ self.fail()
+ except LdbError as e11:
+ (num, _) = e11.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ # Try to Create a user with a valid primary group
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "primaryGroupID": str(group_rid_1)})
+ self.fail()
+ except LdbError as e12:
+ (num, _) = e12.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ # Test to see how we should behave when the user account doesn't
+ # exist
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["primaryGroupID"] = MessageElement("0", FLAG_MOD_REPLACE,
+ "primaryGroupID")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e13:
+ (num, _) = e13.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ # Test to see how we should behave when the account isn't a user
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["primaryGroupID"] = MessageElement("0", FLAG_MOD_REPLACE,
+ "primaryGroupID")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e14:
+ (num, _) = e14.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # Test default primary groups on add operations
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupID"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_USERS)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD)})
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupID"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_USERS)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ # unfortunately the INTERDOMAIN_TRUST_ACCOUNT case cannot be tested
+ # since such accounts aren't directly creatable (ACCESS_DENIED)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT |
+ UF_PASSWD_NOTREQD)})
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupID"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["primaryGroupID"][0]),
+ DOMAIN_RID_DOMAIN_MEMBERS)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_SERVER_TRUST_ACCOUNT |
+ UF_PASSWD_NOTREQD)})
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupID"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_DCS)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ # Read-only DC accounts are only creatable by
+ # UF_WORKSTATION_TRUST_ACCOUNT and work only on DCs >= 2008 (therefore
+ # we have a fallback in the assertion)
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_PARTIAL_SECRETS_ACCOUNT |
+ UF_WORKSTATION_TRUST_ACCOUNT |
+ UF_PASSWD_NOTREQD)})
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupID"])
+ self.assertTrue(len(res1) == 1)
+ self.assertTrue(int(res1[0]["primaryGroupID"][0]) == DOMAIN_RID_READONLY_DCS or
+ int(res1[0]["primaryGroupID"][0]) == DOMAIN_RID_DOMAIN_MEMBERS)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ # Test default primary groups on modify operations
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(str(UF_NORMAL_ACCOUNT |
+ UF_PASSWD_NOTREQD),
+ FLAG_MOD_REPLACE,
+ "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupID"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_USERS)
+
+ # unfortunately the INTERDOMAIN_TRUST_ACCOUNT case cannot be tested
+ # since such accounts aren't directly creatable (ACCESS_DENIED)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_NORMAL_ACCOUNT |
+ UF_PASSWD_NOTREQD)})
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupID"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_USERS)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT |
+ UF_PASSWD_NOTREQD),
+ FLAG_MOD_REPLACE,
+ "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupID"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_DOMAIN_MEMBERS)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(str(UF_SERVER_TRUST_ACCOUNT |
+ UF_PASSWD_NOTREQD),
+ FLAG_MOD_REPLACE,
+ "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupID"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_DCS)
+
+ # Read-only DC accounts are only creatable by
+ # UF_WORKSTATION_TRUST_ACCOUNT and work only on DCs >= 2008 (therefore
+ # we have a fallback in the assertion)
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(str(UF_PARTIAL_SECRETS_ACCOUNT |
+ UF_WORKSTATION_TRUST_ACCOUNT |
+ UF_PASSWD_NOTREQD),
+ FLAG_MOD_REPLACE,
+ "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupID"])
+ self.assertTrue(len(res1) == 1)
+ self.assertTrue(int(res1[0]["primaryGroupID"][0]) == DOMAIN_RID_READONLY_DCS or
+ int(res1[0]["primaryGroupID"][0]) == DOMAIN_RID_DOMAIN_MEMBERS)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ # Recreate account for further tests
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ # Try to set an invalid account name
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("administrator", FLAG_MOD_REPLACE,
+ "sAMAccountName")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e15:
+ (num, _) = e15.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # But to reset the actual "sAMAccountName" should still be possible
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountName"])
+ self.assertTrue(len(res1) == 1)
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["sAMAccountName"] = MessageElement(res1[0]["sAMAccountName"][0], FLAG_MOD_REPLACE,
+ "sAMAccountName")
+ ldb.modify(m)
+
+ # And another (free) name should be possible as well
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("xxx_ldaptestuser_xxx", FLAG_MOD_REPLACE,
+ "sAMAccountName")
+ ldb.modify(m)
+
+ # We should be able to reset our actual primary group
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["primaryGroupID"] = MessageElement(str(DOMAIN_RID_USERS), FLAG_MOD_REPLACE,
+ "primaryGroupID")
+ ldb.modify(m)
+
+ # Try to add invalid primary group
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["primaryGroupID"] = MessageElement("0", FLAG_MOD_REPLACE,
+ "primaryGroupID")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e16:
+ (num, _) = e16.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Try to make group 1 primary - should be denied since it is not yet
+ # secondary
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["primaryGroupID"] = MessageElement(str(group_rid_1),
+ FLAG_MOD_REPLACE, "primaryGroupID")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e17:
+ (num, _) = e17.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Make group 1 secondary
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
+ FLAG_MOD_REPLACE, "member")
+ ldb.modify(m)
+
+ # Make group 1 primary
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["primaryGroupID"] = MessageElement(str(group_rid_1),
+ FLAG_MOD_REPLACE, "primaryGroupID")
+ ldb.modify(m)
+
+ # Try to delete group 1 - should be denied
+ try:
+ ldb.delete("cn=ldaptestgroup,cn=users," + self.base_dn)
+ self.fail()
+ except LdbError as e18:
+ (num, _) = e18.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # Try to add group 1 also as secondary - should be denied
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
+ FLAG_MOD_ADD, "member")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e19:
+ (num, _) = e19.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # Try to add invalid member to group 1 - should be denied
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["member"] = MessageElement(
+ "cn=ldaptestuser3,cn=users," + self.base_dn,
+ FLAG_MOD_ADD, "member")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e20:
+ (num, _) = e20.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ # Make group 2 secondary
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+ m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
+ FLAG_MOD_ADD, "member")
+ ldb.modify(m)
+
+ # Swap the groups
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["primaryGroupID"] = MessageElement(str(group_rid_2),
+ FLAG_MOD_REPLACE, "primaryGroupID")
+ ldb.modify(m)
+
+ # Swap the groups (does not really make sense but does the same)
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["primaryGroupID"] = MessageElement(str(group_rid_1),
+ FLAG_MOD_REPLACE, "primaryGroupID")
+ m["primaryGroupID"] = MessageElement(str(group_rid_2),
+ FLAG_MOD_REPLACE, "primaryGroupID")
+ ldb.modify(m)
+
+ # Old primary group should contain a "member" attribute for the user,
+ # the new shouldn't contain anymore one
+ res1 = ldb.search("cn=ldaptestgroup, cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["member"])
+ self.assertTrue(len(res1) == 1)
+ self.assertTrue(len(res1[0]["member"]) == 1)
+ self.assertEqual(str(res1[0]["member"][0]).lower(),
+ ("cn=ldaptestuser,cn=users," + self.base_dn).lower())
+
+ res1 = ldb.search("cn=ldaptestgroup2, cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["member"])
+ self.assertTrue(len(res1) == 1)
+ self.assertFalse("member" in res1[0])
+
+ # Primary group member
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+ m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
+ FLAG_MOD_DELETE, "member")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e21:
+ (num, _) = e21.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Delete invalid group member
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+ m["member"] = MessageElement("cn=ldaptestuser1,cn=users," + self.base_dn,
+ FLAG_MOD_DELETE, "member")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e22:
+ (num, _) = e22.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Also this should be denied
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "primaryGroupID": "0"})
+ self.fail()
+ except LdbError as e23:
+ (num, _) = e23.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Recreate user accounts
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ ldb.add({
+ "dn": "cn=ldaptestuser2,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+ m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
+ FLAG_MOD_ADD, "member")
+ ldb.modify(m)
+
+ # Already added
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+ m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
+ FLAG_MOD_ADD, "member")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e24:
+ (num, _) = e24.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # Already added, but as <SID=...>
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["objectSid"])
+ self.assertTrue(len(res1) == 1)
+ sid_bin = res1[0]["objectSid"][0]
+ sid_str = ("<SID=" + get_string(ldb.schema_format_value("objectSid", sid_bin)) + ">").upper()
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+ m["member"] = MessageElement(sid_str, FLAG_MOD_ADD, "member")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e25:
+ (num, _) = e25.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ # Invalid member
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+ m["member"] = MessageElement("cn=ldaptestuser1,cn=users," + self.base_dn,
+ FLAG_MOD_REPLACE, "member")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e26:
+ (num, _) = e26.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ # Invalid member
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+ m["member"] = MessageElement(["cn=ldaptestuser,cn=users," + self.base_dn,
+ "cn=ldaptestuser1,cn=users," + self.base_dn],
+ FLAG_MOD_REPLACE, "member")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e27:
+ (num, _) = e27.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ # Invalid member
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+ m["member"] = MessageElement("cn=ldaptestuser,cn=users," + self.base_dn,
+ FLAG_MOD_REPLACE, "member")
+ m["member"] = MessageElement("cn=ldaptestuser1,cn=users," + self.base_dn,
+ FLAG_MOD_ADD, "member")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e28:
+ (num, _) = e28.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+ m["member"] = MessageElement(["cn=ldaptestuser,cn=users," + self.base_dn,
+ "cn=ldaptestuser2,cn=users," + self.base_dn],
+ FLAG_MOD_REPLACE, "member")
+ ldb.modify(m)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestgroup2,cn=users," + self.base_dn)
+
+ # Make also a small test for accounts with special DNs ("," in this case)
+ ldb.add({
+ "dn": r"cn=ldaptest\,specialuser,cn=users," + self.base_dn,
+ "objectclass": "user"})
+ delete_force(self.ldb, r"cn=ldaptest\,specialuser,cn=users," + self.base_dn)
+
+ def test_sam_attributes(self):
+ """Test the behaviour of special attributes of SAM objects"""
+ print("Testing the behaviour of special attributes of SAM objects\n")
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user"})
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(str(GTYPE_SECURITY_GLOBAL_GROUP), FLAG_MOD_ADD,
+ "groupType")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e29:
+ (num, _) = e29.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ # Delete protection tests
+
+ for attr in ["nTSecurityDescriptor", "objectSid", "sAMAccountType",
+ "sAMAccountName", "groupType"]:
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m[attr] = MessageElement([], FLAG_MOD_REPLACE, attr)
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m[attr] = MessageElement([], FLAG_MOD_DELETE, attr)
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["primaryGroupID"] = MessageElement("513", FLAG_MOD_ADD,
+ "primaryGroupID")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e30:
+ (num, _) = e30.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(str(UF_NORMAL_ACCOUNT |
+ UF_PASSWD_NOTREQD),
+ FLAG_MOD_ADD,
+ "userAccountControl")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e31:
+ (num, _) = e31.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["objectSid"] = MessageElement("xxxxxxxxxxxxxxxx", FLAG_MOD_ADD,
+ "objectSid")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e32:
+ (num, _) = e32.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["sAMAccountType"] = MessageElement("0", FLAG_MOD_ADD,
+ "sAMAccountType")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e33:
+ (num, _) = e33.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("test", FLAG_MOD_ADD,
+ "sAMAccountName")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e34:
+ (num, _) = e34.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ # Delete protection tests
+
+ for attr in ["nTSecurityDescriptor", "objectSid", "sAMAccountType",
+ "sAMAccountName", "primaryGroupID", "userAccountControl",
+ "accountExpires", "badPasswordTime", "badPwdCount",
+ "codePage", "countryCode", "lastLogoff", "lastLogon",
+ "logonCount", "pwdLastSet"]:
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m[attr] = MessageElement([], FLAG_MOD_REPLACE, attr)
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e2:
+ (num, _) = e2.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m[attr] = MessageElement([], FLAG_MOD_DELETE, attr)
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_primary_group_token_constructed(self):
+ """Test the primary group token behaviour (hidden-generated-readonly attribute on groups) and some other constructed attributes"""
+ print("Testing primary group token behaviour and other constructed attributes\n")
+
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "primaryGroupToken": "100"})
+ self.fail()
+ except LdbError as e35:
+ (num, _) = e35.args
+ self.assertEqual(num, ERR_UNDEFINED_ATTRIBUTE_TYPE)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ # Testing for one invalid, and one valid operational attribute, but also the things they are built from
+ res1 = ldb.search(self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupToken", "canonicalName", "objectClass", "objectSid"])
+ self.assertTrue(len(res1) == 1)
+ self.assertFalse("primaryGroupToken" in res1[0])
+ self.assertTrue("canonicalName" in res1[0])
+ self.assertTrue("objectClass" in res1[0])
+ self.assertTrue("objectSid" in res1[0])
+
+ res1 = ldb.search(self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupToken", "canonicalName"])
+ self.assertTrue(len(res1) == 1)
+ self.assertFalse("primaryGroupToken" in res1[0])
+ self.assertFalse("objectSid" in res1[0])
+ self.assertFalse("objectClass" in res1[0])
+ self.assertTrue("canonicalName" in res1[0])
+
+ res1 = ldb.search("cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupToken"])
+ self.assertTrue(len(res1) == 1)
+ self.assertFalse("primaryGroupToken" in res1[0])
+
+ res1 = ldb.search("cn=ldaptestuser, cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupToken"])
+ self.assertTrue(len(res1) == 1)
+ self.assertFalse("primaryGroupToken" in res1[0])
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE)
+ self.assertTrue(len(res1) == 1)
+ self.assertFalse("primaryGroupToken" in res1[0])
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["primaryGroupToken", "objectSID"])
+ self.assertTrue(len(res1) == 1)
+ primary_group_token = int(res1[0]["primaryGroupToken"][0])
+
+ obj_sid = get_string(ldb.schema_format_value("objectSID", res1[0]["objectSID"][0]))
+ rid = security.dom_sid(obj_sid).split()[1]
+ self.assertEqual(primary_group_token, rid)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["primaryGroupToken"] = "100"
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e36:
+ (num, _) = e36.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_tokenGroups(self):
+ """Test the tokenGroups behaviour (hidden-generated-readonly attribute on SAM objects)"""
+ print("Testing tokenGroups behaviour\n")
+
+ # The domain object shouldn't contain any "tokenGroups" entry
+ res = ldb.search(self.base_dn, scope=SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("tokenGroups" in res[0])
+
+ # The domain administrator should contain "tokenGroups" entries
+ # (the exact number depends on the domain/forest function level and the
+ # DC software versions)
+ res = ldb.search("cn=Administrator,cn=Users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("tokenGroups" in res[0])
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ # This testuser should contain at least two "tokenGroups" entries
+ # (exactly two on an unmodified "Domain Users" and "Users" group)
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue(len(res[0]["tokenGroups"]) >= 2)
+
+ # one entry which we need to find should point to domains "Domain Users"
+ # group and another entry should point to the builtin "Users"group
+ domain_users_group_found = False
+ users_group_found = False
+ for sid in res[0]["tokenGroups"]:
+ obj_sid = get_string(ldb.schema_format_value("objectSID", sid))
+ rid = security.dom_sid(obj_sid).split()[1]
+ if rid == 513:
+ domain_users_group_found = True
+ if rid == 545:
+ users_group_found = True
+
+ self.assertTrue(domain_users_group_found)
+ self.assertTrue(users_group_found)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_groupType(self):
+ """Test the groupType behaviour"""
+ print("Testing groupType behaviour\n")
+
+ # You can never create or change to a
+ # "GTYPE_SECURITY_BUILTIN_LOCAL_GROUP"
+
+ # Add operation
+
+ # Invalid attribute
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "groupType": "0"})
+ self.fail()
+ except LdbError as e37:
+ (num, _) = e37.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "groupType": str(GTYPE_SECURITY_BUILTIN_LOCAL_GROUP)})
+ self.fail()
+ except LdbError as e38:
+ (num, _) = e38.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "groupType": str(GTYPE_SECURITY_GLOBAL_GROUP)})
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_GLOBAL_GROUP)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "groupType": str(GTYPE_SECURITY_UNIVERSAL_GROUP)})
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_UNIVERSAL_GROUP)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "groupType": str(GTYPE_SECURITY_DOMAIN_LOCAL_GROUP)})
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_LOCAL_GROUP)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "groupType": str(GTYPE_DISTRIBUTION_GLOBAL_GROUP)})
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_DISTRIBUTION_GLOBAL_GROUP)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "groupType": str(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP)})
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_DISTRIBUTION_UNIVERSAL_GROUP)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "groupType": str(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP)})
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_DISTRIBUTION_LOCAL_GROUP)
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ # Modify operation
+
+ ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ # We can change in this direction: global <-> universal <-> local
+ # On each step also the group type itself (security/distribution) is
+ # variable.
+
+ # After creation we should have a "security global group"
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_GLOBAL_GROUP)
+
+ # Invalid attribute
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement("0",
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e39:
+ (num, _) = e39.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Security groups
+
+ # Default is "global group"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_GLOBAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_GLOBAL_GROUP)
+
+ # Change to "local" (shouldn't work)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_DOMAIN_LOCAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e40:
+ (num, _) = e40.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Change to "universal"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_UNIVERSAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_UNIVERSAL_GROUP)
+
+ # Change back to "global"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_GLOBAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_GLOBAL_GROUP)
+
+ # Change back to "universal"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_UNIVERSAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_UNIVERSAL_GROUP)
+
+ # Change to "local"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_DOMAIN_LOCAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_LOCAL_GROUP)
+
+ # Change to "global" (shouldn't work)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_GLOBAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e41:
+ (num, _) = e41.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Change to "builtin local" (shouldn't work)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_BUILTIN_LOCAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e42:
+ (num, _) = e42.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ # Change back to "universal"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_UNIVERSAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_UNIVERSAL_GROUP)
+
+ # Change to "builtin local" (shouldn't work)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_BUILTIN_LOCAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e43:
+ (num, _) = e43.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Change back to "global"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_GLOBAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_GLOBAL_GROUP)
+
+ # Change to "builtin local" (shouldn't work)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_BUILTIN_LOCAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e44:
+ (num, _) = e44.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Distribution groups
+
+ # Default is "global group"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_DISTRIBUTION_GLOBAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_DISTRIBUTION_GLOBAL_GROUP)
+
+ # Change to local (shouldn't work)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e45:
+ (num, _) = e45.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Change to "universal"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_DISTRIBUTION_UNIVERSAL_GROUP)
+
+ # Change back to "global"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_DISTRIBUTION_GLOBAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_DISTRIBUTION_GLOBAL_GROUP)
+
+ # Change back to "universal"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_DISTRIBUTION_UNIVERSAL_GROUP)
+
+ # Change to "local"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_DISTRIBUTION_LOCAL_GROUP)
+
+ # Change to "global" (shouldn't work)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_DISTRIBUTION_GLOBAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e46:
+ (num, _) = e46.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Change back to "universal"
+
+ # Try to add invalid member to group 1 - should be denied
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["member"] = MessageElement(
+ "cn=ldaptestuser3,cn=users," + self.base_dn,
+ FLAG_MOD_ADD, "member")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e47:
+ (num, _) = e47.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ # Make group 2 secondary
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_DISTRIBUTION_UNIVERSAL_GROUP)
+
+ # Change back to "global"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_DISTRIBUTION_GLOBAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_DISTRIBUTION_GLOBAL_GROUP)
+
+ # Both group types: this performs only random checks - all possibilities
+ # would require too much code.
+
+ # Default is "global group"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_GLOBAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_GLOBAL_GROUP)
+
+ # Change to "local" (shouldn't work)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e48:
+ (num, _) = e48.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Change to "universal"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_DISTRIBUTION_UNIVERSAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_DISTRIBUTION_UNIVERSAL_GROUP)
+
+ # Change back to "global"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_GLOBAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_GLOBAL_GROUP)
+
+ # Change back to "universal"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_UNIVERSAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_UNIVERSAL_GROUP)
+
+ # Change to "local"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_DISTRIBUTION_LOCAL_GROUP)
+
+ # Change to "global" (shouldn't work)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_DISTRIBUTION_GLOBAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e49:
+ (num, _) = e49.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # Change back to "universal"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_UNIVERSAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_UNIVERSAL_GROUP)
+
+ # Change back to "global"
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["groupType"] = MessageElement(
+ str(GTYPE_SECURITY_GLOBAL_GROUP),
+ FLAG_MOD_REPLACE, "groupType")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_SECURITY_GLOBAL_GROUP)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_pwdLastSet(self):
+ """Test the pwdLastSet behaviour"""
+ print("Testing pwdLastSet behaviour\n")
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "pwdLastSet": "0"})
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res1[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
+ self.assertEqual(int(res1[0]["pwdLastSet"][0]), 0)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "pwdLastSet": "-1"})
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res1[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
+ self.assertNotEqual(int(res1[0]["pwdLastSet"][0]), 0)
+ lastset = int(res1[0]["pwdLastSet"][0])
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "pwdLastSet": str(1)})
+ self.fail()
+ except LdbError as e50:
+ (num, msg) = e50.args
+ self.assertEqual(num, ERR_OTHER)
+ self.assertTrue('00000057' in msg)
+
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "pwdLastSet": str(lastset)})
+ self.fail()
+ except LdbError as e51:
+ (num, msg) = e51.args
+ self.assertEqual(num, ERR_OTHER)
+ self.assertTrue('00000057' in msg)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res1[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
+ self.assertEqual(int(res1[0]["pwdLastSet"][0]), 0)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["pls1"] = MessageElement(str(0),
+ FLAG_MOD_REPLACE,
+ "pwdLastSet")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["pls1"] = MessageElement(str(0),
+ FLAG_MOD_DELETE,
+ "pwdLastSet")
+ m["pls2"] = MessageElement(str(0),
+ FLAG_MOD_ADD,
+ "pwdLastSet")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["pls1"] = MessageElement(str(-1),
+ FLAG_MOD_REPLACE,
+ "pwdLastSet")
+ ldb.modify(m)
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res1[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
+ self.assertGreater(int(res1[0]["pwdLastSet"][0]), lastset)
+ lastset = int(res1[0]["pwdLastSet"][0])
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["pls1"] = MessageElement(str(0),
+ FLAG_MOD_DELETE,
+ "pwdLastSet")
+ m["pls2"] = MessageElement(str(0),
+ FLAG_MOD_ADD,
+ "pwdLastSet")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e52:
+ (num, msg) = e52.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+ self.assertTrue('00002085' in msg)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["pls1"] = MessageElement(str(-1),
+ FLAG_MOD_DELETE,
+ "pwdLastSet")
+ m["pls2"] = MessageElement(str(0),
+ FLAG_MOD_ADD,
+ "pwdLastSet")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e53:
+ (num, msg) = e53.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+ self.assertTrue('00002085' in msg)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["pls1"] = MessageElement(str(lastset),
+ FLAG_MOD_DELETE,
+ "pwdLastSet")
+ m["pls2"] = MessageElement(str(-1),
+ FLAG_MOD_ADD,
+ "pwdLastSet")
+ time.sleep(0.2)
+ ldb.modify(m)
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res1[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
+ self.assertEqual(int(res1[0]["pwdLastSet"][0]), lastset)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["pls1"] = MessageElement(str(lastset),
+ FLAG_MOD_DELETE,
+ "pwdLastSet")
+ m["pls2"] = MessageElement(str(lastset),
+ FLAG_MOD_ADD,
+ "pwdLastSet")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e54:
+ (num, msg) = e54.args
+ self.assertEqual(num, ERR_OTHER)
+ self.assertTrue('00000057' in msg)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["pls1"] = MessageElement(str(lastset),
+ FLAG_MOD_DELETE,
+ "pwdLastSet")
+ m["pls2"] = MessageElement(str(0),
+ FLAG_MOD_ADD,
+ "pwdLastSet")
+ ldb.modify(m)
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res1[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
+ uac = int(res1[0]["userAccountControl"][0])
+ self.assertEqual(int(res1[0]["pwdLastSet"][0]), 0)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["uac1"] = MessageElement(str(uac |UF_PASSWORD_EXPIRED),
+ FLAG_MOD_REPLACE,
+ "userAccountControl")
+ ldb.modify(m)
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl", "pwdLastSet"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res1[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD)
+ self.assertEqual(int(res1[0]["pwdLastSet"][0]), 0)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_ldap_bind_must_change_pwd(self):
+ """Test the error messages for failing LDAP binds"""
+ print("Test the error messages for failing LDAP binds\n")
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def format_error_msg(hresult_v, dsid_v, werror_v):
+ #
+ # There are 4 lower case hex digits following 'v' at the end,
+ # but different Windows Versions return different values:
+ #
+ # Windows 2008R2 uses 'v1db1'
+ # Windows 2012R2 uses 'v2580'
+ #
+ return "%08X: LdapErr: DSID-%08X, comment: AcceptSecurityContext error, data %x, v" % (
+ hresult_v, dsid_v, werror_v)
+
+ sasl_bind_dsid = 0x0C0904DC
+ simple_bind_dsid = 0x0C0903A9
+
+ error_msg_sasl_wrong_pw = format_error_msg(
+ HRES_SEC_E_LOGON_DENIED,
+ sasl_bind_dsid,
+ werror.WERR_LOGON_FAILURE)
+ error_msg_sasl_must_change = format_error_msg(
+ HRES_SEC_E_LOGON_DENIED,
+ sasl_bind_dsid,
+ werror.WERR_PASSWORD_MUST_CHANGE)
+ error_msg_simple_wrong_pw = format_error_msg(
+ HRES_SEC_E_INVALID_TOKEN,
+ simple_bind_dsid,
+ werror.WERR_LOGON_FAILURE)
+ error_msg_simple_must_change = format_error_msg(
+ HRES_SEC_E_INVALID_TOKEN,
+ simple_bind_dsid,
+ werror.WERR_PASSWORD_MUST_CHANGE)
+
+ username = "ldaptestuser"
+ password = "thatsAcomplPASS2"
+ utf16pw = ('"' + password + '"').encode('utf-16-le')
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "sAMAccountName": username,
+ "userAccountControl": str(UF_NORMAL_ACCOUNT),
+ "unicodePwd": utf16pw,
+ })
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountName", "sAMAccountType", "userAccountControl", "pwdLastSet"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(str(res1[0]["sAMAccountName"][0]), username)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]), ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res1[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT)
+ self.assertNotEqual(int(res1[0]["pwdLastSet"][0]), 0)
+
+ # Open a second LDB connection with the user credentials. Use the
+ # command line credentials for information like the domain, the realm
+ # and the workstation.
+ sasl_creds = Credentials()
+ sasl_creds.set_username(username)
+ sasl_creds.set_password(password)
+ sasl_creds.set_domain(creds.get_domain())
+ sasl_creds.set_workstation(creds.get_workstation())
+ sasl_creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+ sasl_creds.set_kerberos_state(DONT_USE_KERBEROS)
+
+ sasl_wrong_creds = Credentials()
+ sasl_wrong_creds.set_username(username)
+ sasl_wrong_creds.set_password("wrong")
+ sasl_wrong_creds.set_domain(creds.get_domain())
+ sasl_wrong_creds.set_workstation(creds.get_workstation())
+ sasl_wrong_creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+ sasl_wrong_creds.set_kerberos_state(DONT_USE_KERBEROS)
+
+ simple_creds = Credentials()
+ simple_creds.set_bind_dn("cn=ldaptestuser,cn=users," + self.base_dn)
+ simple_creds.set_username(username)
+ simple_creds.set_password(password)
+ simple_creds.set_domain(creds.get_domain())
+ simple_creds.set_workstation(creds.get_workstation())
+ simple_creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+ simple_creds.set_kerberos_state(DONT_USE_KERBEROS)
+
+ simple_wrong_creds = Credentials()
+ simple_wrong_creds.set_bind_dn("cn=ldaptestuser,cn=users," + self.base_dn)
+ simple_wrong_creds.set_username(username)
+ simple_wrong_creds.set_password("wrong")
+ simple_wrong_creds.set_domain(creds.get_domain())
+ simple_wrong_creds.set_workstation(creds.get_workstation())
+ simple_wrong_creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+ simple_wrong_creds.set_kerberos_state(DONT_USE_KERBEROS)
+
+ sasl_ldb = SamDB(url=host, credentials=sasl_creds, lp=lp)
+ self.assertIsNotNone(sasl_ldb)
+ del sasl_ldb
+
+ requires_strong_auth = False
+ try:
+ simple_ldb = SamDB(url=host, credentials=simple_creds, lp=lp)
+ self.assertIsNotNone(simple_ldb)
+ del simple_ldb
+ except LdbError as e55:
+ (num, msg) = e55.args
+ if num != ERR_STRONG_AUTH_REQUIRED:
+ raise
+ requires_strong_auth = True
+
+ def assertLDAPErrorMsg(msg, expected_msg):
+ self.assertTrue(expected_msg in msg,
+ "msg[%s] does not contain expected[%s]" % (
+ msg, expected_msg))
+
+ try:
+ SamDB(url=host, credentials=sasl_wrong_creds, lp=lp)
+ self.fail()
+ except LdbError as e56:
+ (num, msg) = e56.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+ self.assertTrue(error_msg_sasl_wrong_pw in msg)
+
+ if not requires_strong_auth:
+ try:
+ SamDB(url=host, credentials=simple_wrong_creds, lp=lp)
+ self.fail()
+ except LdbError as e4:
+ (num, msg) = e4.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+ assertLDAPErrorMsg(msg, error_msg_simple_wrong_pw)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["pls1"] = MessageElement(str(0),
+ FLAG_MOD_REPLACE,
+ "pwdLastSet")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["pwdLastSet"])
+ self.assertEqual(int(res1[0]["pwdLastSet"][0]), 0)
+
+ try:
+ SamDB(url=host, credentials=sasl_wrong_creds, lp=lp)
+ self.fail()
+ except LdbError as e57:
+ (num, msg) = e57.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+ assertLDAPErrorMsg(msg, error_msg_sasl_wrong_pw)
+
+ try:
+ SamDB(url=host, credentials=sasl_creds, lp=lp)
+ self.fail()
+ except LdbError as e58:
+ (num, msg) = e58.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+ assertLDAPErrorMsg(msg, error_msg_sasl_must_change)
+
+ if not requires_strong_auth:
+ try:
+ SamDB(url=host, credentials=simple_wrong_creds, lp=lp)
+ self.fail()
+ except LdbError as e5:
+ (num, msg) = e5.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+ assertLDAPErrorMsg(msg, error_msg_simple_wrong_pw)
+
+ try:
+ SamDB(url=host, credentials=simple_creds, lp=lp)
+ self.fail()
+ except LdbError as e6:
+ (num, msg) = e6.args
+ self.assertEqual(num, ERR_INVALID_CREDENTIALS)
+ assertLDAPErrorMsg(msg, error_msg_simple_must_change)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_userAccountControl_user_add_0_uac(self):
+ """Test the userAccountControl behaviour"""
+ print("Testing userAccountControl behaviour\n")
+
+ # With a user object
+
+ # Add operation
+
+ # As user you can only set a normal account.
+ # The UF_PASSWD_NOTREQD flag is needed since we haven't requested a
+ # password yet.
+ # With SYSTEM rights you can set a interdomain trust account.
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": "0"})
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_PASSWD_NOTREQD == 0)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_userAccountControl_user_add_normal(self):
+ """Test the userAccountControl behaviour"""
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": str(UF_NORMAL_ACCOUNT)})
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_userAccountControl_user_add_normal_pwnotreq(self):
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD)})
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_userAccountControl_user_add_normal_pwnotreq_lockout_expired(self):
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": str(UF_NORMAL_ACCOUNT |
+ UF_PASSWD_NOTREQD |
+ UF_LOCKOUT |
+ UF_PASSWORD_EXPIRED)})
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl", "lockoutTime", "pwdLastSet"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & (UF_LOCKOUT | UF_PASSWORD_EXPIRED) == 0)
+ self.assertFalse("lockoutTime" in res1[0])
+ self.assertTrue(int(res1[0]["pwdLastSet"][0]) == 0)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_userAccountControl_user_add_temp_dup(self):
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": str(UF_TEMP_DUPLICATE_ACCOUNT)})
+ self.fail()
+ except LdbError as e59:
+ (num, _) = e59.args
+ self.assertEqual(num, ERR_OTHER)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_userAccountControl_user_add_server(self):
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": str(UF_SERVER_TRUST_ACCOUNT)})
+ self.fail()
+ except LdbError as e60:
+ (num, _) = e60.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_userAccountControl_user_add_workstation(self):
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT)})
+ except LdbError as e61:
+ (num, _) = e61.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_userAccountControl_user_add_rodc(self):
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT)})
+ except LdbError as e62:
+ (num, _) = e62.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_userAccountControl_user_add_trust(self):
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": str(UF_INTERDOMAIN_TRUST_ACCOUNT)})
+ self.fail()
+ except LdbError as e63:
+ (num, _) = e63.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ # Modify operation
+
+ def test_userAccountControl_user_modify(self):
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ # After creation we should have a normal account
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE != 0)
+
+ # As user you can only switch from a normal account to a workstation
+ # trust account and back.
+ # The UF_PASSWD_NOTREQD flag is needed since we haven't requested a
+ # password yet.
+ # With SYSTEM rights you can switch to a interdomain trust account.
+
+ # Invalid attribute
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement("0",
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+ except LdbError as e64:
+ (num, _) = e64.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_NORMAL_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+ except LdbError as e65:
+ (num, _) = e65.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_ACCOUNTDISABLE),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_NORMAL_ACCOUNT != 0)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE != 0)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["lockoutTime"] = MessageElement(str(samba.unix2nttime(0)), FLAG_MOD_REPLACE, "lockoutTime")
+ m["pwdLastSet"] = MessageElement(str(samba.unix2nttime(0)), FLAG_MOD_REPLACE, "pwdLastSet")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_LOCKOUT | UF_PASSWORD_EXPIRED),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl", "lockoutTime", "pwdLastSet"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_NORMAL_ACCOUNT != 0)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & (UF_LOCKOUT | UF_PASSWORD_EXPIRED) == 0)
+ self.assertTrue(int(res1[0]["lockoutTime"][0]) == 0)
+ self.assertTrue(int(res1[0]["pwdLastSet"][0]) == 0)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_TEMP_DUPLICATE_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e66:
+ (num, _) = e66.args
+ self.assertEqual(num, ERR_OTHER)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_SERVER_TRUST_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e67:
+ (num, _) = e67.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_WORKSTATION_TRUST_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e68:
+ (num, _) = e68.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_WORKSTATION_TRUST)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_INTERDOMAIN_TRUST_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e69:
+ (num, _) = e69.args
+ self.assertEqual(num, ERR_INSUFFICIENT_ACCESS_RIGHTS)
+
+ def test_userAccountControl_computer_add_0_uac(self):
+ # With a computer object
+
+ # Add operation
+
+ # As computer you can set a normal account and a server trust account.
+ # The UF_PASSWD_NOTREQD flag is needed since we haven't requested a
+ # password yet.
+ # With SYSTEM rights you can set a interdomain trust account.
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": "0"})
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_WORKSTATION_TRUST)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_PASSWD_NOTREQD == 0)
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ def test_userAccountControl_computer_add_normal(self):
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_NORMAL_ACCOUNT)})
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ def test_userAccountControl_computer_add_normal_pwnotreqd(self):
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD)})
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ def test_userAccountControl_computer_add_normal_pwnotreqd_lockout_expired(self):
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_NORMAL_ACCOUNT |
+ UF_PASSWD_NOTREQD |
+ UF_LOCKOUT |
+ UF_PASSWORD_EXPIRED)})
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl", "lockoutTime", "pwdLastSet"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & (UF_LOCKOUT | UF_PASSWORD_EXPIRED) == 0)
+ self.assertFalse("lockoutTime" in res1[0])
+ self.assertTrue(int(res1[0]["pwdLastSet"][0]) == 0)
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ def test_userAccountControl_computer_add_temp_dup(self):
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_TEMP_DUPLICATE_ACCOUNT)})
+ self.fail()
+ except LdbError as e70:
+ (num, _) = e70.args
+ self.assertEqual(num, ERR_OTHER)
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ def test_userAccountControl_computer_add_server(self):
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_SERVER_TRUST_ACCOUNT)})
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_WORKSTATION_TRUST)
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ def test_userAccountControl_computer_add_workstation(self):
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT)})
+ except LdbError as e71:
+ (num, _) = e71.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ def test_userAccountControl_computer_add_trust(self):
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_INTERDOMAIN_TRUST_ACCOUNT)})
+ self.fail()
+ except LdbError as e72:
+ (num, _) = e72.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ def test_userAccountControl_computer_modify(self):
+ # Modify operation
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer"})
+
+ # After creation we should have a normal account
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_WORKSTATION_TRUST)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE != 0)
+
+ # As computer you can switch from a normal account to a workstation
+ # or server trust account and back (also swapping between trust
+ # accounts is allowed).
+ # The UF_PASSWD_NOTREQD flag is needed since we haven't requested a
+ # password yet.
+ # With SYSTEM rights you can switch to a interdomain trust account.
+
+ # Invalid attribute
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement("0",
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+ except LdbError as e73:
+ (num, _) = e73.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_NORMAL_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+ except LdbError as e74:
+ (num, _) = e74.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_ACCOUNTDISABLE),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_NORMAL_ACCOUNT != 0)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE != 0)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["lockoutTime"] = MessageElement(str(samba.unix2nttime(0)), FLAG_MOD_REPLACE, "lockoutTime")
+ m["pwdLastSet"] = MessageElement(str(samba.unix2nttime(0)), FLAG_MOD_REPLACE, "pwdLastSet")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_LOCKOUT | UF_PASSWORD_EXPIRED),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl", "lockoutTime", "pwdLastSet"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_NORMAL_ACCOUNT != 0)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & (UF_LOCKOUT | UF_PASSWORD_EXPIRED) == 0)
+ self.assertTrue(int(res1[0]["lockoutTime"][0]) == 0)
+ self.assertTrue(int(res1[0]["pwdLastSet"][0]) == 0)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_TEMP_DUPLICATE_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e75:
+ (num, _) = e75.args
+ self.assertEqual(num, ERR_OTHER)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_SERVER_TRUST_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_WORKSTATION_TRUST)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_WORKSTATION_TRUST_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_WORKSTATION_TRUST)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_SERVER_TRUST_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_WORKSTATION_TRUST)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_WORKSTATION_TRUST_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountType"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["sAMAccountType"][0]),
+ ATYPE_WORKSTATION_TRUST)
+
+ try:
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_INTERDOMAIN_TRUST_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e76:
+ (num, _) = e76.args
+ self.assertEqual(num, ERR_OBJECT_CLASS_VIOLATION)
+
+ # "primaryGroupID" does not change if account type remains the same
+
+ # For a user account
+
+ ldb.add({
+ "dn": "cn=ldaptestuser2,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": str(UF_NORMAL_ACCOUNT |
+ UF_PASSWD_NOTREQD |
+ UF_ACCOUNTDISABLE)})
+
+ res1 = ldb.search("cn=ldaptestuser2,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["userAccountControl"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE)
+
+ m = Message()
+ m.dn = Dn(ldb, "<SID=" + ldb.get_domain_sid() + "-" + str(DOMAIN_RID_ADMINS) + ">")
+ m["member"] = MessageElement(
+ "cn=ldaptestuser2,cn=users," + self.base_dn, FLAG_MOD_ADD, "member")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
+ m["primaryGroupID"] = MessageElement(str(DOMAIN_RID_ADMINS),
+ FLAG_MOD_REPLACE, "primaryGroupID")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestuser2,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["userAccountControl", "primaryGroupID"])
+ self.assertTrue(len(res1) == 1)
+ self.assertTrue(int(res1[0]["userAccountControl"][0]) & UF_ACCOUNTDISABLE == 0)
+ self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_ADMINS)
+
+ # For a workstation account
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["primaryGroupID"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_DOMAIN_MEMBERS)
+
+ m = Message()
+ m.dn = Dn(ldb, "<SID=" + ldb.get_domain_sid() + "-" + str(DOMAIN_RID_USERS) + ">")
+ m["member"] = MessageElement(
+ "cn=ldaptestcomputer,cn=computers," + self.base_dn, FLAG_MOD_ADD, "member")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["primaryGroupID"] = MessageElement(str(DOMAIN_RID_USERS),
+ FLAG_MOD_REPLACE, "primaryGroupID")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_WORKSTATION_TRUST_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["primaryGroupID"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(int(res1[0]["primaryGroupID"][0]), DOMAIN_RID_USERS)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestuser2,cn=users," + self.base_dn)
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ def find_repl_meta_data(self, rpmd, attid):
+ for i in range(0, rpmd.ctr.count):
+ m = rpmd.ctr.array[i]
+ if m.attid == attid:
+ return m
+ return None
+
+ def test_smartcard_required1(self):
+ """Test the UF_SMARTCARD_REQUIRED behaviour"""
+ print("Testing UF_SMARTCARD_REQUIRED behaviour\n")
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": str(UF_NORMAL_ACCOUNT),
+ "unicodePwd": "\"thatsAcomplPASS2\"".encode('utf-16-le')
+ })
+
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl",
+ "pwdLastSet", "msDS-KeyVersionNumber",
+ "replPropertyMetaData"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(int(res[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT)
+ self.assertNotEqual(int(res[0]["pwdLastSet"][0]), 0)
+ lastset = int(res[0]["pwdLastSet"][0])
+ self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 1)
+ self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
+ rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
+ res[0]["replPropertyMetaData"][0])
+ lastsetmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_pwdLastSet)
+ self.assertIsNotNone(lastsetmd)
+ self.assertEqual(lastsetmd.version, 1)
+ nthashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_unicodePwd)
+ self.assertIsNotNone(nthashmd)
+ self.assertEqual(nthashmd.version, 1)
+ nthistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_ntPwdHistory)
+ self.assertIsNotNone(nthistmd)
+ self.assertEqual(nthistmd.version, 1)
+ lmhashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_dBCSPwd)
+ self.assertIsNotNone(lmhashmd)
+ self.assertEqual(lmhashmd.version, 1)
+ lmhistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_lmPwdHistory)
+ self.assertIsNotNone(lmhistmd)
+ self.assertEqual(lmhistmd.version, 1)
+ spcbmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_supplementalCredentials)
+ self.assertIsNotNone(spcbmd)
+ self.assertEqual(spcbmd.version, 1)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl",
+ "pwdLastSet", "msDS-KeyVersionNumber",
+ "replPropertyMetaData"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(int(res[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED)
+ self.assertEqual(int(res[0]["pwdLastSet"][0]), lastset)
+ self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 2)
+ self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
+ rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
+ res[0]["replPropertyMetaData"][0])
+ lastsetmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_pwdLastSet)
+ self.assertIsNotNone(lastsetmd)
+ self.assertEqual(lastsetmd.version, 1)
+ nthashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_unicodePwd)
+ self.assertIsNotNone(nthashmd)
+ self.assertEqual(nthashmd.version, 2)
+ nthistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_ntPwdHistory)
+ self.assertIsNotNone(nthistmd)
+ self.assertEqual(nthistmd.version, 2)
+ lmhashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_dBCSPwd)
+ self.assertIsNotNone(lmhashmd)
+ self.assertEqual(lmhashmd.version, 2)
+ lmhistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_lmPwdHistory)
+ self.assertIsNotNone(lmhistmd)
+ self.assertEqual(lmhistmd.version, 2)
+ spcbmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_supplementalCredentials)
+ self.assertIsNotNone(spcbmd)
+ self.assertEqual(spcbmd.version, 2)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_smartcard_required2(self):
+ """Test the UF_SMARTCARD_REQUIRED behaviour"""
+ print("Testing UF_SMARTCARD_REQUIRED behaviour\n")
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": str(UF_NORMAL_ACCOUNT |UF_ACCOUNTDISABLE),
+ })
+
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl",
+ "pwdLastSet", "msDS-KeyVersionNumber",
+ "replPropertyMetaData"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(int(res[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT |UF_ACCOUNTDISABLE)
+ self.assertEqual(int(res[0]["pwdLastSet"][0]), 0)
+ self.assertTrue("msDS-KeyVersionNumber" in res[0])
+ self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 1)
+ self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
+ rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
+ res[0]["replPropertyMetaData"][0])
+ lastsetmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_pwdLastSet)
+ self.assertIsNotNone(lastsetmd)
+ self.assertEqual(lastsetmd.version, 1)
+ nthashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_unicodePwd)
+ self.assertIsNotNone(nthashmd)
+ self.assertEqual(nthashmd.version, 1)
+ nthistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_ntPwdHistory)
+ self.assertIsNotNone(nthistmd)
+ self.assertEqual(nthistmd.version, 1)
+ lmhashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_dBCSPwd)
+ self.assertIsNotNone(lmhashmd)
+ self.assertEqual(lmhashmd.version, 1)
+ lmhistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_lmPwdHistory)
+ self.assertIsNotNone(lmhistmd)
+ self.assertEqual(lmhistmd.version, 1)
+ spcbmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_supplementalCredentials)
+ self.assertIsNone(spcbmd)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_NORMAL_ACCOUNT |UF_ACCOUNTDISABLE |UF_SMARTCARD_REQUIRED),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl",
+ "pwdLastSet", "msDS-KeyVersionNumber",
+ "replPropertyMetaData"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(int(res[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT |UF_ACCOUNTDISABLE |UF_SMARTCARD_REQUIRED)
+ self.assertEqual(int(res[0]["pwdLastSet"][0]), 0)
+ self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 2)
+ self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
+ rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
+ res[0]["replPropertyMetaData"][0])
+ lastsetmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_pwdLastSet)
+ self.assertIsNotNone(lastsetmd)
+ self.assertEqual(lastsetmd.version, 1)
+ nthashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_unicodePwd)
+ self.assertIsNotNone(nthashmd)
+ self.assertEqual(nthashmd.version, 2)
+ nthistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_ntPwdHistory)
+ self.assertIsNotNone(nthistmd)
+ self.assertEqual(nthistmd.version, 2)
+ lmhashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_dBCSPwd)
+ self.assertIsNotNone(lmhashmd)
+ self.assertEqual(lmhashmd.version, 2)
+ lmhistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_lmPwdHistory)
+ self.assertIsNotNone(lmhistmd)
+ self.assertEqual(lmhistmd.version, 2)
+ spcbmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_supplementalCredentials)
+ self.assertIsNotNone(spcbmd)
+ self.assertEqual(spcbmd.version, 1)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl",
+ "pwdLastSet", "msDS-KeyVersionNumber",
+ "replPropertyMetaData"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(int(res[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED)
+ self.assertEqual(int(res[0]["pwdLastSet"][0]), 0)
+ self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 2)
+ self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
+ rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
+ res[0]["replPropertyMetaData"][0])
+ lastsetmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_pwdLastSet)
+ self.assertIsNotNone(lastsetmd)
+ self.assertEqual(lastsetmd.version, 1)
+ nthashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_unicodePwd)
+ self.assertIsNotNone(nthashmd)
+ self.assertEqual(nthashmd.version, 2)
+ nthistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_ntPwdHistory)
+ self.assertIsNotNone(nthistmd)
+ self.assertEqual(nthistmd.version, 2)
+ lmhashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_dBCSPwd)
+ self.assertIsNotNone(lmhashmd)
+ self.assertEqual(lmhashmd.version, 2)
+ lmhistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_lmPwdHistory)
+ self.assertIsNotNone(lmhistmd)
+ self.assertEqual(lmhistmd.version, 2)
+ spcbmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_supplementalCredentials)
+ self.assertIsNotNone(spcbmd)
+ self.assertEqual(spcbmd.version, 1)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_smartcard_required3(self):
+ """Test the UF_SMARTCARD_REQUIRED behaviour"""
+ print("Testing UF_SMARTCARD_REQUIRED behaviour\n")
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "userAccountControl": str(UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED |UF_ACCOUNTDISABLE),
+ })
+
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl",
+ "pwdLastSet", "msDS-KeyVersionNumber",
+ "replPropertyMetaData"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(int(res[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED |UF_ACCOUNTDISABLE)
+ self.assertEqual(int(res[0]["pwdLastSet"][0]), 0)
+ self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 1)
+ self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
+ rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
+ res[0]["replPropertyMetaData"][0])
+ lastsetmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_pwdLastSet)
+ self.assertIsNotNone(lastsetmd)
+ self.assertEqual(lastsetmd.version, 1)
+ nthashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_unicodePwd)
+ self.assertIsNotNone(nthashmd)
+ self.assertEqual(nthashmd.version, 1)
+ nthistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_ntPwdHistory)
+ self.assertIsNotNone(nthistmd)
+ self.assertEqual(nthistmd.version, 1)
+ lmhashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_dBCSPwd)
+ self.assertIsNotNone(lmhashmd)
+ self.assertEqual(lmhashmd.version, 1)
+ lmhistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_lmPwdHistory)
+ self.assertIsNotNone(lmhistmd)
+ self.assertEqual(lmhistmd.version, 1)
+ spcbmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_supplementalCredentials)
+ self.assertIsNotNone(spcbmd)
+ self.assertEqual(spcbmd.version, 1)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["sAMAccountType", "userAccountControl",
+ "pwdLastSet", "msDS-KeyVersionNumber",
+ "replPropertyMetaData"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(int(res[0]["sAMAccountType"][0]),
+ ATYPE_NORMAL_ACCOUNT)
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT |UF_SMARTCARD_REQUIRED)
+ self.assertEqual(int(res[0]["pwdLastSet"][0]), 0)
+ self.assertEqual(int(res[0]["msDS-KeyVersionNumber"][0]), 1)
+ self.assertTrue(len(res[0]["replPropertyMetaData"]) == 1)
+ rpmd = ndr_unpack(drsblobs.replPropertyMetaDataBlob,
+ res[0]["replPropertyMetaData"][0])
+ lastsetmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_pwdLastSet)
+ self.assertIsNotNone(lastsetmd)
+ self.assertEqual(lastsetmd.version, 1)
+ nthashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_unicodePwd)
+ self.assertIsNotNone(nthashmd)
+ self.assertEqual(nthashmd.version, 1)
+ nthistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_ntPwdHistory)
+ self.assertIsNotNone(nthistmd)
+ self.assertEqual(nthistmd.version, 1)
+ lmhashmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_dBCSPwd)
+ self.assertIsNotNone(lmhashmd)
+ self.assertEqual(lmhashmd.version, 1)
+ lmhistmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_lmPwdHistory)
+ self.assertIsNotNone(lmhistmd)
+ self.assertEqual(lmhistmd.version, 1)
+ spcbmd = self.find_repl_meta_data(rpmd,
+ drsuapi.DRSUAPI_ATTID_supplementalCredentials)
+ self.assertIsNotNone(spcbmd)
+ self.assertEqual(spcbmd.version, 1)
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_isCriticalSystemObject_user(self):
+ """Test the isCriticalSystemObject behaviour"""
+ print("Testing isCriticalSystemObject behaviour\n")
+
+ # Add tests (of a user)
+
+ ldb.add({
+ "dn": "cn=ldaptestuser,cn=users," + self.base_dn,
+ "objectclass": "user"})
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res1) == 1)
+ self.assertTrue("isCriticalSystemObject" not in res1[0])
+
+ # Modification tests
+ m = Message()
+
+ m.dn = Dn(ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestuser,cn=users," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res1) == 1)
+ self.assertTrue("isCriticalSystemObject" in res1[0])
+ self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "FALSE")
+
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+ def test_isCriticalSystemObject(self):
+ """Test the isCriticalSystemObject behaviour"""
+ print("Testing isCriticalSystemObject behaviour\n")
+
+ # Add tests
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer"})
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res1) == 1)
+ self.assertTrue("isCriticalSystemObject" in res1[0])
+ self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "FALSE")
+
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT)})
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "FALSE")
+
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT)})
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "TRUE")
+
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "userAccountControl": str(UF_SERVER_TRUST_ACCOUNT)})
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "TRUE")
+
+ # Modification tests
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "TRUE")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "FALSE")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(
+ str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "TRUE")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(str(UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "TRUE")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(str(UF_SERVER_TRUST_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "TRUE")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["userAccountControl"] = MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT),
+ FLAG_MOD_REPLACE, "userAccountControl")
+ ldb.modify(m)
+
+ res1 = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE,
+ attrs=["isCriticalSystemObject"])
+ self.assertTrue(len(res1) == 1)
+ self.assertEqual(str(res1[0]["isCriticalSystemObject"][0]), "FALSE")
+
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ def test_service_principal_name_updates(self):
+ """Test the servicePrincipalNames update behaviour"""
+ print("Testing servicePrincipalNames update behaviour\n")
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "dNSHostName": "testname.testdom"})
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("servicePrincipalName" in res[0])
+
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "servicePrincipalName": "HOST/testname.testdom"})
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["dNSHostName"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("dNSHostName" in res[0])
+
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "dNSHostName": "testname2.testdom",
+ "servicePrincipalName": "HOST/testname.testdom"})
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["dNSHostName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["dNSHostName"][0]), "testname2.testdom")
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/testname.testdom")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["dNSHostName"] = MessageElement("testname.testdoM",
+ FLAG_MOD_REPLACE, "dNSHostName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/testname.testdom")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["dNSHostName"] = MessageElement("testname2.testdom2",
+ FLAG_MOD_REPLACE, "dNSHostName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/testname2.testdom2")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["dNSHostName"] = MessageElement([],
+ FLAG_MOD_DELETE, "dNSHostName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/testname2.testdom2")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["dNSHostName"] = MessageElement("testname.testdom3",
+ FLAG_MOD_REPLACE, "dNSHostName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/testname2.testdom2")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["dNSHostName"] = MessageElement("testname2.testdom2",
+ FLAG_MOD_REPLACE, "dNSHostName")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["dNSHostName"] = MessageElement("testname3.testdom3",
+ FLAG_MOD_REPLACE, "dNSHostName")
+ m["servicePrincipalName"] = MessageElement("HOST/testname2.testdom2",
+ FLAG_MOD_REPLACE,
+ "servicePrincipalName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/testname3.testdom3")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["servicePrincipalName"] = MessageElement("HOST/testname2.testdom2",
+ FLAG_MOD_REPLACE,
+ "servicePrincipalName")
+ m["dNSHostName"] = MessageElement("testname4.testdom4",
+ FLAG_MOD_REPLACE, "dNSHostName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/testname2.testdom2")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["servicePrincipalName"] = MessageElement([],
+ FLAG_MOD_DELETE,
+ "servicePrincipalName")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["dNSHostName"] = MessageElement("testname2.testdom2",
+ FLAG_MOD_REPLACE, "dNSHostName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("servicePrincipalName" in res[0])
+
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "sAMAccountName": "testname$"})
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("servicePrincipalName" in res[0])
+
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "servicePrincipalName": "HOST/testname"})
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountName"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("sAMAccountName" in res[0])
+
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "sAMAccountName": "testname$",
+ "servicePrincipalName": "HOST/testname"})
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["sAMAccountName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["sAMAccountName"][0]), "testname$")
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/testname")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("testnamE$",
+ FLAG_MOD_REPLACE, "sAMAccountName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/testname")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("testname",
+ FLAG_MOD_REPLACE, "sAMAccountName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/testname")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("test$name$",
+ FLAG_MOD_REPLACE, "sAMAccountName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/test$name")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("testname2",
+ FLAG_MOD_REPLACE, "sAMAccountName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/testname2")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("testname3",
+ FLAG_MOD_REPLACE, "sAMAccountName")
+ m["servicePrincipalName"] = MessageElement("HOST/testname2",
+ FLAG_MOD_REPLACE,
+ "servicePrincipalName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/testname3")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["servicePrincipalName"] = MessageElement("HOST/testname2",
+ FLAG_MOD_REPLACE,
+ "servicePrincipalName")
+ m["sAMAccountName"] = MessageElement("testname4",
+ FLAG_MOD_REPLACE, "sAMAccountName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["servicePrincipalName"][0]),
+ "HOST/testname2")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["servicePrincipalName"] = MessageElement([],
+ FLAG_MOD_DELETE,
+ "servicePrincipalName")
+ ldb.modify(m)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("testname2",
+ FLAG_MOD_REPLACE, "sAMAccountName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("servicePrincipalName" in res[0])
+
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "dNSHostName": "testname.testdom",
+ "sAMAccountName": "testname$",
+ "servicePrincipalName": ["HOST/testname.testdom", "HOST/testname"]
+ })
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["dNSHostName"] = MessageElement("testname2.testdom",
+ FLAG_MOD_REPLACE, "dNSHostName")
+ m["sAMAccountName"] = MessageElement("testname2$",
+ FLAG_MOD_REPLACE, "sAMAccountName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["dNSHostName", "sAMAccountName", "servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["dNSHostName"][0]), "testname2.testdom")
+ self.assertEqual(str(res[0]["sAMAccountName"][0]), "testname2$")
+ self.assertTrue(str(res[0]["servicePrincipalName"][0]) == "HOST/testname2" or
+ str(res[0]["servicePrincipalName"][1]) == "HOST/testname2")
+ self.assertTrue(str(res[0]["servicePrincipalName"][0]) == "HOST/testname2.testdom" or
+ str(res[0]["servicePrincipalName"][1]) == "HOST/testname2.testdom")
+
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "dNSHostName": "testname.testdom",
+ "sAMAccountName": "testname$",
+ "servicePrincipalName": ["HOST/testname.testdom", "HOST/testname"]
+ })
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["sAMAccountName"] = MessageElement("testname2$",
+ FLAG_MOD_REPLACE, "sAMAccountName")
+ m["dNSHostName"] = MessageElement("testname2.testdom",
+ FLAG_MOD_REPLACE, "dNSHostName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["dNSHostName", "sAMAccountName", "servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["dNSHostName"][0]), "testname2.testdom")
+ self.assertEqual(str(res[0]["sAMAccountName"][0]), "testname2$")
+ self.assertTrue(len(res[0]["servicePrincipalName"]) == 2)
+ self.assertTrue("HOST/testname2" in [str(x) for x in res[0]["servicePrincipalName"]])
+ self.assertTrue("HOST/testname2.testdom" in [str(x) for x in res[0]["servicePrincipalName"]])
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["servicePrincipalName"] = MessageElement("HOST/testname2.testdom",
+ FLAG_MOD_ADD, "servicePrincipalName")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e77:
+ (num, _) = e77.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["servicePrincipalName"] = MessageElement("HOST/testname3",
+ FLAG_MOD_ADD, "servicePrincipalName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["dNSHostName", "sAMAccountName", "servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["dNSHostName"][0]), "testname2.testdom")
+ self.assertEqual(str(res[0]["sAMAccountName"][0]), "testname2$")
+ self.assertTrue(len(res[0]["servicePrincipalName"]) == 3)
+ self.assertTrue("HOST/testname2" in [str(x) for x in res[0]["servicePrincipalName"]])
+ self.assertTrue("HOST/testname3" in [str(x) for x in res[0]["servicePrincipalName"]])
+ self.assertTrue("HOST/testname2.testdom" in [str(x) for x in res[0]["servicePrincipalName"]])
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+ m["dNSHostName"] = MessageElement("testname3.testdom",
+ FLAG_MOD_REPLACE, "dNSHostName")
+ m["servicePrincipalName"] = MessageElement("HOST/testname3.testdom",
+ FLAG_MOD_ADD, "servicePrincipalName")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["dNSHostName", "sAMAccountName", "servicePrincipalName"])
+ self.assertTrue(len(res) == 1)
+ self.assertEqual(str(res[0]["dNSHostName"][0]), "testname3.testdom")
+ self.assertEqual(str(res[0]["sAMAccountName"][0]), "testname2$")
+ self.assertTrue(len(res[0]["servicePrincipalName"]) == 3)
+ self.assertTrue("HOST/testname2" in [str(x) for x in res[0]["servicePrincipalName"]])
+ self.assertTrue("HOST/testname3" in [str(x) for x in res[0]["servicePrincipalName"]])
+ self.assertTrue("HOST/testname3.testdom" in [str(x) for x in res[0]["servicePrincipalName"]])
+
+ delete_force(self.ldb, "cn=ldaptestcomputer,cn=computers," + self.base_dn)
+
+ def test_service_principal_name_uniqueness(self):
+ """Test the servicePrincipalName uniqueness behaviour"""
+ print("Testing servicePrincipalName uniqueness behaviour")
+
+ ldb.add({
+ "dn": "cn=ldaptestcomputer,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "servicePrincipalName": "HOST/testname.testdom"})
+
+ try:
+ ldb.add({
+ "dn": "cn=ldaptestcomputer2,cn=computers," + self.base_dn,
+ "objectclass": "computer",
+ "servicePrincipalName": "HOST/testname.testdom"})
+ except LdbError as e:
+ num, _ = e.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+ else:
+ self.fail()
+
+ def test_sam_description_attribute(self):
+ """Test SAM description attribute"""
+ print("Test SAM description attribute")
+
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "description": "desc1"
+ })
+
+ res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res[0])
+ self.assertTrue(len(res[0]["description"]) == 1)
+ self.assertEqual(str(res[0]["description"][0]), "desc1")
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "description": ["desc1", "desc2"]})
+
+ res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res[0])
+ self.assertTrue(len(res[0]["description"]) == 2)
+ self.assertTrue(str(res[0]["description"][0]) == "desc1" or
+ str(res[0]["description"][1]) == "desc1")
+ self.assertTrue(str(res[0]["description"][0]) == "desc2" or
+ str(res[0]["description"][1]) == "desc2")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement(["desc1", "desc2"], FLAG_MOD_REPLACE,
+ "description")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e78:
+ (num, _) = e78.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement(["desc1", "desc2"], FLAG_MOD_DELETE,
+ "description")
+ ldb.modify(m)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement("desc1", FLAG_MOD_REPLACE,
+ "description")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res[0])
+ self.assertTrue(len(res[0]["description"]) == 1)
+ self.assertEqual(str(res[0]["description"][0]), "desc1")
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "description": ["desc1", "desc2"]})
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement("desc1", FLAG_MOD_REPLACE,
+ "description")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res[0])
+ self.assertTrue(len(res[0]["description"]) == 1)
+ self.assertEqual(str(res[0]["description"][0]), "desc1")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement("desc3", FLAG_MOD_ADD,
+ "description")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e79:
+ (num, _) = e79.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement(["desc1", "desc2"], FLAG_MOD_DELETE,
+ "description")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e80:
+ (num, _) = e80.args
+ self.assertEqual(num, ERR_NO_SUCH_ATTRIBUTE)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement("desc1", FLAG_MOD_DELETE,
+ "description")
+ ldb.modify(m)
+ res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["description"])
+ self.assertTrue(len(res) == 1)
+ self.assertFalse("description" in res[0])
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement(["desc1", "desc2"], FLAG_MOD_REPLACE,
+ "description")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e81:
+ (num, _) = e81.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement(["desc3", "desc4"], FLAG_MOD_ADD,
+ "description")
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e82:
+ (num, _) = e82.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m["description"] = MessageElement("desc1", FLAG_MOD_ADD,
+ "description")
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res[0])
+ self.assertTrue(len(res[0]["description"]) == 1)
+ self.assertEqual(str(res[0]["description"][0]), "desc1")
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m.add(MessageElement("desc1", FLAG_MOD_DELETE, "description"))
+ m.add(MessageElement("desc2", FLAG_MOD_ADD, "description"))
+ ldb.modify(m)
+
+ res = ldb.search("cn=ldaptestgroup,cn=users," + self.base_dn,
+ scope=SCOPE_BASE, attrs=["description"])
+ self.assertTrue(len(res) == 1)
+ self.assertTrue("description" in res[0])
+ self.assertTrue(len(res[0]["description"]) == 1)
+ self.assertEqual(str(res[0]["description"][0]), "desc2")
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_fSMORoleOwner_attribute(self):
+ """Test fSMORoleOwner attribute"""
+ print("Test fSMORoleOwner attribute")
+
+ ds_service_name = self.ldb.get_dsServiceName()
+
+ # The "fSMORoleOwner" attribute can only be set to "nTDSDSA" entries,
+ # invalid DNs return ERR_UNWILLING_TO_PERFORM
+
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "fSMORoleOwner": self.base_dn})
+ self.fail()
+ except LdbError as e83:
+ (num, _) = e83.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ try:
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "fSMORoleOwner": []})
+ self.fail()
+ except LdbError as e84:
+ (num, _) = e84.args
+ self.assertEqual(num, ERR_CONSTRAINT_VIOLATION)
+
+ # We are able to set it to a valid "nTDSDSA" entry if the server is
+ # capable of handling the role
+
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group",
+ "fSMORoleOwner": ds_service_name})
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ self.ldb.add({
+ "dn": "cn=ldaptestgroup,cn=users," + self.base_dn,
+ "objectclass": "group"})
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m.add(MessageElement(self.base_dn, FLAG_MOD_REPLACE, "fSMORoleOwner"))
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e85:
+ (num, _) = e85.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m.add(MessageElement([], FLAG_MOD_REPLACE, "fSMORoleOwner"))
+ try:
+ ldb.modify(m)
+ self.fail()
+ except LdbError as e86:
+ (num, _) = e86.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ # We are able to set it to a valid "nTDSDSA" entry if the server is
+ # capable of handling the role
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m.add(MessageElement(ds_service_name, FLAG_MOD_REPLACE, "fSMORoleOwner"))
+ ldb.modify(m)
+
+ # A clean-out works on plain entries, not master (schema, PDC...) DNs
+
+ m = Message()
+ m.dn = Dn(ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+ m.add(MessageElement([], FLAG_MOD_DELETE, "fSMORoleOwner"))
+ ldb.modify(m)
+
+ delete_force(self.ldb, "cn=ldaptestgroup,cn=users," + self.base_dn)
+
+ def test_protected_sid_objects(self):
+ """Test deletion of objects with RID < 1000"""
+ # a list of some well-known sids
+ # objects in Builtin are already covered by objectclass
+ protected_list = [
+ ["CN=Domain Admins", "CN=Users,"],
+ ["CN=Schema Admins", "CN=Users,"],
+ ["CN=Enterprise Admins", "CN=Users,"],
+ ["CN=Administrator", "CN=Users,"],
+ ["CN=Domain Controllers", "CN=Users,"],
+ ["CN=Protected Users", "CN=Users,"],
+ ]
+
+ for pr_object in protected_list:
+ try:
+ self.ldb.delete(pr_object[0] + "," + pr_object[1] + self.base_dn)
+ except LdbError as e7:
+ (num, _) = e7.args
+ self.assertEqual(num, ERR_OTHER)
+ else:
+ self.fail("Deleted " + pr_object[0])
+
+ try:
+ self.ldb.rename(pr_object[0] + "," + pr_object[1] + self.base_dn,
+ pr_object[0] + "2," + pr_object[1] + self.base_dn)
+ except LdbError as e8:
+ (num, _) = e8.args
+ self.fail("Could not rename " + pr_object[0])
+
+ self.ldb.rename(pr_object[0] + "2," + pr_object[1] + self.base_dn,
+ pr_object[0] + "," + pr_object[1] + self.base_dn)
+
+ def test_new_user_default_attributes(self):
+ """Test default attributes for new user objects"""
+ print("Test default attributes for new User objects\n")
+
+ user_name = "ldaptestuser"
+ user_dn = "CN=%s,CN=Users,%s" % (user_name, self.base_dn)
+ ldb.add({
+ "dn": user_dn,
+ "objectclass": "user",
+ "sAMAccountName": user_name})
+
+ res = ldb.search(user_dn, scope=SCOPE_BASE)
+ self.assertTrue(len(res) == 1)
+ user_obj = res[0]
+
+ expected_attrs = {"primaryGroupID": MessageElement(["513"]),
+ "logonCount": MessageElement(["0"]),
+ "cn": MessageElement([user_name]),
+ "countryCode": MessageElement(["0"]),
+ "objectClass": MessageElement(["top", "person", "organizationalPerson", "user"]),
+ "instanceType": MessageElement(["4"]),
+ "distinguishedName": MessageElement([user_dn]),
+ "sAMAccountType": MessageElement(["805306368"]),
+ "objectSid": "**SKIP**",
+ "whenCreated": "**SKIP**",
+ "uSNCreated": "**SKIP**",
+ "badPasswordTime": MessageElement(["0"]),
+ "dn": Dn(ldb, user_dn),
+ "pwdLastSet": MessageElement(["0"]),
+ "sAMAccountName": MessageElement([user_name]),
+ "objectCategory": MessageElement(["CN=Person,%s" % ldb.get_schema_basedn().get_linearized()]),
+ "objectGUID": "**SKIP**",
+ "whenChanged": "**SKIP**",
+ "badPwdCount": MessageElement(["0"]),
+ "accountExpires": MessageElement(["9223372036854775807"]),
+ "name": MessageElement([user_name]),
+ "codePage": MessageElement(["0"]),
+ "userAccountControl": MessageElement(["546"]),
+ "lastLogon": MessageElement(["0"]),
+ "uSNChanged": "**SKIP**",
+ "lastLogoff": MessageElement(["0"])}
+ # assert we have expected attribute names
+ actual_names = set(user_obj.keys())
+ # Samba does not use 'dSCorePropagationData', so skip it
+ actual_names -= set(['dSCorePropagationData'])
+ self.assertEqual(set(expected_attrs.keys()), actual_names, "Actual object does not have expected attributes")
+ # check attribute values
+ for name in expected_attrs.keys():
+ actual_val = user_obj.get(name)
+ self.assertFalse(actual_val is None, "No value for attribute '%s'" % name)
+ expected_val = expected_attrs[name]
+ if expected_val == "**SKIP**":
+ # "**ANY**" values means "any"
+ continue
+ self.assertEqual(expected_val, actual_val,
+ "Unexpected value[%r] for '%s' expected[%r]" %
+ (actual_val, name, expected_val))
+ # clean up
+ delete_force(self.ldb, "cn=ldaptestuser,cn=users," + self.base_dn)
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+ldb = SamDB(host, credentials=creds, session_info=system_session(lp), lp=lp)
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/sec_descriptor.py b/source4/dsdb/tests/python/sec_descriptor.py
new file mode 100755
index 0000000..1f22b2b
--- /dev/null
+++ b/source4/dsdb/tests/python/sec_descriptor.py
@@ -0,0 +1,2705 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+import os
+import base64
+import re
+import random
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+# Some error messages that are being tested
+from ldb import SCOPE_SUBTREE, SCOPE_BASE, LdbError, ERR_NO_SUCH_OBJECT
+
+# For running the test unit
+from samba.ndr import ndr_pack, ndr_unpack
+from samba.dcerpc import security
+
+from samba import gensec, sd_utils
+from samba.samdb import SamDB
+from samba.credentials import Credentials, DONT_USE_KERBEROS
+from samba.auth import system_session
+from samba.dsdb import DS_DOMAIN_FUNCTION_2008
+from samba.dcerpc.security import (
+ SECINFO_OWNER, SECINFO_GROUP, SECINFO_DACL, SECINFO_SACL)
+import samba.tests
+from samba.tests import delete_force
+
+parser = optparse.OptionParser("sec_descriptor.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+#
+# Tests start here
+#
+
+
+class DescriptorTests(samba.tests.TestCase):
+
+ def get_users_domain_dn(self, name):
+ return "CN=%s,CN=Users,%s" % (name, self.base_dn)
+
+ def create_schema_class(self, _ldb, desc=None):
+ while True:
+ class_id = random.randint(0, 65535)
+ class_name = "descriptor-test-class%s" % class_id
+ class_dn = "CN=%s,%s" % (class_name, self.schema_dn)
+ try:
+ self.ldb_admin.search(base=class_dn, attrs=["name"])
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+ break
+
+ ldif = """
+dn: """ + class_dn + """
+objectClass: classSchema
+objectCategory: CN=Class-Schema,""" + self.schema_dn + """
+defaultObjectCategory: """ + class_dn + """
+governsId: 1.3.6.1.4.1.7165.4.6.2.3.""" + str(class_id) + """
+instanceType: 4
+objectClassCategory: 1
+subClassOf: organizationalPerson
+systemFlags: 16
+rDNAttID: cn
+systemMustContain: cn
+systemOnly: FALSE
+"""
+ if desc:
+ assert(isinstance(desc, str) or isinstance(desc, security.descriptor))
+ if isinstance(desc, str):
+ ldif += "nTSecurityDescriptor: %s" % desc
+ elif isinstance(desc, security.descriptor):
+ ldif += "nTSecurityDescriptor:: %s" % base64.b64encode(ndr_pack(desc)).decode('utf8')
+ _ldb.add_ldif(ldif)
+ return class_dn
+
+ def create_configuration_container(self, _ldb, object_dn, desc=None):
+ ldif = """
+dn: """ + object_dn + """
+objectClass: container
+objectCategory: CN=Container,""" + self.schema_dn + """
+showInAdvancedViewOnly: TRUE
+instanceType: 4
+"""
+ if desc:
+ assert(isinstance(desc, str) or isinstance(desc, security.descriptor))
+ if isinstance(desc, str):
+ ldif += "nTSecurityDescriptor: %s" % desc
+ elif isinstance(desc, security.descriptor):
+ ldif += "nTSecurityDescriptor:: %s" % base64.b64encode(ndr_pack(desc)).decode('utf8')
+ _ldb.add_ldif(ldif)
+
+ def create_configuration_specifier(self, _ldb, object_dn, desc=None):
+ ldif = """
+dn: """ + object_dn + """
+objectClass: displaySpecifier
+showInAdvancedViewOnly: TRUE
+"""
+ if desc:
+ assert(isinstance(desc, str) or isinstance(desc, security.descriptor))
+ if isinstance(desc, str):
+ ldif += "nTSecurityDescriptor: %s" % desc
+ elif isinstance(desc, security.descriptor):
+ ldif += "nTSecurityDescriptor:: %s" % base64.b64encode(ndr_pack(desc)).decode('utf8')
+ _ldb.add_ldif(ldif)
+
+ def get_ldb_connection(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop
+ ldb_target = SamDB(url=host, credentials=creds_tmp, lp=lp)
+ return ldb_target
+
+ def setUp(self):
+ super(DescriptorTests, self).setUp()
+ self.ldb_admin = SamDB(host, credentials=creds, session_info=system_session(lp), lp=lp,
+ options=ldb_options)
+ self.base_dn = self.ldb_admin.domain_dn()
+ self.configuration_dn = self.ldb_admin.get_config_basedn().get_linearized()
+ self.schema_dn = self.ldb_admin.get_schema_basedn().get_linearized()
+ self.domain_sid = security.dom_sid(self.ldb_admin.get_domain_sid())
+ self.sd_utils = sd_utils.SDUtils(self.ldb_admin)
+ self.addCleanup(self.delete_admin_connection)
+ print("baseDN: %s" % self.base_dn)
+
+ def delete_admin_connection(self):
+ del self.sd_utils
+ del self.ldb_admin
+
+ ################################################################################################
+
+ # Tests for DOMAIN
+
+ # Default descriptor tests #####################################################################
+
+
+class OwnerGroupDescriptorTests(DescriptorTests):
+
+ def deleteAll(self):
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser1"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser2"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser3"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser4"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser5"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser6"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser7"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser8"))
+ # DOMAIN
+ delete_force(self.ldb_admin, self.get_users_domain_dn("test_domain_group1"))
+ delete_force(self.ldb_admin, "CN=test_domain_user1,OU=test_domain_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_domain_ou2,OU=test_domain_ou1," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_domain_ou1," + self.base_dn)
+ # SCHEMA
+ mod = "(A;CI;WDCC;;;AU)(A;;CC;;;AU)"
+ self.sd_utils.dacl_delete_aces(self.schema_dn, mod)
+ # CONFIGURATION
+ delete_force(self.ldb_admin, "CN=test-specifier1,CN=test-container1,CN=DisplaySpecifiers,"
+ + self.configuration_dn)
+ delete_force(self.ldb_admin, "CN=test-container1,CN=DisplaySpecifiers," + self.configuration_dn)
+
+ def setUp(self):
+ super(OwnerGroupDescriptorTests, self).setUp()
+ self.deleteAll()
+ # Create users
+ # User 1 - Enterprise Admins
+ self.ldb_admin.newuser("testuser1", "samba123@")
+ # User 2 - Domain Admins
+ self.ldb_admin.newuser("testuser2", "samba123@")
+ # User 3 - Schema Admins
+ self.ldb_admin.newuser("testuser3", "samba123@")
+ # User 4 - regular user
+ self.ldb_admin.newuser("testuser4", "samba123@")
+ # User 5 - Enterprise Admins and Domain Admins
+ self.ldb_admin.newuser("testuser5", "samba123@")
+ # User 6 - Enterprise Admins, Domain Admins, Schema Admins
+ self.ldb_admin.newuser("testuser6", "samba123@")
+ # User 7 - Domain Admins and Schema Admins
+ self.ldb_admin.newuser("testuser7", "samba123@")
+ # User 5 - Enterprise Admins and Schema Admins
+ self.ldb_admin.newuser("testuser8", "samba123@")
+
+ self.ldb_admin.add_remove_group_members("Enterprise Admins",
+ ["testuser1", "testuser5", "testuser6", "testuser8"],
+ add_members_operation=True)
+ self.ldb_admin.add_remove_group_members("Domain Admins",
+ ["testuser2", "testuser5", "testuser6", "testuser7"],
+ add_members_operation=True)
+ self.ldb_admin.add_remove_group_members("Schema Admins",
+ ["testuser3", "testuser6", "testuser7", "testuser8"],
+ add_members_operation=True)
+
+ self.results = {
+ # msDS-Behavior-Version < DS_DOMAIN_FUNCTION_2008
+ "ds_behavior_win2003": {
+ "100": "O:EAG:DU",
+ "101": "O:DAG:DU",
+ "102": "O:%sG:DU",
+ "103": "O:%sG:DU",
+ "104": "O:DAG:DU",
+ "105": "O:DAG:DU",
+ "106": "O:DAG:DU",
+ "107": "O:EAG:DU",
+ "108": "O:DAG:DA",
+ "109": "O:DAG:DA",
+ "110": "O:%sG:DA",
+ "111": "O:%sG:DA",
+ "112": "O:DAG:DA",
+ "113": "O:DAG:DA",
+ "114": "O:DAG:DA",
+ "115": "O:DAG:DA",
+ "130": "O:EAG:DU",
+ "131": "O:DAG:DU",
+ "132": "O:SAG:DU",
+ "133": "O:%sG:DU",
+ "134": "O:EAG:DU",
+ "135": "O:SAG:DU",
+ "136": "O:SAG:DU",
+ "137": "O:SAG:DU",
+ "138": "O:DAG:DA",
+ "139": "O:DAG:DA",
+ "140": "O:%sG:DA",
+ "141": "O:%sG:DA",
+ "142": "O:DAG:DA",
+ "143": "O:DAG:DA",
+ "144": "O:DAG:DA",
+ "145": "O:DAG:DA",
+ "160": "O:EAG:DU",
+ "161": "O:DAG:DU",
+ "162": "O:%sG:DU",
+ "163": "O:%sG:DU",
+ "164": "O:EAG:DU",
+ "165": "O:EAG:DU",
+ "166": "O:DAG:DU",
+ "167": "O:EAG:DU",
+ "168": "O:DAG:DA",
+ "169": "O:DAG:DA",
+ "170": "O:%sG:DA",
+ "171": "O:%sG:DA",
+ "172": "O:DAG:DA",
+ "173": "O:DAG:DA",
+ "174": "O:DAG:DA",
+ "175": "O:DAG:DA",
+ },
+ # msDS-Behavior-Version >= DS_DOMAIN_FUNCTION_2008
+ "ds_behavior_win2008": {
+ "100": "O:EAG:EA",
+ "101": "O:DAG:DA",
+ "102": "O:%sG:DU",
+ "103": "O:%sG:DU",
+ "104": "O:DAG:DA",
+ "105": "O:DAG:DA",
+ "106": "O:DAG:DA",
+ "107": "O:EAG:EA",
+ "108": "O:DAG:DA",
+ "109": "O:DAG:DA",
+ "110": "O:%sG:DA",
+ "111": "O:%sG:DA",
+ "112": "O:DAG:DA",
+ "113": "O:DAG:DA",
+ "114": "O:DAG:DA",
+ "115": "O:DAG:DA",
+ "130": "O:EAG:EA",
+ "131": "O:DAG:DA",
+ "132": "O:SAG:SA",
+ "133": "O:%sG:DU",
+ "134": "O:EAG:EA",
+ "135": "O:SAG:SA",
+ "136": "O:SAG:SA",
+ "137": "O:SAG:SA",
+ "138": "",
+ "139": "",
+ "140": "O:%sG:DA",
+ "141": "O:%sG:DA",
+ "142": "",
+ "143": "",
+ "144": "",
+ "145": "",
+ "160": "O:EAG:EA",
+ "161": "O:DAG:DA",
+ "162": "O:%sG:DU",
+ "163": "O:%sG:DU",
+ "164": "O:EAG:EA",
+ "165": "O:EAG:EA",
+ "166": "O:DAG:DA",
+ "167": "O:EAG:EA",
+ "168": "O:DAG:DA",
+ "169": "O:DAG:DA",
+ "170": "O:%sG:DA",
+ "171": "O:%sG:DA",
+ "172": "O:DAG:DA",
+ "173": "O:DAG:DA",
+ "174": "O:DAG:DA",
+ "175": "O:DAG:DA",
+ },
+ }
+ # Discover 'domainControllerFunctionality'
+ res = self.ldb_admin.search(base="", scope=SCOPE_BASE,
+ attrs=['domainControllerFunctionality'])
+ res = int(res[0]['domainControllerFunctionality'][0])
+ if res < DS_DOMAIN_FUNCTION_2008:
+ self.DS_BEHAVIOR = "ds_behavior_win2003"
+ else:
+ self.DS_BEHAVIOR = "ds_behavior_win2008"
+
+ def tearDown(self):
+ super(OwnerGroupDescriptorTests, self).tearDown()
+ self.deleteAll()
+
+ def check_user_belongs(self, user_dn, groups=None):
+ """ Test whether user is member of the expected group(s) """
+ if groups is None:
+ groups = []
+
+ if groups != []:
+ # User is member of at least one additional group
+ res = self.ldb_admin.search(user_dn, attrs=["memberOf"])
+ res = [str(x).upper() for x in sorted(list(res[0]["memberOf"]))]
+ expected = []
+ for x in groups:
+ expected.append(self.get_users_domain_dn(x))
+ expected = [x.upper() for x in sorted(expected)]
+ self.assertEqual(expected, res)
+ else:
+ # User is not a member of any additional groups but default
+ res = self.ldb_admin.search(user_dn, attrs=["*"])
+ res = [x.upper() for x in res[0].keys()]
+ self.assertNotIn("MEMBEROF", res)
+
+ def check_modify_inheritance(self, _ldb, object_dn, owner_group=""):
+ # Modify
+ sd_user_utils = sd_utils.SDUtils(_ldb)
+ ace = "(D;;CC;;;LG)" # Deny Create Children to Guest account
+ if owner_group != "":
+ sd_user_utils.modify_sd_on_dn(object_dn, owner_group + "D:" + ace)
+ else:
+ sd_user_utils.modify_sd_on_dn(object_dn, "D:" + ace)
+ # Make sure the modify operation has been applied
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ self.assertIn(ace, desc_sddl)
+ # Make sure we have identical result for both "add" and "modify"
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ print(self._testMethodName)
+ test_number = self._testMethodName[5:]
+ self.assertEqual(self.results[self.DS_BEHAVIOR][test_number], res)
+
+ def test_100(self):
+ """ Enterprise admin group member creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser1"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newgroup("test_domain_group1", grouptype=4)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_101(self):
+ """ Domain admin group member creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser2"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newgroup("test_domain_group1", grouptype=4)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_102(self):
+ """ Schema admin group member with CC right creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser3"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;WPWDCC;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create additional object into the first one
+ object_dn = "CN=test_domain_user1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newuser("test_domain_user1", "samba123@",
+ userou="OU=test_domain_ou1", setpassword=False)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+ # This fails, research why
+ #self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_103(self):
+ """ Regular user with CC right creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser4"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), [])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;WPWDCC;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create additional object into the first one
+ object_dn = "CN=test_domain_user1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newuser("test_domain_user1", "samba123@",
+ userou="OU=test_domain_ou1", setpassword=False)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+ # this fails, research why
+ #self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_104(self):
+ """ Enterprise & Domain admin group member creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser5"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newgroup("test_domain_group1", grouptype=4)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_105(self):
+ """ Enterprise & Domain & Schema admin group member creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser6"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newgroup("test_domain_group1", grouptype=4)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_106(self):
+ """ Domain & Schema admin group member creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser7"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newgroup("test_domain_group1", grouptype=4)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_107(self):
+ """ Enterprise & Schema admin group member creates object (default nTSecurityDescriptor) in DOMAIN
+ """
+ user_name = "testuser8"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newgroup("test_domain_group1", grouptype=4)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ # Control descriptor tests #####################################################################
+
+ def test_108(self):
+ """ Enterprise admin group member creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser1"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ _ldb.newgroup("test_domain_group1", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+
+ def test_109(self):
+ """ Domain admin group member creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser2"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ _ldb.newgroup("test_domain_group1", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+
+ def test_110(self):
+ """ Schema admin group member with CC right creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser3"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;WOWDCC;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create a custom security descriptor
+ # NB! Problematic owner part won't accept DA only <User Sid> !!!
+ sddl = "O:%sG:DAD:(A;;RP;;;DU)" % str(user_sid)
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ # Create additional object into the first one
+ object_dn = "CN=test_domain_user1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newuser("test_domain_user1", "samba123@",
+ userou="OU=test_domain_ou1", sd=tmp_desc, setpassword=False)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+
+ def test_111(self):
+ """ Regular user with CC right creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser4"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), [])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;WOWDCC;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create a custom security descriptor
+ # NB! Problematic owner part won't accept DA only <User Sid> !!!
+ sddl = "O:%sG:DAD:(A;;RP;;;DU)" % str(user_sid)
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ # Create additional object into the first one
+ object_dn = "CN=test_domain_user1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ _ldb.newuser("test_domain_user1", "samba123@",
+ userou="OU=test_domain_ou1", sd=tmp_desc, setpassword=False)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+
+ def test_112(self):
+ """ Domain & Enterprise admin group member creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser5"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ _ldb.newgroup("test_domain_group1", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+
+ def test_113(self):
+ """ Domain & Enterprise & Schema admin group member creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser6"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ _ldb.newgroup("test_domain_group1", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+
+ def test_114(self):
+ """ Domain & Schema admin group member creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser7"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ _ldb.newgroup("test_domain_group1", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+
+ def test_115(self):
+ """ Enterprise & Schema admin group member creates object (custom descriptor) in DOMAIN
+ """
+ user_name = "testuser8"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ object_dn = "CN=test_domain_group1,CN=Users," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ _ldb.newgroup("test_domain_group1", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+
+ def test_999(self):
+ user_name = "Administrator"
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(D;CI;WP;;;S-1-3-0)"
+ #mod = ""
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ # Create additional object into the first one
+ object_dn = "OU=test_domain_ou2," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+
+ # Tests for SCHEMA
+
+ # Default descriptor tests ##################################################################
+
+ def test_130(self):
+ user_name = "testuser1"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ try:
+ class_dn = self.create_schema_class(_ldb)
+ except LdbError:
+ self.fail()
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_131(self):
+ user_name = "testuser2"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_132(self):
+ user_name = "testuser3"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ #self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_133(self):
+ user_name = "testuser4"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), [])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+ #self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_134(self):
+ user_name = "testuser5"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_135(self):
+ user_name = "testuser6"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_136(self):
+ user_name = "testuser7"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, class_dn)
+
+ def test_137(self):
+ user_name = "testuser8"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, class_dn)
+
+ # Custom descriptor tests ##################################################################
+
+ def test_138(self):
+ user_name = "testuser1"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_139(self):
+ user_name = "testuser2"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_140(self):
+ user_name = "testuser3"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create a custom security descriptor
+ # NB! Problematic owner part won't accept DA only <User Sid> !!!
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ desc_sddl = "O:%sG:DAD:(A;;RP;;;DU)" % str(user_sid)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+
+ def test_141(self):
+ user_name = "testuser4"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), [])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create a custom security descriptor
+ # NB! Problematic owner part won't accept DA only <User Sid> !!!
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ desc_sddl = "O:%sG:DAD:(A;;RP;;;DU)" % str(user_sid)
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+
+ def test_142(self):
+ user_name = "testuser5"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_143(self):
+ user_name = "testuser6"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_144(self):
+ user_name = "testuser7"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_145(self):
+ user_name = "testuser8"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Change Schema partition descriptor
+ mod = "(A;;CC;;;AU)"
+ self.sd_utils.dacl_add_ace(self.schema_dn, mod)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ # Create example Schema class
+ class_dn = self.create_schema_class(_ldb, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(class_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ # Tests for CONFIGURATION
+
+ # Default descriptor tests ##################################################################
+
+ def test_160(self):
+ user_name = "testuser1"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(_ldb, object_dn, )
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_161(self):
+ user_name = "testuser2"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(_ldb, object_dn, )
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_162(self):
+ user_name = "testuser3"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ object_dn = "CN=test-container1,CN=DisplaySpecifiers," + self.configuration_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(self.ldb_admin, object_dn, )
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create child object with user's credentials
+ object_dn = "CN=test-specifier1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ try:
+ self.create_configuration_specifier(_ldb, object_dn)
+ except LdbError:
+ self.fail()
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+ #self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_163(self):
+ user_name = "testuser4"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), [])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ object_dn = "CN=test-container1,CN=DisplaySpecifiers," + self.configuration_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(self.ldb_admin, object_dn, )
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;WDCC;;;AU)"
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create child object with user's credentials
+ object_dn = "CN=test-specifier1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_specifier(_ldb, object_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+ #self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_164(self):
+ user_name = "testuser5"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(_ldb, object_dn, )
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_165(self):
+ user_name = "testuser6"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(_ldb, object_dn, )
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_166(self):
+ user_name = "testuser7"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(_ldb, object_dn, )
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ def test_167(self):
+ user_name = "testuser8"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(_ldb, object_dn, )
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]], res)
+ self.check_modify_inheritance(_ldb, object_dn)
+
+ # Custom descriptor tests ##################################################################
+
+ def test_168(self):
+ user_name = "testuser1"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ self.create_configuration_container(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_169(self):
+ user_name = "testuser2"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ self.create_configuration_container(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_170(self):
+ user_name = "testuser3"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ object_dn = "CN=test-container1,CN=DisplaySpecifiers," + self.configuration_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(self.ldb_admin, object_dn, )
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;CCWD;;;AU)"
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create child object with user's credentials
+ object_dn = "CN=test-specifier1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ # NB! Problematic owner part won't accept DA only <User Sid> !!!
+ desc_sddl = "O:%sG:DAD:(A;;RP;;;DU)" % str(user_sid)
+ try:
+ self.create_configuration_specifier(_ldb, object_dn, desc_sddl)
+ except LdbError:
+ self.fail()
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+
+ def test_171(self):
+ user_name = "testuser4"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), [])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ object_dn = "CN=test-container1,CN=DisplaySpecifiers," + self.configuration_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.create_configuration_container(self.ldb_admin, object_dn, )
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn(user_name))
+ mod = "(A;CI;CCWD;;;AU)"
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ # Create child object with user's credentials
+ object_dn = "CN=test-specifier1," + object_dn
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ # NB! Problematic owner part won't accept DA only <User Sid> !!!
+ desc_sddl = "O:%sG:DAD:(A;;RP;;;DU)" % str(user_sid)
+ try:
+ self.create_configuration_specifier(_ldb, object_dn, desc_sddl)
+ except LdbError:
+ self.fail()
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual(self.results[self.DS_BEHAVIOR][self._testMethodName[5:]] % str(user_sid), res)
+
+ def test_172(self):
+ user_name = "testuser5"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ self.create_configuration_container(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_173(self):
+ user_name = "testuser6"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ self.create_configuration_container(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_174(self):
+ user_name = "testuser7"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Domain Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ self.create_configuration_container(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ def test_175(self):
+ user_name = "testuser8"
+ self.check_user_belongs(self.get_users_domain_dn(user_name), ["Enterprise Admins", "Schema Admins"])
+ # Open Ldb connection with the tested user
+ _ldb = self.get_ldb_connection(user_name, "samba123@")
+ # Create example Configuration container
+ container_name = "test-container1"
+ object_dn = "CN=%s,CN=DisplaySpecifiers,%s" % (container_name, self.configuration_dn)
+ delete_force(self.ldb_admin, object_dn)
+ # Create a custom security descriptor
+ desc_sddl = "O:DAG:DAD:(A;;RP;;;DU)"
+ self.create_configuration_container(_ldb, object_dn, desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ res = re.search("(O:.*G:.*?)D:", desc_sddl).group(1)
+ self.assertEqual("O:DAG:DA", res)
+
+ ########################################################################################
+ # Inheritance tests for DACL
+
+
+class DaclDescriptorTests(DescriptorTests):
+
+ def deleteAll(self):
+ delete_force(self.ldb_admin, "CN=test_inherit_group,OU=test_inherit_ou," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou5,OU=test_inherit_ou1,OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou6,OU=test_inherit_ou2,OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou1,OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou2,OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou3,OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou4,OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou_p," + self.base_dn)
+ delete_force(self.ldb_admin, "OU=test_inherit_ou," + self.base_dn)
+
+ def setUp(self):
+ super(DaclDescriptorTests, self).setUp()
+ self.deleteAll()
+
+ def create_clean_ou(self, object_dn):
+ """ Base repeating setup for unittests to follow """
+ res = self.ldb_admin.search(base=self.base_dn, scope=SCOPE_SUBTREE,
+ expression="distinguishedName=%s" % object_dn)
+ # Make sure top testing OU has been deleted before starting the test
+ self.assertEqual(len(res), 0)
+ self.ldb_admin.create_ou(object_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ # Make sure there are inheritable ACEs initially
+ self.assertTrue("CI" in desc_sddl or "OI" in desc_sddl)
+ # Find and remove all inherit ACEs
+ res = re.findall(r"\(.*?\)", desc_sddl)
+ res = [x for x in res if ("CI" in x) or ("OI" in x)]
+ for x in res:
+ desc_sddl = desc_sddl.replace(x, "")
+ # Add flag 'protected' in both DACL and SACL so no inherit ACEs
+ # can propagate from above
+ # remove SACL, we are not interested
+ desc_sddl = desc_sddl.replace(":AI", ":AIP")
+ self.sd_utils.modify_sd_on_dn(object_dn, desc_sddl)
+ # Verify all inheritable ACEs are gone
+ desc_sddl = self.sd_utils.get_sd_as_sddl(object_dn)
+ self.assertNotIn("CI", desc_sddl)
+ self.assertNotIn("OI", desc_sddl)
+
+ def test_200(self):
+ """ OU with protected flag and child group. See if the group has inherit ACEs.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Create group child object
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4)
+ # Make sure created group object contains NO inherit ACEs
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn("ID", desc_sddl)
+
+ def test_201(self):
+ """ OU with protected flag and no inherit ACEs, child group with custom descriptor.
+ Verify group has custom and default ACEs only.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Create group child object using custom security descriptor
+ sddl = "O:AUG:AUD:AI(D;;WP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(sddl, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group descriptor has NO additional ACEs
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertEqual(desc_sddl, sddl)
+ sddl = "O:AUG:AUD:AI(D;;CC;;;LG)"
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, sddl)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertEqual(desc_sddl, sddl)
+
+ def test_202(self):
+ """ OU with protected flag and add couple non-inheritable ACEs, child group.
+ See if the group has any of the added ACEs.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom non-inheritable ACEs
+ mod = "(D;;WP;;;DU)(A;;RP;;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ # Verify all inheritable ACEs are gone
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4)
+ # Make sure created group object contains NO inherit ACEs
+ # also make sure the added above non-inheritable ACEs are absent too
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn("ID", desc_sddl)
+ for x in re.findall(r"\(.*?\)", mod):
+ self.assertNotIn(x, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn("ID", desc_sddl)
+ for x in re.findall(r"\(.*?\)", mod):
+ self.assertNotIn(x, desc_sddl)
+
+ def test_203(self):
+ """ OU with protected flag and add 'CI' ACE, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'CI' ACE
+ mod = "(D;CI;WP;;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";CI;", ";CIID;")
+ self.assertIn(mod, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn(mod, desc_sddl)
+
+ def test_204(self):
+ """ OU with protected flag and add 'OI' ACE, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'CI' ACE
+ mod = "(D;OI;WP;;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";OI;", ";OIIOID;") # change it how it's gonna look like
+ self.assertIn(mod, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn(mod, desc_sddl)
+
+ def test_205(self):
+ """ OU with protected flag and add 'OA' for GUID & 'CI' ACE, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'OA' for 'name' attribute & 'CI' ACE
+ mod = "(OA;CI;WP;bf967a0e-0de6-11d0-a285-00aa003049e2;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";CI;", ";CIID;") # change it how it's gonna look like
+ self.assertIn(mod, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn(mod, desc_sddl)
+
+ def test_206(self):
+ """ OU with protected flag and add 'OA' for GUID & 'OI' ACE, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'OA' for 'name' attribute & 'OI' ACE
+ mod = "(OA;OI;WP;bf967a0e-0de6-11d0-a285-00aa003049e2;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";OI;", ";OIIOID;") # change it how it's gonna look like
+ self.assertIn(mod, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn(mod, desc_sddl)
+
+ def test_207(self):
+ """ OU with protected flag and add 'OA' for OU specific GUID & 'CI' ACE, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'OA' for 'st' attribute (OU specific) & 'CI' ACE
+ mod = "(OA;CI;WP;bf967a39-0de6-11d0-a285-00aa003049e2;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";CI;", ";CIID;") # change it how it's gonna look like
+ self.assertIn(mod, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn(mod, desc_sddl)
+
+ def test_208(self):
+ """ OU with protected flag and add 'OA' for OU specific GUID & 'OI' ACE, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'OA' for 'st' attribute (OU specific) & 'OI' ACE
+ mod = "(OA;OI;WP;bf967a39-0de6-11d0-a285-00aa003049e2;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";OI;", ";OIIOID;") # change it how it's gonna look like
+ self.assertIn(mod, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:(OA;OI;WP;bf967a39-0de6-11d0-a285-00aa003049e2;;DU)" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn(mod, desc_sddl)
+
+ def test_209(self):
+ """ OU with protected flag and add 'CI' ACE with 'CO' SID, child group.
+ See if the group has the added inherited ACE.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'CI' ACE
+ mod = "(D;CI;WP;;;CO)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE(s)
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn("(D;ID;WP;;;AU)", desc_sddl)
+ self.assertIn("(D;CIIOID;WP;;;CO)", desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn("(D;ID;WP;;;DA)", desc_sddl)
+ self.assertIn("(D;CIIOID;WP;;;CO)", desc_sddl)
+
+ def test_210(self):
+ """ OU with protected flag, provide ACEs with ID flag raised. Should be ignored.
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ self.create_clean_ou(ou_dn)
+ # Add some custom ACE
+ mod = "D:(D;CIIO;WP;;;CO)(A;ID;WP;;;AU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object does not contain the ID ace
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn("(A;ID;WP;;;AU)", desc_sddl)
+
+ def test_211(self):
+ """ Provide ACE with CO SID, should be expanded and replaced
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'CI' ACE
+ mod = "D:(D;CI;WP;;;CO)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn("(D;;WP;;;DA)", desc_sddl)
+ self.assertIn("(D;CIIO;WP;;;CO)", desc_sddl)
+
+ def test_212(self):
+ """ Provide ACE with IO flag, should be ignored
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'CI' ACE
+ mod = "D:(D;CIIO;WP;;;CO)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE(s)
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn("(D;CIIO;WP;;;CO)", desc_sddl)
+ self.assertNotIn("(D;;WP;;;DA)", desc_sddl)
+ self.assertNotIn("(D;CIIO;WP;;;CO)(D;CIIO;WP;;;CO)", desc_sddl)
+
+ def test_213(self):
+ """ Provide ACE with IO flag, should be ignored
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "D:(D;IO;WP;;;DA)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object contains only the above inherited ACE(s)
+ # that we've added manually
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn("(D;IO;WP;;;DA)", desc_sddl)
+
+ def test_214(self):
+ """ Test behavior of ACEs containing generic rights
+ """
+ ou_dn = "OU=test_inherit_ou_p," + self.base_dn
+ ou_dn1 = "OU=test_inherit_ou1," + ou_dn
+ ou_dn2 = "OU=test_inherit_ou2," + ou_dn
+ ou_dn3 = "OU=test_inherit_ou3," + ou_dn
+ ou_dn4 = "OU=test_inherit_ou4," + ou_dn
+ ou_dn5 = "OU=test_inherit_ou5," + ou_dn1
+ ou_dn6 = "OU=test_inherit_ou6," + ou_dn2
+ # Create inheritable-free OU
+ mod = "D:P(A;CI;WPRPLCCCDCWDRCSD;;;DA)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, sd=tmp_desc)
+ mod = "D:(A;CI;GA;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn1, sd=tmp_desc)
+ mod = "D:(A;CIIO;GA;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn2, sd=tmp_desc)
+ mod = "D:(A;;GA;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn3, sd=tmp_desc)
+ mod = "D:(A;IO;GA;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn4, sd=tmp_desc)
+
+ self.ldb_admin.create_ou(ou_dn5)
+ self.ldb_admin.create_ou(ou_dn6)
+
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn1)
+ self.assertIn("(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DU)", desc_sddl)
+ self.assertIn("(A;CIIO;GA;;;DU)", desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn2)
+ self.assertNotIn("(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DU)", desc_sddl)
+ self.assertIn("(A;CIIO;GA;;;DU)", desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn3)
+ self.assertIn("(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DU)", desc_sddl)
+ self.assertNotIn("(A;CIIO;GA;;;DU)", desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn4)
+ self.assertNotIn("(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DU)", desc_sddl)
+ self.assertNotIn("(A;CIIO;GA;;;DU)", desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn5)
+ self.assertIn("(A;ID;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DU)", desc_sddl)
+ self.assertIn("(A;CIIOID;GA;;;DU)", desc_sddl)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn6)
+ self.assertIn("(A;ID;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DU)", desc_sddl)
+ self.assertIn("(A;CIIOID;GA;;;DU)", desc_sddl)
+
+ def test_215(self):
+ """ Make sure IO flag is removed in child objects
+ """
+ ou_dn = "OU=test_inherit_ou_p," + self.base_dn
+ ou_dn1 = "OU=test_inherit_ou1," + ou_dn
+ ou_dn5 = "OU=test_inherit_ou5," + ou_dn1
+ # Create inheritable-free OU
+ mod = "D:P(A;CI;WPRPLCCCDCWDRCSD;;;DA)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, sd=tmp_desc)
+ mod = "D:(A;CIIO;WP;;;DU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn1, sd=tmp_desc)
+ self.ldb_admin.create_ou(ou_dn5)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn5)
+ self.assertIn("(A;CIID;WP;;;DU)", desc_sddl)
+ self.assertNotIn("(A;CIIOID;WP;;;DU)", desc_sddl)
+
+ def test_216(self):
+ """ Make sure ID ACES provided by user are ignored
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ mod = "D:P(A;;WPRPLCCCDCWDRCSD;;;DA)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, sd=tmp_desc)
+ # Add some custom ACE
+ mod = "D:(D;ID;WP;;;AU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object does not contain the ID ace
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn("(A;ID;WP;;;AU)", desc_sddl)
+ self.assertNotIn("(A;;WP;;;AU)", desc_sddl)
+
+ def test_217(self):
+ """ Make sure ID ACES provided by user are not ignored if P flag is set
+ """
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ mod = "D:P(A;;WPRPLCCCDCWDRCSD;;;DA)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.create_ou(ou_dn, sd=tmp_desc)
+ # Add some custom ACE
+ mod = "D:P(A;ID;WP;;;AU)"
+ tmp_desc = security.descriptor.from_sddl(mod, self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ # Make sure created group object does not contain the ID ace
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn("(A;ID;WP;;;AU)", desc_sddl)
+ self.assertIn("(A;;WP;;;AU)", desc_sddl)
+
+ def test_ci_and_io_on_attribute(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CIOI;WP;bf967a0e-0de6-11d0-a285-00aa003049e2;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";CIOI;", ";OICIID;") # change it how it's gonna look like
+ self.assertIn(mod, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn(mod, desc_sddl)
+
+ def test_ci_and_np_on_attribute(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CINP;WP;bf967a0e-0de6-11d0-a285-00aa003049e2;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";CINP;", ";ID;") # change it how it's gonna look like
+ self.assertIn(mod, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn(mod, desc_sddl)
+
+ def test_oi_and_np_on_attribute(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;OINP;WP;bf967a0e-0de6-11d0-a285-00aa003049e2;;DU)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ mod = mod.replace(";OINP;", ";ID;") # change it how it's gonna look like
+ self.assertNotIn(mod, desc_sddl)
+ self.assertNotIn("bf967a0e-0de6-11d0-a285-00aa003049e2", desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertNotIn(mod, desc_sddl)
+ self.assertNotIn("bf967a0e-0de6-11d0-a285-00aa003049e2", desc_sddl)
+
+ def test_ci_ga_no_attr_objectclass_same(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CI;GA;;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ modob = "(A;ID;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DA)"
+ modid = "(OA;CIIOID;GA;;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(modob, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn(modob, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+
+ def test_ci_ga_no_attr_objectclass_different(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CI;GA;;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ modno = "(A;ID;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DA)"
+ modid = "(OA;CIIOID;GA;;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+
+ def test_ci_ga_name_attr_objectclass_same(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CI;GA;bf967a0e-0de6-11d0-a285-00aa003049e2;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ modob = "(OA;ID;CCDCLCSWRPWPDTLOCRSDRCWDWO;bf967a0e-0de6-11d0-a285-00aa003049e2;;DA)"
+ modid = "(OA;CIIOID;GA;bf967a0e-0de6-11d0-a285-00aa003049e2;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(modob, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn(modob, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+
+ def test_ci_ga_name_attr_objectclass_different(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CI;GA;bf967a0e-0de6-11d0-a285-00aa003049e2;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ modno = "(OA;ID;CCDCLCSWRPWPDTLOCRSDRCWDWO;bf967a0e-0de6-11d0-a285-00aa003049e2;;DA)"
+ modid = "(OA;CIIOID;GA;bf967a0e-0de6-11d0-a285-00aa003049e2;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+
+ def test_ci_lc_no_attr_objectclass_same(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CI;LC;;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ modno = "(A;ID;LC;;;DA)"
+ modid = "(OA;CIID;LC;;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+
+ def test_ci_lc_no_attr_objectclass_different(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CI;LC;;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ modno = "(A;ID;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DA)"
+ modid = "(OA;CIIOID;LC;;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+
+ def test_ci_lc_name_attr_objectclass_same(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CI;LC;bf967a0e-0de6-11d0-a285-00aa003049e2;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ modob = "(OA;ID;LC;bf967a0e-0de6-11d0-a285-00aa003049e2;;DA)"
+ modid = "(OA;CIID;LC;bf967a0e-0de6-11d0-a285-00aa003049e2;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn(modob, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertNotIn(modob, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+
+ def test_ci_lc_name_attr_objectclass_different(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CI;LC;bf967a0e-0de6-11d0-a285-00aa003049e2;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ modno = "(OA;ID;LC;bf967a0e-0de6-11d0-a285-00aa003049e2;;DA)"
+ modid = "(OA;CIIOID;LC;bf967a0e-0de6-11d0-a285-00aa003049e2;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertIn(modid, desc_sddl)
+
+ def test_ci_np_ga_no_attr_objectclass_same(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ # Add some custom 'OA' for 'name' attribute & 'CI'+'OI' ACE
+ mod = "(OA;CINP;GA;;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ modob = "(A;ID;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DA)"
+ modid = "(OA;CIIOID;GA;;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(modob, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("bf967a9c-0de6-11d0-a285-00aa003049e2", desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("bf967a9c-0de6-11d0-a285-00aa003049e2", desc_sddl)
+
+ def test_ci_np_ga_no_attr_objectclass_different(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CINP;GA;;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ modno = "(A;ID;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;DA)"
+ modid = "(OA;CIIOID;GA;;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", desc_sddl)
+
+ def test_ci_np_ga_name_attr_objectclass_same(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CINP;GA;bf967a0e-0de6-11d0-a285-00aa003049e2;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ modob = "(OA;ID;CCDCLCSWRPWPDTLOCRSDRCWDWO;bf967a0e-0de6-11d0-a285-00aa003049e2;;DA)"
+ modid = "(OA;CIIOID;GA;bf967a0e-0de6-11d0-a285-00aa003049e2;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(modob, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("bf967a9c-0de6-11d0-a285-00aa003049e2", desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn(modob, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("bf967a9c-0de6-11d0-a285-00aa003049e2", desc_sddl)
+
+ def test_ci_np_ga_name_attr_objectclass_different(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CINP;GA;bf967a0e-0de6-11d0-a285-00aa003049e2;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn("bf967a0e-0de6-11d0-a285-00aa003049e2", desc_sddl)
+ self.assertNotIn("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertNotIn("bf967a0e-0de6-11d0-a285-00aa003049e2", desc_sddl)
+ self.assertNotIn("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", desc_sddl)
+
+ def test_ci_np_lc_no_attr_objectclass_same(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CINP;LC;;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ modno = "(A;ID;LC;;;DA)"
+ modid = "(OA;CIID;LC;;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(modno, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("bf967a9c-0de6-11d0-a285-00aa003049e2", desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn(modno, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("bf967a9c-0de6-11d0-a285-00aa003049e2", desc_sddl)
+
+ def test_ci_np_lc_no_attr_objectclass_different(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CINP;LC;;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ modno = "(A;ID;LC;;;DA)"
+ modid = "(OA;CIIOID;LC;;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", desc_sddl)
+
+ def test_ci_np_lc_name_attr_objectclass_same(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CINP;LC;bf967a0e-0de6-11d0-a285-00aa003049e2;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ modob = "(OA;ID;LC;bf967a0e-0de6-11d0-a285-00aa003049e2;;DA)"
+ modid = "(OA;CIID;LC;bf967a0e-0de6-11d0-a285-00aa003049e2;bf967a9c-0de6-11d0-a285-00aa003049e2;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(modob, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("bf967a9c-0de6-11d0-a285-00aa003049e2", desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertIn(modob, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("bf967a9c-0de6-11d0-a285-00aa003049e2", desc_sddl)
+
+ def test_ci_np_lc_name_attr_objectclass_different(self):
+ ou_dn = "OU=test_inherit_ou," + self.base_dn
+ group_dn = "CN=test_inherit_group," + ou_dn
+ # Create inheritable-free OU
+ self.create_clean_ou(ou_dn)
+ mod = "(OA;CINP;LC;bf967a0e-0de6-11d0-a285-00aa003049e2;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ modno = "(OA;ID;LC;bf967a0e-0de6-11d0-a285-00aa003049e2;;DA)"
+ modid = "(OA;CIIOID;LC;bf967a0e-0de6-11d0-a285-00aa003049e2;aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee;DA)"
+ moded = "(D;;CC;;;LG)"
+ self.sd_utils.dacl_add_ace(ou_dn, mod)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # Create group child object
+ tmp_desc = security.descriptor.from_sddl("O:AUG:AUD:AI(A;;CC;;;AU)", self.domain_sid)
+ self.ldb_admin.newgroup("test_inherit_group", groupou="OU=test_inherit_ou", grouptype=4, sd=tmp_desc)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("bf967a0e-0de6-11d0-a285-00aa003049e2", desc_sddl)
+ self.assertNotIn("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", desc_sddl)
+ try:
+ self.sd_utils.modify_sd_on_dn(group_dn, "D:" + moded)
+ except LdbError as e:
+ self.fail(str(e))
+ desc_sddl = self.sd_utils.get_sd_as_sddl(group_dn)
+ self.assertIn(moded, desc_sddl)
+ self.assertNotIn(modno, desc_sddl)
+ self.assertNotIn(modid, desc_sddl)
+ self.assertNotIn("bf967a0e-0de6-11d0-a285-00aa003049e2", desc_sddl)
+ self.assertNotIn("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", desc_sddl)
+
+ ########################################################################################
+
+
+class SdFlagsDescriptorTests(DescriptorTests):
+ def deleteAll(self):
+ delete_force(self.ldb_admin, "OU=test_sdflags_ou," + self.base_dn)
+
+ def setUp(self):
+ super(SdFlagsDescriptorTests, self).setUp()
+ self.test_descr = "O:AUG:AUD:(D;;CC;;;LG)S:(OU;;WP;;;AU)"
+ self.deleteAll()
+
+ def test_301(self):
+ """ Modify a descriptor with OWNER_SECURITY_INFORMATION set.
+ See that only the owner has been changed.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ self.sd_utils.modify_sd_on_dn(ou_dn, self.test_descr, controls=["sd_flags:1:%d" % (SECINFO_OWNER)])
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # make sure we have modified the owner
+ self.assertIn("O:AU", desc_sddl)
+ # make sure nothing else has been modified
+ self.assertNotIn("G:AU", desc_sddl)
+ self.assertNotIn("D:(D;;CC;;;LG)", desc_sddl)
+ self.assertNotIn("(OU;;WP;;;AU)", desc_sddl)
+
+ def test_302(self):
+ """ Modify a descriptor with GROUP_SECURITY_INFORMATION set.
+ See that only the owner has been changed.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ self.sd_utils.modify_sd_on_dn(ou_dn, self.test_descr, controls=["sd_flags:1:%d" % (SECINFO_GROUP)])
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # make sure we have modified the group
+ self.assertIn("G:AU", desc_sddl)
+ # make sure nothing else has been modified
+ self.assertNotIn("O:AU", desc_sddl)
+ self.assertNotIn("D:(D;;CC;;;LG)", desc_sddl)
+ self.assertNotIn("(OU;;WP;;;AU)", desc_sddl)
+
+ def test_303(self):
+ """ Modify a descriptor with SACL_SECURITY_INFORMATION set.
+ See that only the owner has been changed.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ self.sd_utils.modify_sd_on_dn(ou_dn, self.test_descr, controls=["sd_flags:1:%d" % (SECINFO_DACL)])
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # make sure we have modified the DACL
+ self.assertIn("(D;;CC;;;LG)", desc_sddl)
+ # make sure nothing else has been modified
+ self.assertNotIn("O:AU", desc_sddl)
+ self.assertNotIn("G:AU", desc_sddl)
+ self.assertNotIn("(OU;;WP;;;AU)", desc_sddl)
+
+ def test_304(self):
+ """ Modify a descriptor with SACL_SECURITY_INFORMATION set.
+ See that only the owner has been changed.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ self.sd_utils.modify_sd_on_dn(ou_dn, self.test_descr, controls=["sd_flags:1:%d" % (SECINFO_SACL)])
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # make sure we have modified the DACL
+ self.assertIn("(OU;;WP;;;AU)", desc_sddl)
+ # make sure nothing else has been modified
+ self.assertNotIn("O:AU", desc_sddl)
+ self.assertNotIn("G:AU", desc_sddl)
+ self.assertNotIn("(D;;CC;;;LG)", desc_sddl)
+
+ def test_305(self):
+ """ Modify a descriptor with 0x0 set.
+ Contrary to logic this is interpreted as no control,
+ which is the same as 0xF
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ self.sd_utils.modify_sd_on_dn(ou_dn, self.test_descr, controls=["sd_flags:1:0"])
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # make sure we have modified the DACL
+ self.assertIn("(OU;;WP;;;AU)", desc_sddl)
+ # make sure nothing else has been modified
+ self.assertIn("O:AU", desc_sddl)
+ self.assertIn("G:AU", desc_sddl)
+ self.assertIn("(D;;CC;;;LG)", desc_sddl)
+
+ def test_306(self):
+ """ Modify a descriptor with 0xF set.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ self.sd_utils.modify_sd_on_dn(ou_dn, self.test_descr, controls=["sd_flags:1:15"])
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn)
+ # make sure we have modified the DACL
+ self.assertIn("(OU;;WP;;;AU)", desc_sddl)
+ # make sure nothing else has been modified
+ self.assertIn("O:AU", desc_sddl)
+ self.assertIn("G:AU", desc_sddl)
+ self.assertIn("(D;;CC;;;LG)", desc_sddl)
+
+ def test_307(self):
+ """ Read a descriptor with OWNER_SECURITY_INFORMATION
+ Only the owner part should be returned.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn, controls=["sd_flags:1:%d" % (SECINFO_OWNER)])
+ # make sure we have read the owner
+ self.assertIn("O:", desc_sddl)
+ # make sure we have read nothing else
+ self.assertNotIn("G:", desc_sddl)
+ self.assertNotIn("D:", desc_sddl)
+ self.assertNotIn("S:", desc_sddl)
+
+ def test_308(self):
+ """ Read a descriptor with GROUP_SECURITY_INFORMATION
+ Only the group part should be returned.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn, controls=["sd_flags:1:%d" % (SECINFO_GROUP)])
+ # make sure we have read the owner
+ self.assertIn("G:", desc_sddl)
+ # make sure we have read nothing else
+ self.assertNotIn("O:", desc_sddl)
+ self.assertNotIn("D:", desc_sddl)
+ self.assertNotIn("S:", desc_sddl)
+
+ def test_309(self):
+ """ Read a descriptor with SACL_SECURITY_INFORMATION
+ Only the sacl part should be returned.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn, controls=["sd_flags:1:%d" % (SECINFO_SACL)])
+ # make sure we have read the owner
+ self.assertIn("S:", desc_sddl)
+ # make sure we have read nothing else
+ self.assertNotIn("O:", desc_sddl)
+ self.assertNotIn("D:", desc_sddl)
+ self.assertNotIn("G:", desc_sddl)
+
+ def test_310(self):
+ """ Read a descriptor with DACL_SECURITY_INFORMATION
+ Only the dacl part should be returned.
+ """
+ ou_dn = "OU=test_sdflags_ou," + self.base_dn
+ self.ldb_admin.create_ou(ou_dn)
+ desc_sddl = self.sd_utils.get_sd_as_sddl(ou_dn, controls=["sd_flags:1:%d" % (SECINFO_DACL)])
+ # make sure we have read the owner
+ self.assertIn("D:", desc_sddl)
+ # make sure we have read nothing else
+ self.assertNotIn("O:", desc_sddl)
+ self.assertNotIn("S:", desc_sddl)
+ self.assertNotIn("G:", desc_sddl)
+
+ def test_311(self):
+ sd_flags = (SECINFO_OWNER |
+ SECINFO_GROUP |
+ SECINFO_DACL |
+ SECINFO_SACL)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ [], controls=None)
+ self.assertNotIn("nTSecurityDescriptor", res[0])
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["name"], controls=None)
+ self.assertNotIn("nTSecurityDescriptor", res[0])
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["name"], controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertNotIn("nTSecurityDescriptor", res[0])
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertIn("nTSecurityDescriptor", res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertIn("O:", sddl)
+ self.assertIn("G:", sddl)
+ self.assertIn("D:", sddl)
+ self.assertIn("S:", sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["*"], controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertIn("nTSecurityDescriptor", res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertIn("O:", sddl)
+ self.assertIn("G:", sddl)
+ self.assertIn("D:", sddl)
+ self.assertIn("S:", sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["nTSecurityDescriptor", "*"], controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertIn("nTSecurityDescriptor", res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertIn("O:", sddl)
+ self.assertIn("G:", sddl)
+ self.assertIn("D:", sddl)
+ self.assertIn("S:", sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["*", "nTSecurityDescriptor"], controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertIn("nTSecurityDescriptor", res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertIn("O:", sddl)
+ self.assertIn("G:", sddl)
+ self.assertIn("D:", sddl)
+ self.assertIn("S:", sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["nTSecurityDescriptor", "name"], controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertIn("nTSecurityDescriptor", res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertIn("O:", sddl)
+ self.assertIn("G:", sddl)
+ self.assertIn("D:", sddl)
+ self.assertIn("S:", sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["name", "nTSecurityDescriptor"], controls=["sd_flags:1:%d" % (sd_flags)])
+ self.assertIn("nTSecurityDescriptor", res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertIn("O:", sddl)
+ self.assertIn("G:", sddl)
+ self.assertIn("D:", sddl)
+ self.assertIn("S:", sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["nTSecurityDescriptor"], controls=None)
+ self.assertIn("nTSecurityDescriptor", res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertIn("O:", sddl)
+ self.assertIn("G:", sddl)
+ self.assertIn("D:", sddl)
+ self.assertIn("S:", sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["name", "nTSecurityDescriptor"], controls=None)
+ self.assertIn("nTSecurityDescriptor", res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertIn("O:", sddl)
+ self.assertIn("G:", sddl)
+ self.assertIn("D:", sddl)
+ self.assertIn("S:", sddl)
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None,
+ ["nTSecurityDescriptor", "name"], controls=None)
+ self.assertIn("nTSecurityDescriptor", res[0])
+ tmp = res[0]["nTSecurityDescriptor"][0]
+ sd = ndr_unpack(security.descriptor, tmp)
+ sddl = sd.as_sddl(self.sd_utils.domain_sid)
+ self.assertIn("O:", sddl)
+ self.assertIn("G:", sddl)
+ self.assertIn("D:", sddl)
+ self.assertIn("S:", sddl)
+
+ def test_312(self):
+ """This search is done by the windows dc join..."""
+
+ res = self.ldb_admin.search(self.base_dn, SCOPE_BASE, None, ["1.1"],
+ controls=["extended_dn:1:0", "sd_flags:1:0", "search_options:1:1"])
+ self.assertNotIn("nTSecurityDescriptor", res[0])
+
+
+class RightsAttributesTests(DescriptorTests):
+
+ def deleteAll(self):
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser_attr"))
+ delete_force(self.ldb_admin, self.get_users_domain_dn("testuser_attr2"))
+ delete_force(self.ldb_admin, "OU=test_domain_ou1," + self.base_dn)
+
+ def setUp(self):
+ super(RightsAttributesTests, self).setUp()
+ self.deleteAll()
+ # Create users
+ # User 1
+ self.ldb_admin.newuser("testuser_attr", "samba123@")
+ # User 2, Domain Admins
+ self.ldb_admin.newuser("testuser_attr2", "samba123@")
+ self.ldb_admin.add_remove_group_members("Domain Admins",
+ ["testuser_attr2"],
+ add_members_operation=True)
+
+ def test_sDRightsEffective(self):
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ print(self.get_users_domain_dn("testuser_attr"))
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn("testuser_attr"))
+ # give testuser1 read access so attributes can be retrieved
+ mod = "(A;CI;RP;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ _ldb = self.get_ldb_connection("testuser_attr", "samba123@")
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["sDRightsEffective"])
+ # user should have no rights at all
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["sDRightsEffective"][0]), "0")
+ # give the user Write DACL and see what happens
+ mod = "(A;CI;WD;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["sDRightsEffective"])
+ # user should have DACL_SECURITY_INFORMATION
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["sDRightsEffective"][0]), ("%d") % SECINFO_DACL)
+ # give the user Write Owners and see what happens
+ mod = "(A;CI;WO;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["sDRightsEffective"])
+ # user should have DACL_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION, GROUP_SECURITY_INFORMATION
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["sDRightsEffective"][0]), ("%d") % (SECINFO_DACL | SECINFO_GROUP | SECINFO_OWNER))
+ # no way to grant security privilege bu adding ACE's so we use a member of Domain Admins
+ _ldb = self.get_ldb_connection("testuser_attr2", "samba123@")
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["sDRightsEffective"])
+ # user should have DACL_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION, GROUP_SECURITY_INFORMATION
+ self.assertEqual(len(res), 1)
+ self.assertEqual(str(res[0]["sDRightsEffective"][0]),
+ ("%d") % (SECINFO_DACL | SECINFO_GROUP | SECINFO_OWNER | SECINFO_SACL))
+
+ def test_allowedChildClassesEffective(self):
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn("testuser_attr"))
+ # give testuser1 read access so attributes can be retrieved
+ mod = "(A;CI;RP;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ _ldb = self.get_ldb_connection("testuser_attr", "samba123@")
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["allowedChildClassesEffective"])
+ # there should be no allowed child classes
+ self.assertEqual(len(res), 1)
+ self.assertNotIn("allowedChildClassesEffective", res[0].keys())
+ # give the user the right to create children of type user
+ mod = "(OA;CI;CC;bf967aba-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["allowedChildClassesEffective"])
+ # allowedChildClassesEffective should only have one value, user
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]["allowedChildClassesEffective"]), 1)
+ self.assertEqual(str(res[0]["allowedChildClassesEffective"][0]), "user")
+
+ def test_allowedAttributesEffective(self):
+ object_dn = "OU=test_domain_ou1," + self.base_dn
+ delete_force(self.ldb_admin, object_dn)
+ self.ldb_admin.create_ou(object_dn)
+ user_sid = self.sd_utils.get_object_sid(self.get_users_domain_dn("testuser_attr"))
+ # give testuser1 read access so attributes can be retrieved
+ mod = "(A;CI;RP;;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod)
+ _ldb = self.get_ldb_connection("testuser_attr", "samba123@")
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["allowedAttributesEffective"])
+ # there should be no allowed attributes
+ self.assertEqual(len(res), 1)
+ self.assertNotIn("allowedAttributesEffective", res[0].keys())
+ # give the user the right to write displayName and managedBy
+ mod2 = "(OA;CI;WP;bf967953-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+ mod = "(OA;CI;WP;0296c120-40da-11d1-a9c0-0000f80367c1;;%s)" % str(user_sid)
+ # also rights to modify an read only attribute, fromEntry
+ mod3 = "(OA;CI;WP;9a7ad949-ca53-11d1-bbd0-0080c76670c0;;%s)" % str(user_sid)
+ self.sd_utils.dacl_add_ace(object_dn, mod + mod2 + mod3)
+ res = _ldb.search(base=object_dn, expression="", scope=SCOPE_BASE,
+ attrs=["allowedAttributesEffective"])
+ # value should only contain user and managedBy
+ self.assertEqual(len(res), 1)
+ self.assertEqual(len(res[0]["allowedAttributesEffective"]), 2)
+ self.assertIn(b"displayName", res[0]["allowedAttributesEffective"])
+ self.assertIn(b"managedBy", res[0]["allowedAttributesEffective"])
+
+
+class SdAutoInheritTests(DescriptorTests):
+ def deleteAll(self):
+ delete_force(self.ldb_admin, self.sub_dn)
+ delete_force(self.ldb_admin, self.ou_dn)
+
+ def setUp(self):
+ super(SdAutoInheritTests, self).setUp()
+ self.ou_dn = "OU=test_SdAutoInherit_ou," + self.base_dn
+ self.sub_dn = "OU=test_sub," + self.ou_dn
+ self.deleteAll()
+
+ def test_301(self):
+ """ Modify a descriptor with OWNER_SECURITY_INFORMATION set.
+ See that only the owner has been changed.
+ """
+ attrs = ["nTSecurityDescriptor", "replPropertyMetaData", "uSNChanged"]
+ controls = ["sd_flags:1:%d" % (SECINFO_DACL)]
+ ace = "(A;CI;CC;;;NU)"
+ sub_ace = "(A;CIID;CC;;;NU)"
+ sd_sddl = "O:BAG:BAD:P(A;CI;0x000f01ff;;;AU)"
+ sd = security.descriptor.from_sddl(sd_sddl, self.domain_sid)
+
+ self.ldb_admin.create_ou(self.ou_dn, sd=sd)
+ self.ldb_admin.create_ou(self.sub_dn)
+
+ ou_res0 = self.sd_utils.ldb.search(self.ou_dn, SCOPE_BASE,
+ None, attrs, controls=controls)
+ sub_res0 = self.sd_utils.ldb.search(self.sub_dn, SCOPE_BASE,
+ None, attrs, controls=controls)
+
+ ou_sd0 = ndr_unpack(security.descriptor, ou_res0[0]["nTSecurityDescriptor"][0])
+ sub_sd0 = ndr_unpack(security.descriptor, sub_res0[0]["nTSecurityDescriptor"][0])
+
+ ou_sddl0 = ou_sd0.as_sddl(self.domain_sid)
+ sub_sddl0 = sub_sd0.as_sddl(self.domain_sid)
+
+ self.assertNotIn(ace, ou_sddl0)
+ self.assertNotIn(ace, sub_sddl0)
+
+ ou_sddl1 = (ou_sddl0[:ou_sddl0.index("(")] + ace +
+ ou_sddl0[ou_sddl0.index("("):])
+
+ sub_sddl1 = (sub_sddl0[:sub_sddl0.index("(")] + ace +
+ sub_sddl0[sub_sddl0.index("("):])
+
+ self.sd_utils.modify_sd_on_dn(self.ou_dn, ou_sddl1, controls=controls)
+
+ self.sd_utils.modify_sd_on_dn(self.sub_dn, sub_sddl1, controls=controls)
+
+ sub_res2 = self.sd_utils.ldb.search(self.sub_dn, SCOPE_BASE,
+ None, attrs, controls=controls)
+ ou_res2 = self.sd_utils.ldb.search(self.ou_dn, SCOPE_BASE,
+ None, attrs, controls=controls)
+
+ ou_sd2 = ndr_unpack(security.descriptor, ou_res2[0]["nTSecurityDescriptor"][0])
+ sub_sd2 = ndr_unpack(security.descriptor, sub_res2[0]["nTSecurityDescriptor"][0])
+
+ ou_sddl2 = ou_sd2.as_sddl(self.domain_sid)
+ sub_sddl2 = sub_sd2.as_sddl(self.domain_sid)
+
+ self.assertNotEqual(ou_sddl2, ou_sddl0)
+ self.assertNotEqual(sub_sddl2, sub_sddl0)
+
+ if ace not in ou_sddl2:
+ print("ou0: %s" % ou_sddl0)
+ print("ou2: %s" % ou_sddl2)
+
+ if sub_ace not in sub_sddl2:
+ print("sub0: %s" % sub_sddl0)
+ print("sub2: %s" % sub_sddl2)
+
+ self.assertIn(ace, ou_sddl2)
+ self.assertIn(sub_ace, sub_sddl2)
+
+ ou_usn0 = int(ou_res0[0]["uSNChanged"][0])
+ ou_usn2 = int(ou_res2[0]["uSNChanged"][0])
+ self.assertGreater(ou_usn2, ou_usn0)
+
+ sub_usn0 = int(sub_res0[0]["uSNChanged"][0])
+ sub_usn2 = int(sub_res2[0]["uSNChanged"][0])
+ self.assertGreater(sub_usn2, sub_usn0)
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+# use 'paged_search' module when connecting remotely
+if host.lower().startswith("ldap://"):
+ ldb_options = ["modules:paged_searches"]
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/sites.py b/source4/dsdb/tests/python/sites.py
new file mode 100755
index 0000000..f6efdae
--- /dev/null
+++ b/source4/dsdb/tests/python/sites.py
@@ -0,0 +1,637 @@
+#!/usr/bin/env python3
+#
+# Unit tests for sites manipulation in samba
+# 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/>.
+
+import optparse
+import sys
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+
+import samba.getopt as options
+from samba import sites
+from samba import subnets
+from samba.auth import system_session
+from samba.samdb import SamDB
+from samba import gensec
+from samba.credentials import Credentials, DONT_USE_KERBEROS
+import samba.tests
+from samba.tests import delete_force
+from samba.dcerpc import security
+from ldb import SCOPE_SUBTREE, LdbError, ERR_INSUFFICIENT_ACCESS_RIGHTS
+
+parser = optparse.OptionParser("sites.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+#
+# Tests start here
+#
+
+
+class SitesBaseTests(samba.tests.TestCase):
+
+ def setUp(self):
+ super(SitesBaseTests, self).setUp()
+ self.ldb = SamDB(ldaphost, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.domain_dn()
+ self.domain_sid = security.dom_sid(self.ldb.get_domain_sid())
+ self.configuration_dn = self.ldb.get_config_basedn().get_linearized()
+
+ def get_user_dn(self, name):
+ return "CN=%s,CN=Users,%s" % (name, self.base_dn)
+
+
+# tests on sites
+class SimpleSitesTests(SitesBaseTests):
+
+ def test_create_and_delete(self):
+ """test creation and deletion of 1 site"""
+
+ sites.create_site(self.ldb, self.ldb.get_config_basedn(),
+ "testsamba")
+
+ self.assertRaises(sites.SiteAlreadyExistsException,
+ sites.create_site, self.ldb,
+ self.ldb.get_config_basedn(),
+ "testsamba")
+
+ sites.delete_site(self.ldb, self.ldb.get_config_basedn(),
+ "testsamba")
+
+ self.assertRaises(sites.SiteNotFoundException,
+ sites.delete_site, self.ldb,
+ self.ldb.get_config_basedn(),
+ "testsamba")
+
+ def test_delete_not_empty(self):
+ """test removal of 1 site with servers"""
+
+ self.assertRaises(sites.SiteServerNotEmptyException,
+ sites.delete_site, self.ldb,
+ self.ldb.get_config_basedn(),
+ "Default-First-Site-Name")
+
+
+# tests for subnets
+class SimpleSubnetTests(SitesBaseTests):
+
+ def setUp(self):
+ super(SimpleSubnetTests, self).setUp()
+ self.basedn = self.ldb.get_config_basedn()
+ self.sitename = "testsite"
+ self.sitename2 = "testsite2"
+ self.ldb.transaction_start()
+ sites.create_site(self.ldb, self.basedn, self.sitename)
+ sites.create_site(self.ldb, self.basedn, self.sitename2)
+ self.ldb.transaction_commit()
+
+ def tearDown(self):
+ self.ldb.transaction_start()
+ sites.delete_site(self.ldb, self.basedn, self.sitename)
+ sites.delete_site(self.ldb, self.basedn, self.sitename2)
+ self.ldb.transaction_commit()
+ super(SimpleSubnetTests, self).tearDown()
+
+ def test_create_delete(self):
+ """Create a subnet and delete it again."""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.11.12.0/24"
+
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+
+ self.assertRaises(subnets.SubnetAlreadyExists,
+ subnets.create_subnet, self.ldb, basedn, cidr,
+ self.sitename)
+
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 0, 'Failed to delete subnet %s' % cidr)
+
+ def test_create_shift_delete(self):
+ """Create a subnet, shift it to another site, then delete it."""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.11.12.0/24"
+
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+
+ subnets.set_subnet_site(self.ldb, basedn, cidr, self.sitename2)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ sites = ret[0]['siteObject']
+ self.assertEqual(len(sites), 1)
+ self.assertEqual(str(sites[0]),
+ 'CN=testsite2,CN=Sites,%s' % self.ldb.get_config_basedn())
+
+ self.assertRaises(subnets.SubnetAlreadyExists,
+ subnets.create_subnet, self.ldb, basedn, cidr,
+ self.sitename)
+
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 0, 'Failed to delete subnet %s' % cidr)
+
+ def test_delete_subnet_that_does_not_exist(self):
+ """Ensure we can't delete a site that isn't there."""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.15.0.0/16"
+
+ self.assertRaises(subnets.SubnetNotFound,
+ subnets.delete_subnet, self.ldb, basedn, cidr)
+
+ def get_user_and_ldb(self, username, password, hostname=ldaphost):
+ """Get a connection for a temporarily user that will vanish as soon as
+ the test is over."""
+ user = self.ldb.newuser(username, password)
+ creds_tmp = Credentials()
+ creds_tmp.set_username(username)
+ creds_tmp.set_password(password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS)
+ ldb_target = SamDB(url=hostname, credentials=creds_tmp, lp=lp)
+ self.addCleanup(delete_force, self.ldb, self.get_user_dn(username))
+ return (user, ldb_target)
+
+ def test_rename_delete_good_subnet_to_good_subnet_other_user(self):
+ """Make sure that we can't rename or delete subnets when we aren't
+ admin."""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.16.0.0/24"
+ new_cidr = "10.16.1.0/24"
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+ user, non_admin_ldb = self.get_user_and_ldb("notadmin", "samba123@")
+ try:
+ subnets.rename_subnet(non_admin_ldb, basedn, cidr, new_cidr)
+ except LdbError as e:
+ self.assertEqual(e.args[0], ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ ("subnet rename by non-admin failed "
+ "in the wrong way: %s" % e))
+ else:
+ self.fail("subnet rename by non-admin succeeded")
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 1, ('Subnet %s destroyed or renamed '
+ 'by non-admin' % cidr))
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression=('(&(objectclass=subnet)(cn=%s))'
+ % new_cidr))
+
+ self.assertEqual(len(ret), 0,
+ 'New subnet %s created by non-admin' % cidr)
+
+ try:
+ subnets.delete_subnet(non_admin_ldb, basedn, cidr)
+ except LdbError as e:
+ self.assertEqual(e.args[0], ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ ("subnet delete by non-admin failed "
+ "in the wrong way: %s" % e))
+ else:
+ self.fail("subnet delete by non-admin succeeded:")
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 1, 'Subnet %s deleted non-admin' % cidr)
+
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+
+ def test_create_good_subnet_other_user(self):
+ """Make sure that we can't create subnets when we aren't admin."""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.16.0.0/24"
+ user, non_admin_ldb = self.get_user_and_ldb("notadmin", "samba123@")
+ try:
+ subnets.create_subnet(non_admin_ldb, basedn, cidr, self.sitename)
+ except LdbError as e:
+ self.assertEqual(e.args[0], ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ ("subnet create by non-admin failed "
+ "in the wrong way: %s" % e))
+ else:
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+ self.fail("subnet create by non-admin succeeded: %s")
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 0, 'New subnet %s created by non-admin' % cidr)
+
+ def test_rename_good_subnet_to_good_subnet(self):
+ """Make sure that we can rename subnets"""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.16.0.0/24"
+ new_cidr = "10.16.1.0/24"
+
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+
+ subnets.rename_subnet(self.ldb, basedn, cidr, new_cidr)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % new_cidr)
+
+ self.assertEqual(len(ret), 1, 'Failed to rename subnet %s' % cidr)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 0, 'Failed to remove old subnet during rename %s' % cidr)
+
+ subnets.delete_subnet(self.ldb, basedn, new_cidr)
+
+ def test_rename_good_subnet_to_bad_subnet(self):
+ """Make sure that the CIDR checking runs during rename"""
+ basedn = self.ldb.get_config_basedn()
+ cidr = "10.17.0.0/24"
+ bad_cidr = "10.11.12.0/14"
+
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+
+ self.assertRaises(subnets.SubnetInvalid, subnets.rename_subnet,
+ self.ldb, basedn, cidr, bad_cidr)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % bad_cidr)
+
+ self.assertEqual(len(ret), 0, 'Failed to rename subnet %s' % cidr)
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression='(&(objectclass=subnet)(cn=%s))' % cidr)
+
+ self.assertEqual(len(ret), 1, 'Failed to remove old subnet during rename %s' % cidr)
+
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+
+ def test_create_bad_ranges(self):
+ """These CIDR ranges all have something wrong with them, and they
+ should all fail."""
+ basedn = self.ldb.get_config_basedn()
+
+ cidrs = [
+ # IPv4
+ # insufficient zeros
+ "10.11.12.0/14",
+ "110.0.0.0/6",
+ "1.0.0.0/0",
+ "10.11.13.1/24",
+ "1.2.3.4/29",
+ "10.11.12.0/21",
+ # out of range mask
+ "110.0.0.0/33",
+ "110.0.0.0/-1",
+ "4.0.0.0/111",
+ # out of range address
+ "310.0.0.0/24",
+ "10.0.0.256/32",
+ "1.1.-20.0/24",
+ # badly formed
+ "1.0.0.0/1e",
+ "1.0.0.0/24.0",
+ "1.0.0.0/1/1",
+ "1.0.0.0",
+ "1.c.0.0/24",
+ "1.2.0.0.0/27",
+ "1.23.0/24",
+ "1.23.0.-7/24",
+ "1.-23.0.7/24",
+ "1.23.-0.7/24",
+ "1.23.0.0/0x10",
+ # IPv6 insufficient zeros -- this could be a subtle one
+ # due to the vagaries of endianness in the 16 bit groups.
+ "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/119",
+ "aaaa:bbbb::/31",
+ "a:b::/31",
+ "c000::/1",
+ "a::b00/119",
+ "1::1/127",
+ "1::2/126",
+ "1::100/119",
+ "1::8000/112",
+ # out of range mask
+ "a:b::/130",
+ "a:b::/-1",
+ "::/129",
+ # An IPv4 address can't be exactly the bitmask (MS ADTS)
+ "128.0.0.0/1",
+ "192.0.0.0/2",
+ "255.192.0.0/10",
+ "255.255.255.0/24",
+ "255.255.255.255/32",
+ "0.0.0.0/0",
+ # The address can't have leading zeros (not RFC 4632, but MS ADTS)
+ "00.1.2.0/24",
+ "003.1.2.0/24",
+ "022.1.0.0/16",
+ "00000000000000000000000003.1.2.0/24",
+ "09876::abfc/126",
+ "0aaaa:bbbb::/32",
+ "009876::abfc/126",
+ "000a:bbbb::/32",
+
+ # How about extraneous zeros later on
+ "3.01.2.0/24",
+ "3.1.2.00/24",
+ "22.001.0.0/16",
+ "3.01.02.0/24",
+ "100a:0bbb:0023::/48",
+ "100a::0023/128",
+
+ # Windows doesn't like the zero IPv4 address
+ "0.0.0.0/8",
+ # or the zero mask on IPv6
+ "::/0",
+
+ # various violations of RFC5952
+ "0:0:0:0:0:0:0:0/8",
+ "0::0/0",
+ "::0:0/48",
+ "::0:4/128",
+ "0::/8",
+ "0::4f/128",
+ "0::42:0:0:0:0/64",
+ "4f::0/48",
+
+ # badly formed -- mostly the wrong arrangement of colons
+ "a::b::0/120",
+ "a::abcdf:0/120",
+ "a::g:0/120",
+ "::0::3/48",
+ "2001:3::110::3/118",
+ "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1111:0000/128",
+ "a:::5:0/120",
+
+ # non-canonical representations (vs RFC 5952)
+ # "2001:0:c633:63::1:0/120" is correct
+ "2001:0:c633:63:0:0:1:0/120",
+ "2001::c633:63:0:0:1:0/120",
+ "2001:0:c633:63:0:0:1::/120",
+
+ # "10:0:0:42::/64" is correct
+ "10::42:0:0:0:0/64",
+ "10:0:0:42:0:0:0:0/64",
+
+ # "1::4:5:0:0:8/127" is correct
+ "1:0:0:4:5:0:0:8/127",
+ "1:0:0:4:5::8/127",
+
+ # "2001:db8:0:1:1:1:1:1/128" is correct
+ "2001:db8::1:1:1:1:1/128",
+
+ # IP4 embedded - rejected
+ "a::10.0.0.0/120",
+ "a::10.9.8.7/128",
+
+ # The next ones tinker indirectly with IPv4 embedding,
+ # where Windows has some odd behaviour.
+ #
+ # Samba's libreplace inet_ntop6 expects IPv4 embedding
+ # with addresses in these forms:
+ #
+ # ::wx:yz
+ # ::FFFF:wx:yz
+ #
+ # these will be stringified with trailing dottted decimal, thus:
+ #
+ # ::w.x.y.z
+ # ::ffff:w.x.y.z
+ #
+ # and this will cause the address to be rejected by Samba,
+ # because it uses a inet_pton / inet_ntop round trip to
+ # ascertain correctness.
+
+ "::ffff:0:0/96", # this one fails on WIN2012r2
+ "::ffff:aaaa:a000/120",
+ "::ffff:10:0/120",
+ "::ffff:2:300/120",
+ "::3:0/120",
+ "::2:30/124",
+ "::ffff:2:30/124",
+
+ # completely wrong
+ None,
+ "bob",
+ 3.1415,
+ False,
+ "10.11.16.0/24\x00hidden bytes past a zero",
+ self,
+ ]
+
+ failures = []
+ for cidr in cidrs:
+ try:
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+ except subnets.SubnetInvalid:
+ print("%s fails properly" % (cidr,), file=sys.stderr)
+ continue
+
+ # we are here because it succeeded when it shouldn't have.
+ print("CIDR %s fails to fail" % (cidr,), file=sys.stderr)
+ failures.append(cidr)
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+
+ if failures:
+ print("These bad subnet names were accepted:")
+ for cidr in failures:
+ print(" %s" % cidr)
+ self.fail()
+
+ def test_create_good_ranges(self):
+ """All of these CIDRs are good, and the subnet creation should
+ succeed."""
+ basedn = self.ldb.get_config_basedn()
+
+ cidrs = [
+ # IPv4
+ "10.11.12.0/24",
+ "10.11.12.0/23",
+ "10.11.12.0/25",
+ "110.0.0.0/7",
+ "1.0.0.0/32",
+ "10.11.13.0/32",
+ "10.11.13.1/32",
+ "99.0.97.0/24",
+ "1.2.3.4/30",
+ "10.11.12.0/22",
+ "0.12.13.0/24",
+ # IPv6
+ "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/120",
+ "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:11f0/124",
+ "aaaa:bbbb:cccc:dddd:eeee:ffff:2222:11fc/126",
+ # don't forget upper case
+ "FFFF:FFFF:FFFF:FFFF:ABCD:EfFF:FFFF:FFeF/128",
+ "9876::ab00/120",
+ "9876::abf0/124",
+ "9876::abfc/126",
+ "aaaa:bbbb::/32",
+ "aaaa:bbba::/31",
+ "aaaa:ba00::/23",
+ "aaaa:bb00::/24",
+ "aaaa:bb00::/77",
+ "::/48",
+ "a:b::/32",
+ "c000::/2",
+ "a::b00/120",
+ "1::2/127",
+ # this pattern of address suffix == mask is forbidden with
+ # IPv4 but OK for IPv6.
+ "8000::/1",
+ "c000::/2",
+ "ffff:ffff:ffc0::/42",
+ "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF/128",
+ # leading zeros are forbidden, but implicit IPv6 zeros
+ # (via "::") are OK.
+ "::1000/116",
+ "::8000/113",
+ # taken to the logical conclusion, "::/0" should be OK, but no.
+ "::/48",
+
+ # Try some reserved ranges, which it might be reasonable
+ # to exclude, but which are not excluded in practice.
+ "129.0.0.0/16",
+ "129.255.0.0/16",
+ "100.64.0.0/10",
+ "127.0.0.0/8",
+ "127.0.0.0/24",
+ "169.254.0.0/16",
+ "169.254.1.0/24",
+ "192.0.0.0/24",
+ "192.0.2.0/24",
+ "198.18.0.0/15",
+ "198.51.100.0/24",
+ "203.0.113.0/24",
+ "224.0.0.0/4",
+ "130.129.0.0/16",
+ "130.255.0.0/16",
+ "192.12.0.0/24",
+ "223.255.255.0/24",
+ "240.255.255.0/24",
+ "224.0.0.0/8",
+ "::/96",
+ "100::/64",
+ "2001:10::/28",
+ "fec0::/10",
+ "ff00::/8",
+ "::1/128",
+ "2001:db8::/32",
+ "2001:10::/28",
+ "2002::/24",
+ "2002:a00::/24",
+ "2002:7f00::/24",
+ "2002:a9fe::/32",
+ "2002:ac10::/28",
+ "2002:c000::/40",
+ "2002:c000:200::/40",
+ "2002:c0a8::/32",
+ "2002:c612::/31",
+ "2002:c633:6400::/40",
+ "2002:cb00:7100::/40",
+ "2002:e000::/20",
+ "2002:f000::/20",
+ "2002:ffff:ffff::/48",
+ "2001::/40",
+ "2001:0:a00::/40",
+ "2001:0:7f00::/40",
+ "2001:0:a9fe::/48",
+ "2001:0:ac10::/44",
+ "2001:0:c000::/56",
+ "2001:0:c000:200::/56",
+ "2001:0:c0a8::/48",
+ "2001:0:c612::/47",
+ "2001:0:c633:6400::/56",
+ "2001:0:cb00:7100::/56",
+ "2001:0:e000::/36",
+ "2001:0:f000::/36",
+ "2001:0:ffff:ffff::/64",
+
+ # non-RFC-5952 versions of these are tested in create_bad_ranges
+ "2001:0:c633:63::1:0/120",
+ "10:0:0:42::/64",
+ "1::4:5:0:0:8/127",
+ "2001:db8:0:1:1:1:1:1/128",
+
+ # The "well-known prefix" 64::ff9b is another IPv4
+ # embedding scheme. Let's try that.
+ "64:ff9b::aaaa:aaaa/127",
+ "64:ff9b::/120",
+ "64:ff9b::ffff:2:3/128",
+ ]
+ failures = []
+
+ for cidr in cidrs:
+ try:
+ subnets.create_subnet(self.ldb, basedn, cidr, self.sitename)
+ except subnets.SubnetInvalid as e:
+ print(e)
+ failures.append(cidr)
+ continue
+
+ ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE,
+ expression=('(&(objectclass=subnet)(cn=%s))' %
+ cidr))
+
+ if len(ret) != 1:
+ print("%s was not created" % cidr)
+ failures.append(cidr)
+ continue
+ subnets.delete_subnet(self.ldb, basedn, cidr)
+
+ if failures:
+ print("These good subnet names were not accepted:")
+ for cidr in failures:
+ print(" %s" % cidr)
+ self.fail()
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/sort.py b/source4/dsdb/tests/python/sort.py
new file mode 100644
index 0000000..8c5ba4e
--- /dev/null
+++ b/source4/dsdb/tests/python/sort.py
@@ -0,0 +1,379 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Originally based on ./sam.py
+from unicodedata import normalize
+import locale
+locale.setlocale(locale.LC_ALL, ('en_US', 'UTF-8'))
+
+import optparse
+import sys
+import os
+import re
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions
+from samba.common import cmp
+from functools import cmp_to_key
+import samba.getopt as options
+
+from samba.auth import system_session
+import ldb
+from samba.samdb import SamDB
+
+parser = optparse.OptionParser("sort.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+
+parser.add_option('--elements', type='int', default=33,
+ help="use this many elements in the tests")
+
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+datadir = os.getenv("DATA_DIR", None)
+if not datadir:
+ print("Please specify the location of the sort expected results with env variable DATA_DIR")
+ sys.exit(1)
+
+host = os.getenv("SERVER", None)
+if not host:
+ print("Please specify the host with env variable SERVER")
+ sys.exit(1)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+def norm(x):
+ if not isinstance(x, str):
+ x = x.decode('utf8')
+ return normalize('NFKC', x).upper()
+
+
+# Python, Windows, and Samba all sort the following sequence in
+# drastically different ways. The order here is what you get from
+# Windows2012R2.
+FIENDISH_TESTS = [' ', ' e', '\t-\t', '\n\t\t', '!@#!@#!', '¼', '¹', '1',
+ '1/4', '1⁄4', '1\xe2\x81\x845', '3', 'abc', 'fo\x00od',
+
+ # Here we also had '\x00food', but that seems to sort
+ # non-deterministically on Windows vis-a-vis 'fo\x00od'.
+
+ 'kōkako', 'ŋđ¼³ŧ “«đð', 'ŋđ¼³ŧ“«đð',
+ 'sorttest', 'sorttēst11,', 'śorttest2', 'śoRttest2',
+ 'ś-o-r-t-t-e-s-t-2', 'soRTTēst2,', 'ṡorttest4', 'ṡorttesT4',
+ 'sörttest-5', 'sÖrttest-5', 'so-rttest7,', '桑巴']
+
+
+class BaseSortTests(samba.tests.TestCase):
+ avoid_tricky_sort = False
+ maxDiff = 2000
+
+ def create_user(self, i, n, prefix='sorttest', suffix='', attrs=None,
+ tricky=False):
+ name = "%s%d%s" % (prefix, i, suffix)
+ user = {
+ 'cn': name,
+ "objectclass": "user",
+ 'givenName': "abcdefghijklmnopqrstuvwxyz"[i % 26],
+ "roomNumber": "%sb\x00c" % (n - i),
+ # with python3 re.sub(r'[^\w,.]', repl, string) doesn't
+ # work as expected with unicode as value for carLicense
+ "carLicense": "XXXXXXXXX" if self.avoid_tricky_sort else "后来经",
+ "employeeNumber": "%s%sx" % (abs(i * (99 - i)), '\n' * (i & 255)),
+ "accountExpires": "%s" % (10 ** 9 + 1000000 * i),
+ "msTSExpireDate4": "19%02d0101010000.0Z" % (i % 100),
+ "flags": str(i * (n - i)),
+ "serialNumber": "abc %s%s%s" % ('AaBb |-/'[i & 7],
+ ' 3z}'[i & 3],
+ '"@'[i & 1],),
+ "comment": "Favourite colour is %d" % (n % (i + 1)),
+ }
+
+ if self.avoid_tricky_sort:
+ # We are not even going to try passing tests that assume
+ # some kind of Unicode awareness.
+ for k, v in user.items():
+ user[k] = re.sub(r'[^\w,.]', 'X', v)
+ else:
+ # Add some even trickier ones!
+ fiendish_index = i % len(FIENDISH_TESTS)
+ user.update({
+ # Sort doesn't look past a NUL byte.
+ "photo": "\x00%d" % (n - i),
+ "audio": "%sn octet string %s%s ♫♬\x00lalala" % ('Aa'[i & 1],
+ chr(i & 255),
+ i),
+ "displayNamePrintable": "%d\x00%c" % (i, i & 255),
+ "adminDisplayName": "%d\x00b" % (n - i),
+ "title": "%d%sb" % (n - i, '\x00' * i),
+
+ # Names that vary only in case. Windows returns
+ # equivalent addresses in the order they were put
+ # in ('a st', 'A st',...). We don't check that.
+ "street": "%s st" % (chr(65 | (i & 14) | ((i & 1) * 32))),
+
+ "streetAddress": FIENDISH_TESTS[fiendish_index],
+ "postalAddress": FIENDISH_TESTS[-fiendish_index],
+ })
+
+ if attrs is not None:
+ user.update(attrs)
+
+ user['dn'] = "cn=%s,%s" % (user['cn'], self.ou)
+
+ self.users.append(user)
+ self.ldb.add(user)
+ return user
+
+ def setUp(self):
+ super(BaseSortTests, self).setUp()
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+
+ self.base_dn = self.ldb.domain_dn()
+ self.ou = "ou=sort,%s" % self.base_dn
+ if False:
+ try:
+ self.ldb.delete(self.ou, ['tree_delete:1'])
+ except ldb.LdbError as e:
+ print("tried deleting %s, got error %s" % (self.ou, e))
+
+ self.ldb.add({
+ "dn": self.ou,
+ "objectclass": "organizationalUnit"})
+ self.users = []
+ n = opts.elements
+ for i in range(n):
+ self.create_user(i, n)
+
+ attrs = set(self.users[0].keys()) - set([
+ 'objectclass', 'dn'])
+ self.binary_sorted_keys = attrs.intersection(['audio',
+ 'photo',
+ "msTSExpireDate4",
+ 'serialNumber',
+ "displayNamePrintable"])
+
+ self.numeric_sorted_keys = attrs.intersection(['flags',
+ 'accountExpires'])
+
+ self.timestamp_keys = attrs.intersection(['msTSExpireDate4'])
+
+ self.int64_keys = set(['accountExpires'])
+
+ self.locale_sorted_keys = [x for x in attrs if
+ x not in (self.binary_sorted_keys |
+ self.numeric_sorted_keys)]
+
+ self.expected_results = {}
+ self.expected_results_binary = {}
+
+ for k in self.binary_sorted_keys:
+ forward = sorted((x[k] for x in self.users))
+ reverse = list(reversed(forward))
+ self.expected_results_binary[k] = (forward, reverse)
+
+ # FYI: Expected result data was generated from the old
+ # code that was manually sorting (while executing with
+ # python2)
+ # The resulting data was injected into the data file with
+ # code similar to:
+ #
+ # for k in self.expected_results:
+ # f.write("%s = %s\n" % (k, repr(self.expected_results[k][0])))
+
+ f = open(self.results_file, "r")
+ for line in f:
+ if len(line.split('=', 1)) == 2:
+ key = line.split('=', 1)[0].strip()
+ value = line.split('=', 1)[1].strip()
+ if value.startswith('['):
+ import ast
+ fwd_list = ast.literal_eval(value)
+ rev_list = list(reversed(fwd_list))
+ self.expected_results[key] = (fwd_list, rev_list)
+ f.close()
+ def tearDown(self):
+ super(BaseSortTests, self).tearDown()
+ self.ldb.delete(self.ou, ['tree_delete:1'])
+
+ def _test_server_sort_default(self):
+ attrs = self.locale_sorted_keys
+
+ for attr in attrs:
+ for rev in (0, 1):
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL, attrs=[attr],
+ controls=["server_sort:1:%d:%s" %
+ (rev, attr)])
+ self.assertEqual(len(res), len(self.users))
+
+ expected_order = self.expected_results[attr][rev]
+ received_order = [norm(x[attr][0]) for x in res]
+ if expected_order != received_order:
+ print(attr, ['forward', 'reverse'][rev])
+ print("expected", expected_order)
+ print("received", received_order)
+ print("unnormalised:", [x[attr][0] for x in res])
+ print("unnormalised: «%s»" % '» «'.join(str(x[attr][0])
+ for x in res))
+ self.assertEqual(expected_order, received_order)
+
+ def _test_server_sort_binary(self):
+ for attr in self.binary_sorted_keys:
+ for rev in (0, 1):
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL, attrs=[attr],
+ controls=["server_sort:1:%d:%s" %
+ (rev, attr)])
+
+ self.assertEqual(len(res), len(self.users))
+ expected_order = self.expected_results_binary[attr][rev]
+ received_order = [str(x[attr][0]) for x in res]
+ if expected_order != received_order:
+ print(attr)
+ print(expected_order)
+ print(received_order)
+ self.assertEqual(expected_order, received_order)
+
+ def _test_server_sort_us_english(self):
+ # Windows doesn't support many matching rules, but does allow
+ # the locale specific sorts -- if it has the locale installed.
+ # The most reliable locale is the default US English, which
+ # won't change the sort order.
+
+ for lang, oid in [('en_US', '1.2.840.113556.1.4.1499'),
+ ]:
+
+ for attr in self.locale_sorted_keys:
+ for rev in (0, 1):
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=["server_sort:1:%d:%s:%s" %
+ (rev, attr, oid)])
+
+ self.assertTrue(len(res) == len(self.users))
+ expected_order = self.expected_results[attr][rev]
+ received_order = [norm(x[attr][0]) for x in res]
+ if expected_order != received_order:
+ print(attr, lang)
+ print(['forward', 'reverse'][rev])
+ print("expected: ", expected_order)
+ print("received: ", received_order)
+ print("unnormalised:", [x[attr][0] for x in res])
+ print("unnormalised: «%s»" % '» «'.join(str(x[attr][0])
+ for x in res))
+
+ self.assertEqual(expected_order, received_order)
+
+ def _test_server_sort_different_attr(self):
+
+ def cmp_locale(a, b):
+ return locale.strcoll(a[0], b[0])
+
+ def cmp_binary(a, b):
+ return cmp(a[0], b[0])
+
+ def cmp_numeric(a, b):
+ return cmp(int(a[0]), int(b[0]))
+
+ # For testing simplicity, the attributes in here need to be
+ # unique for each user. Otherwise there are multiple possible
+ # valid answers.
+ sort_functions = {'cn': cmp_binary,
+ "employeeNumber": cmp_locale,
+ "accountExpires": cmp_numeric,
+ "msTSExpireDate4": cmp_binary}
+ attrs = list(sort_functions.keys())
+ attr_pairs = zip(attrs, attrs[1:] + attrs[:1])
+
+ for sort_attr, result_attr in attr_pairs:
+ forward = sorted(((norm(x[sort_attr]), norm(x[result_attr]))
+ for x in self.users),
+ key=cmp_to_key(sort_functions[sort_attr]))
+ reverse = list(reversed(forward))
+
+ for rev in (0, 1):
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[result_attr],
+ controls=["server_sort:1:%d:%s" %
+ (rev, sort_attr)])
+ self.assertEqual(len(res), len(self.users))
+ pairs = (forward, reverse)[rev]
+
+ expected_order = [x[1] for x in pairs]
+ received_order = [norm(x[result_attr][0]) for x in res]
+
+ if expected_order != received_order:
+ print(sort_attr, result_attr, ['forward', 'reverse'][rev])
+ print("expected", expected_order)
+ print("received", received_order)
+ print("unnormalised:", [x[result_attr][0] for x in res])
+ print("unnormalised: «%s»" % '» «'.join(str(x[result_attr][0])
+ for x in res))
+ print("pairs:", pairs)
+ # There are bugs in Windows that we don't want (or
+ # know how) to replicate regarding timestamp sorting.
+ # Let's remind ourselves.
+ if result_attr == "msTSExpireDate4":
+ print('-' * 72)
+ print("This test fails against Windows with the "
+ "default number of elements (33).")
+ print("Try with --elements=27 (or similar).")
+ print('-' * 72)
+
+ self.assertEqual(expected_order, received_order)
+ for x in res:
+ if sort_attr in x:
+ self.fail('the search for %s should not return %s' %
+ (result_attr, sort_attr))
+
+
+class SimpleSortTests(BaseSortTests):
+ avoid_tricky_sort = True
+ results_file = os.path.join(datadir, "simplesort.expected")
+ def test_server_sort_different_attr(self):
+ self._test_server_sort_different_attr()
+
+ def test_server_sort_default(self):
+ self._test_server_sort_default()
+
+ def test_server_sort_binary(self):
+ self._test_server_sort_binary()
+
+ def test_server_sort_us_english(self):
+ self._test_server_sort_us_english()
+
+
+class UnicodeSortTests(BaseSortTests):
+ avoid_tricky_sort = False
+ results_file = os.path.join(datadir, "unicodesort.expected")
+
+ def test_server_sort_default(self):
+ self._test_server_sort_default()
+
+ def test_server_sort_us_english(self):
+ self._test_server_sort_us_english()
+
+ def test_server_sort_different_attr(self):
+ self._test_server_sort_different_attr()
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
diff --git a/source4/dsdb/tests/python/subtree_rename.py b/source4/dsdb/tests/python/subtree_rename.py
new file mode 100644
index 0000000..2828fe5
--- /dev/null
+++ b/source4/dsdb/tests/python/subtree_rename.py
@@ -0,0 +1,422 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Originally based on ./sam.py
+import optparse
+import sys
+import os
+from time import time
+from binascii import hexlify
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+import ldb
+from samba.samdb import SamDB
+from samba.dcerpc import misc
+from samba import colour
+
+parser = optparse.OptionParser("subtree_rename.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+
+parser.add_option('--delete-in-setup', action='store_true',
+ help="cleanup in setup")
+
+parser.add_option('--no-cleanup', action='store_true',
+ help="don't cleanup in teardown")
+
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+
+def debug(*args, **kwargs):
+ kwargs['file'] = sys.stderr
+ print(*args, **kwargs)
+
+
+class SubtreeRenameTestException(Exception):
+ pass
+
+
+class SubtreeRenameTests(samba.tests.TestCase):
+
+ def delete_ous(self):
+ for ou in (self.ou1, self.ou2, self.ou3):
+ try:
+ self.samdb.delete(ou, ['tree_delete:1'])
+ except ldb.LdbError as e:
+ pass
+
+ def setUp(self):
+ super(SubtreeRenameTests, self).setUp()
+ self.samdb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+
+ self.base_dn = self.samdb.domain_dn()
+ self.ou1 = "OU=subtree1,%s" % self.base_dn
+ self.ou2 = "OU=subtree2,%s" % self.base_dn
+ self.ou3 = "OU=subtree3,%s" % self.base_dn
+ if opts.delete_in_setup:
+ self.delete_ous()
+ self.samdb.add({'objectclass': 'organizationalUnit',
+ 'dn': self.ou1})
+ self.samdb.add({'objectclass': 'organizationalUnit',
+ 'dn': self.ou2})
+
+ debug(colour.c_REV_RED(self.id()))
+
+ def tearDown(self):
+ super(SubtreeRenameTests, self).tearDown()
+ if not opts.no_cleanup:
+ self.delete_ous()
+
+ def add_object(self, cn, objectclass, ou=None, more_attrs=None):
+ if more_attrs is None:
+ more_attrs = {}
+
+ dn = "CN=%s,%s" % (cn, ou)
+ attrs = {'cn': cn,
+ 'objectclass': objectclass,
+ 'dn': dn}
+ attrs.update(more_attrs)
+ self.samdb.add(attrs)
+
+ return dn
+
+ def add_objects(self, n, objectclass, prefix=None, ou=None, more_attrs=None):
+ if more_attrs is None:
+ more_attrs = {}
+
+ if prefix is None:
+ prefix = objectclass
+ dns = []
+ for i in range(n):
+ dns.append(self.add_object("%s%d" % (prefix, i + 1),
+ objectclass,
+ more_attrs=more_attrs,
+ ou=ou))
+ return dns
+
+ def add_linked_attribute(self, src, dest, attr='member',
+ controls=None):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, src)
+ m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_ADD, attr)
+ self.samdb.modify(m, controls=controls)
+
+ def remove_linked_attribute(self, src, dest, attr='member',
+ controls=None):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, src)
+ m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_DELETE, attr)
+ self.samdb.modify(m, controls=controls)
+
+ def add_binary_link(self, src, dest, binary,
+ attr='msDS-RevealedUsers',
+ controls=None):
+ b = hexlify(str(binary).encode('utf-8')).decode('utf-8').upper()
+ dest = 'B:%d:%s:%s' % (len(b), b, dest)
+ self.add_linked_attribute(src, dest, attr, controls)
+ return dest
+
+ def remove_binary_link(self, src, dest, binary,
+ attr='msDS-RevealedUsers',
+ controls=None):
+ b = str(binary).encode('utf-8')
+ dest = 'B:%s:%s' % (hexlify(b), dest)
+ self.remove_linked_attribute(src, dest, attr, controls)
+
+ def replace_linked_attribute(self, src, dest, attr='member',
+ controls=None):
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.samdb, src)
+ m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_REPLACE, attr)
+ self.samdb.modify(m, controls=controls)
+
+ def attr_search(self, obj, attr, scope=ldb.SCOPE_BASE, **controls):
+
+ controls = ['%s:%d' % (k, int(v)) for k, v in controls.items()]
+
+ res = self.samdb.search(obj,
+ scope=scope,
+ attrs=[attr],
+ controls=controls)
+ return res
+
+ def assert_links(self, obj, expected, attr, msg='', **kwargs):
+ res = self.attr_search(obj, attr, **kwargs)
+
+ if len(expected) == 0:
+ if attr in res[0]:
+ self.fail("found attr '%s' in %s" % (attr, res[0]))
+ return
+
+ try:
+ results = [str(x) for x in res[0][attr]]
+ except KeyError:
+ self.fail("missing attr '%s' on %s" % (attr, obj))
+
+ expected = sorted(expected)
+ results = sorted(results)
+
+ if expected != results:
+ debug(msg)
+ debug("expected %s" % expected)
+ debug("received %s" % results)
+ debug("missing %s" % (sorted(set(expected) - set(results))))
+ debug("unexpected %s" % (sorted(set(results) - set(expected))))
+
+
+ self.assertEqual(results, expected)
+
+ def assert_back_links(self, obj, expected, attr='memberOf', **kwargs):
+ self.assert_links(obj, expected, attr=attr,
+ msg='%s back links do not match for %s' %
+ (attr, obj),
+ **kwargs)
+
+ def assert_forward_links(self, obj, expected, attr='member', **kwargs):
+ self.assert_links(obj, expected, attr=attr,
+ msg='%s forward links do not match for %s' %
+ (attr, obj),
+ **kwargs)
+
+ def get_object_guid(self, dn):
+ res = self.samdb.search(dn,
+ scope=ldb.SCOPE_BASE,
+ attrs=['objectGUID'])
+ return str(misc.GUID(res[0]['objectGUID'][0]))
+
+ def test_la_move_ou_tree(self):
+ tag = 'move_tree'
+
+ u1, u2 = self.add_objects(2, 'user', '%s_u_' % tag, ou=self.ou1)
+ g1, g2 = self.add_objects(2, 'group', '%s_g_' % tag, ou=self.ou1)
+ c1, c2, c3 = self.add_objects(3, 'computer',
+ '%s_c_' % tag,
+ ou=self.ou1)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g1, g2)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+ c1u1 = self.add_binary_link(c1, u1, 'a').replace(self.ou1, self.ou3)
+ c2u1 = self.add_binary_link(c2, u1, 'b').replace(self.ou1, self.ou3)
+ c3u1 = self.add_binary_link(c3, u1, 124.543).replace(self.ou1, self.ou3)
+ c1g1 = self.add_binary_link(c1, g1, 'd').replace(self.ou1, self.ou3)
+ c2g2 = self.add_binary_link(c2, g2, 'd').replace(self.ou1, self.ou3)
+ c2c1 = self.add_binary_link(c2, c1, 'd').replace(self.ou1, self.ou3)
+ c1u2 = self.add_binary_link(c1, u2, 'd').replace(self.ou1, self.ou3)
+ c1u1_2 = self.add_binary_link(c1, u1, 'b').replace(self.ou1, self.ou3)
+
+ self.assertRaisesLdbError(20,
+ "Attribute msDS-RevealedUsers already exists",
+ self.add_binary_link, c1, u2, 'd')
+
+ self.samdb.rename(self.ou1, self.ou3)
+ debug(colour.c_CYAN("rename FINISHED"))
+ u1, u2, g1, g2, c1, c2, c3 = [x.replace(self.ou1, self.ou3)
+ for x in (u1, u2, g1, g2, c1, c2, c3)]
+
+ self.samdb.delete(g2, ['tree_delete:1'])
+
+ self.assert_forward_links(g1, [u1])
+ self.assert_back_links(u1, [g1])
+ self.assert_back_links(u2, set())
+ self.assert_forward_links(c1, [c1u1, c1u1_2, c1u2, c1g1],
+ attr='msDS-RevealedUsers')
+ self.assert_forward_links(c2, [c2u1, c2c1], attr='msDS-RevealedUsers')
+ self.assert_forward_links(c3, [c3u1], attr='msDS-RevealedUsers')
+ self.assert_back_links(u1, [c1, c1, c2, c3], attr='msDS-RevealedDSAs')
+ self.assert_back_links(u2, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(g1, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(c1, [c2], attr='msDS-RevealedDSAs')
+
+ def test_la_move_ou_groups(self):
+ tag = 'move_groups'
+
+ u1, u2 = self.add_objects(2, 'user', '%s_u_' % tag, ou=self.ou2)
+ g1, g2 = self.add_objects(2, 'group', '%s_g_' % tag, ou=self.ou1)
+ c1, c2, c3 = self.add_objects(3, 'computer',
+ '%s_c_' % tag,
+ ou=self.ou1)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g1, g2)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+ c1u1 = self.add_binary_link(c1, u1, 'a').replace(self.ou1, self.ou3)
+ c2u1 = self.add_binary_link(c2, u1, 'b').replace(self.ou1, self.ou3)
+ c3u1 = self.add_binary_link(c3, u1, 124.543).replace(self.ou1, self.ou3)
+ c1g1 = self.add_binary_link(c1, g1, 'd').replace(self.ou1, self.ou3)
+ c2g2 = self.add_binary_link(c2, g2, 'd').replace(self.ou1, self.ou3)
+ c2c1 = self.add_binary_link(c2, c1, 'd').replace(self.ou1, self.ou3)
+ c1u2 = self.add_binary_link(c1, u2, 'd').replace(self.ou1, self.ou3)
+ c1u1_2 = self.add_binary_link(c1, u1, 'b').replace(self.ou1, self.ou3)
+
+ self.samdb.rename(self.ou1, self.ou3)
+ debug(colour.c_CYAN("rename FINISHED"))
+ u1, u2, g1, g2, c1, c2, c3 = [x.replace(self.ou1, self.ou3)
+ for x in (u1, u2, g1, g2, c1, c2, c3)]
+
+ self.samdb.delete(g2, ['tree_delete:1'])
+
+ self.assert_forward_links(g1, [u1])
+ self.assert_back_links(u1, [g1])
+ self.assert_back_links(u2, set())
+ self.assert_forward_links(c1, [c1u1, c1u1_2, c1u2, c1g1],
+ attr='msDS-RevealedUsers')
+ self.assert_forward_links(c2, [c2u1, c2c1], attr='msDS-RevealedUsers')
+ self.assert_forward_links(c3, [c3u1], attr='msDS-RevealedUsers')
+ self.assert_back_links(u1, [c1, c1, c2, c3], attr='msDS-RevealedDSAs')
+ self.assert_back_links(u2, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(g1, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(c1, [c2], attr='msDS-RevealedDSAs')
+
+ def test_la_move_ou_users(self):
+ tag = 'move_users'
+
+ u1, u2 = self.add_objects(2, 'user', '%s_u_' % tag, ou=self.ou1)
+ g1, g2 = self.add_objects(2, 'group', '%s_g_' % tag, ou=self.ou2)
+ c1, c2 = self.add_objects(2, 'computer', '%s_c_' % tag, ou=self.ou1)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g1, g2)
+ self.add_linked_attribute(g2, u1)
+ self.add_linked_attribute(g2, u2)
+ c1u1 = self.add_binary_link(c1, u1, 'a').replace(self.ou1, self.ou3)
+ c2u1 = self.add_binary_link(c2, u1, 'b').replace(self.ou1, self.ou3)
+ c1g1 = self.add_binary_link(c1, g1, 'd').replace(self.ou1, self.ou3)
+ c2g2 = self.add_binary_link(c2, g2, 'd').replace(self.ou1, self.ou3)
+ c2c1 = self.add_binary_link(c2, c1, 'd').replace(self.ou1, self.ou3)
+ c1u2 = self.add_binary_link(c1, u2, 'd').replace(self.ou1, self.ou3)
+ c1u1_2 = self.add_binary_link(c1, u1, 'b').replace(self.ou1, self.ou3)
+
+
+ self.samdb.rename(self.ou1, self.ou3)
+ debug(colour.c_CYAN("rename FINISHED"))
+ u1, u2, g1, g2, c1, c2 = [x.replace(self.ou1, self.ou3)
+ for x in (u1, u2, g1, g2, c1, c2)]
+
+ self.samdb.delete(g2, ['tree_delete:1'])
+
+ self.assert_forward_links(g1, [u1])
+ self.assert_back_links(u1, [g1])
+ self.assert_back_links(u2, set())
+ self.assert_forward_links(c1, [c1u1, c1u1_2, c1u2, c1g1],
+ attr='msDS-RevealedUsers')
+ self.assert_forward_links(c2, [c2u1, c2c1], attr='msDS-RevealedUsers')
+ self.assert_back_links(u1, [c1, c1, c2], attr='msDS-RevealedDSAs')
+ self.assert_back_links(u2, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(g1, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(c1, [c2], attr='msDS-RevealedDSAs')
+
+ def test_la_move_ou_noncomputers(self):
+ """Here we are especially testing the msDS-RevealedDSAs links"""
+ tag = 'move_noncomputers'
+
+ u1, u2 = self.add_objects(2, 'user', '%s_u_' % tag, ou=self.ou1)
+ g1, g2 = self.add_objects(2, 'group', '%s_g_' % tag, ou=self.ou1)
+ c1, c2, c3 = self.add_objects(3, 'computer', '%s_c_' % tag, ou=self.ou2)
+
+ self.add_linked_attribute(g1, u1)
+ self.add_linked_attribute(g1, g2)
+ c1u1 = self.add_binary_link(c1, u1, 'a').replace(self.ou1, self.ou3)
+ c2u1 = self.add_binary_link(c2, u1, 'b').replace(self.ou1, self.ou3)
+ c2u1_2 = self.add_binary_link(c2, u1, 'c').replace(self.ou1, self.ou3)
+ c3u1 = self.add_binary_link(c3, g1, 'b').replace(self.ou1, self.ou3)
+ c1g1 = self.add_binary_link(c1, g1, 'd').replace(self.ou1, self.ou3)
+ c2g2 = self.add_binary_link(c2, g2, 'd').replace(self.ou1, self.ou3)
+ c2c1 = self.add_binary_link(c2, c1, 'd').replace(self.ou1, self.ou3)
+ c1u2 = self.add_binary_link(c1, u2, 'd').replace(self.ou1, self.ou3)
+ c1u1_2 = self.add_binary_link(c1, u1, 'b').replace(self.ou1, self.ou3)
+ c1u1_3 = self.add_binary_link(c1, u1, 'c').replace(self.ou1, self.ou3)
+ c2u1_3 = self.add_binary_link(c2, u1, 'e').replace(self.ou1, self.ou3)
+ c3u2 = self.add_binary_link(c3, u2, 'b').replace(self.ou1, self.ou3)
+
+ self.samdb.rename(self.ou1, self.ou3)
+ debug(colour.c_CYAN("rename FINISHED"))
+ u1, u2, g1, g2, c1, c2, c3 = [x.replace(self.ou1, self.ou3)
+ for x in (u1, u2, g1, g2, c1, c2, c3)]
+
+ self.samdb.delete(c3, ['tree_delete:1'])
+
+ self.assert_forward_links(g1, [g2, u1])
+ self.assert_back_links(u1, [g1])
+ self.assert_back_links(u2, [])
+ self.assert_forward_links(c1, [c1u1, c1u1_2, c1u1_3, c1u2, c1g1],
+ attr='msDS-RevealedUsers')
+ self.assert_forward_links(c2, [c2u1, c2u1_2, c2u1_3, c2c1, c2g2],
+ attr='msDS-RevealedUsers')
+ self.assert_back_links(u1, [c1, c1, c1, c2, c2, c2],
+ attr='msDS-RevealedDSAs')
+ self.assert_back_links(u2, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(g1, [c1], attr='msDS-RevealedDSAs')
+ self.assert_back_links(c1, [c2], attr='msDS-RevealedDSAs')
+
+ def test_la_move_ou_tree_big(self):
+ tag = 'move_ou_big'
+ USERS, GROUPS, COMPUTERS = 50, 10, 7
+
+ users = self.add_objects(USERS, 'user', '%s_u_' % tag, ou=self.ou1)
+ groups = self.add_objects(GROUPS, 'group', '%s_g_' % tag, ou=self.ou1)
+ computers = self.add_objects(COMPUTERS, 'computer', '%s_c_' % tag,
+ ou=self.ou1)
+
+ start = time()
+ for i in range(USERS):
+ u = users[i]
+ for j in range(i % GROUPS):
+ g = groups[j]
+ self.add_linked_attribute(g, u)
+ for j in range(i % COMPUTERS):
+ c = computers[j]
+ self.add_binary_link(c, u, 'a')
+
+ debug("linking took %.3fs" % (time() - start))
+ start = time()
+ self.samdb.rename(self.ou1, self.ou3)
+ debug("rename ou took %.3fs" % (time() - start))
+
+ g1 = groups[0].replace(self.ou1, self.ou3)
+ start = time()
+ self.samdb.rename(g1, g1.replace(self.ou3, self.ou2))
+ debug("rename group took %.3fs" % (time() - start))
+
+ u1 = users[0].replace(self.ou1, self.ou3)
+ start = time()
+ self.samdb.rename(u1, u1.replace(self.ou3, self.ou2))
+ debug("rename user took %.3fs" % (time() - start))
+
+ c1 = computers[0].replace(self.ou1, self.ou3)
+ start = time()
+ self.samdb.rename(c1, c1.replace(self.ou3, self.ou2))
+ debug("rename computer took %.3fs" % (time() - start))
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/testdata/modify_order_account_locality_device-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_account_locality_device-non-admin.expected
new file mode 100644
index 0000000..572ed5e
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_account_locality_device-non-admin.expected
@@ -0,0 +1,31 @@
+modify_order_account_locality_device-non-admin
+initial attrs:
+ objectclass: 'account'
+ l: 'a'
+== result ===[ 6]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ objectclass replace ['device', 'top']
+ l delete a
+ owner add c
+----------------------------------
+ objectclass replace ['device', 'top']
+ owner add c
+ l delete a
+----------------------------------
+ l delete a
+ objectclass replace ['device', 'top']
+ owner add c
+----------------------------------
+ l delete a
+ owner add c
+ objectclass replace ['device', 'top']
+----------------------------------
+ owner add c
+ objectclass replace ['device', 'top']
+ l delete a
+----------------------------------
+ owner add c
+ l delete a
+ objectclass replace ['device', 'top']
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_account_locality_device.expected b/source4/dsdb/tests/python/testdata/modify_order_account_locality_device.expected
new file mode 100644
index 0000000..dc5e162
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_account_locality_device.expected
@@ -0,0 +1,34 @@
+modify_order_account_locality_device
+initial attrs:
+ objectclass: 'account'
+ l: 'a'
+== result ===[ 3]=======================
+ERR_CONSTRAINT_VIOLATION (19)
+-- operations ---------------------------
+ l delete a
+ owner add c
+ objectclass replace ['device', 'top']
+----------------------------------
+ owner add c
+ objectclass replace ['device', 'top']
+ l delete a
+----------------------------------
+ owner add c
+ l delete a
+ objectclass replace ['device', 'top']
+----------------------------------
+== result ===[ 3]=======================
+ERR_OBJECT_CLASS_VIOLATION (65)
+-- operations ---------------------------
+ objectclass replace ['device', 'top']
+ l delete a
+ owner add c
+----------------------------------
+ objectclass replace ['device', 'top']
+ owner add c
+ l delete a
+----------------------------------
+ l delete a
+ objectclass replace ['device', 'top']
+ owner add c
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_container_flags-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_container_flags-non-admin.expected
new file mode 100644
index 0000000..9a46588
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_container_flags-non-admin.expected
@@ -0,0 +1,129 @@
+modify_order_container_flags-non-admin
+initial attrs:
+ objectclass: 'container'
+== result ===[ 12]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ flags add 0x6
+ flags add 5
+ flags delete c
+ flags replace 101
+----------------------------------
+ flags add 0x6
+ flags replace 101
+ flags delete c
+ flags add 5
+----------------------------------
+ flags add 0x6
+ flags delete c
+ flags add 5
+ flags replace 101
+----------------------------------
+ flags add 0x6
+ flags delete c
+ flags replace 101
+ flags add 5
+----------------------------------
+ flags add 5
+ flags add 0x6
+ flags delete c
+ flags replace 101
+----------------------------------
+ flags add 5
+ flags delete c
+ flags add 0x6
+ flags replace 101
+----------------------------------
+ flags replace 101
+ flags add 0x6
+ flags delete c
+ flags add 5
+----------------------------------
+ flags replace 101
+ flags delete c
+ flags add 0x6
+ flags add 5
+----------------------------------
+ flags delete c
+ flags add 0x6
+ flags add 5
+ flags replace 101
+----------------------------------
+ flags delete c
+ flags add 0x6
+ flags replace 101
+ flags add 5
+----------------------------------
+ flags delete c
+ flags add 5
+ flags add 0x6
+ flags replace 101
+----------------------------------
+ flags delete c
+ flags replace 101
+ flags add 0x6
+ flags add 5
+----------------------------------
+== result ===[ 12]=======================
+ERR_INVALID_ATTRIBUTE_SYNTAX (21)
+-- operations ---------------------------
+ flags add 0x6
+ flags add 5
+ flags replace 101
+ flags delete c
+----------------------------------
+ flags add 0x6
+ flags replace 101
+ flags add 5
+ flags delete c
+----------------------------------
+ flags add 5
+ flags add 0x6
+ flags replace 101
+ flags delete c
+----------------------------------
+ flags add 5
+ flags replace 101
+ flags add 0x6
+ flags delete c
+----------------------------------
+ flags add 5
+ flags replace 101
+ flags delete c
+ flags add 0x6
+----------------------------------
+ flags add 5
+ flags delete c
+ flags replace 101
+ flags add 0x6
+----------------------------------
+ flags replace 101
+ flags add 0x6
+ flags add 5
+ flags delete c
+----------------------------------
+ flags replace 101
+ flags add 5
+ flags add 0x6
+ flags delete c
+----------------------------------
+ flags replace 101
+ flags add 5
+ flags delete c
+ flags add 0x6
+----------------------------------
+ flags replace 101
+ flags delete c
+ flags add 5
+ flags add 0x6
+----------------------------------
+ flags delete c
+ flags add 5
+ flags replace 101
+ flags add 0x6
+----------------------------------
+ flags delete c
+ flags replace 101
+ flags add 5
+ flags add 0x6
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_container_flags.expected b/source4/dsdb/tests/python/testdata/modify_order_container_flags.expected
new file mode 100644
index 0000000..eee3c52
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_container_flags.expected
@@ -0,0 +1,134 @@
+modify_order_container_flags
+initial attrs:
+ objectclass: 'container'
+== result ===[ 6]=======================
+ flags: [b'101']
+ objectClass: [b'container', b'top']
+-- operations ---------------------------
+ flags add 0x6
+ flags add 5
+ flags delete c
+ flags replace 101
+----------------------------------
+ flags add 0x6
+ flags delete c
+ flags add 5
+ flags replace 101
+----------------------------------
+ flags add 5
+ flags add 0x6
+ flags delete c
+ flags replace 101
+----------------------------------
+ flags add 5
+ flags delete c
+ flags add 0x6
+ flags replace 101
+----------------------------------
+ flags delete c
+ flags add 0x6
+ flags add 5
+ flags replace 101
+----------------------------------
+ flags delete c
+ flags add 5
+ flags add 0x6
+ flags replace 101
+----------------------------------
+== result ===[ 6]=======================
+ flags: [b'5']
+ objectClass: [b'container', b'top']
+-- operations ---------------------------
+ flags add 0x6
+ flags replace 101
+ flags delete c
+ flags add 5
+----------------------------------
+ flags add 0x6
+ flags delete c
+ flags replace 101
+ flags add 5
+----------------------------------
+ flags replace 101
+ flags add 0x6
+ flags delete c
+ flags add 5
+----------------------------------
+ flags replace 101
+ flags delete c
+ flags add 0x6
+ flags add 5
+----------------------------------
+ flags delete c
+ flags add 0x6
+ flags replace 101
+ flags add 5
+----------------------------------
+ flags delete c
+ flags replace 101
+ flags add 0x6
+ flags add 5
+----------------------------------
+== result ===[ 12]=======================
+ERR_INVALID_ATTRIBUTE_SYNTAX (21)
+-- operations ---------------------------
+ flags add 0x6
+ flags add 5
+ flags replace 101
+ flags delete c
+----------------------------------
+ flags add 0x6
+ flags replace 101
+ flags add 5
+ flags delete c
+----------------------------------
+ flags add 5
+ flags add 0x6
+ flags replace 101
+ flags delete c
+----------------------------------
+ flags add 5
+ flags replace 101
+ flags add 0x6
+ flags delete c
+----------------------------------
+ flags add 5
+ flags replace 101
+ flags delete c
+ flags add 0x6
+----------------------------------
+ flags add 5
+ flags delete c
+ flags replace 101
+ flags add 0x6
+----------------------------------
+ flags replace 101
+ flags add 0x6
+ flags add 5
+ flags delete c
+----------------------------------
+ flags replace 101
+ flags add 5
+ flags add 0x6
+ flags delete c
+----------------------------------
+ flags replace 101
+ flags add 5
+ flags delete c
+ flags add 0x6
+----------------------------------
+ flags replace 101
+ flags delete c
+ flags add 5
+ flags add 0x6
+----------------------------------
+ flags delete c
+ flags add 5
+ flags replace 101
+ flags add 0x6
+----------------------------------
+ flags delete c
+ flags replace 101
+ flags add 5
+ flags add 0x6
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue-non-admin.expected
new file mode 100644
index 0000000..c6f89c0
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue-non-admin.expected
@@ -0,0 +1,127 @@
+modify_order_container_flags_multivalue-non-admin
+initial attrs:
+ objectclass: 'container'
+ wWWHomePage: 'a'
+== result ===[ 24]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ flags add ['0', '1']
+ flags add 65355
+ flags delete 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags add ['0', '1']
+ flags add 65355
+ flags replace ['2', '101']
+ flags delete 65355
+----------------------------------
+ flags add ['0', '1']
+ flags delete 65355
+ flags add 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags add ['0', '1']
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add 65355
+----------------------------------
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags add 65355
+ flags delete 65355
+----------------------------------
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add 65355
+----------------------------------
+ flags add 65355
+ flags add ['0', '1']
+ flags delete 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags add 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags delete 65355
+----------------------------------
+ flags add 65355
+ flags delete 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+----------------------------------
+ flags add 65355
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+----------------------------------
+ flags add 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags delete 65355
+----------------------------------
+ flags add 65355
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add ['0', '1']
+----------------------------------
+ flags delete 65355
+ flags add ['0', '1']
+ flags add 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags delete 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags add 65355
+----------------------------------
+ flags delete 65355
+ flags add 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+----------------------------------
+ flags delete 65355
+ flags add 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+----------------------------------
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags add 65355
+----------------------------------
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add 65355
+ flags add ['0', '1']
+----------------------------------
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags add 65355
+ flags delete 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags delete 65355
+ flags add 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags add 65355
+ flags add ['0', '1']
+ flags delete 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags add 65355
+ flags delete 65355
+ flags add ['0', '1']
+----------------------------------
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add ['0', '1']
+ flags add 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add 65355
+ flags add ['0', '1']
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue.expected b/source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue.expected
new file mode 100644
index 0000000..99ee5a7
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_container_flags_multivalue.expected
@@ -0,0 +1,138 @@
+modify_order_container_flags_multivalue
+initial attrs:
+ objectclass: 'container'
+ wWWHomePage: 'a'
+== result ===[ 6]=======================
+ flags: [b'65355']
+ objectClass: [b'container', b'top']
+ wWWHomePage: [b'a']
+-- operations ---------------------------
+ flags add ['0', '1']
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add 65355
+----------------------------------
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add 65355
+----------------------------------
+ flags delete 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags add 65355
+----------------------------------
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags add 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags delete 65355
+ flags add 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add ['0', '1']
+ flags add 65355
+----------------------------------
+== result ===[ 6]=======================
+ERR_ATTRIBUTE_OR_VALUE_EXISTS (20)
+-- operations ---------------------------
+ flags add 65355
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+----------------------------------
+ flags add 65355
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add ['0', '1']
+----------------------------------
+ flags delete 65355
+ flags add 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+----------------------------------
+ flags delete 65355
+ flags replace ['2', '101']
+ flags add 65355
+ flags add ['0', '1']
+----------------------------------
+ flags replace ['2', '101']
+ flags add 65355
+ flags delete 65355
+ flags add ['0', '1']
+----------------------------------
+ flags replace ['2', '101']
+ flags delete 65355
+ flags add 65355
+ flags add ['0', '1']
+----------------------------------
+== result ===[ 6]=======================
+ERR_CONSTRAINT_VIOLATION (19)
+-- operations ---------------------------
+ flags add ['0', '1']
+ flags add 65355
+ flags delete 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags add ['0', '1']
+ flags delete 65355
+ flags add 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags add 65355
+ flags add ['0', '1']
+ flags delete 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags add 65355
+ flags delete 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+----------------------------------
+ flags delete 65355
+ flags add ['0', '1']
+ flags add 65355
+ flags replace ['2', '101']
+----------------------------------
+ flags delete 65355
+ flags add 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+----------------------------------
+== result ===[ 6]=======================
+ERR_NO_SUCH_ATTRIBUTE (16)
+-- operations ---------------------------
+ flags add ['0', '1']
+ flags add 65355
+ flags replace ['2', '101']
+ flags delete 65355
+----------------------------------
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags add 65355
+ flags delete 65355
+----------------------------------
+ flags add 65355
+ flags add ['0', '1']
+ flags replace ['2', '101']
+ flags delete 65355
+----------------------------------
+ flags add 65355
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags delete 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags add ['0', '1']
+ flags add 65355
+ flags delete 65355
+----------------------------------
+ flags replace ['2', '101']
+ flags add 65355
+ flags add ['0', '1']
+ flags delete 65355
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_inapplicable-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_inapplicable-non-admin.expected
new file mode 100644
index 0000000..0adb093
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_inapplicable-non-admin.expected
@@ -0,0 +1,31 @@
+modify_order_inapplicable-non-admin
+initial attrs:
+ objectclass: 'user'
+ givenName: 'a'
+== result ===[ 6]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ dhcpSites replace b
+ dhcpSites delete b
+ dhcpSites add c
+----------------------------------
+ dhcpSites replace b
+ dhcpSites add c
+ dhcpSites delete b
+----------------------------------
+ dhcpSites delete b
+ dhcpSites replace b
+ dhcpSites add c
+----------------------------------
+ dhcpSites delete b
+ dhcpSites add c
+ dhcpSites replace b
+----------------------------------
+ dhcpSites add c
+ dhcpSites replace b
+ dhcpSites delete b
+----------------------------------
+ dhcpSites add c
+ dhcpSites delete b
+ dhcpSites replace b
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_inapplicable.expected b/source4/dsdb/tests/python/testdata/modify_order_inapplicable.expected
new file mode 100644
index 0000000..f16ef8c
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_inapplicable.expected
@@ -0,0 +1,34 @@
+modify_order_inapplicable
+initial attrs:
+ objectclass: 'user'
+ givenName: 'a'
+== result ===[ 2]=======================
+ERR_NO_SUCH_ATTRIBUTE (16)
+-- operations ---------------------------
+ dhcpSites replace b
+ dhcpSites add c
+ dhcpSites delete b
+----------------------------------
+ dhcpSites add c
+ dhcpSites replace b
+ dhcpSites delete b
+----------------------------------
+== result ===[ 4]=======================
+ERR_OBJECT_CLASS_VIOLATION (65)
+-- operations ---------------------------
+ dhcpSites replace b
+ dhcpSites delete b
+ dhcpSites add c
+----------------------------------
+ dhcpSites delete b
+ dhcpSites replace b
+ dhcpSites add c
+----------------------------------
+ dhcpSites delete b
+ dhcpSites add c
+ dhcpSites replace b
+----------------------------------
+ dhcpSites add c
+ dhcpSites delete b
+ dhcpSites replace b
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_member-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_member-non-admin.expected
new file mode 100644
index 0000000..ea7d26b
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_member-non-admin.expected
@@ -0,0 +1,127 @@
+modify_order_member-non-admin
+initial attrs:
+ objectclass: 'group'
+ member: 'cn=modify_order_member_other_group,{base dn}'
+== result ===[ 24]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ member delete cn=ldaptest_modify_order_member-non-admin_0,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_0,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_0,cn=users,{base dn}
+----------------------------------
+ member delete cn=ldaptest_modify_order_member-non-admin_1,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_1,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_1,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member delete cn=ldaptest_modify_order_member-non-admin_2,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_2,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_2,cn=users,{base dn}
+----------------------------------
+ member delete cn=ldaptest_modify_order_member-non-admin_3,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_3,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_3,cn=users,{base dn}
+----------------------------------
+ member delete cn=ldaptest_modify_order_member-non-admin_4,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_4,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_4,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member delete cn=ldaptest_modify_order_member-non-admin_5,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_5,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_5,cn=users,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member-non-admin_6,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_6,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_6,cn=users,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member-non-admin_7,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_7,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_7,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member-non-admin_8,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_8,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_8,cn=users,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member-non-admin_9,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_9,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_9,cn=users,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member-non-admin_10,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_10,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_10,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member-non-admin_11,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_11,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_11,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_12,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_12,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_12,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_13,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_13,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_13,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_14,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_14,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_14,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_15,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_15,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_15,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_16,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_16,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_16,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member-non-admin_17,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_17,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_17,cn=users,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member-non-admin_18,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_18,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_18,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member-non-admin_19,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_19,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_19,cn=users,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member-non-admin_20,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_20,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_20,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member-non-admin_21,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_21,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_21,cn=users,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member-non-admin_22,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_22,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_22,cn=users,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member-non-admin_23,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member-non-admin_23,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member-non-admin_23,cn=users,{base dn}
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_member.expected b/source4/dsdb/tests/python/testdata/modify_order_member.expected
new file mode 100644
index 0000000..1882c34
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_member.expected
@@ -0,0 +1,190 @@
+modify_order_member
+initial attrs:
+ objectclass: 'group'
+ member: 'cn=modify_order_member_other_group,{base dn}'
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_0,CN=Users,{base dn}', b'CN=modify_order_member_other_group,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_0,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=ldaptest_modify_order_member_0,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_0,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member_0,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_12,CN=Users,{base dn}', b'CN=modify_order_member_other_group,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_12,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member_12,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_12,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_12,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_13,CN=Users,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_13,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member_13,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_13,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_13,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_14,CN=Users,{base dn}', b'CN=modify_order_member_other_group,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_14,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member_14,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_14,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_14,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_16,CN=Users,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_16,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member_16,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_16,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_16,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_19,CN=Users,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_19,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member add cn=ldaptest_modify_order_member_19,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_19,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member_19,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_2,CN=Users,{base dn}', b'CN=modify_order_member_other_group,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_2,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=ldaptest_modify_order_member_2,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member_2,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_2,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_22,CN=Users,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_22,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member add cn=ldaptest_modify_order_member_22,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member_22,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_22,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_3,CN=Users,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_3,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=ldaptest_modify_order_member_3,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member_3,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_3,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_5,CN=Users,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_5,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=ldaptest_modify_order_member_5,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_5,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member_5,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_6,CN=Users,{base dn}', b'CN=modify_order_member_other_group,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_6,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member replace cn=ldaptest_modify_order_member_6,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_6,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member_6,cn=users,{base dn}
+----------------------------------
+== result ===[ 1]=======================
+ member: [b'CN=ldaptest_modify_order_member_8,CN=Users,{base dn}', b'CN=modify_order_member_other_group,{base dn}']
+ memberOf: [b'CN=ldaptest_modify_order_member_8,CN=Users,{base dn}']
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member replace cn=ldaptest_modify_order_member_8,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member_8,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_8,cn=users,{base dn}
+----------------------------------
+== result ===[ 6]=======================
+ objectClass: [b'group', b'top']
+-- operations ---------------------------
+ member delete cn=ldaptest_modify_order_member_1,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_1,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_1,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member delete cn=ldaptest_modify_order_member_4,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_4,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_4,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member_7,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_7,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_7,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member_10,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_10,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_10,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member_18,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_18,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_18,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member_20,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_20,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_20,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+----------------------------------
+== result ===[ 6]=======================
+ERR_UNWILLING_TO_PERFORM (53)
+-- operations ---------------------------
+ member replace cn=ldaptest_modify_order_member_9,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member_9,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_9,cn=users,{base dn}
+----------------------------------
+ member replace cn=ldaptest_modify_order_member_11,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_11,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member_11,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member_15,cn=users,{base dn}
+ member add cn=ldaptest_modify_order_member_15,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_15,cn=users,{base dn}
+----------------------------------
+ member delete cn=modify_order_member_other_group,{base dn}
+ member add cn=ldaptest_modify_order_member_17,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_17,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_17,cn=users,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member_21,cn=users,{base dn}
+ member replace cn=ldaptest_modify_order_member_21,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member delete cn=ldaptest_modify_order_member_21,cn=users,{base dn}
+----------------------------------
+ member add cn=ldaptest_modify_order_member_23,cn=users,{base dn}
+ member delete cn=modify_order_member_other_group,{base dn}
+ member replace cn=ldaptest_modify_order_member_23,cn=users,{base dn}
+ member delete cn=ldaptest_modify_order_member_23,cn=users,{base dn}
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_mixed-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_mixed-non-admin.expected
new file mode 100644
index 0000000..544c31c
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_mixed-non-admin.expected
@@ -0,0 +1,128 @@
+modify_order_mixed-non-admin
+initial attrs:
+ objectclass: 'user'
+ carLicense: ['1', '2', '3']
+ otherTelephone: '123'
+== result ===[ 24]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ carLicense delete 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense delete 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 3
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense delete 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 3
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense delete 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense delete 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 3
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense delete 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense delete 3
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_mixed.expected b/source4/dsdb/tests/python/testdata/modify_order_mixed.expected
new file mode 100644
index 0000000..d80f572
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_mixed.expected
@@ -0,0 +1,143 @@
+modify_order_mixed
+initial attrs:
+ objectclass: 'user'
+ carLicense: ['1', '2', '3']
+ otherTelephone: '123'
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2', b'3', b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 3
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2', b'3', b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'4']
+-- operations ---------------------------
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 3
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 3
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense delete 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense delete 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense delete 3
+----------------------------------
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'4']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense delete 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense delete 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense delete 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense delete 3
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_mixed2-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_mixed2-non-admin.expected
new file mode 100644
index 0000000..7f812cc
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_mixed2-non-admin.expected
@@ -0,0 +1,128 @@
+modify_order_mixed2-non-admin
+initial attrs:
+ objectclass: 'user'
+ carLicense: ['1', '2', '3']
+ ipPhone: '123'
+== result ===[ 24]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ carLicense delete 3
+ carLicense add 4
+ ipPhone replace 4
+ ipPhone delete 123
+----------------------------------
+ carLicense delete 3
+ carLicense add 4
+ ipPhone delete 123
+ ipPhone replace 4
+----------------------------------
+ carLicense delete 3
+ ipPhone replace 4
+ carLicense add 4
+ ipPhone delete 123
+----------------------------------
+ carLicense delete 3
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense add 4
+----------------------------------
+ carLicense delete 3
+ ipPhone delete 123
+ carLicense add 4
+ ipPhone replace 4
+----------------------------------
+ carLicense delete 3
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense add 4
+----------------------------------
+ carLicense add 4
+ carLicense delete 3
+ ipPhone replace 4
+ ipPhone delete 123
+----------------------------------
+ carLicense add 4
+ carLicense delete 3
+ ipPhone delete 123
+ ipPhone replace 4
+----------------------------------
+ carLicense add 4
+ ipPhone replace 4
+ carLicense delete 3
+ ipPhone delete 123
+----------------------------------
+ carLicense add 4
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense delete 3
+----------------------------------
+ carLicense add 4
+ ipPhone delete 123
+ carLicense delete 3
+ ipPhone replace 4
+----------------------------------
+ carLicense add 4
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense delete 3
+----------------------------------
+ ipPhone replace 4
+ carLicense delete 3
+ carLicense add 4
+ ipPhone delete 123
+----------------------------------
+ ipPhone replace 4
+ carLicense delete 3
+ ipPhone delete 123
+ carLicense add 4
+----------------------------------
+ ipPhone replace 4
+ carLicense add 4
+ carLicense delete 3
+ ipPhone delete 123
+----------------------------------
+ ipPhone replace 4
+ carLicense add 4
+ ipPhone delete 123
+ carLicense delete 3
+----------------------------------
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense add 4
+ carLicense delete 3
+----------------------------------
+ ipPhone delete 123
+ carLicense delete 3
+ carLicense add 4
+ ipPhone replace 4
+----------------------------------
+ ipPhone delete 123
+ carLicense delete 3
+ ipPhone replace 4
+ carLicense add 4
+----------------------------------
+ ipPhone delete 123
+ carLicense add 4
+ carLicense delete 3
+ ipPhone replace 4
+----------------------------------
+ ipPhone delete 123
+ carLicense add 4
+ ipPhone replace 4
+ carLicense delete 3
+----------------------------------
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense add 4
+ carLicense delete 3
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_mixed2.expected b/source4/dsdb/tests/python/testdata/modify_order_mixed2.expected
new file mode 100644
index 0000000..3500a8c
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_mixed2.expected
@@ -0,0 +1,143 @@
+modify_order_mixed2
+initial attrs:
+ objectclass: 'user'
+ carLicense: ['1', '2', '3']
+ ipPhone: '123'
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2', b'3', b'4']
+ ipPhone: [b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense delete 3
+ carLicense add 4
+ ipPhone delete 123
+ ipPhone replace 4
+----------------------------------
+ carLicense delete 3
+ ipPhone delete 123
+ carLicense add 4
+ ipPhone replace 4
+----------------------------------
+ carLicense delete 3
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense add 4
+----------------------------------
+ ipPhone delete 123
+ carLicense delete 3
+ carLicense add 4
+ ipPhone replace 4
+----------------------------------
+ ipPhone delete 123
+ carLicense delete 3
+ ipPhone replace 4
+ carLicense add 4
+----------------------------------
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2', b'3', b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense delete 3
+ carLicense add 4
+ ipPhone replace 4
+ ipPhone delete 123
+----------------------------------
+ carLicense delete 3
+ ipPhone replace 4
+ carLicense add 4
+ ipPhone delete 123
+----------------------------------
+ carLicense delete 3
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense add 4
+----------------------------------
+ ipPhone replace 4
+ carLicense delete 3
+ carLicense add 4
+ ipPhone delete 123
+----------------------------------
+ ipPhone replace 4
+ carLicense delete 3
+ ipPhone delete 123
+ carLicense add 4
+----------------------------------
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense delete 3
+ carLicense add 4
+----------------------------------
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2']
+ ipPhone: [b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense delete 3
+ ipPhone delete 123
+ ipPhone replace 4
+----------------------------------
+ carLicense add 4
+ ipPhone delete 123
+ carLicense delete 3
+ ipPhone replace 4
+----------------------------------
+ carLicense add 4
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense delete 3
+----------------------------------
+ ipPhone delete 123
+ carLicense add 4
+ carLicense delete 3
+ ipPhone replace 4
+----------------------------------
+ ipPhone delete 123
+ carLicense add 4
+ ipPhone replace 4
+ carLicense delete 3
+----------------------------------
+ ipPhone delete 123
+ ipPhone replace 4
+ carLicense add 4
+ carLicense delete 3
+----------------------------------
+== result ===[ 6]=======================
+ carLicense: [b'1', b'2']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense delete 3
+ ipPhone replace 4
+ ipPhone delete 123
+----------------------------------
+ carLicense add 4
+ ipPhone replace 4
+ carLicense delete 3
+ ipPhone delete 123
+----------------------------------
+ carLicense add 4
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense delete 3
+----------------------------------
+ ipPhone replace 4
+ carLicense add 4
+ carLicense delete 3
+ ipPhone delete 123
+----------------------------------
+ ipPhone replace 4
+ carLicense add 4
+ ipPhone delete 123
+ carLicense delete 3
+----------------------------------
+ ipPhone replace 4
+ ipPhone delete 123
+ carLicense add 4
+ carLicense delete 3
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_objectclass-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_objectclass-non-admin.expected
new file mode 100644
index 0000000..1e9650a
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_objectclass-non-admin.expected
@@ -0,0 +1,31 @@
+modify_order_objectclass-non-admin
+initial attrs:
+ objectclass: 'user'
+ otherTelephone: '123'
+== result ===[ 6]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass delete user
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass delete user
+----------------------------------
+ objectclass delete user
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass delete user
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass delete user
+----------------------------------
+ objectclass delete person
+ objectclass delete user
+ objectclass replace computer
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_objectclass.expected b/source4/dsdb/tests/python/testdata/modify_order_objectclass.expected
new file mode 100644
index 0000000..0ec6d4a
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_objectclass.expected
@@ -0,0 +1,35 @@
+modify_order_objectclass
+initial attrs:
+ objectclass: 'user'
+ otherTelephone: '123'
+== result ===[ 2]=======================
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'123']
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass delete user
+ objectclass delete person
+----------------------------------
+ objectclass delete user
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+== result ===[ 4]=======================
+ERR_OBJECT_CLASS_VIOLATION (65)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass delete user
+----------------------------------
+ objectclass delete user
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass delete user
+----------------------------------
+ objectclass delete person
+ objectclass delete user
+ objectclass replace computer
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_objectclass2-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_objectclass2-non-admin.expected
new file mode 100644
index 0000000..2515154
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_objectclass2-non-admin.expected
@@ -0,0 +1,726 @@
+modify_order_objectclass2-non-admin
+initial attrs:
+ objectclass: 'user'
+== result ===[120]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_objectclass2.expected b/source4/dsdb/tests/python/testdata/modify_order_objectclass2.expected
new file mode 100644
index 0000000..4f51708
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_objectclass2.expected
@@ -0,0 +1,735 @@
+modify_order_objectclass2
+initial attrs:
+ objectclass: 'user'
+== result ===[ 24]=======================
+ objectClass: [b'inetOrgPerson', b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+----------------------------------
+== result ===[ 24]=======================
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+== result ===[ 24]=======================
+ERR_ATTRIBUTE_OR_VALUE_EXISTS (20)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+ objectclass add user
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+ objectclass add user
+----------------------------------
+== result ===[ 48]=======================
+ERR_OBJECT_CLASS_VIOLATION (65)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass delete person
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass add inetOrgPerson
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace computer
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass add inetOrgPerson
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass replace attributeSchema
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace computer
+ objectclass add user
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace computer
+ objectclass replace attributeSchema
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass add user
+ objectclass replace attributeSchema
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ objectclass add inetOrgPerson
+ objectclass replace attributeSchema
+ objectclass add user
+ objectclass replace computer
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_singlevalue-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_singlevalue-non-admin.expected
new file mode 100644
index 0000000..f9d717d
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_singlevalue-non-admin.expected
@@ -0,0 +1,727 @@
+modify_order_singlevalue-non-admin
+initial attrs:
+ objectclass: 'user'
+ givenName: 'a'
+== result ===[120]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_singlevalue.expected b/source4/dsdb/tests/python/testdata/modify_order_singlevalue.expected
new file mode 100644
index 0000000..9946165
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_singlevalue.expected
@@ -0,0 +1,740 @@
+modify_order_singlevalue
+initial attrs:
+ objectclass: 'user'
+ givenName: 'a'
+== result ===[ 24]=======================
+ givenName: [b'a']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+----------------------------------
+== result ===[ 24]=======================
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+----------------------------------
+== result ===[ 24]=======================
+ERR_ATTRIBUTE_OR_VALUE_EXISTS (20)
+-- operations ---------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+ givenName replace a
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+----------------------------------
+== result ===[ 24]=======================
+ERR_CONSTRAINT_VIOLATION (19)
+-- operations ---------------------------
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete b
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName delete b
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete b
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete b
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName delete b
+ givenName replace ['b', 'a']
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName delete b
+ givenName replace a
+ givenName replace ['b', 'a']
+----------------------------------
+== result ===[ 24]=======================
+ERR_NO_SUCH_ATTRIBUTE (16)
+-- operations ---------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName add c
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName add c
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName delete a
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace a
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName replace ['b', 'a']
+ givenName delete a
+ givenName replace a
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace a
+ givenName replace ['b', 'a']
+ givenName delete b
+----------------------------------
+ givenName add c
+ givenName delete a
+ givenName replace ['b', 'a']
+ givenName replace a
+ givenName delete b
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable-non-admin.expected
new file mode 100644
index 0000000..fd144d7
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable-non-admin.expected
@@ -0,0 +1,127 @@
+modify_order_sometimes_inapplicable-non-admin
+initial attrs:
+ objectclass: 'user'
+ givenName: 'a'
+== result ===[ 24]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName add b
+ dnsHostName replace c
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName replace c
+ dnsHostName add b
+----------------------------------
+ objectclass replace computer
+ dnsHostName add b
+ objectclass delete person
+ dnsHostName replace c
+----------------------------------
+ objectclass replace computer
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ dnsHostName replace c
+ objectclass delete person
+ dnsHostName add b
+----------------------------------
+ objectclass replace computer
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass delete person
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName add b
+ dnsHostName replace c
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName replace c
+ dnsHostName add b
+----------------------------------
+ objectclass delete person
+ dnsHostName add b
+ objectclass replace computer
+ dnsHostName replace c
+----------------------------------
+ objectclass delete person
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ dnsHostName replace c
+ objectclass replace computer
+ dnsHostName add b
+----------------------------------
+ objectclass delete person
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass replace computer
+----------------------------------
+ dnsHostName add b
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName replace c
+----------------------------------
+ dnsHostName add b
+ objectclass replace computer
+ dnsHostName replace c
+ objectclass delete person
+----------------------------------
+ dnsHostName add b
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName replace c
+----------------------------------
+ dnsHostName add b
+ objectclass delete person
+ dnsHostName replace c
+ objectclass replace computer
+----------------------------------
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ dnsHostName replace c
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName add b
+----------------------------------
+ dnsHostName replace c
+ objectclass replace computer
+ dnsHostName add b
+ objectclass delete person
+----------------------------------
+ dnsHostName replace c
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName add b
+----------------------------------
+ dnsHostName replace c
+ objectclass delete person
+ dnsHostName add b
+ objectclass replace computer
+----------------------------------
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass delete person
+ objectclass replace computer
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable.expected b/source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable.expected
new file mode 100644
index 0000000..a8af7f0
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_sometimes_inapplicable.expected
@@ -0,0 +1,127 @@
+modify_order_sometimes_inapplicable
+initial attrs:
+ objectclass: 'user'
+ givenName: 'a'
+== result ===[ 24]=======================
+ERR_OBJECT_CLASS_VIOLATION (65)
+-- operations ---------------------------
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName add b
+ dnsHostName replace c
+----------------------------------
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName replace c
+ dnsHostName add b
+----------------------------------
+ objectclass replace computer
+ dnsHostName add b
+ objectclass delete person
+ dnsHostName replace c
+----------------------------------
+ objectclass replace computer
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass delete person
+----------------------------------
+ objectclass replace computer
+ dnsHostName replace c
+ objectclass delete person
+ dnsHostName add b
+----------------------------------
+ objectclass replace computer
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass delete person
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName add b
+ dnsHostName replace c
+----------------------------------
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName replace c
+ dnsHostName add b
+----------------------------------
+ objectclass delete person
+ dnsHostName add b
+ objectclass replace computer
+ dnsHostName replace c
+----------------------------------
+ objectclass delete person
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass replace computer
+----------------------------------
+ objectclass delete person
+ dnsHostName replace c
+ objectclass replace computer
+ dnsHostName add b
+----------------------------------
+ objectclass delete person
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass replace computer
+----------------------------------
+ dnsHostName add b
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName replace c
+----------------------------------
+ dnsHostName add b
+ objectclass replace computer
+ dnsHostName replace c
+ objectclass delete person
+----------------------------------
+ dnsHostName add b
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName replace c
+----------------------------------
+ dnsHostName add b
+ objectclass delete person
+ dnsHostName replace c
+ objectclass replace computer
+----------------------------------
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ dnsHostName add b
+ dnsHostName replace c
+ objectclass delete person
+ objectclass replace computer
+----------------------------------
+ dnsHostName replace c
+ objectclass replace computer
+ objectclass delete person
+ dnsHostName add b
+----------------------------------
+ dnsHostName replace c
+ objectclass replace computer
+ dnsHostName add b
+ objectclass delete person
+----------------------------------
+ dnsHostName replace c
+ objectclass delete person
+ objectclass replace computer
+ dnsHostName add b
+----------------------------------
+ dnsHostName replace c
+ objectclass delete person
+ dnsHostName add b
+ objectclass replace computer
+----------------------------------
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass replace computer
+ objectclass delete person
+----------------------------------
+ dnsHostName replace c
+ dnsHostName add b
+ objectclass delete person
+ objectclass replace computer
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_telephone-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_telephone-non-admin.expected
new file mode 100644
index 0000000..fd46b3a
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_telephone-non-admin.expected
@@ -0,0 +1,727 @@
+modify_order_telephone-non-admin
+initial attrs:
+ objectclass: 'user'
+ otherTelephone: '123'
+== result ===[120]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_telephone.expected b/source4/dsdb/tests/python/testdata/modify_order_telephone.expected
new file mode 100644
index 0000000..d17de03
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_telephone.expected
@@ -0,0 +1,752 @@
+modify_order_telephone
+initial attrs:
+ objectclass: 'user'
+ otherTelephone: '123'
+== result ===[ 20]=======================
+ carLicense: [b'3']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+== result ===[ 20]=======================
+ carLicense: [b'3']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'123', b'4']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+== result ===[ 20]=======================
+ carLicense: [b'3']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'4']
+-- operations ---------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ carLicense replace 3
+----------------------------------
+== result ===[ 20]=======================
+ carLicense: [b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+== result ===[ 20]=======================
+ carLicense: [b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'123', b'4']
+-- operations ---------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+== result ===[ 20]=======================
+ carLicense: [b'4']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'4']
+-- operations ---------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone add 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ carLicense add 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense add 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone add 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense add 4
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete-non-admin.expected b/source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete-non-admin.expected
new file mode 100644
index 0000000..96fc4fd
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete-non-admin.expected
@@ -0,0 +1,727 @@
+modify_order_telephone_delete_delete-non-admin
+initial attrs:
+ objectclass: 'user'
+ otherTelephone: '123'
+== result ===[120]=======================
+ERR_INSUFFICIENT_ACCESS_RIGHTS (50)
+-- operations ---------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete.expected b/source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete.expected
new file mode 100644
index 0000000..14983ba
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/modify_order_telephone_delete_delete.expected
@@ -0,0 +1,736 @@
+modify_order_telephone_delete_delete
+initial attrs:
+ objectclass: 'user'
+ otherTelephone: '123'
+== result ===[ 20]=======================
+ carLicense: [b'3']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+-- operations ---------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+== result ===[ 20]=======================
+ carLicense: [b'3']
+ objectClass: [b'organizationalPerson', b'person', b'top', b'user']
+ otherTelephone: [b'4']
+-- operations ---------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+== result ===[ 80]=======================
+ERR_NO_SUCH_ATTRIBUTE (16)
+-- operations ---------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone replace 4
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 123
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 123
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone replace 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 4
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense replace 3
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ carLicense delete 4
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense replace 3
+ otherTelephone delete 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ carLicense replace 3
+ otherTelephone delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ carLicense delete 4
+ otherTelephone delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone replace 4
+ otherTelephone delete 4
+ carLicense delete 4
+ carLicense replace 3
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ carLicense delete 4
+ otherTelephone replace 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ carLicense replace 3
+ otherTelephone replace 4
+ carLicense delete 4
+----------------------------------
+ otherTelephone delete 123
+ otherTelephone delete 4
+ otherTelephone replace 4
+ carLicense replace 3
+ carLicense delete 4
+---------------------------------- \ No newline at end of file
diff --git a/source4/dsdb/tests/python/testdata/simplesort.expected b/source4/dsdb/tests/python/testdata/simplesort.expected
new file mode 100644
index 0000000..045337b
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/simplesort.expected
@@ -0,0 +1,8 @@
+comment = [u'FAVOURITEXCOLOURXISX0', u'FAVOURITEXCOLOURXISX0', u'FAVOURITEXCOLOURXISX0', u'FAVOURITEXCOLOURXISX0', u'FAVOURITEXCOLOURXISX1', u'FAVOURITEXCOLOURXISX1', u'FAVOURITEXCOLOURXISX1', u'FAVOURITEXCOLOURXISX1', u'FAVOURITEXCOLOURXISX1', u'FAVOURITEXCOLOURXISX10', u'FAVOURITEXCOLOURXISX11', u'FAVOURITEXCOLOURXISX12', u'FAVOURITEXCOLOURXISX13', u'FAVOURITEXCOLOURXISX14', u'FAVOURITEXCOLOURXISX15', u'FAVOURITEXCOLOURXISX16', u'FAVOURITEXCOLOURXISX2', u'FAVOURITEXCOLOURXISX3', u'FAVOURITEXCOLOURXISX3', u'FAVOURITEXCOLOURXISX3', u'FAVOURITEXCOLOURXISX3', u'FAVOURITEXCOLOURXISX3', u'FAVOURITEXCOLOURXISX4', u'FAVOURITEXCOLOURXISX5', u'FAVOURITEXCOLOURXISX5', u'FAVOURITEXCOLOURXISX5', u'FAVOURITEXCOLOURXISX6', u'FAVOURITEXCOLOURXISX6', u'FAVOURITEXCOLOURXISX7', u'FAVOURITEXCOLOURXISX7', u'FAVOURITEXCOLOURXISX8', u'FAVOURITEXCOLOURXISX9', u'FAVOURITEXCOLOURXISX9']
+msTSExpireDate4 = ['19000101010000.0Z', '19010101010000.0Z', '19020101010000.0Z', '19030101010000.0Z', '19040101010000.0Z', '19050101010000.0Z', '19060101010000.0Z', '19070101010000.0Z', '19080101010000.0Z', '19090101010000.0Z', '19100101010000.0Z', '19110101010000.0Z', '19120101010000.0Z', '19130101010000.0Z', '19140101010000.0Z', '19150101010000.0Z', '19160101010000.0Z', '19170101010000.0Z', '19180101010000.0Z', '19190101010000.0Z', '19200101010000.0Z', '19210101010000.0Z', '19220101010000.0Z', '19230101010000.0Z', '19240101010000.0Z', '19250101010000.0Z', '19260101010000.0Z', '19270101010000.0Z', '19280101010000.0Z', '19290101010000.0Z', '19300101010000.0Z', '19310101010000.0Z', '19320101010000.0Z']
+cn = [u'SORTTEST0', u'SORTTEST1', u'SORTTEST10', u'SORTTEST11', u'SORTTEST12', u'SORTTEST13', u'SORTTEST14', u'SORTTEST15', u'SORTTEST16', u'SORTTEST17', u'SORTTEST18', u'SORTTEST19', u'SORTTEST2', u'SORTTEST20', u'SORTTEST21', u'SORTTEST22', u'SORTTEST23', u'SORTTEST24', u'SORTTEST25', u'SORTTEST26', u'SORTTEST27', u'SORTTEST28', u'SORTTEST29', u'SORTTEST3', u'SORTTEST30', u'SORTTEST31', u'SORTTEST32', u'SORTTEST4', u'SORTTEST5', u'SORTTEST6', u'SORTTEST7', u'SORTTEST8', u'SORTTEST9']
+serialNumber = ['abcXAXX', 'abcXAXX', 'abcXAXX', 'abcXAXX', 'abcXAXX', 'abcXBzX', 'abcXBzX', 'abcXBzX', 'abcXBzX', 'abcXX3X', 'abcXX3X', 'abcXX3X', 'abcXX3X', 'abcXXXX', 'abcXXXX', 'abcXXXX', 'abcXXXX', 'abcXXXX', 'abcXXXX', 'abcXXXX', 'abcXXXX', 'abcXXzX', 'abcXXzX', 'abcXXzX', 'abcXXzX', 'abcXa3X', 'abcXa3X', 'abcXa3X', 'abcXa3X', 'abcXbXX', 'abcXbXX', 'abcXbXX', 'abcXbXX']
+roomNumber = [u'10BXC', u'11BXC', u'12BXC', u'13BXC', u'14BXC', u'15BXC', u'16BXC', u'17BXC', u'18BXC', u'19BXC', u'1BXC', u'20BXC', u'21BXC', u'22BXC', u'23BXC', u'24BXC', u'25BXC', u'26BXC', u'27BXC', u'28BXC', u'29BXC', u'2BXC', u'30BXC', u'31BXC', u'32BXC', u'33BXC', u'3BXC', u'4BXC', u'5BXC', u'6BXC', u'7BXC', u'8BXC', u'9BXC']
+carLicense = [u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX', u'XXXXXXXXX']
+employeeNumber = [u'0X', u'1044XXXXXXXXXXXXX', u'1118XXXXXXXXXXXXXX', u'1190XXXXXXXXXXXXXXX', u'1260XXXXXXXXXXXXXXXX', u'1328XXXXXXXXXXXXXXXXX', u'1394XXXXXXXXXXXXXXXXXX', u'1458XXXXXXXXXXXXXXXXXXX', u'1520XXXXXXXXXXXXXXXXXXXX', u'1580XXXXXXXXXXXXXXXXXXXXX', u'1638XXXXXXXXXXXXXXXXXXXXXX', u'1694XXXXXXXXXXXXXXXXXXXXXXX', u'1748XXXXXXXXXXXXXXXXXXXXXXXX', u'1800XXXXXXXXXXXXXXXXXXXXXXXXX', u'1850XXXXXXXXXXXXXXXXXXXXXXXXXX', u'1898XXXXXXXXXXXXXXXXXXXXXXXXXXX', u'1944XXXXXXXXXXXXXXXXXXXXXXXXXXXX', u'194XXX', u'1988XXXXXXXXXXXXXXXXXXXXXXXXXXXXX', u'2030XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', u'2070XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', u'2108XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', u'2144XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', u'288XXXX', u'380XXXXX', u'470XXXXXX', u'558XXXXXXX', u'644XXXXXXXX', u'728XXXXXXXXX', u'810XXXXXXXXXX', u'890XXXXXXXXXXX', u'968XXXXXXXXXXXX', u'98XX']
+givenName = [u'A', u'A', u'B', u'B', u'C', u'C', u'D', u'D', u'E', u'E', u'F', u'F', u'G', u'G', u'H', u'I', u'J', u'K', u'L', u'M', u'N', u'O', u'P', u'Q', u'R', u'S', u'T', u'U', u'V', u'W', u'X', u'Y', u'Z']
diff --git a/source4/dsdb/tests/python/testdata/unicodesort.expected b/source4/dsdb/tests/python/testdata/unicodesort.expected
new file mode 100644
index 0000000..de07cfc
--- /dev/null
+++ b/source4/dsdb/tests/python/testdata/unicodesort.expected
@@ -0,0 +1,16 @@
+comment = [u'FAVOURITE COLOUR IS 0', u'FAVOURITE COLOUR IS 0', u'FAVOURITE COLOUR IS 0', u'FAVOURITE COLOUR IS 0', u'FAVOURITE COLOUR IS 1', u'FAVOURITE COLOUR IS 1', u'FAVOURITE COLOUR IS 1', u'FAVOURITE COLOUR IS 1', u'FAVOURITE COLOUR IS 1', u'FAVOURITE COLOUR IS 10', u'FAVOURITE COLOUR IS 11', u'FAVOURITE COLOUR IS 12', u'FAVOURITE COLOUR IS 13', u'FAVOURITE COLOUR IS 14', u'FAVOURITE COLOUR IS 15', u'FAVOURITE COLOUR IS 16', u'FAVOURITE COLOUR IS 2', u'FAVOURITE COLOUR IS 3', u'FAVOURITE COLOUR IS 3', u'FAVOURITE COLOUR IS 3', u'FAVOURITE COLOUR IS 3', u'FAVOURITE COLOUR IS 3', u'FAVOURITE COLOUR IS 4', u'FAVOURITE COLOUR IS 5', u'FAVOURITE COLOUR IS 5', u'FAVOURITE COLOUR IS 5', u'FAVOURITE COLOUR IS 6', u'FAVOURITE COLOUR IS 6', u'FAVOURITE COLOUR IS 7', u'FAVOURITE COLOUR IS 7', u'FAVOURITE COLOUR IS 8', u'FAVOURITE COLOUR IS 9', u'FAVOURITE COLOUR IS 9']
+msTSExpireDate4 = ['19000101010000.0Z', '19010101010000.0Z', '19020101010000.0Z', '19030101010000.0Z', '19040101010000.0Z', '19050101010000.0Z', '19060101010000.0Z', '19070101010000.0Z', '19080101010000.0Z', '19090101010000.0Z', '19100101010000.0Z', '19110101010000.0Z', '19120101010000.0Z', '19130101010000.0Z', '19140101010000.0Z', '19150101010000.0Z', '19160101010000.0Z', '19170101010000.0Z', '19180101010000.0Z', '19190101010000.0Z', '19200101010000.0Z', '19210101010000.0Z', '19220101010000.0Z', '19230101010000.0Z', '19240101010000.0Z', '19250101010000.0Z', '19260101010000.0Z', '19270101010000.0Z', '19280101010000.0Z', '19290101010000.0Z', '19300101010000.0Z', '19310101010000.0Z', '19320101010000.0Z']
+audio = ['An octet string \x000 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x022 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x044 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x066 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x088 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \n10 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x0c12 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x0e14 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1016 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1218 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1420 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1622 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1824 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1a26 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1c28 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string \x1e30 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'An octet string 32 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x011 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x033 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x055 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x077 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \t9 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x0b11 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \r13 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x0f15 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1117 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1319 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1521 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1723 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1925 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1b27 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1d29 \xe2\x99\xab\xe2\x99\xac\x00lalala', 'an octet string \x1f31 \xe2\x99\xab\xe2\x99\xac\x00lalala']
+adminDisplayName = [u'10\x00B', u'11\x00B', u'12\x00B', u'13\x00B', u'14\x00B', u'15\x00B', u'16\x00B', u'17\x00B', u'18\x00B', u'19\x00B', u'1\x00B', u'20\x00B', u'21\x00B', u'22\x00B', u'23\x00B', u'24\x00B', u'25\x00B', u'26\x00B', u'27\x00B', u'28\x00B', u'29\x00B', u'2\x00B', u'30\x00B', u'31\x00B', u'32\x00B', u'33\x00B', u'3\x00B', u'4\x00B', u'5\x00B', u'6\x00B', u'7\x00B', u'8\x00B', u'9\x00B']
+cn = [u'SORTTEST0', u'SORTTEST1', u'SORTTEST10', u'SORTTEST11', u'SORTTEST12', u'SORTTEST13', u'SORTTEST14', u'SORTTEST15', u'SORTTEST16', u'SORTTEST17', u'SORTTEST18', u'SORTTEST19', u'SORTTEST2', u'SORTTEST20', u'SORTTEST21', u'SORTTEST22', u'SORTTEST23', u'SORTTEST24', u'SORTTEST25', u'SORTTEST26', u'SORTTEST27', u'SORTTEST28', u'SORTTEST29', u'SORTTEST3', u'SORTTEST30', u'SORTTEST31', u'SORTTEST32', u'SORTTEST4', u'SORTTEST5', u'SORTTEST6', u'SORTTEST7', u'SORTTEST8', u'SORTTEST9']
+title = [u'10\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'12\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'13\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'14\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'18\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'19\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'20\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'21\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'22\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'23\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'24\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'25\x00\x00\x00\x00\x00\x00\x00\x00B', u'26\x00\x00\x00\x00\x00\x00\x00B', u'27\x00\x00\x00\x00\x00\x00B', u'28\x00\x00\x00\x00\x00B', u'29\x00\x00\x00\x00B', u'2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'30\x00\x00\x00B', u'31\x00\x00B', u'32\x00B', u'33B', u'3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'5\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'6\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'7\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B', u'9\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00B']
+photo = ['\x001', '\x0010', '\x0011', '\x0012', '\x0013', '\x0014', '\x0015', '\x0016', '\x0017', '\x0018', '\x0019', '\x002', '\x0020', '\x0021', '\x0022', '\x0023', '\x0024', '\x0025', '\x0026', '\x0027', '\x0028', '\x0029', '\x003', '\x0030', '\x0031', '\x0032', '\x0033', '\x004', '\x005', '\x006', '\x007', '\x008', '\x009']
+serialNumber = ['abc "', 'abc "', 'abc "', 'abc "', 'abc -z"', 'abc -z"', 'abc -z"', 'abc -z"', 'abc /}@', 'abc /}@', 'abc /}@', 'abc /}@', 'abc A "', 'abc A "', 'abc A "', 'abc A "', 'abc A "', 'abc Bz"', 'abc Bz"', 'abc Bz"', 'abc Bz"', 'abc a3@', 'abc a3@', 'abc a3@', 'abc a3@', 'abc b}@', 'abc b}@', 'abc b}@', 'abc b}@', 'abc |3@', 'abc |3@', 'abc |3@', 'abc |3@']
+roomNumber = [u'10B\x00C', u'11B\x00C', u'12B\x00C', u'13B\x00C', u'14B\x00C', u'15B\x00C', u'16B\x00C', u'17B\x00C', u'18B\x00C', u'19B\x00C', u'1B\x00C', u'20B\x00C', u'21B\x00C', u'22B\x00C', u'23B\x00C', u'24B\x00C', u'25B\x00C', u'26B\x00C', u'27B\x00C', u'28B\x00C', u'29B\x00C', u'2B\x00C', u'30B\x00C', u'31B\x00C', u'32B\x00C', u'33B\x00C', u'3B\x00C', u'4B\x00C', u'5B\x00C', u'6B\x00C', u'7B\x00C', u'8B\x00C', u'9B\x00C']
+carLicense = [u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf', u'\u540e\u6765\u7ecf']
+streetAddress = [u' ', u' ', u' E', u' E', u'\t-\t', u'\t-\t', u'\n\t\t', u'\n\t\t', u'!@#!@#!', u'1\u20444', u'1', u'1', u'1/4', u'1\u20444', u'1\u20445', u'3', u'ABC', u'K\u014cKAKO', u'\u014a\u01101\u204443\u0166 \u201c\xab\u0110\xd0', u'\u014a\u01101\u204443\u0166\u201c\xab\u0110\xd0', u'SORTTEST', u'SORTT\u0112ST11,', u'\u015aORTTEST2', u'\u015aORTTEST2', u'\u015a-O-R-T-T-E-S-T-2', u'SORTT\u0112ST2,', u'\u1e60ORTTEST4', u'\u1e60ORTTEST4', u'S\xd6RTTEST-5', u'S\xd6RTTEST-5', u'SO-RTTEST7,', u'\u6851\u5df4', u'FO\x00OD']
+street = [u'A ST', u'A ST', u'A ST', u'A ST', u'A ST', u'C ST', u'C ST', u'C ST', u'C ST', u'E ST', u'E ST', u'E ST', u'E ST', u'G ST', u'G ST', u'G ST', u'G ST', u'I ST', u'I ST', u'I ST', u'I ST', u'K ST', u'K ST', u'K ST', u'K ST', u'M ST', u'M ST', u'M ST', u'M ST', u'O ST', u'O ST', u'O ST', u'O ST']
+employeeNumber = [u'0X', u'1044\n\n\n\n\n\n\n\n\n\n\n\nX', u'1118\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1190\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1260\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1328\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1394\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1458\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1520\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1580\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1638\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1694\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1748\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1800\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1850\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1898\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'194\n\nX', u'1944\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'1988\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'2030\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'2070\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'2108\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'2144\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nX', u'288\n\n\nX', u'380\n\n\n\nX', u'470\n\n\n\n\nX', u'558\n\n\n\n\n\nX', u'644\n\n\n\n\n\n\nX', u'728\n\n\n\n\n\n\n\nX', u'810\n\n\n\n\n\n\n\n\nX', u'890\n\n\n\n\n\n\n\n\n\nX', u'968\n\n\n\n\n\n\n\n\n\n\nX', u'98\nX']
+postalAddress = [u' ', u' ', u' E', u'\t-\t', u'\n\t\t', u'!@#!@#!', u'1\u20444', u'1', u'1', u'1/4', u'1\u20444', u'1\u20445', u'3', u'ABC', u'K\u014cKAKO', u'\u014a\u01101\u204443\u0166 \u201c\xab\u0110\xd0', u'\u014a\u01101\u204443\u0166\u201c\xab\u0110\xd0', u'SORTTEST', u'SORTT\u0112ST11,', u'\u015aORTTEST2', u'\u015aORTTEST2', u'\u015a-O-R-T-T-E-S-T-2', u'SORTT\u0112ST2,', u'\u1e60ORTTEST4', u'\u1e60ORTTEST4', u'S\xd6RTTEST-5', u'S\xd6RTTEST-5', u'SO-RTTEST7,', u'SO-RTTEST7,', u'\u6851\u5df4', u'\u6851\u5df4', u'FO\x00OD', u'FO\x00OD']
+givenName = [u'A', u'A', u'B', u'B', u'C', u'C', u'D', u'D', u'E', u'E', u'F', u'F', u'G', u'G', u'H', u'I', u'J', u'K', u'L', u'M', u'N', u'O', u'P', u'Q', u'R', u'S', u'T', u'U', u'V', u'W', u'X', u'Y', u'Z']
+displayNamePrintable = ['0\x00\x00', '1\x00\x01', '10\x00\n', '11\x00\x0b', '12\x00\x0c', '13\x00\r', '14\x00\x0e', '15\x00\x0f', '16\x00\x10', '17\x00\x11', '18\x00\x12', '19\x00\x13', '2\x00\x02', '20\x00\x14', '21\x00\x15', '22\x00\x16', '23\x00\x17', '24\x00\x18', '25\x00\x19', '26\x00\x1a', '27\x00\x1b', '28\x00\x1c', '29\x00\x1d', '3\x00\x03', '30\x00\x1e', '31\x00\x1f', '32\x00 ', '4\x00\x04', '5\x00\x05', '6\x00\x06', '7\x00\x07', '8\x00\x08', '9\x00\t']
diff --git a/source4/dsdb/tests/python/token_group.py b/source4/dsdb/tests/python/token_group.py
new file mode 100755
index 0000000..df45ee0
--- /dev/null
+++ b/source4/dsdb/tests/python/token_group.py
@@ -0,0 +1,738 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# test tokengroups attribute against internal token calculation
+
+import optparse
+import sys
+import os
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+from samba import ldb, dsdb
+from samba.samdb import SamDB
+from samba.auth import AuthContext
+from samba.ndr import ndr_unpack
+from samba import gensec
+from samba.credentials import Credentials, DONT_USE_KERBEROS, MUST_USE_KERBEROS, AUTO_USE_KERBEROS
+from samba.dsdb import GTYPE_SECURITY_GLOBAL_GROUP, GTYPE_SECURITY_UNIVERSAL_GROUP
+import samba.tests
+from samba.tests import delete_force
+from samba.dcerpc import security
+from samba.auth import AUTH_SESSION_INFO_DEFAULT_GROUPS, AUTH_SESSION_INFO_AUTHENTICATED, AUTH_SESSION_INFO_SIMPLE_PRIVILEGES, AUTH_SESSION_INFO_NTLM
+
+
+parser = optparse.OptionParser("token_group.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+url = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+
+def closure(vSet, wSet, aSet):
+ for edge in aSet:
+ start, end = edge
+ if start in wSet:
+ if end not in wSet and end in vSet:
+ wSet.add(end)
+ closure(vSet, wSet, aSet)
+
+
+class StaticTokenTest(samba.tests.TestCase):
+
+ def setUp(self):
+ super(StaticTokenTest, self).setUp()
+
+ self.assertNotEqual(creds.get_kerberos_state(), AUTO_USE_KERBEROS)
+
+ self.ldb = SamDB(url, credentials=creds, session_info=system_session(lp), lp=lp)
+ self.base_dn = self.ldb.domain_dn()
+
+ res = self.ldb.search("", scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ self.user_sid_dn = "<SID=%s>" % str(ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["tokenGroups"][0]))
+
+ session_info_flags = (AUTH_SESSION_INFO_DEFAULT_GROUPS |
+ AUTH_SESSION_INFO_AUTHENTICATED |
+ AUTH_SESSION_INFO_SIMPLE_PRIVILEGES)
+ if creds.get_kerberos_state() == DONT_USE_KERBEROS:
+ session_info_flags |= AUTH_SESSION_INFO_NTLM
+
+ session = samba.auth.user_session(self.ldb, lp_ctx=lp, dn=self.user_sid_dn,
+ session_info_flags=session_info_flags)
+
+ token = session.security_token
+ self.user_sids = []
+ for s in token.sids:
+ self.user_sids.append(str(s))
+
+ # Add asserted identity and Claims Valid for Kerberos
+ if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+ self.user_sids.append(str(security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY))
+ self.user_sids.append(str(security.SID_CLAIMS_VALID))
+
+
+ def test_rootDSE_tokenGroups(self):
+ """Testing rootDSE tokengroups against internal calculation"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ res = self.ldb.search("", scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ print("Getting tokenGroups from rootDSE")
+ tokengroups = []
+ for sid in res[0]['tokenGroups']:
+ tokengroups.append(str(ndr_unpack(samba.dcerpc.security.dom_sid, sid)))
+
+ sidset1 = set(tokengroups)
+ sidset2 = set(self.user_sids)
+ if len(sidset1.symmetric_difference(sidset2)):
+ print("token sids don't match")
+ print("tokengroups: %s" % tokengroups)
+ print("calculated : %s" % self.user_sids)
+ print("difference : %s" % sidset1.symmetric_difference(sidset2))
+ self.fail(msg="calculated groups don't match against rootDSE tokenGroups")
+
+ def test_dn_tokenGroups(self):
+ print("Getting tokenGroups from user DN")
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ dn_tokengroups = []
+ for sid in res[0]['tokenGroups']:
+ dn_tokengroups.append(str(ndr_unpack(samba.dcerpc.security.dom_sid, sid)))
+
+ sidset1 = set(dn_tokengroups)
+ sidset2 = set(self.user_sids)
+
+ # The tokenGroups is just a subset of the user_sids
+ # so we don't check symmetric_difference() here.
+ if len(sidset1.difference(sidset2)):
+ print("dn token sids no subset of user token")
+ print("tokengroups: %s" % dn_tokengroups)
+ print("user sids : %s" % self.user_sids)
+ print("difference : %s" % sidset1.difference(sidset2))
+ self.fail(msg="DN tokenGroups no subset of full user token")
+
+ missing_sidset = sidset2.difference(sidset1)
+
+ extra_sids = []
+ extra_sids.append(self.user_sids[0])
+ extra_sids.append(security.SID_WORLD)
+ extra_sids.append(security.SID_NT_NETWORK)
+ extra_sids.append(security.SID_NT_AUTHENTICATED_USERS)
+ extra_sids.append(security.SID_BUILTIN_PREW2K)
+ if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+ extra_sids.append(security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY)
+ extra_sids.append(security.SID_CLAIMS_VALID)
+ if creds.get_kerberos_state() == DONT_USE_KERBEROS:
+ extra_sids.append(security.SID_NT_NTLM_AUTHENTICATION)
+
+ extra_sidset = set(extra_sids)
+
+ if len(missing_sidset.symmetric_difference(extra_sidset)):
+ print("dn token sids unexpected")
+ print("tokengroups: %s" % dn_tokengroups)
+ print("user sids: %s" % self.user_sids)
+ print("actual difference: %s" % missing_sidset)
+ print("expected difference: %s" % extra_sidset)
+ print("unexpected difference : %s" %
+ missing_sidset.symmetric_difference(extra_sidset))
+ self.fail(msg="DN tokenGroups unexpected difference to full user token")
+
+ def test_pac_groups(self):
+ if creds.get_kerberos_state() != MUST_USE_KERBEROS:
+ self.skipTest("Kerberos disabled, skipping PAC test")
+
+ settings = {}
+ settings["lp_ctx"] = lp
+ settings["target_hostname"] = lp.get("netbios name")
+
+ gensec_client = gensec.Security.start_client(settings)
+ gensec_client.set_credentials(creds)
+ gensec_client.want_feature(gensec.FEATURE_SEAL)
+ gensec_client.start_mech_by_sasl_name("GSSAPI")
+
+ auth_context = AuthContext(lp_ctx=lp, ldb=self.ldb, methods=[])
+
+ gensec_server = gensec.Security.start_server(settings, auth_context)
+ machine_creds = Credentials()
+ machine_creds.guess(lp)
+ machine_creds.set_machine_account(lp)
+ gensec_server.set_credentials(machine_creds)
+
+ gensec_server.want_feature(gensec.FEATURE_SEAL)
+ gensec_server.start_mech_by_sasl_name("GSSAPI")
+
+ client_finished = False
+ server_finished = False
+ server_to_client = b""
+
+ # Run the actual call loop.
+ while not client_finished and not server_finished:
+ if not client_finished:
+ print("running client gensec_update")
+ (client_finished, client_to_server) = gensec_client.update(server_to_client)
+ if not server_finished:
+ print("running server gensec_update")
+ (server_finished, server_to_client) = gensec_server.update(client_to_server)
+
+ session = gensec_server.session_info()
+
+ token = session.security_token
+ pac_sids = []
+ for s in token.sids:
+ pac_sids.append(str(s))
+
+ sidset1 = set(pac_sids)
+ sidset2 = set(self.user_sids)
+ if len(sidset1.symmetric_difference(sidset2)):
+ print("token sids don't match")
+ print("pac sids: %s" % pac_sids)
+ print("user sids : %s" % self.user_sids)
+ print("difference : %s" % sidset1.symmetric_difference(sidset2))
+ self.fail(msg="calculated groups don't match against user PAC tokenGroups")
+
+
+class DynamicTokenTest(samba.tests.TestCase):
+
+ def get_creds(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_kerberos_state(creds.get_kerberos_state())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ return creds_tmp
+
+ def get_ldb_connection(self, target_username, target_password):
+ creds_tmp = self.get_creds(target_username, target_password)
+ ldb_target = SamDB(url=url, credentials=creds_tmp, lp=lp)
+ return ldb_target
+
+ def setUp(self):
+ super(DynamicTokenTest, self).setUp()
+
+ self.assertNotEqual(creds.get_kerberos_state(), AUTO_USE_KERBEROS)
+
+ self.admin_ldb = SamDB(url, credentials=creds, session_info=system_session(lp), lp=lp)
+
+ self.base_dn = self.admin_ldb.domain_dn()
+
+ self.test_user = "tokengroups_user1"
+ self.test_user_pass = "samba123@"
+ self.admin_ldb.newuser(self.test_user, self.test_user_pass)
+ self.test_group0 = "tokengroups_group0"
+ self.admin_ldb.newgroup(self.test_group0, grouptype=dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP)
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group0, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group0_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group0, [self.test_user],
+ add_members_operation=True)
+
+ self.test_group1 = "tokengroups_group1"
+ self.admin_ldb.newgroup(self.test_group1, grouptype=dsdb.GTYPE_SECURITY_GLOBAL_GROUP)
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group1, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group1_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group1, [self.test_user],
+ add_members_operation=True)
+
+ self.test_group2 = "tokengroups_group2"
+ self.admin_ldb.newgroup(self.test_group2, grouptype=dsdb.GTYPE_SECURITY_UNIVERSAL_GROUP)
+
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group2, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group2_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group2, [self.test_user],
+ add_members_operation=True)
+
+ self.test_group3 = "tokengroups_group3"
+ self.admin_ldb.newgroup(self.test_group3, grouptype=dsdb.GTYPE_SECURITY_UNIVERSAL_GROUP)
+
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group3, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group3_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group3, [self.test_group1],
+ add_members_operation=True)
+
+ self.test_group4 = "tokengroups_group4"
+ self.admin_ldb.newgroup(self.test_group4, grouptype=dsdb.GTYPE_SECURITY_UNIVERSAL_GROUP)
+
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group4, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group4_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group4, [self.test_group3],
+ add_members_operation=True)
+
+ self.test_group5 = "tokengroups_group5"
+ self.admin_ldb.newgroup(self.test_group5, grouptype=dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP)
+
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group5, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group5_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group5, [self.test_group4],
+ add_members_operation=True)
+
+ self.test_group6 = "tokengroups_group6"
+ self.admin_ldb.newgroup(self.test_group6, grouptype=dsdb.GTYPE_SECURITY_DOMAIN_LOCAL_GROUP)
+
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (self.test_group6, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ self.test_group6_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ self.admin_ldb.add_remove_group_members(self.test_group6, [self.test_user],
+ add_members_operation=True)
+
+ self.ldb = self.get_ldb_connection(self.test_user, self.test_user_pass)
+
+ res = self.ldb.search("", scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ self.user_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["tokenGroups"][0])
+ self.user_sid_dn = "<SID=%s>" % str(self.user_sid)
+
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=[])
+ self.assertEqual(len(res), 1)
+
+ self.test_user_dn = res[0].dn
+
+ session_info_flags = (AUTH_SESSION_INFO_DEFAULT_GROUPS |
+ AUTH_SESSION_INFO_AUTHENTICATED |
+ AUTH_SESSION_INFO_SIMPLE_PRIVILEGES)
+
+ if creds.get_kerberos_state() == DONT_USE_KERBEROS:
+ session_info_flags |= AUTH_SESSION_INFO_NTLM
+
+ session = samba.auth.user_session(self.ldb, lp_ctx=lp, dn=self.user_sid_dn,
+ session_info_flags=session_info_flags)
+
+ token = session.security_token
+ self.user_sids = []
+ for s in token.sids:
+ self.user_sids.append(str(s))
+
+ # Add asserted identity and Claims Valid for Kerberos
+ if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+ self.user_sids.append(str(security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY))
+ self.user_sids.append(str(security.SID_CLAIMS_VALID))
+
+ def tearDown(self):
+ super(DynamicTokenTest, self).tearDown()
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_user, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group0, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group1, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group2, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group3, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group4, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group5, "cn=users", self.base_dn))
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (self.test_group6, "cn=users", self.base_dn))
+
+ def test_rootDSE_tokenGroups(self):
+ """Testing rootDSE tokengroups against internal calculation"""
+ if not url.startswith("ldap"):
+ self.fail(msg="This test is only valid on ldap")
+
+ res = self.ldb.search("", scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ print("Getting tokenGroups from rootDSE")
+ tokengroups = []
+ for sid in res[0]['tokenGroups']:
+ tokengroups.append(str(ndr_unpack(samba.dcerpc.security.dom_sid, sid)))
+
+ sidset1 = set(tokengroups)
+ sidset2 = set(self.user_sids)
+ if len(sidset1.symmetric_difference(sidset2)):
+ print("token sids don't match")
+ print("tokengroups: %s" % tokengroups)
+ print("calculated : %s" % self.user_sids)
+ print("difference : %s" % sidset1.symmetric_difference(sidset2))
+ self.fail(msg="calculated groups don't match against rootDSE tokenGroups")
+
+ def test_dn_tokenGroups(self):
+ print("Getting tokenGroups from user DN")
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ dn_tokengroups = []
+ for sid in res[0]['tokenGroups']:
+ dn_tokengroups.append(str(ndr_unpack(samba.dcerpc.security.dom_sid, sid)))
+
+ sidset1 = set(dn_tokengroups)
+ sidset2 = set(self.user_sids)
+
+ # The tokenGroups is just a subset of the user_sids
+ # so we don't check symmetric_difference() here.
+ if len(sidset1.difference(sidset2)):
+ print("dn token sids no subset of user token")
+ print("tokengroups: %s" % dn_tokengroups)
+ print("user sids : %s" % self.user_sids)
+ print("difference : %s" % sidset1.difference(sidset2))
+ self.fail(msg="DN tokenGroups no subset of full user token")
+
+ missing_sidset = sidset2.difference(sidset1)
+
+ extra_sids = []
+ extra_sids.append(self.user_sids[0])
+ extra_sids.append(security.SID_WORLD)
+ extra_sids.append(security.SID_NT_NETWORK)
+ extra_sids.append(security.SID_NT_AUTHENTICATED_USERS)
+ extra_sids.append(security.SID_BUILTIN_PREW2K)
+ if creds.get_kerberos_state() == MUST_USE_KERBEROS:
+ extra_sids.append(security.SID_AUTHENTICATION_AUTHORITY_ASSERTED_IDENTITY)
+ extra_sids.append(security.SID_CLAIMS_VALID)
+ if creds.get_kerberos_state() == DONT_USE_KERBEROS:
+ extra_sids.append(security.SID_NT_NTLM_AUTHENTICATION)
+
+ extra_sidset = set(extra_sids)
+
+ if len(missing_sidset.symmetric_difference(extra_sidset)):
+ print("dn token sids unexpected")
+ print("tokengroups: %s" % dn_tokengroups)
+ print("user sids: %s" % self.user_sids)
+ print("actual difference: %s" % missing_sidset)
+ print("expected difference: %s" % extra_sidset)
+ print("unexpected difference : %s" %
+ missing_sidset.symmetric_difference(extra_sidset))
+ self.fail(msg="DN tokenGroups unexpected difference to full user token")
+
+ def test_pac_groups(self):
+ if creds.get_kerberos_state() != MUST_USE_KERBEROS:
+ self.skipTest("Kerberos disabled, skipping PAC test")
+
+ settings = {}
+ settings["lp_ctx"] = lp
+ settings["target_hostname"] = lp.get("netbios name")
+
+ gensec_client = gensec.Security.start_client(settings)
+ gensec_client.set_credentials(self.get_creds(self.test_user, self.test_user_pass))
+ gensec_client.want_feature(gensec.FEATURE_SEAL)
+ gensec_client.start_mech_by_sasl_name("GSSAPI")
+
+ auth_context = AuthContext(lp_ctx=lp, ldb=self.ldb, methods=[])
+
+ gensec_server = gensec.Security.start_server(settings, auth_context)
+ machine_creds = Credentials()
+ machine_creds.guess(lp)
+ machine_creds.set_machine_account(lp)
+ gensec_server.set_credentials(machine_creds)
+
+ gensec_server.want_feature(gensec.FEATURE_SEAL)
+ gensec_server.start_mech_by_sasl_name("GSSAPI")
+
+ client_finished = False
+ server_finished = False
+ server_to_client = b""
+
+ # Run the actual call loop.
+ while not client_finished and not server_finished:
+ if not client_finished:
+ print("running client gensec_update")
+ (client_finished, client_to_server) = gensec_client.update(server_to_client)
+ if not server_finished:
+ print("running server gensec_update")
+ (server_finished, server_to_client) = gensec_server.update(client_to_server)
+
+ session = gensec_server.session_info()
+
+ token = session.security_token
+ pac_sids = []
+ for s in token.sids:
+ pac_sids.append(str(s))
+
+ sidset1 = set(pac_sids)
+ sidset2 = set(self.user_sids)
+ if len(sidset1.symmetric_difference(sidset2)):
+ print("token sids don't match")
+ print("pac sids: %s" % pac_sids)
+ print("user sids : %s" % self.user_sids)
+ print("difference : %s" % sidset1.symmetric_difference(sidset2))
+ self.fail(msg="calculated groups don't match against user PAC tokenGroups")
+
+ def test_tokenGroups_manual(self):
+ # Manually run the tokenGroups algorithm from MS-ADTS 3.1.1.4.5.19 and MS-DRSR 4.1.8.3
+ # and compare the result
+ res = self.admin_ldb.search(base=self.base_dn, scope=ldb.SCOPE_SUBTREE,
+ expression="(|(objectclass=user)(objectclass=group))",
+ attrs=["memberOf"])
+ aSet = set()
+ aSetR = set()
+ vSet = set()
+ for obj in res:
+ if "memberOf" in obj:
+ for dn in obj["memberOf"]:
+ first = obj.dn.get_casefold()
+ second = ldb.Dn(self.admin_ldb, dn.decode('utf8')).get_casefold()
+ aSet.add((first, second))
+ aSetR.add((second, first))
+ vSet.add(first)
+ vSet.add(second)
+
+ res = self.admin_ldb.search(base=self.base_dn, scope=ldb.SCOPE_SUBTREE,
+ expression="(objectclass=user)",
+ attrs=["primaryGroupID"])
+ for obj in res:
+ if "primaryGroupID" in obj:
+ sid = "%s-%d" % (self.admin_ldb.get_domain_sid(), int(obj["primaryGroupID"][0]))
+ res2 = self.admin_ldb.search(base="<SID=%s>" % sid, scope=ldb.SCOPE_BASE,
+ attrs=[])
+ first = obj.dn.get_casefold()
+ second = res2[0].dn.get_casefold()
+
+ aSet.add((first, second))
+ aSetR.add((second, first))
+ vSet.add(first)
+ vSet.add(second)
+
+ wSet = set()
+ wSet.add(self.test_user_dn.get_casefold())
+ closure(vSet, wSet, aSet)
+ wSet.remove(self.test_user_dn.get_casefold())
+
+ tokenGroupsSet = set()
+
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=["tokenGroups"])
+ self.assertEqual(len(res), 1)
+
+ for sid in res[0]['tokenGroups']:
+ sid = ndr_unpack(samba.dcerpc.security.dom_sid, sid)
+ res3 = self.admin_ldb.search(base="<SID=%s>" % sid, scope=ldb.SCOPE_BASE,
+ attrs=[])
+ tokenGroupsSet.add(res3[0].dn.get_casefold())
+
+ if len(wSet.difference(tokenGroupsSet)):
+ self.fail(msg="additional calculated: %s" % wSet.difference(tokenGroupsSet))
+
+ if len(tokenGroupsSet.difference(wSet)):
+ self.fail(msg="additional tokenGroups: %s" % tokenGroupsSet.difference(wSet))
+
+ def filtered_closure(self, wSet, filter_grouptype):
+ res = self.admin_ldb.search(base=self.base_dn, scope=ldb.SCOPE_SUBTREE,
+ expression="(|(objectclass=user)(objectclass=group))",
+ attrs=["memberOf"])
+ aSet = set()
+ aSetR = set()
+ vSet = set()
+ for obj in res:
+ vSet.add(obj.dn.get_casefold())
+ if "memberOf" in obj:
+ for dn in obj["memberOf"]:
+ first = obj.dn.get_casefold()
+ second = ldb.Dn(self.admin_ldb, dn.decode('utf8')).get_casefold()
+ aSet.add((first, second))
+ aSetR.add((second, first))
+ vSet.add(first)
+ vSet.add(second)
+
+ res = self.admin_ldb.search(base=self.base_dn, scope=ldb.SCOPE_SUBTREE,
+ expression="(objectclass=user)",
+ attrs=["primaryGroupID"])
+ for obj in res:
+ if "primaryGroupID" in obj:
+ sid = "%s-%d" % (self.admin_ldb.get_domain_sid(), int(obj["primaryGroupID"][0]))
+ res2 = self.admin_ldb.search(base="<SID=%s>" % sid, scope=ldb.SCOPE_BASE,
+ attrs=[])
+ first = obj.dn.get_casefold()
+ second = res2[0].dn.get_casefold()
+
+ aSet.add((first, second))
+ aSetR.add((second, first))
+ vSet.add(first)
+ vSet.add(second)
+
+ uSet = set()
+ for v in vSet:
+ res_group = self.admin_ldb.search(base=v, scope=ldb.SCOPE_BASE,
+ attrs=["groupType"],
+ expression="objectClass=group")
+ if len(res_group) == 1:
+ if hex(int(res_group[0]["groupType"][0]) & 0x00000000FFFFFFFF) == hex(filter_grouptype):
+ uSet.add(v)
+ else:
+ uSet.add(v)
+
+ closure(uSet, wSet, aSet)
+
+ def test_tokenGroupsGlobalAndUniversal_manual(self):
+ # Manually run the tokenGroups algorithm from MS-ADTS 3.1.1.4.5.19 and MS-DRSR 4.1.8.3
+ # and compare the result
+
+ # The variable names come from MS-ADTS May 15, 2014
+
+ S = set()
+ S.add(self.test_user_dn.get_casefold())
+
+ self.filtered_closure(S, GTYPE_SECURITY_GLOBAL_GROUP)
+
+ T = set()
+ # Not really a SID, we do this on DNs...
+ for sid in S:
+ X = set()
+ X.add(sid)
+ self.filtered_closure(X, GTYPE_SECURITY_UNIVERSAL_GROUP)
+
+ T = T.union(X)
+
+ T.remove(self.test_user_dn.get_casefold())
+
+ tokenGroupsSet = set()
+
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=["tokenGroupsGlobalAndUniversal"])
+ self.assertEqual(len(res), 1)
+
+ for sid in res[0]['tokenGroupsGlobalAndUniversal']:
+ sid = ndr_unpack(samba.dcerpc.security.dom_sid, sid)
+ res3 = self.admin_ldb.search(base="<SID=%s>" % sid, scope=ldb.SCOPE_BASE,
+ attrs=[])
+ tokenGroupsSet.add(res3[0].dn.get_casefold())
+
+ if len(T.difference(tokenGroupsSet)):
+ self.fail(msg="additional calculated: %s" % T.difference(tokenGroupsSet))
+
+ if len(tokenGroupsSet.difference(T)):
+ self.fail(msg="additional tokenGroupsGlobalAndUniversal: %s" % tokenGroupsSet.difference(T))
+
+ def test_samr_GetGroupsForUser(self):
+ # Confirm that we get the correct results against SAMR also
+ if not url.startswith("ldap://"):
+ self.fail(msg="This test is only valid on ldap (so we an find the hostname and use SAMR)")
+ host = url.split("://")[1]
+ (domain_sid, user_rid) = self.user_sid.split()
+ samr_conn = samba.dcerpc.samr.samr("ncacn_ip_tcp:%s[seal]" % host, lp, creds)
+ samr_handle = samr_conn.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
+ samr_domain = samr_conn.OpenDomain(samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED,
+ domain_sid)
+ user_handle = samr_conn.OpenUser(samr_domain, security.SEC_FLAG_MAXIMUM_ALLOWED, user_rid)
+ rids = samr_conn.GetGroupsForUser(user_handle)
+ samr_dns = set()
+ for rid in rids.rids:
+ self.assertEqual(rid.attributes, security.SE_GROUP_DEFAULT_FLAGS)
+ sid = "%s-%d" % (domain_sid, rid.rid)
+ res = self.admin_ldb.search(base="<SID=%s>" % sid, scope=ldb.SCOPE_BASE,
+ attrs=[])
+ samr_dns.add(res[0].dn.get_casefold())
+
+ user_info = samr_conn.QueryUserInfo(user_handle, 1)
+ self.assertEqual(rids.rids[0].rid, user_info.primary_gid)
+
+ tokenGroupsSet = set()
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=["tokenGroupsGlobalAndUniversal"])
+ for sid in res[0]['tokenGroupsGlobalAndUniversal']:
+ sid = ndr_unpack(samba.dcerpc.security.dom_sid, sid)
+ res3 = self.admin_ldb.search(base="<SID=%s>" % sid, scope=ldb.SCOPE_BASE,
+ attrs=[],
+ expression="(&(|(grouptype=%d)(grouptype=%d))(objectclass=group))"
+ % (GTYPE_SECURITY_GLOBAL_GROUP, GTYPE_SECURITY_UNIVERSAL_GROUP))
+ if len(res) == 1:
+ tokenGroupsSet.add(res3[0].dn.get_casefold())
+
+ if len(samr_dns.difference(tokenGroupsSet)):
+ self.fail(msg="additional samr_GetUserGroups over tokenGroups: %s" % samr_dns.difference(tokenGroupsSet))
+
+ memberOf = set()
+ # Add the primary group
+ primary_group_sid = "%s-%d" % (domain_sid, user_info.primary_gid)
+ res2 = self.admin_ldb.search(base="<SID=%s>" % primary_group_sid, scope=ldb.SCOPE_BASE,
+ attrs=[])
+
+ memberOf.add(res2[0].dn.get_casefold())
+ res = self.ldb.search(self.user_sid_dn, scope=ldb.SCOPE_BASE, attrs=["memberOf"])
+ for dn in res[0]['memberOf']:
+ res3 = self.admin_ldb.search(base=dn, scope=ldb.SCOPE_BASE,
+ attrs=[],
+ expression="(&(|(grouptype=%d)(grouptype=%d))(objectclass=group))"
+ % (GTYPE_SECURITY_GLOBAL_GROUP, GTYPE_SECURITY_UNIVERSAL_GROUP))
+ if len(res3) == 1:
+ memberOf.add(res3[0].dn.get_casefold())
+
+ if len(memberOf.difference(samr_dns)):
+ self.fail(msg="additional memberOf over samr_GetUserGroups: %s" % memberOf.difference(samr_dns))
+
+ if len(samr_dns.difference(memberOf)):
+ self.fail(msg="additional samr_GetUserGroups over memberOf: %s" % samr_dns.difference(memberOf))
+
+ S = set()
+ S.add(self.test_user_dn.get_casefold())
+
+ self.filtered_closure(S, GTYPE_SECURITY_GLOBAL_GROUP)
+ self.filtered_closure(S, GTYPE_SECURITY_UNIVERSAL_GROUP)
+
+ # Now remove the user DN and primary group
+ S.remove(self.test_user_dn.get_casefold())
+
+ if len(samr_dns.difference(S)):
+ self.fail(msg="additional samr_GetUserGroups over filtered_closure: %s" % samr_dns.difference(S))
+
+ def test_samr_GetGroupsForUser_nomember(self):
+ # Confirm that we get the correct results against SAMR also
+ if not url.startswith("ldap://"):
+ self.fail(msg="This test is only valid on ldap (so we an find the hostname and use SAMR)")
+ host = url.split("://")[1]
+
+ test_user = "tokengroups_user2"
+ self.admin_ldb.newuser(test_user, self.test_user_pass)
+ res = self.admin_ldb.search(base="cn=%s,cn=users,%s" % (test_user, self.base_dn),
+ attrs=["objectSid"], scope=ldb.SCOPE_BASE)
+ user_sid = ndr_unpack(samba.dcerpc.security.dom_sid, res[0]["objectSid"][0])
+
+ (domain_sid, user_rid) = user_sid.split()
+ samr_conn = samba.dcerpc.samr.samr("ncacn_ip_tcp:%s[seal]" % host, lp, creds)
+ samr_handle = samr_conn.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
+ samr_domain = samr_conn.OpenDomain(samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED,
+ domain_sid)
+ user_handle = samr_conn.OpenUser(samr_domain, security.SEC_FLAG_MAXIMUM_ALLOWED, user_rid)
+ rids = samr_conn.GetGroupsForUser(user_handle)
+ user_info = samr_conn.QueryUserInfo(user_handle, 1)
+ delete_force(self.admin_ldb, "CN=%s,%s,%s" %
+ (test_user, "cn=users", self.base_dn))
+ self.assertEqual(len(rids.rids), 1)
+ self.assertEqual(rids.rids[0].rid, user_info.primary_gid)
+
+
+if "://" not in url:
+ if os.path.isfile(url):
+ url = "tdb://%s" % url
+ else:
+ url = "ldap://%s" % url
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/tombstone_reanimation.py b/source4/dsdb/tests/python/tombstone_reanimation.py
new file mode 100755
index 0000000..88aebd6
--- /dev/null
+++ b/source4/dsdb/tests/python/tombstone_reanimation.py
@@ -0,0 +1,958 @@
+#!/usr/bin/env python3
+#
+# Tombstone reanimation tests
+#
+# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2014
+# Copyright (C) Nadezhda Ivanova <nivanova@symas.com> 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/>.
+
+import sys
+import unittest
+
+sys.path.insert(0, "bin/python")
+import samba
+
+from samba.ndr import ndr_unpack, ndr_print
+from samba.dcerpc import misc
+from samba.dcerpc import drsblobs
+from samba.dcerpc.drsuapi import *
+from samba.tests.password_test import PasswordCommon
+from samba.common import get_string
+
+import samba.tests
+from ldb import (SCOPE_BASE, FLAG_MOD_ADD, FLAG_MOD_DELETE, FLAG_MOD_REPLACE, Dn, Message,
+ MessageElement, LdbError,
+ ERR_ATTRIBUTE_OR_VALUE_EXISTS, ERR_NO_SUCH_OBJECT, ERR_ENTRY_ALREADY_EXISTS,
+ ERR_OPERATIONS_ERROR, ERR_UNWILLING_TO_PERFORM)
+
+
+class RestoredObjectAttributesBaseTestCase(samba.tests.TestCase):
+ """ verify Samba restores required attributes when
+ user restores a Deleted object
+ """
+
+ def setUp(self):
+ super(RestoredObjectAttributesBaseTestCase, self).setUp()
+ self.samdb = samba.tests.connect_samdb_env("TEST_SERVER", "TEST_USERNAME", "TEST_PASSWORD")
+ self.base_dn = self.samdb.domain_dn()
+ self.schema_dn = self.samdb.get_schema_basedn().get_linearized()
+ self.configuration_dn = self.samdb.get_config_basedn().get_linearized()
+
+ # permit password changes during this test
+ PasswordCommon.allow_password_changes(self, self.samdb)
+
+ def tearDown(self):
+ super(RestoredObjectAttributesBaseTestCase, self).tearDown()
+
+ def GUID_string(self, guid):
+ return get_string(self.samdb.schema_format_value("objectGUID", guid))
+
+ def search_guid(self, guid, attrs=None):
+ if attrs is None:
+ attrs = ["*"]
+
+ res = self.samdb.search(base="<GUID=%s>" % self.GUID_string(guid),
+ scope=SCOPE_BASE, attrs=attrs,
+ controls=["show_deleted:1"])
+ self.assertEqual(len(res), 1)
+ return res[0]
+
+ def search_dn(self, dn):
+ res = self.samdb.search(expression="(objectClass=*)",
+ base=dn,
+ scope=SCOPE_BASE,
+ controls=["show_recycled:1"])
+ self.assertEqual(len(res), 1)
+ return res[0]
+
+ def _create_object(self, msg):
+ """:param msg: dict with dn and attributes to create an object from"""
+ # delete an object if leftover from previous test
+ samba.tests.delete_force(self.samdb, msg['dn'])
+ self.samdb.add(msg)
+ return self.search_dn(msg['dn'])
+
+ def assertNamesEqual(self, attrs_expected, attrs_extra):
+ self.assertEqual(attrs_expected, attrs_extra,
+ "Actual object does not have expected attributes, missing from expected (%s), extra (%s)"
+ % (str(attrs_expected.difference(attrs_extra)), str(attrs_extra.difference(attrs_expected))))
+
+ def assertAttributesEqual(self, obj_orig, attrs_orig, obj_restored, attrs_rest):
+ self.assertNamesEqual(attrs_orig, attrs_rest)
+ # remove volatile attributes, they can't be equal
+ attrs_orig -= set(["uSNChanged", "dSCorePropagationData", "whenChanged"])
+ for attr in attrs_orig:
+ # convert original attr value to ldif
+ orig_val = obj_orig.get(attr)
+ if orig_val is None:
+ continue
+ if not isinstance(orig_val, MessageElement):
+ orig_val = MessageElement(str(orig_val), 0, attr)
+ m = Message()
+ m.add(orig_val)
+ orig_ldif = self.samdb.write_ldif(m, 0)
+ # convert restored attr value to ldif
+ rest_val = obj_restored.get(attr)
+ self.assertFalse(rest_val is None)
+ m = Message()
+ if not isinstance(rest_val, MessageElement):
+ rest_val = MessageElement(str(rest_val), 0, attr)
+ m.add(rest_val)
+ rest_ldif = self.samdb.write_ldif(m, 0)
+ # compare generated ldif's
+ self.assertEqual(orig_ldif, rest_ldif)
+
+ def assertAttributesExists(self, attr_expected, obj_msg):
+ """Check object contains at least expected attrbigutes
+ :param attr_expected: dict of expected attributes with values. ** is any value
+ :param obj_msg: Ldb.Message for the object under test
+ """
+ actual_names = set(obj_msg.keys())
+ # Samba does not use 'dSCorePropagationData', so skip it
+ actual_names -= set(['dSCorePropagationData'])
+ expected_names = set(attr_expected.keys())
+ self.assertNamesEqual(expected_names, actual_names)
+ for name in attr_expected.keys():
+ expected_val = attr_expected[name]
+ actual_val = obj_msg.get(name)
+ self.assertFalse(actual_val is None, "No value for attribute '%s'" % name)
+ if expected_val == "**":
+ # "**" values means "any"
+ continue
+ # if expected_val is e.g. ldb.bytes we can't depend on
+ # str(actual_value) working, we may just get a decoding
+ # error. Better to just compare raw values
+ if not isinstance(expected_val, str):
+ actual_val = actual_val[0]
+ else:
+ actual_val = str(actual_val)
+ self.assertEqual(expected_val, actual_val,
+ "Unexpected value (%s) for '%s', expected (%s)" % (
+ repr(actual_val), name, repr(expected_val)))
+
+ def _check_metadata(self, metadata, expected):
+ repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, metadata[0])
+
+ repl_array = []
+ for o in repl.ctr.array:
+ repl_array.append((o.attid, o.version))
+ repl_set = set(repl_array)
+
+ expected_set = set(expected)
+ self.assertEqual(len(repl_set), len(expected),
+ "Unexpected metadata, missing from expected (%s), extra (%s)), repl: \n%s" % (
+ str(expected_set.difference(repl_set)),
+ str(repl_set.difference(expected_set)),
+ ndr_print(repl)))
+
+ i = 0
+ for o in repl.ctr.array:
+ e = expected[i]
+ (attid, version) = e
+ self.assertEqual(attid, o.attid,
+ "(LDAP) Wrong attid "
+ "for expected value %d, wanted 0x%08x got 0x%08x, "
+ "repl: \n%s"
+ % (i, attid, o.attid, ndr_print(repl)))
+ # Allow version to be skipped when it does not matter
+ if version is not None:
+ self.assertEqual(o.version, version,
+ "(LDAP) Wrong version for expected value %d, "
+ "attid 0x%08x, "
+ "wanted %d got %d, repl: \n%s"
+ % (i, o.attid,
+ version, o.version, ndr_print(repl)))
+ i = i + 1
+
+ @staticmethod
+ def restore_deleted_object(samdb, del_dn, new_dn, new_attrs=None):
+ """Restores a deleted object
+ :param samdb: SamDB connection to SAM
+ :param del_dn: str Deleted object DN
+ :param new_dn: str Where to restore the object
+ :param new_attrs: dict Additional attributes to set
+ """
+ msg = Message()
+ msg.dn = Dn(samdb, str(del_dn))
+ msg["isDeleted"] = MessageElement([], FLAG_MOD_DELETE, "isDeleted")
+ msg["distinguishedName"] = MessageElement([str(new_dn)], FLAG_MOD_REPLACE, "distinguishedName")
+ if new_attrs is not None:
+ assert isinstance(new_attrs, dict)
+ for attr in new_attrs:
+ msg[attr] = MessageElement(new_attrs[attr], FLAG_MOD_REPLACE, attr)
+ samdb.modify(msg, ["show_deleted:1"])
+
+
+class BaseRestoreObjectTestCase(RestoredObjectAttributesBaseTestCase):
+ def setUp(self):
+ super(BaseRestoreObjectTestCase, self).setUp()
+
+ def enable_recycle_bin(self):
+ msg = Message()
+ msg.dn = Dn(self.samdb, "")
+ msg["enableOptionalFeature"] = MessageElement(
+ "CN=Partitions," + self.configuration_dn + ":766ddcd8-acd0-445e-f3b9-a7f9b6744f2a",
+ FLAG_MOD_ADD, "enableOptionalFeature")
+ try:
+ self.samdb.modify(msg)
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_ATTRIBUTE_OR_VALUE_EXISTS)
+
+ def test_undelete(self):
+ print("Testing standard undelete operation")
+ usr1 = "cn=testuser,cn=users," + self.base_dn
+ samba.tests.delete_force(self.samdb, usr1)
+ self.samdb.add({
+ "dn": usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": "testuser"})
+ objLive1 = self.search_dn(usr1)
+ guid1 = objLive1["objectGUID"][0]
+ self.samdb.delete(usr1)
+ objDeleted1 = self.search_guid(guid1)
+ self.restore_deleted_object(self.samdb, objDeleted1.dn, usr1)
+ objLive2 = self.search_dn(usr1)
+ self.assertEqual(str(objLive2.dn).lower(), str(objLive1.dn).lower())
+ samba.tests.delete_force(self.samdb, usr1)
+
+ def test_rename(self):
+ print("Testing attempt to rename deleted object")
+ usr1 = "cn=testuser,cn=users," + self.base_dn
+ self.samdb.add({
+ "dn": usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": "testuser"})
+ objLive1 = self.search_dn(usr1)
+ guid1 = objLive1["objectGUID"][0]
+ self.samdb.delete(usr1)
+ objDeleted1 = self.search_guid(guid1)
+ # just to make sure we get the correct error if the show deleted is missing
+ try:
+ self.samdb.rename(str(objDeleted1.dn), usr1)
+ self.fail()
+ except LdbError as e1:
+ (num, _) = e1.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ try:
+ self.samdb.rename(str(objDeleted1.dn), usr1, ["show_deleted:1"])
+ self.fail()
+ except LdbError as e2:
+ (num, _) = e2.args
+ self.assertEqual(num, ERR_UNWILLING_TO_PERFORM)
+
+ def test_undelete_with_mod(self):
+ print("Testing standard undelete operation with modification of additional attributes")
+ usr1 = "cn=testuser,cn=users," + self.base_dn
+ self.samdb.add({
+ "dn": usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": "testuser"})
+ objLive1 = self.search_dn(usr1)
+ guid1 = objLive1["objectGUID"][0]
+ self.samdb.delete(usr1)
+ objDeleted1 = self.search_guid(guid1)
+ self.restore_deleted_object(self.samdb, objDeleted1.dn, usr1, {"url": "www.samba.org"})
+ objLive2 = self.search_dn(usr1)
+ self.assertEqual(str(objLive2["url"][0]), "www.samba.org")
+ samba.tests.delete_force(self.samdb, usr1)
+
+ def test_undelete_newuser(self):
+ print("Testing undelete user with a different dn")
+ usr1 = "cn=testuser,cn=users," + self.base_dn
+ usr2 = "cn=testuser2,cn=users," + self.base_dn
+ samba.tests.delete_force(self.samdb, usr1)
+ self.samdb.add({
+ "dn": usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": "testuser"})
+ objLive1 = self.search_dn(usr1)
+ guid1 = objLive1["objectGUID"][0]
+ self.samdb.delete(usr1)
+ objDeleted1 = self.search_guid(guid1)
+ self.restore_deleted_object(self.samdb, objDeleted1.dn, usr2)
+ objLive2 = self.search_dn(usr2)
+ samba.tests.delete_force(self.samdb, usr1)
+ samba.tests.delete_force(self.samdb, usr2)
+
+ def test_undelete_existing(self):
+ print("Testing undelete user after a user with the same dn has been created")
+ usr1 = "cn=testuser,cn=users," + self.base_dn
+ self.samdb.add({
+ "dn": usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": "testuser"})
+ objLive1 = self.search_dn(usr1)
+ guid1 = objLive1["objectGUID"][0]
+ self.samdb.delete(usr1)
+ self.samdb.add({
+ "dn": usr1,
+ "objectclass": "user",
+ "description": "test user description",
+ "samaccountname": "testuser"})
+ objDeleted1 = self.search_guid(guid1)
+ try:
+ self.restore_deleted_object(self.samdb, objDeleted1.dn, usr1)
+ self.fail()
+ except LdbError as e3:
+ (num, _) = e3.args
+ self.assertEqual(num, ERR_ENTRY_ALREADY_EXISTS)
+
+ def test_undelete_cross_nc(self):
+ print("Cross NC undelete")
+ c1 = "cn=ldaptestcontainer," + self.base_dn
+ c2 = "cn=ldaptestcontainer2," + self.configuration_dn
+ c3 = "cn=ldaptestcontainer," + self.configuration_dn
+ c4 = "cn=ldaptestcontainer2," + self.base_dn
+ samba.tests.delete_force(self.samdb, c1)
+ samba.tests.delete_force(self.samdb, c2)
+ samba.tests.delete_force(self.samdb, c3)
+ samba.tests.delete_force(self.samdb, c4)
+ self.samdb.add({
+ "dn": c1,
+ "objectclass": "container"})
+ self.samdb.add({
+ "dn": c2,
+ "objectclass": "container"})
+ objLive1 = self.search_dn(c1)
+ objLive2 = self.search_dn(c2)
+ guid1 = objLive1["objectGUID"][0]
+ guid2 = objLive2["objectGUID"][0]
+ self.samdb.delete(c1)
+ self.samdb.delete(c2)
+ objDeleted1 = self.search_guid(guid1)
+ objDeleted2 = self.search_guid(guid2)
+ # try to undelete from base dn to config
+ try:
+ self.restore_deleted_object(self.samdb, objDeleted1.dn, c3)
+ self.fail()
+ except LdbError as e4:
+ (num, _) = e4.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ # try to undelete from config to base dn
+ try:
+ self.restore_deleted_object(self.samdb, objDeleted2.dn, c4)
+ self.fail()
+ except LdbError as e5:
+ (num, _) = e5.args
+ self.assertEqual(num, ERR_OPERATIONS_ERROR)
+ # assert undeletion will work in same nc
+ self.restore_deleted_object(self.samdb, objDeleted1.dn, c4)
+ self.restore_deleted_object(self.samdb, objDeleted2.dn, c3)
+
+
+class RestoreUserObjectTestCase(RestoredObjectAttributesBaseTestCase):
+ """Test cases for delete/reanimate user objects"""
+
+ def _expected_user_add_attributes(self, username, user_dn, category):
+ return {'dn': user_dn,
+ 'objectClass': '**',
+ 'cn': username,
+ 'distinguishedName': user_dn,
+ 'instanceType': '4',
+ 'whenCreated': '**',
+ 'whenChanged': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'name': username,
+ 'objectGUID': '**',
+ 'userAccountControl': '546',
+ 'badPwdCount': '0',
+ 'badPasswordTime': '0',
+ 'codePage': '0',
+ 'countryCode': '0',
+ 'lastLogon': '0',
+ 'lastLogoff': '0',
+ 'pwdLastSet': '0',
+ 'primaryGroupID': '513',
+ 'objectSid': '**',
+ 'accountExpires': '9223372036854775807',
+ 'logonCount': '0',
+ 'sAMAccountName': username,
+ 'sAMAccountType': '805306368',
+ 'objectCategory': 'CN=%s,%s' % (category, self.schema_dn)
+ }
+
+ def _expected_user_add_metadata(self):
+ return [
+ (DRSUAPI_ATTID_objectClass, 1),
+ (DRSUAPI_ATTID_cn, 1),
+ (DRSUAPI_ATTID_instanceType, 1),
+ (DRSUAPI_ATTID_whenCreated, 1),
+ (DRSUAPI_ATTID_ntSecurityDescriptor, 1),
+ (DRSUAPI_ATTID_name, 1),
+ (DRSUAPI_ATTID_userAccountControl, None),
+ (DRSUAPI_ATTID_codePage, 1),
+ (DRSUAPI_ATTID_countryCode, 1),
+ (DRSUAPI_ATTID_dBCSPwd, 1),
+ (DRSUAPI_ATTID_logonHours, 1),
+ (DRSUAPI_ATTID_unicodePwd, 1),
+ (DRSUAPI_ATTID_ntPwdHistory, 1),
+ (DRSUAPI_ATTID_pwdLastSet, 1),
+ (DRSUAPI_ATTID_primaryGroupID, 1),
+ (DRSUAPI_ATTID_objectSid, 1),
+ (DRSUAPI_ATTID_accountExpires, 1),
+ (DRSUAPI_ATTID_lmPwdHistory, 1),
+ (DRSUAPI_ATTID_sAMAccountName, 1),
+ (DRSUAPI_ATTID_sAMAccountType, 1),
+ (DRSUAPI_ATTID_objectCategory, 1)]
+
+ def _expected_user_del_attributes(self, username, _guid, _sid):
+ guid = ndr_unpack(misc.GUID, _guid)
+ dn = "CN=%s\\0ADEL:%s,CN=Deleted Objects,%s" % (username, guid, self.base_dn)
+ cn = "%s\nDEL:%s" % (username, guid)
+ return {'dn': dn,
+ 'objectClass': '**',
+ 'cn': cn,
+ 'distinguishedName': dn,
+ 'isDeleted': 'TRUE',
+ 'isRecycled': 'TRUE',
+ 'instanceType': '4',
+ 'whenCreated': '**',
+ 'whenChanged': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'name': cn,
+ 'objectGUID': _guid,
+ 'userAccountControl': '546',
+ 'objectSid': _sid,
+ 'sAMAccountName': username,
+ 'lastKnownParent': 'CN=Users,%s' % self.base_dn,
+ }
+
+ def _expected_user_del_metadata(self):
+ return [
+ (DRSUAPI_ATTID_objectClass, 1),
+ (DRSUAPI_ATTID_cn, 2),
+ (DRSUAPI_ATTID_instanceType, 1),
+ (DRSUAPI_ATTID_whenCreated, 1),
+ (DRSUAPI_ATTID_isDeleted, 1),
+ (DRSUAPI_ATTID_ntSecurityDescriptor, 1),
+ (DRSUAPI_ATTID_name, 2),
+ (DRSUAPI_ATTID_userAccountControl, None),
+ (DRSUAPI_ATTID_codePage, 2),
+ (DRSUAPI_ATTID_countryCode, 2),
+ (DRSUAPI_ATTID_dBCSPwd, 1),
+ (DRSUAPI_ATTID_logonHours, 1),
+ (DRSUAPI_ATTID_unicodePwd, 1),
+ (DRSUAPI_ATTID_ntPwdHistory, 1),
+ (DRSUAPI_ATTID_pwdLastSet, 2),
+ (DRSUAPI_ATTID_primaryGroupID, 2),
+ (DRSUAPI_ATTID_objectSid, 1),
+ (DRSUAPI_ATTID_accountExpires, 2),
+ (DRSUAPI_ATTID_lmPwdHistory, 1),
+ (DRSUAPI_ATTID_sAMAccountName, 1),
+ (DRSUAPI_ATTID_sAMAccountType, 2),
+ (DRSUAPI_ATTID_lastKnownParent, 1),
+ (DRSUAPI_ATTID_objectCategory, 2),
+ (DRSUAPI_ATTID_isRecycled, 1)]
+
+ def _expected_user_restore_attributes(self, username, guid, sid, user_dn, category):
+ return {'dn': user_dn,
+ 'objectClass': '**',
+ 'cn': username,
+ 'distinguishedName': user_dn,
+ 'instanceType': '4',
+ 'whenCreated': '**',
+ 'whenChanged': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'name': username,
+ 'objectGUID': guid,
+ 'userAccountControl': '546',
+ 'badPwdCount': '0',
+ 'badPasswordTime': '0',
+ 'codePage': '0',
+ 'countryCode': '0',
+ 'lastLogon': '0',
+ 'lastLogoff': '0',
+ 'pwdLastSet': '0',
+ 'primaryGroupID': '513',
+ 'operatorCount': '0',
+ 'objectSid': sid,
+ 'adminCount': '0',
+ 'accountExpires': '0',
+ 'logonCount': '0',
+ 'sAMAccountName': username,
+ 'sAMAccountType': '805306368',
+ 'lastKnownParent': 'CN=Users,%s' % self.base_dn,
+ 'objectCategory': 'CN=%s,%s' % (category, self.schema_dn)
+ }
+
+ def _expected_user_restore_metadata(self):
+ return [
+ (DRSUAPI_ATTID_objectClass, 1),
+ (DRSUAPI_ATTID_cn, 3),
+ (DRSUAPI_ATTID_instanceType, 1),
+ (DRSUAPI_ATTID_whenCreated, 1),
+ (DRSUAPI_ATTID_isDeleted, 2),
+ (DRSUAPI_ATTID_ntSecurityDescriptor, 1),
+ (DRSUAPI_ATTID_name, 3),
+ (DRSUAPI_ATTID_userAccountControl, None),
+ (DRSUAPI_ATTID_codePage, 3),
+ (DRSUAPI_ATTID_countryCode, 3),
+ (DRSUAPI_ATTID_dBCSPwd, 1),
+ (DRSUAPI_ATTID_logonHours, 1),
+ (DRSUAPI_ATTID_unicodePwd, 1),
+ (DRSUAPI_ATTID_ntPwdHistory, 1),
+ (DRSUAPI_ATTID_pwdLastSet, 3),
+ (DRSUAPI_ATTID_primaryGroupID, 3),
+ (DRSUAPI_ATTID_operatorCount, 1),
+ (DRSUAPI_ATTID_objectSid, 1),
+ (DRSUAPI_ATTID_adminCount, 1),
+ (DRSUAPI_ATTID_accountExpires, 3),
+ (DRSUAPI_ATTID_lmPwdHistory, 1),
+ (DRSUAPI_ATTID_sAMAccountName, 1),
+ (DRSUAPI_ATTID_sAMAccountType, 3),
+ (DRSUAPI_ATTID_lastKnownParent, 1),
+ (DRSUAPI_ATTID_objectCategory, 3),
+ (DRSUAPI_ATTID_isRecycled, 2)]
+
+ def test_restore_user(self):
+ print("Test restored user attributes")
+ username = "restore_user"
+ usr_dn = "CN=%s,CN=Users,%s" % (username, self.base_dn)
+ samba.tests.delete_force(self.samdb, usr_dn)
+ self.samdb.add({
+ "dn": usr_dn,
+ "objectClass": "user",
+ "sAMAccountName": username})
+ obj = self.search_dn(usr_dn)
+ guid = obj["objectGUID"][0]
+ sid = obj["objectSID"][0]
+ obj_rmd = self.search_guid(guid, attrs=["replPropertyMetaData"])
+ self.assertAttributesExists(self._expected_user_add_attributes(username, usr_dn, "Person"), obj)
+ self._check_metadata(obj_rmd["replPropertyMetaData"],
+ self._expected_user_add_metadata())
+ self.samdb.delete(usr_dn)
+ obj_del = self.search_guid(guid)
+ obj_del_rmd = self.search_guid(guid, attrs=["replPropertyMetaData"])
+ orig_attrs = set(obj.keys())
+ del_attrs = set(obj_del.keys())
+ self.assertAttributesExists(self._expected_user_del_attributes(username, guid, sid), obj_del)
+ self._check_metadata(obj_del_rmd["replPropertyMetaData"],
+ self._expected_user_del_metadata())
+ # restore the user and fetch what's restored
+ self.restore_deleted_object(self.samdb, obj_del.dn, usr_dn)
+ obj_restore = self.search_guid(guid)
+ obj_restore_rmd = self.search_guid(guid, attrs=["replPropertyMetaData"])
+ # check original attributes and restored one are same
+ orig_attrs = set(obj.keys())
+ # windows restore more attributes that originally we have
+ orig_attrs.update(['adminCount', 'operatorCount', 'lastKnownParent'])
+ rest_attrs = set(obj_restore.keys())
+ self.assertAttributesExists(self._expected_user_restore_attributes(username, guid, sid, usr_dn, "Person"), obj_restore)
+ self._check_metadata(obj_restore_rmd["replPropertyMetaData"],
+ self._expected_user_restore_metadata())
+
+
+class RestoreUserPwdObjectTestCase(RestoredObjectAttributesBaseTestCase):
+ """Test cases for delete/reanimate user objects with password"""
+
+ def _expected_userpw_add_attributes(self, username, user_dn, category):
+ return {'dn': user_dn,
+ 'objectClass': '**',
+ 'cn': username,
+ 'distinguishedName': user_dn,
+ 'instanceType': '4',
+ 'whenCreated': '**',
+ 'whenChanged': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'name': username,
+ 'objectGUID': '**',
+ 'userAccountControl': '546',
+ 'badPwdCount': '0',
+ 'badPasswordTime': '0',
+ 'codePage': '0',
+ 'countryCode': '0',
+ 'lastLogon': '0',
+ 'lastLogoff': '0',
+ 'pwdLastSet': '**',
+ 'primaryGroupID': '513',
+ 'objectSid': '**',
+ 'accountExpires': '9223372036854775807',
+ 'logonCount': '0',
+ 'sAMAccountName': username,
+ 'sAMAccountType': '805306368',
+ 'objectCategory': 'CN=%s,%s' % (category, self.schema_dn)
+ }
+
+ def _expected_userpw_add_metadata(self):
+ return [
+ (DRSUAPI_ATTID_objectClass, 1),
+ (DRSUAPI_ATTID_cn, 1),
+ (DRSUAPI_ATTID_instanceType, 1),
+ (DRSUAPI_ATTID_whenCreated, 1),
+ (DRSUAPI_ATTID_ntSecurityDescriptor, 1),
+ (DRSUAPI_ATTID_name, 1),
+ (DRSUAPI_ATTID_userAccountControl, None),
+ (DRSUAPI_ATTID_codePage, 1),
+ (DRSUAPI_ATTID_countryCode, 1),
+ (DRSUAPI_ATTID_dBCSPwd, 1),
+ (DRSUAPI_ATTID_logonHours, 1),
+ (DRSUAPI_ATTID_unicodePwd, 1),
+ (DRSUAPI_ATTID_ntPwdHistory, 1),
+ (DRSUAPI_ATTID_pwdLastSet, 1),
+ (DRSUAPI_ATTID_primaryGroupID, 1),
+ (DRSUAPI_ATTID_supplementalCredentials, 1),
+ (DRSUAPI_ATTID_objectSid, 1),
+ (DRSUAPI_ATTID_accountExpires, 1),
+ (DRSUAPI_ATTID_lmPwdHistory, 1),
+ (DRSUAPI_ATTID_sAMAccountName, 1),
+ (DRSUAPI_ATTID_sAMAccountType, 1),
+ (DRSUAPI_ATTID_objectCategory, 1)]
+
+ def _expected_userpw_del_attributes(self, username, _guid, _sid):
+ guid = ndr_unpack(misc.GUID, _guid)
+ dn = "CN=%s\\0ADEL:%s,CN=Deleted Objects,%s" % (username, guid, self.base_dn)
+ cn = "%s\nDEL:%s" % (username, guid)
+ return {'dn': dn,
+ 'objectClass': '**',
+ 'cn': cn,
+ 'distinguishedName': dn,
+ 'isDeleted': 'TRUE',
+ 'isRecycled': 'TRUE',
+ 'instanceType': '4',
+ 'whenCreated': '**',
+ 'whenChanged': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'name': cn,
+ 'objectGUID': _guid,
+ 'userAccountControl': '546',
+ 'objectSid': _sid,
+ 'sAMAccountName': username,
+ 'lastKnownParent': 'CN=Users,%s' % self.base_dn,
+ }
+
+ def _expected_userpw_del_metadata(self):
+ return [
+ (DRSUAPI_ATTID_objectClass, 1),
+ (DRSUAPI_ATTID_cn, 2),
+ (DRSUAPI_ATTID_instanceType, 1),
+ (DRSUAPI_ATTID_whenCreated, 1),
+ (DRSUAPI_ATTID_isDeleted, 1),
+ (DRSUAPI_ATTID_ntSecurityDescriptor, 1),
+ (DRSUAPI_ATTID_name, 2),
+ (DRSUAPI_ATTID_userAccountControl, None),
+ (DRSUAPI_ATTID_codePage, 2),
+ (DRSUAPI_ATTID_countryCode, 2),
+ (DRSUAPI_ATTID_dBCSPwd, 1),
+ (DRSUAPI_ATTID_logonHours, 1),
+ (DRSUAPI_ATTID_unicodePwd, 2),
+ (DRSUAPI_ATTID_ntPwdHistory, 2),
+ (DRSUAPI_ATTID_pwdLastSet, 2),
+ (DRSUAPI_ATTID_primaryGroupID, 2),
+ (DRSUAPI_ATTID_supplementalCredentials, 2),
+ (DRSUAPI_ATTID_objectSid, 1),
+ (DRSUAPI_ATTID_accountExpires, 2),
+ (DRSUAPI_ATTID_lmPwdHistory, None),
+ (DRSUAPI_ATTID_sAMAccountName, 1),
+ (DRSUAPI_ATTID_sAMAccountType, 2),
+ (DRSUAPI_ATTID_lastKnownParent, 1),
+ (DRSUAPI_ATTID_objectCategory, 2),
+ (DRSUAPI_ATTID_isRecycled, 1)]
+
+ def _expected_userpw_restore_attributes(self, username, guid, sid, user_dn, category):
+ return {'dn': user_dn,
+ 'objectClass': '**',
+ 'cn': username,
+ 'distinguishedName': user_dn,
+ 'instanceType': '4',
+ 'whenCreated': '**',
+ 'whenChanged': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'name': username,
+ 'objectGUID': guid,
+ 'userAccountControl': '546',
+ 'badPwdCount': '0',
+ 'badPasswordTime': '0',
+ 'codePage': '0',
+ 'countryCode': '0',
+ 'lastLogon': '0',
+ 'lastLogoff': '0',
+ 'pwdLastSet': '**',
+ 'primaryGroupID': '513',
+ 'operatorCount': '0',
+ 'objectSid': sid,
+ 'adminCount': '0',
+ 'accountExpires': '0',
+ 'logonCount': '0',
+ 'sAMAccountName': username,
+ 'sAMAccountType': '805306368',
+ 'lastKnownParent': 'CN=Users,%s' % self.base_dn,
+ 'objectCategory': 'CN=%s,%s' % (category, self.schema_dn)
+ }
+
+ def _expected_userpw_restore_metadata(self):
+ return [
+ (DRSUAPI_ATTID_objectClass, 1),
+ (DRSUAPI_ATTID_cn, 3),
+ (DRSUAPI_ATTID_instanceType, 1),
+ (DRSUAPI_ATTID_whenCreated, 1),
+ (DRSUAPI_ATTID_isDeleted, 2),
+ (DRSUAPI_ATTID_ntSecurityDescriptor, 1),
+ (DRSUAPI_ATTID_name, 3),
+ (DRSUAPI_ATTID_userAccountControl, None),
+ (DRSUAPI_ATTID_codePage, 3),
+ (DRSUAPI_ATTID_countryCode, 3),
+ (DRSUAPI_ATTID_dBCSPwd, 2),
+ (DRSUAPI_ATTID_logonHours, 1),
+ (DRSUAPI_ATTID_unicodePwd, 3),
+ (DRSUAPI_ATTID_ntPwdHistory, 3),
+ (DRSUAPI_ATTID_pwdLastSet, 4),
+ (DRSUAPI_ATTID_primaryGroupID, 3),
+ (DRSUAPI_ATTID_supplementalCredentials, 3),
+ (DRSUAPI_ATTID_operatorCount, 1),
+ (DRSUAPI_ATTID_objectSid, 1),
+ (DRSUAPI_ATTID_adminCount, 1),
+ (DRSUAPI_ATTID_accountExpires, 3),
+ (DRSUAPI_ATTID_lmPwdHistory, None),
+ (DRSUAPI_ATTID_sAMAccountName, 1),
+ (DRSUAPI_ATTID_sAMAccountType, 3),
+ (DRSUAPI_ATTID_lastKnownParent, 1),
+ (DRSUAPI_ATTID_objectCategory, 3),
+ (DRSUAPI_ATTID_isRecycled, 2)]
+
+ def test_restorepw_user(self):
+ print("Test restored user attributes")
+ username = "restorepw_user"
+ usr_dn = "CN=%s,CN=Users,%s" % (username, self.base_dn)
+ samba.tests.delete_force(self.samdb, usr_dn)
+ self.samdb.add({
+ "dn": usr_dn,
+ "objectClass": "user",
+ "userPassword": "thatsAcomplPASS0",
+ "sAMAccountName": username})
+ obj = self.search_dn(usr_dn)
+ guid = obj["objectGUID"][0]
+ sid = obj["objectSID"][0]
+ obj_rmd = self.search_guid(guid, attrs=["replPropertyMetaData"])
+ self.assertAttributesExists(self._expected_userpw_add_attributes(username, usr_dn, "Person"), obj)
+ self._check_metadata(obj_rmd["replPropertyMetaData"],
+ self._expected_userpw_add_metadata())
+ self.samdb.delete(usr_dn)
+ obj_del = self.search_guid(guid)
+ obj_del_rmd = self.search_guid(guid, attrs=["replPropertyMetaData"])
+ orig_attrs = set(obj.keys())
+ del_attrs = set(obj_del.keys())
+ self.assertAttributesExists(self._expected_userpw_del_attributes(username, guid, sid), obj_del)
+ self._check_metadata(obj_del_rmd["replPropertyMetaData"],
+ self._expected_userpw_del_metadata())
+ # restore the user and fetch what's restored
+ self.restore_deleted_object(self.samdb, obj_del.dn, usr_dn, {"userPassword": ["thatsAcomplPASS1"]})
+ obj_restore = self.search_guid(guid)
+ obj_restore_rmd = self.search_guid(guid, attrs=["replPropertyMetaData"])
+ # check original attributes and restored one are same
+ orig_attrs = set(obj.keys())
+ # windows restore more attributes that originally we have
+ orig_attrs.update(['adminCount', 'operatorCount', 'lastKnownParent'])
+ rest_attrs = set(obj_restore.keys())
+ self.assertAttributesExists(self._expected_userpw_restore_attributes(username, guid, sid, usr_dn, "Person"), obj_restore)
+ self._check_metadata(obj_restore_rmd["replPropertyMetaData"],
+ self._expected_userpw_restore_metadata())
+
+
+class RestoreGroupObjectTestCase(RestoredObjectAttributesBaseTestCase):
+ """Test different scenarios for delete/reanimate group objects"""
+
+ def _make_object_dn(self, name):
+ return "CN=%s,CN=Users,%s" % (name, self.base_dn)
+
+ def _create_test_user(self, user_name):
+ user_dn = self._make_object_dn(user_name)
+ ldif = {
+ "dn": user_dn,
+ "objectClass": "user",
+ "sAMAccountName": user_name,
+ }
+ # delete an object if leftover from previous test
+ samba.tests.delete_force(self.samdb, user_dn)
+ # finally, create the group
+ self.samdb.add(ldif)
+ return self.search_dn(user_dn)
+
+ def _create_test_group(self, group_name, members=None):
+ group_dn = self._make_object_dn(group_name)
+ ldif = {
+ "dn": group_dn,
+ "objectClass": "group",
+ "sAMAccountName": group_name,
+ }
+ try:
+ ldif["member"] = [str(usr_dn) for usr_dn in members]
+ except TypeError:
+ pass
+ # delete an object if leftover from previous test
+ samba.tests.delete_force(self.samdb, group_dn)
+ # finally, create the group
+ self.samdb.add(ldif)
+ return self.search_dn(group_dn)
+
+ def _expected_group_attributes(self, groupname, group_dn, category):
+ return {'dn': group_dn,
+ 'groupType': '-2147483646',
+ 'distinguishedName': group_dn,
+ 'sAMAccountName': groupname,
+ 'name': groupname,
+ 'objectCategory': 'CN=%s,%s' % (category, self.schema_dn),
+ 'objectClass': '**',
+ 'objectGUID': '**',
+ 'lastKnownParent': 'CN=Users,%s' % self.base_dn,
+ 'whenChanged': '**',
+ 'sAMAccountType': '268435456',
+ 'objectSid': '**',
+ 'whenCreated': '**',
+ 'uSNCreated': '**',
+ 'operatorCount': '0',
+ 'uSNChanged': '**',
+ 'instanceType': '4',
+ 'adminCount': '0',
+ 'cn': groupname}
+
+ def test_plain_group(self):
+ print("Test restored Group attributes")
+ # create test group
+ obj = self._create_test_group("r_group")
+ guid = obj["objectGUID"][0]
+ # delete the group
+ self.samdb.delete(str(obj.dn))
+ obj_del = self.search_guid(guid)
+ # restore the Group and fetch what's restored
+ self.restore_deleted_object(self.samdb, obj_del.dn, obj.dn)
+ obj_restore = self.search_guid(guid)
+ # check original attributes and restored one are same
+ attr_orig = set(obj.keys())
+ # windows restore more attributes that originally we have
+ attr_orig.update(['adminCount', 'operatorCount', 'lastKnownParent'])
+ attr_rest = set(obj_restore.keys())
+ self.assertAttributesEqual(obj, attr_orig, obj_restore, attr_rest)
+ self.assertAttributesExists(self._expected_group_attributes("r_group", str(obj.dn), "Group"), obj_restore)
+
+ def test_group_with_members(self):
+ print("Test restored Group with members attributes")
+ # create test group
+ usr1 = self._create_test_user("r_user_1")
+ usr2 = self._create_test_user("r_user_2")
+ obj = self._create_test_group("r_group", [usr1.dn, usr2.dn])
+ guid = obj["objectGUID"][0]
+ # delete the group
+ self.samdb.delete(str(obj.dn))
+ obj_del = self.search_guid(guid)
+ # restore the Group and fetch what's restored
+ self.restore_deleted_object(self.samdb, obj_del.dn, obj.dn)
+ obj_restore = self.search_guid(guid)
+ # check original attributes and restored one are same
+ attr_orig = set(obj.keys())
+ # windows restore more attributes that originally we have
+ attr_orig.update(['adminCount', 'operatorCount', 'lastKnownParent'])
+ # and does not restore following attributes
+ attr_orig.remove("member")
+ attr_rest = set(obj_restore.keys())
+ self.assertAttributesEqual(obj, attr_orig, obj_restore, attr_rest)
+ self.assertAttributesExists(self._expected_group_attributes("r_group", str(obj.dn), "Group"), obj_restore)
+
+
+class RestoreContainerObjectTestCase(RestoredObjectAttributesBaseTestCase):
+ """Test different scenarios for delete/reanimate OU/container objects"""
+
+ def _expected_container_attributes(self, rdn, name, dn, category):
+ if rdn == 'OU':
+ lastKnownParent = '%s' % self.base_dn
+ else:
+ lastKnownParent = 'CN=Users,%s' % self.base_dn
+ return {'dn': dn,
+ 'distinguishedName': dn,
+ 'name': name,
+ 'objectCategory': 'CN=%s,%s' % (category, self.schema_dn),
+ 'objectClass': '**',
+ 'objectGUID': '**',
+ 'lastKnownParent': lastKnownParent,
+ 'whenChanged': '**',
+ 'whenCreated': '**',
+ 'uSNCreated': '**',
+ 'uSNChanged': '**',
+ 'instanceType': '4',
+ rdn.lower(): name}
+
+ def _create_test_ou(self, rdn, name=None, description=None):
+ ou_dn = "OU=%s,%s" % (rdn, self.base_dn)
+ # delete an object if leftover from previous test
+ samba.tests.delete_force(self.samdb, ou_dn)
+ # create ou and return created object
+ self.samdb.create_ou(ou_dn, name=name, description=description)
+ return self.search_dn(ou_dn)
+
+ def test_ou_with_name_description(self):
+ print("Test OU reanimation")
+ # create OU to test with
+ obj = self._create_test_ou(rdn="r_ou",
+ name="r_ou name",
+ description="r_ou description")
+ guid = obj["objectGUID"][0]
+ # delete the object
+ self.samdb.delete(str(obj.dn))
+ obj_del = self.search_guid(guid)
+ # restore the Object and fetch what's restored
+ self.restore_deleted_object(self.samdb, obj_del.dn, obj.dn)
+ obj_restore = self.search_guid(guid)
+ # check original attributes and restored one are same
+ attr_orig = set(obj.keys())
+ attr_rest = set(obj_restore.keys())
+ # windows restore more attributes that originally we have
+ attr_orig.update(["lastKnownParent"])
+ # and does not restore following attributes
+ attr_orig -= set(["description"])
+ self.assertAttributesEqual(obj, attr_orig, obj_restore, attr_rest)
+ expected_attrs = self._expected_container_attributes("OU", "r_ou", str(obj.dn), "Organizational-Unit")
+ self.assertAttributesExists(expected_attrs, obj_restore)
+
+ def test_container(self):
+ print("Test Container reanimation")
+ # create test Container
+ obj = self._create_object({
+ "dn": "CN=r_container,CN=Users,%s" % self.base_dn,
+ "objectClass": "container"
+ })
+ guid = obj["objectGUID"][0]
+ # delete the object
+ self.samdb.delete(str(obj.dn))
+ obj_del = self.search_guid(guid)
+ # restore the Object and fetch what's restored
+ self.restore_deleted_object(self.samdb, obj_del.dn, obj.dn)
+ obj_restore = self.search_guid(guid)
+ # check original attributes and restored one are same
+ attr_orig = set(obj.keys())
+ attr_rest = set(obj_restore.keys())
+ # windows restore more attributes that originally we have
+ attr_orig.update(["lastKnownParent"])
+ # and does not restore following attributes
+ attr_orig -= set(["showInAdvancedViewOnly"])
+ self.assertAttributesEqual(obj, attr_orig, obj_restore, attr_rest)
+ expected_attrs = self._expected_container_attributes("CN", "r_container",
+ str(obj.dn), "Container")
+ self.assertAttributesExists(expected_attrs, obj_restore)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/source4/dsdb/tests/python/unicodepwd_encrypted.py b/source4/dsdb/tests/python/unicodepwd_encrypted.py
new file mode 100644
index 0000000..768cbf8
--- /dev/null
+++ b/source4/dsdb/tests/python/unicodepwd_encrypted.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import sys
+import optparse
+
+sys.path.insert(0, "bin/python")
+import samba.getopt as options
+from ldb import Message, MessageElement, Dn
+from ldb import LdbError, FLAG_MOD_REPLACE, ERR_UNWILLING_TO_PERFORM, SCOPE_BASE
+from samba import gensec
+from samba.auth import system_session
+from samba.samdb import SamDB
+from samba.tests import delete_force
+from samba.tests.password_test import PasswordTestCase
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+parser = optparse.OptionParser("unicodepwd_encrypted.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+lp = sambaopts.get_loadparm()
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+host_ldaps = f"ldaps://{host}"
+host_ldap = f"ldap://{host}"
+
+
+class UnicodePwdEncryptedConnectionTests(PasswordTestCase):
+
+ def setUp(self):
+ super().setUp()
+ self.creds = self.insta_creds(template=credopts.get_credentials(lp))
+ self.ldb = SamDB(host_ldap, credentials=self.creds,
+ session_info=system_session(lp),
+ lp=lp)
+ self.base_dn = self.ldb.domain_dn()
+ self.user_dn_str = f"cn=testuser,cn=users,{self.base_dn}"
+ self.user_dn = Dn(self.ldb, self.user_dn_str)
+ print(f"baseDN: {self.base_dn}\n")
+
+ # permit password changes during this test
+ self.allow_password_changes()
+
+ # (Re)adds the test user "testuser" with no password.
+ delete_force(self.ldb, str(self.user_dn))
+ self.ldb.add({
+ "dn": str(self.user_dn),
+ "objectclass": "user",
+ "sAMAccountName": "testuser"
+ })
+
+ # Set the test user initial password and enable account.
+ m = Message(self.user_dn)
+ m["0"] = MessageElement("Password#2", FLAG_MOD_REPLACE, "userPassword")
+ self.ldb.modify(m)
+ self.ldb.enable_account("(sAMAccountName=testuser)")
+
+ def modify_unicode_pwd(self, ldb, password):
+ """Replaces user password using unicodePwd."""
+ m = Message()
+ m.dn = self.user_dn
+ m["unicodePwd"] = MessageElement(
+ f'"{password}"'.encode('utf-16-le'),
+ FLAG_MOD_REPLACE, "unicodePwd"
+ )
+ ldb.modify(m)
+
+ def get_admin_sid(self, ldb):
+ res = self.ldb.search(
+ base="", expression="", scope=SCOPE_BASE, attrs=["tokenGroups"])
+
+ return self.ldb.schema_format_value(
+ "tokenGroups", res[0]["tokenGroups"][0]).decode("utf8")
+
+ def test_with_seal(self):
+ """Test unicodePwd on connection with seal.
+
+ This should allow unicodePwd.
+ """
+ self.modify_unicode_pwd(self.ldb, "thatsAcomplPASS2")
+
+ def test_without_seal(self):
+ """Test unicodePwd on connection without seal.
+
+ Should not allow unicodePwd on an unencrypted connection.
+
+ Requires --use-kerberos=required, or it automatically upgrades
+ to an encrypted connection.
+ """
+ # Remove FEATURE_SEAL which gets added by insta_creds.
+ creds_noseal = self.insta_creds(template=credopts.get_credentials(lp))
+ creds_noseal.set_gensec_features(creds_noseal.get_gensec_features() &
+ ~gensec.FEATURE_SEAL)
+
+ sasl_wrap = lp.get('client ldap sasl wrapping')
+ self.addCleanup(lp.set, 'client ldap sasl wrapping', sasl_wrap)
+ lp.set('client ldap sasl wrapping', 'sign')
+
+ # Create a second ldb connection without seal.
+ ldb = SamDB(host_ldap, credentials=creds_noseal,
+ session_info=system_session(lp),
+ lp=lp)
+
+ with self.assertRaises(LdbError) as e:
+ self.modify_unicode_pwd(ldb, "thatsAcomplPASS2")
+
+ # Server should not allow unicodePwd on an unencrypted connection.
+ self.assertEqual(e.exception.args[0], ERR_UNWILLING_TO_PERFORM)
+ self.assertIn(
+ "Password modification over LDAP must be over an encrypted connection",
+ e.exception.args[1]
+ )
+
+ def test_simple_bind_plain(self):
+ """Test unicodePwd using simple bind without encryption."""
+ admin_sid = self.get_admin_sid(self.ldb)
+
+ self.creds.set_bind_dn(admin_sid)
+ ldb = SamDB(url=host_ldap, credentials=self.creds, lp=lp)
+
+ with self.assertRaises(LdbError) as e:
+ self.modify_unicode_pwd(ldb, "thatsAcomplPASS2")
+
+ # Server should not allow unicodePwd on an unencrypted connection.
+ self.assertEqual(e.exception.args[0], ERR_UNWILLING_TO_PERFORM)
+ self.assertIn(
+ "Password modification over LDAP must be over an encrypted connection",
+ e.exception.args[1]
+ )
+
+ def test_simple_bind_tls(self):
+ """Test unicodePwd using simple bind with encryption."""
+ admin_sid = self.get_admin_sid(self.ldb)
+
+ self.creds.set_bind_dn(admin_sid)
+ ldb = SamDB(url=host_ldaps, credentials=self.creds, lp=lp)
+
+ self.modify_unicode_pwd(ldb, "thatsAcomplPASS2")
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/urgent_replication.py b/source4/dsdb/tests/python/urgent_replication.py
new file mode 100755
index 0000000..0d39c38
--- /dev/null
+++ b/source4/dsdb/tests/python/urgent_replication.py
@@ -0,0 +1,339 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import optparse
+import sys
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import TestProgram, SubunitOptions
+
+from ldb import (LdbError, ERR_NO_SUCH_OBJECT, Message,
+ MessageElement, Dn, FLAG_MOD_REPLACE)
+import samba.tests
+import samba.dsdb as dsdb
+import samba.getopt as options
+import random
+
+parser = optparse.OptionParser("urgent_replication.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+
+class UrgentReplicationTests(samba.tests.TestCase):
+
+ def delete_force(self, ldb, dn):
+ try:
+ ldb.delete(dn, ["relax:0"])
+ except LdbError as e:
+ (num, _) = e.args
+ self.assertEqual(num, ERR_NO_SUCH_OBJECT)
+
+ def setUp(self):
+ super(UrgentReplicationTests, self).setUp()
+ self.ldb = samba.tests.connect_samdb(host, global_schema=False)
+ self.base_dn = self.ldb.domain_dn()
+
+ print("baseDN: %s\n" % self.base_dn)
+
+ def test_nonurgent_object(self):
+ """Test if the urgent replication is not activated when handling a non urgent object."""
+ self.ldb.add({
+ "dn": "cn=nonurgenttest,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "samaccountname": "nonurgenttest",
+ "description": "nonurgenttest description"})
+
+ # urgent replication should not be enabled when creating
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should not be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=nonurgenttest,cn=users," + self.base_dn)
+ m["description"] = MessageElement("new description", FLAG_MOD_REPLACE,
+ "description")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should not be enabled when deleting
+ self.delete_force(self.ldb, "cn=nonurgenttest,cn=users," + self.base_dn)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ def test_nTDSDSA_object(self):
+ """Test if the urgent replication is activated when handling a nTDSDSA object."""
+ self.ldb.add({
+ "dn": "cn=test server,cn=Servers,cn=Default-First-Site-Name,cn=Sites,%s" %
+ self.ldb.get_config_basedn(),
+ "objectclass": "server",
+ "cn": "test server",
+ "name": "test server",
+ "systemFlags": "50000000"}, ["relax:0"])
+
+ self.ldb.add_ldif(
+ """dn: cn=NTDS Settings test,cn=test server,cn=Servers,cn=Default-First-Site-Name,cn=Sites,cn=Configuration,%s""" % (self.base_dn) + """
+objectclass: nTDSDSA
+cn: NTDS Settings test
+options: 1
+instanceType: 4
+systemFlags: 33554432""", ["relax:0"])
+
+ # urgent replication should be enabled when creation
+ res = self.ldb.load_partition_usn("cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should NOT be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=NTDS Settings test,cn=test server,cn=Servers,cn=Default-First-Site-Name,cn=Sites,cn=Configuration," + self.base_dn)
+ m["options"] = MessageElement("0", FLAG_MOD_REPLACE,
+ "options")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn("cn=Configuration," + self.base_dn)
+ self.assertNotEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when deleting
+ self.delete_force(self.ldb, "cn=NTDS Settings test,cn=test server,cn=Servers,cn=Default-First-Site-Name,cn=Sites,cn=Configuration," + self.base_dn)
+ res = self.ldb.load_partition_usn("cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ self.delete_force(self.ldb, "cn=test server,cn=Servers,cn=Default-First-Site-Name,cn=Sites,cn=Configuration," + self.base_dn)
+
+ def test_crossRef_object(self):
+ """Test if the urgent replication is activated when handling a crossRef object."""
+ self.ldb.add({
+ "dn": "CN=test crossRef,CN=Partitions,CN=Configuration," + self.base_dn,
+ "objectClass": "crossRef",
+ "cn": "test crossRef",
+ "dnsRoot": self.get_loadparm().get("realm").lower(),
+ "instanceType": "4",
+ "nCName": self.base_dn,
+ "showInAdvancedViewOnly": "TRUE",
+ "name": "test crossRef",
+ "systemFlags": "1"}, ["relax:0"])
+
+ # urgent replication should be enabled when creating
+ res = self.ldb.load_partition_usn("cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should NOT be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=test crossRef,CN=Partitions,CN=Configuration," + self.base_dn)
+ m["systemFlags"] = MessageElement("0", FLAG_MOD_REPLACE,
+ "systemFlags")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn("cn=Configuration," + self.base_dn)
+ self.assertNotEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when deleting
+ self.delete_force(self.ldb, "cn=test crossRef,CN=Partitions,CN=Configuration," + self.base_dn)
+ res = self.ldb.load_partition_usn("cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ def test_attributeSchema_object(self):
+ """Test if the urgent replication is activated when handling an attributeSchema object"""
+
+ self.ldb.add_ldif(
+ """dn: CN=test attributeSchema,cn=Schema,CN=Configuration,%s""" % self.base_dn + """
+objectClass: attributeSchema
+cn: test attributeSchema
+instanceType: 4
+isSingleValued: FALSE
+showInAdvancedViewOnly: FALSE
+attributeID: 1.3.6.1.4.1.7165.4.6.1.4.""" + str(random.randint(1, 100000)) + """
+attributeSyntax: 2.5.5.12
+adminDisplayName: test attributeSchema
+adminDescription: test attributeSchema
+oMSyntax: 64
+systemOnly: FALSE
+searchFlags: 8
+lDAPDisplayName: testAttributeSchema
+name: test attributeSchema""")
+
+ # urgent replication should be enabled when creating
+ res = self.ldb.load_partition_usn("cn=Schema,cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=test attributeSchema,CN=Schema,CN=Configuration," + self.base_dn)
+ m["lDAPDisplayName"] = MessageElement("updatedTestAttributeSchema", FLAG_MOD_REPLACE,
+ "lDAPDisplayName")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn("cn=Schema,cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ def test_classSchema_object(self):
+ """Test if the urgent replication is activated when handling a classSchema object."""
+ try:
+ self.ldb.add_ldif(
+ """dn: CN=test classSchema,CN=Schema,CN=Configuration,%s""" % self.base_dn + """
+objectClass: classSchema
+cn: test classSchema
+instanceType: 4
+subClassOf: top
+governsId: 1.3.6.1.4.1.7165.4.6.2.4.""" + str(random.randint(1, 100000)) + """
+rDNAttID: cn
+showInAdvancedViewOnly: TRUE
+adminDisplayName: test classSchema
+adminDescription: test classSchema
+objectClassCategory: 1
+lDAPDisplayName: testClassSchema
+name: test classSchema
+systemOnly: FALSE
+systemPossSuperiors: dfsConfiguration
+systemMustContain: msDFS-SchemaMajorVersion
+defaultSecurityDescriptor: D:(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;DA)(A;;RPWPCRCCD
+ CLCLORCWOWDSDDTSW;;;SY)(A;;RPLCLORC;;;AU)(A;;RPWPCRCCDCLCLORCWOWDSDDTSW;;;CO)
+systemFlags: 16
+defaultHidingValue: TRUE""")
+
+ # urgent replication should be enabled when creating
+ res = self.ldb.load_partition_usn("cn=Schema,cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ except LdbError:
+ print("Not testing urgent replication when creating classSchema object ...\n")
+
+ # urgent replication should be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=test classSchema,CN=Schema,CN=Configuration," + self.base_dn)
+ m["lDAPDisplayName"] = MessageElement("updatedTestClassSchema", FLAG_MOD_REPLACE,
+ "lDAPDisplayName")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn("cn=Schema,cn=Configuration," + self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ def test_secret_object(self):
+ """Test if the urgent replication is activated when handling a secret object."""
+
+ self.ldb.add({
+ "dn": "cn=test secret,cn=System," + self.base_dn,
+ "objectClass": "secret",
+ "cn": "test secret",
+ "name": "test secret",
+ "currentValue": "xxxxxxx"})
+
+ # urgent replication should be enabled when creating
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=test secret,cn=System," + self.base_dn)
+ m["currentValue"] = MessageElement("yyyyyyyy", FLAG_MOD_REPLACE,
+ "currentValue")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should NOT be enabled when deleting
+ self.delete_force(self.ldb, "cn=test secret,cn=System," + self.base_dn)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ def test_rIDManager_object(self):
+ """Test if the urgent replication is activated when handling a rIDManager object."""
+ self.ldb.add_ldif(
+ """dn: CN=RID Manager test,CN=System,%s""" % self.base_dn + """
+objectClass: rIDManager
+cn: RID Manager test
+instanceType: 4
+showInAdvancedViewOnly: TRUE
+name: RID Manager test
+systemFlags: -1946157056
+isCriticalSystemObject: TRUE
+rIDAvailablePool: 133001-1073741823""", ["relax:0"])
+
+ # urgent replication should be enabled when creating
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when modifying
+ m = Message()
+ m.dn = Dn(self.ldb, "CN=RID Manager test,CN=System," + self.base_dn)
+ m["systemFlags"] = MessageElement("0", FLAG_MOD_REPLACE,
+ "systemFlags")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should NOT be enabled when deleting
+ self.delete_force(self.ldb, "CN=RID Manager test,CN=System," + self.base_dn)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ def test_urgent_attributes(self):
+ """Test if the urgent replication is activated when handling urgent attributes of an object."""
+
+ self.ldb.add({
+ "dn": "cn=user UrgAttr test,cn=users," + self.base_dn,
+ "objectclass": "user",
+ "samaccountname": "user UrgAttr test",
+ "userAccountControl": str(dsdb.UF_NORMAL_ACCOUNT),
+ "lockoutTime": "0",
+ "pwdLastSet": "0",
+ "description": "urgent attributes test description"})
+
+ # urgent replication should NOT be enabled when creating
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when modifying userAccountControl
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=user UrgAttr test,cn=users," + self.base_dn)
+ m["userAccountControl"] = MessageElement(str(dsdb.UF_NORMAL_ACCOUNT + dsdb.UF_DONT_EXPIRE_PASSWD), FLAG_MOD_REPLACE,
+ "userAccountControl")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when modifying lockoutTime
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=user UrgAttr test,cn=users," + self.base_dn)
+ m["lockoutTime"] = MessageElement("1", FLAG_MOD_REPLACE,
+ "lockoutTime")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should be enabled when modifying pwdLastSet
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=user UrgAttr test,cn=users," + self.base_dn)
+ m["pwdLastSet"] = MessageElement("-1", FLAG_MOD_REPLACE,
+ "pwdLastSet")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should NOT be enabled when modifying a not-urgent
+ # attribute
+ m = Message()
+ m.dn = Dn(self.ldb, "cn=user UrgAttr test,cn=users," + self.base_dn)
+ m["description"] = MessageElement("updated urgent attributes test description",
+ FLAG_MOD_REPLACE, "description")
+ self.ldb.modify(m)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEqual(res["uSNHighest"], res["uSNUrgent"])
+
+ # urgent replication should NOT be enabled when deleting
+ self.delete_force(self.ldb, "cn=user UrgAttr test,cn=users," + self.base_dn)
+ res = self.ldb.load_partition_usn(self.base_dn)
+ self.assertNotEqual(res["uSNHighest"], res["uSNUrgent"])
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/tests/python/user_account_control.py b/source4/dsdb/tests/python/user_account_control.py
new file mode 100755
index 0000000..edc0fa0
--- /dev/null
+++ b/source4/dsdb/tests/python/user_account_control.py
@@ -0,0 +1,1298 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# This tests the restrictions on userAccountControl that apply even if write access is permitted
+#
+# Copyright Samuel Cabrero 2014 <samuelcabrero@kernevil.me>
+# Copyright Andrew Bartlett 2014 <abartlet@samba.org>
+#
+# Licenced under the GPLv3
+#
+
+import optparse
+import sys
+import unittest
+import samba
+import samba.getopt as options
+import samba.tests
+import ldb
+
+sys.path.insert(0, "bin/python")
+
+from samba.subunit.run import SubunitTestRunner
+from samba.auth import system_session
+from samba.samdb import SamDB
+from samba.dcerpc import samr, security
+from samba.credentials import Credentials
+from samba.ndr import ndr_unpack, ndr_pack
+from samba.tests import delete_force, DynamicTestCase
+from samba import gensec, sd_utils
+from samba.credentials import DONT_USE_KERBEROS
+from ldb import SCOPE_SUBTREE, SCOPE_BASE, LdbError
+from samba.dsdb import UF_SCRIPT, UF_ACCOUNTDISABLE, UF_00000004, UF_HOMEDIR_REQUIRED, \
+ UF_LOCKOUT, UF_PASSWD_NOTREQD, UF_PASSWD_CANT_CHANGE, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,\
+ UF_TEMP_DUPLICATE_ACCOUNT, UF_NORMAL_ACCOUNT, UF_00000400, UF_INTERDOMAIN_TRUST_ACCOUNT, \
+ UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_00004000, \
+ UF_00008000, UF_DONT_EXPIRE_PASSWD, UF_MNS_LOGON_ACCOUNT, UF_SMARTCARD_REQUIRED, \
+ UF_TRUSTED_FOR_DELEGATION, UF_NOT_DELEGATED, UF_USE_DES_KEY_ONLY, UF_DONT_REQUIRE_PREAUTH, \
+ UF_PASSWORD_EXPIRED, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_NO_AUTH_DATA_REQUIRED, \
+ UF_PARTIAL_SECRETS_ACCOUNT, UF_USE_AES_KEYS
+from samba import dsdb
+
+
+parser = optparse.OptionParser("user_account_control.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+host = args[0]
+
+if "://" not in host:
+ ldaphost = "ldap://%s" % host
+else:
+ ldaphost = host
+ start = host.rindex("://")
+ host = host.lstrip(start + 3)
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL)
+
+bits = [UF_SCRIPT, UF_ACCOUNTDISABLE, UF_00000004, UF_HOMEDIR_REQUIRED,
+ UF_LOCKOUT, UF_PASSWD_NOTREQD, UF_PASSWD_CANT_CHANGE,
+ UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,
+ UF_TEMP_DUPLICATE_ACCOUNT, UF_NORMAL_ACCOUNT, UF_00000400,
+ UF_INTERDOMAIN_TRUST_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_00004000,
+ UF_00008000, UF_DONT_EXPIRE_PASSWD, UF_MNS_LOGON_ACCOUNT, UF_SMARTCARD_REQUIRED,
+ UF_TRUSTED_FOR_DELEGATION, UF_NOT_DELEGATED, UF_USE_DES_KEY_ONLY,
+ UF_DONT_REQUIRE_PREAUTH,
+ UF_PASSWORD_EXPIRED, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
+ UF_NO_AUTH_DATA_REQUIRED,
+ UF_PARTIAL_SECRETS_ACCOUNT, UF_USE_AES_KEYS,
+ int("0x10000000", 16), int("0x20000000", 16), int("0x40000000", 16), int("0x80000000", 16)]
+
+account_types = set([UF_NORMAL_ACCOUNT, UF_WORKSTATION_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT, UF_INTERDOMAIN_TRUST_ACCOUNT])
+
+
+@DynamicTestCase
+class UserAccountControlTests(samba.tests.TestCase):
+ @classmethod
+ def setUpDynamicTestCases(cls):
+ for priv in [(True, "priv"), (False, "cc")]:
+ for account_type in [UF_NORMAL_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT,
+ UF_SERVER_TRUST_ACCOUNT]:
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(account_type)
+ for objectclass in ["computer", "user"]:
+ for name in [("oc_uac_lock$", "withdollar"),
+ ("oc_uac_lock", "plain")]:
+ test_name = f"{account_type_str}_{objectclass}_{priv[1]}_{name[1]}"
+ cls.generate_dynamic_test("test_objectclass_uac_dollar_lock",
+ test_name,
+ account_type,
+ objectclass,
+ name[0],
+ priv[0])
+
+ for priv in [(True, "priv"), (False, "wp")]:
+ for account_type in [UF_NORMAL_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT,
+ UF_SERVER_TRUST_ACCOUNT]:
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(account_type)
+ for account_type2 in [UF_NORMAL_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT,
+ UF_SERVER_TRUST_ACCOUNT]:
+ for how in ["replace", "deladd"]:
+ account_type2_str = dsdb.user_account_control_flag_bit_to_string(account_type2)
+ test_name = f"{account_type_str}_{account_type2_str}_{how}_{priv[1]}"
+ cls.generate_dynamic_test("test_objectclass_uac_mod_lock",
+ test_name,
+ account_type,
+ account_type2,
+ how,
+ priv[0])
+
+ for objectclass in ["computer", "user"]:
+ account_types = [UF_NORMAL_ACCOUNT]
+ if objectclass == "computer":
+ account_types.append(UF_WORKSTATION_TRUST_ACCOUNT)
+ account_types.append(UF_SERVER_TRUST_ACCOUNT)
+
+ for account_type in account_types:
+ account_type_str = (
+ dsdb.user_account_control_flag_bit_to_string(
+ account_type))
+ for account_type2 in [UF_NORMAL_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT,
+ UF_SERVER_TRUST_ACCOUNT,
+ UF_PARTIAL_SECRETS_ACCOUNT,
+ None]:
+ if account_type2 is None:
+ account_type2_str = None
+ else:
+ account_type2_str = (
+ dsdb.user_account_control_flag_bit_to_string(
+ account_type2))
+
+ for objectclass2 in ["computer", "user", None]:
+ for name2 in [("oc_uac_lock", "remove_dollar"),
+ (None, "keep_dollar")]:
+ test_name = (f"{priv[1]}_{objectclass}_"
+ f"{account_type_str}_to_"
+ f"{objectclass2}_"
+ f"{account_type2_str}_"
+ f"{name2[1]}")
+ cls.generate_dynamic_test("test_mod_lock",
+ test_name,
+ objectclass,
+ objectclass2,
+ account_type,
+ account_type2,
+ name2[0],
+ priv[0])
+
+ for account_type in [UF_NORMAL_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT,
+ UF_SERVER_TRUST_ACCOUNT]:
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(account_type)
+ for objectclass in ["user", "computer"]:
+ for how in ["replace", "deladd"]:
+ test_name = f"{account_type_str}_{objectclass}_{how}"
+ cls.generate_dynamic_test("test_objectclass_mod_lock",
+ test_name,
+ account_type,
+ objectclass,
+ how)
+
+ for account_type in [UF_NORMAL_ACCOUNT, UF_WORKSTATION_TRUST_ACCOUNT]:
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(account_type)
+ cls.generate_dynamic_test("test_uac_bits_unrelated_modify",
+ account_type_str, account_type)
+
+ for bit in bits:
+ try:
+ bit_str = dsdb.user_account_control_flag_bit_to_string(bit)
+ except KeyError:
+ bit_str = hex(bit)
+
+ cls.generate_dynamic_test("test_uac_bits_add",
+ bit_str, bit, bit_str)
+
+ cls.generate_dynamic_test("test_uac_bits_set",
+ bit_str, bit, bit_str)
+
+ cls.generate_dynamic_test("test_uac_bits_add",
+ "UF_NORMAL_ACCOUNT_UF_PASSWD_NOTREQD",
+ UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD,
+ "UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD")
+
+
+ def add_computer_ldap(self, computername, others=None, samdb=None):
+ if samdb is None:
+ samdb = self.samdb
+ dn = "CN=%s,%s" % (computername, self.OU)
+ samaccountname = "%s$" % computername
+ msg_dict = {
+ "dn": dn,
+ "objectclass": "computer"}
+ if others is not None:
+ msg_dict = dict(list(msg_dict.items()) + list(others.items()))
+
+ msg = ldb.Message.from_dict(self.samdb, msg_dict)
+ msg["sAMAccountName"] = samaccountname
+
+ print("Adding computer account %s" % computername)
+ samdb.add(msg)
+
+ def add_user_ldap(self, username, others=None, samdb=None):
+ if samdb is None:
+ samdb = self.samdb
+ dn = "CN=%s,%s" % (username, self.OU)
+ samaccountname = "%s" % username
+ msg_dict = {
+ "dn": dn,
+ "objectclass": "user"}
+ if others is not None:
+ msg_dict = dict(list(msg_dict.items()) + list(others.items()))
+
+ msg = ldb.Message.from_dict(self.samdb, msg_dict)
+ msg["sAMAccountName"] = samaccountname
+
+ print("Adding user account %s" % username)
+ samdb.add(msg)
+
+ def get_creds(self, target_username, target_password):
+ creds_tmp = Credentials()
+ creds_tmp.set_username(target_username)
+ creds_tmp.set_password(target_password)
+ creds_tmp.set_domain(creds.get_domain())
+ creds_tmp.set_realm(creds.get_realm())
+ creds_tmp.set_workstation(creds.get_workstation())
+ creds_tmp.set_gensec_features(creds_tmp.get_gensec_features()
+ | gensec.FEATURE_SEAL)
+ creds_tmp.set_kerberos_state(DONT_USE_KERBEROS) # kinit is too expensive to use in a tight loop
+ return creds_tmp
+
+ def setUp(self):
+ super(UserAccountControlTests, self).setUp()
+ self.admin_creds = creds
+ self.admin_samdb = SamDB(url=ldaphost,
+ session_info=system_session(),
+ credentials=self.admin_creds, lp=lp)
+ self.domain_sid = security.dom_sid(self.admin_samdb.get_domain_sid())
+ self.base_dn = self.admin_samdb.domain_dn()
+
+ self.unpriv_user = "testuser1"
+ self.unpriv_user_pw = "samba123@"
+ self.unpriv_creds = self.get_creds(self.unpriv_user, self.unpriv_user_pw)
+
+ self.OU = "OU=test_computer_ou1,%s" % (self.base_dn)
+
+ delete_force(self.admin_samdb, self.OU, controls=["tree_delete:0"])
+ delete_force(self.admin_samdb, "CN=%s,CN=Users,%s" % (self.unpriv_user, self.base_dn))
+
+ self.admin_samdb.newuser(self.unpriv_user, self.unpriv_user_pw)
+ res = self.admin_samdb.search("CN=%s,CN=Users,%s" % (self.unpriv_user, self.admin_samdb.domain_dn()),
+ scope=SCOPE_BASE,
+ attrs=["objectSid"])
+ self.assertEqual(1, len(res))
+
+ self.unpriv_user_sid = ndr_unpack(security.dom_sid, res[0]["objectSid"][0])
+ self.unpriv_user_dn = res[0].dn
+ self.addCleanup(self.admin_samdb.delete, self.unpriv_user_dn)
+
+ self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp)
+
+ self.samr = samr.samr("ncacn_ip_tcp:%s[seal]" % host, lp, self.unpriv_creds)
+ self.samr_handle = self.samr.Connect2(None, security.SEC_FLAG_MAXIMUM_ALLOWED)
+ self.samr_domain = self.samr.OpenDomain(self.samr_handle, security.SEC_FLAG_MAXIMUM_ALLOWED, self.domain_sid)
+
+ self.sd_utils = sd_utils.SDUtils(self.admin_samdb)
+ self.admin_samdb.create_ou(self.OU)
+ self.addCleanup(self.admin_samdb.delete, self.OU, ["tree_delete:1"])
+
+ self.unpriv_user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(self.unpriv_user_sid)
+
+ old_sd = self.sd_utils.read_sd_on_dn(self.OU)
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ self.add_computer_ldap("testcomputer-t")
+
+ self.sd_utils.modify_sd_on_dn(self.OU, old_sd)
+
+ self.computernames = ["testcomputer-0"]
+
+ # Get the SD of the template account, then force it to match
+ # what we expect for SeMachineAccountPrivilege accounts, so we
+ # can confirm we created the accounts correctly
+ self.sd_reference_cc = self.sd_utils.read_sd_on_dn("CN=testcomputer-t,%s" % (self.OU))
+
+ self.sd_reference_modify = self.sd_utils.read_sd_on_dn("CN=testcomputer-t,%s" % (self.OU))
+ for ace in self.sd_reference_modify.dacl.aces:
+ if ace.type == security.SEC_ACE_TYPE_ACCESS_ALLOWED and ace.trustee == self.unpriv_user_sid:
+ ace.access_mask = ace.access_mask | security.SEC_ADS_SELF_WRITE | security.SEC_ADS_WRITE_PROP
+
+ # Now reconnect without domain admin rights
+ self.samdb = SamDB(url=ldaphost, credentials=self.unpriv_creds, lp=lp)
+
+ def test_add_computer_sd_cc(self):
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ mod = f"(OA;CI;WDCC;{dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})"
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ computername = self.computernames[0]
+ sd = ldb.MessageElement((ndr_pack(self.sd_reference_modify)),
+ ldb.FLAG_MOD_ADD,
+ "nTSecurityDescriptor")
+ try:
+ self.add_computer_ldap(computername,
+ others={"nTSecurityDescriptor": sd})
+ except LdbError as e:
+ self.fail(str(e))
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["ntSecurityDescriptor"])
+
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc, allow_remaining=True)
+
+ sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(self.sd_reference_modify.as_sddl(self.domain_sid), sddl)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["description"] = ldb.MessageElement(
+ ("A description"), ldb.FLAG_MOD_REPLACE,
+ "description")
+ self.samdb.modify(m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_SERVER_TRUST_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Unexpectedly able to set userAccountControl to be a DC on {m.dn}",
+ self.samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
+ samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Unexpectedly able to set userAccountControl to be a RODC on {m.dn}",
+ self.samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Unexpectedly able to set userAccountControl to be a Workstation on {m.dn}",
+ self.samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ try:
+ self.samdb.modify(m)
+ except LdbError as e:
+ (enum, estr) = e.args
+ self.fail(f"got {estr} setting userAccountControl to UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD")
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["primaryGroupID"] = ldb.MessageElement(str(security.DOMAIN_RID_ADMINS),
+ ldb.FLAG_MOD_REPLACE, "primaryGroupID")
+ self.assertRaisesLdbError(ldb.ERR_UNWILLING_TO_PERFORM,
+ f"Unexpectedly able to set primaryGroupID on {m.dn}",
+ self.samdb.modify, m)
+
+
+ def test_mod_computer_cc(self):
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ computername = self.computernames[0]
+ self.add_computer_ldap(computername)
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=[])
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["description"] = ldb.MessageElement(
+ ("A description"), ldb.FLAG_MOD_REPLACE,
+ "description")
+ self.samdb.modify(m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT |
+ samba.dsdb.UF_PARTIAL_SECRETS_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Unexpectedly able to set userAccountControl as RODC on {m.dn}",
+ self.samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_SERVER_TRUST_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Unexpectedly able to set userAccountControl as DC on {m.dn}",
+ self.samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+
+ self.assertRaisesLdbError(ldb.ERR_OBJECT_CLASS_VIOLATION,
+ f"Unexpectedly able to set userAccountControl as to UF_NORMAL_ACCOUNT|UF_PASSWD_NOTREQD on {m.dn}",
+ self.samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_WORKSTATION_TRUST_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Unexpectedly able to set userAccountControl to be a workstation on {m.dn}",
+ self.samdb.modify, m)
+
+
+ def test_add_computer_cc_normal_bare(self):
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ mod = f"(OA;CI;CC;{dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})"
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ computername = self.computernames[0]
+ sd = ldb.MessageElement((ndr_pack(self.sd_reference_modify)),
+ ldb.FLAG_MOD_ADD,
+ "nTSecurityDescriptor")
+ try:
+ self.add_computer_ldap(computername,
+ others={"nTSecurityDescriptor": sd})
+ except LdbError as e:
+ self.fail(str(e))
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["ntSecurityDescriptor"])
+
+ desc = res[0]["nTSecurityDescriptor"][0]
+ desc = ndr_unpack(security.descriptor, desc, allow_remaining=True)
+
+ sddl = desc.as_sddl(self.domain_sid)
+ self.assertEqual(self.sd_reference_modify.as_sddl(self.domain_sid), sddl)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["description"] = ldb.MessageElement(
+ ("A description"), ldb.FLAG_MOD_REPLACE,
+ "description")
+ self.samdb.modify(m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(samba.dsdb.UF_NORMAL_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError([ldb.ERR_OBJECT_CLASS_VIOLATION,
+ ldb.ERR_UNWILLING_TO_PERFORM],
+ "Unexpectedly able to set userAccountControl to be a Normal "
+ "account without |UF_PASSWD_NOTREQD",
+ self.samdb.modify, m)
+
+
+ def test_admin_mod_uac(self):
+ computername = self.computernames[0]
+ self.add_computer_ldap(computername, samdb=self.admin_samdb)
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ self.assertEqual(int(res[0]["userAccountControl"][0]), (UF_WORKSTATION_TRUST_ACCOUNT |
+ UF_ACCOUNTDISABLE |
+ UF_PASSWD_NOTREQD))
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT |
+ UF_PARTIAL_SECRETS_ACCOUNT |
+ UF_TRUSTED_FOR_DELEGATION),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.assertRaisesLdbError(ldb.ERR_OTHER,
+ "Unexpectedly able to set userAccountControl to "
+ "UF_WORKSTATION_TRUST_ACCOUNT|UF_PARTIAL_SECRETS_ACCOUNT|"
+ f"UF_TRUSTED_FOR_DELEGATION on {m.dn}",
+ self.admin_samdb.modify, m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(UF_WORKSTATION_TRUST_ACCOUNT |
+ UF_PARTIAL_SECRETS_ACCOUNT),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.admin_samdb.modify(m)
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ self.assertEqual(int(res[0]["userAccountControl"][0]), (UF_WORKSTATION_TRUST_ACCOUNT |
+ UF_PARTIAL_SECRETS_ACCOUNT))
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(UF_ACCOUNTDISABLE),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ try:
+ self.admin_samdb.modify(m)
+ except LdbError as e:
+ (enum, estr) = e.args
+ self.fail(f"got {estr} setting userAccountControl to UF_ACCOUNTDISABLE (as admin)")
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ self.assertEqual(int(res[0]["userAccountControl"][0]), UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE)
+
+ def _test_uac_bits_set_with_args(self, bit, bit_str):
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ # Allow the creation of any children and write to any
+ # attributes (this is not a test of ACLs, this is a test of
+ # non-ACL userAccountControl rules
+ mod = f"(OA;CI;WP;;;{user_sid})(OA;;CC;;;{user_sid})"
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ # We want to start with UF_NORMAL_ACCOUNT, so we make a user
+ computername = self.computernames[0]
+ self.add_user_ldap(computername)
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=user)(cn=%s))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=[])
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["description"] = ldb.MessageElement(
+ ("A description"), ldb.FLAG_MOD_REPLACE,
+ "description")
+ self.samdb.modify(m)
+
+ # These bits are privileged, but authenticated users have that CAR by default, so this is a pain to test
+ priv_to_auth_users_bits = set([UF_PASSWD_NOTREQD, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,
+ UF_DONT_EXPIRE_PASSWD])
+
+ # These bits really are privileged, or can't be changed from UF_NORMAL as a non-admin
+ priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT,
+ UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
+ UF_WORKSTATION_TRUST_ACCOUNT])
+
+ invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT])
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(bit | UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ try:
+ self.samdb.modify(m)
+ if (bit in priv_bits):
+ self.fail("Unexpectedly able to set userAccountControl bit 0x%08X (%s), on %s"
+ % (bit, bit_str, m.dn))
+ if (bit in account_types and bit != UF_NORMAL_ACCOUNT):
+ self.fail("Unexpectedly able to set userAccountControl bit 0x%08X (%s), on %s"
+ % (bit, bit_str, m.dn))
+ except LdbError as e:
+ (enum, estr) = e.args
+ if bit in invalid_bits:
+ self.assertEqual(enum,
+ ldb.ERR_OTHER,
+ "was not able to set 0x%08X (%s) on %s"
+ % (bit, bit_str, m.dn))
+ elif (bit in account_types):
+ self.assertIn(enum, [ldb.ERR_OBJECT_CLASS_VIOLATION, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS])
+ elif (bit in priv_bits):
+ self.assertEqual(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS, enum)
+ else:
+ self.fail("Unable to set userAccountControl bit 0x%08X (%s) on %s: %s"
+ % (bit, bit_str, m.dn, estr))
+
+ def _test_uac_bits_unrelated_modify_with_args(self, account_type):
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+
+ # Allow the creation of any children and write to any
+ # attributes (this is not a test of ACLs, this is a test of
+ # non-ACL userAccountControl rules
+ mod = f"(OA;CI;WP;;;{user_sid})(OA;;CC;;;{user_sid})"
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ computername = self.computernames[0]
+ if account_type == UF_WORKSTATION_TRUST_ACCOUNT:
+ self.add_computer_ldap(computername)
+ else:
+ self.add_user_ldap(computername)
+
+ res = self.admin_samdb.search(self.OU,
+ expression=f"(&(objectclass=user)(cn={computername}))",
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+ self.assertEqual(len(res), 1)
+
+ orig_uac = int(res[0]["userAccountControl"][0])
+ self.assertEqual(orig_uac & account_type,
+ account_type)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["description"] = ldb.MessageElement(
+ ("A description"), ldb.FLAG_MOD_REPLACE,
+ "description")
+ self.samdb.modify(m)
+
+ invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT, UF_PARTIAL_SECRETS_ACCOUNT])
+
+ # UF_LOCKOUT isn't actually ignored, it changes other
+ # attributes but does not stick here. See MS-SAMR 2.2.1.13
+ # UF_FLAG Codes clarification that UF_SCRIPT and
+ # UF_PASSWD_CANT_CHANGE are simply ignored by both clients and
+ # servers. Other bits are ignored as they are undefined, or
+ # are not set into the attribute (instead triggering other
+ # events).
+ ignored_bits = set([UF_SCRIPT, UF_00000004, UF_LOCKOUT, UF_PASSWD_CANT_CHANGE,
+ UF_00000400, UF_00004000, UF_00008000, UF_PASSWORD_EXPIRED,
+ int("0x10000000", 16), int("0x20000000", 16), int("0x40000000", 16), int("0x80000000", 16)])
+ super_priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT])
+
+ priv_to_remove_bits = set([UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, UF_WORKSTATION_TRUST_ACCOUNT])
+
+ for bit in bits:
+ # Reset this to the initial position, just to be sure
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(orig_uac),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ try:
+ self.admin_samdb.modify(m)
+ except LdbError as e:
+ (enum, estr) = e.args
+ self.fail(f"got {estr} resetting userAccountControl to initial value {orig_uac:#08x}")
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=user)(cn=%s))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ self.assertEqual(len(res), 1)
+ reset_uac = int(res[0]["userAccountControl"][0])
+ self.assertEqual(orig_uac, reset_uac)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(bit | UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ try:
+ self.admin_samdb.modify(m)
+
+ if bit in invalid_bits:
+ self.fail("Should have been unable to set userAccountControl bit 0x%08X on %s" % (bit, m.dn))
+
+ except LdbError as e1:
+ (enum, estr) = e1.args
+ if bit in invalid_bits:
+ self.assertEqual(enum, ldb.ERR_OTHER)
+ # No point going on, try the next bit
+ continue
+ elif bit in super_priv_bits:
+ self.assertIn(enum, (ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ ldb.ERR_OBJECT_CLASS_VIOLATION))
+ # No point going on, try the next bit
+ continue
+
+ elif (account_type == UF_NORMAL_ACCOUNT) \
+ and (bit in account_types) \
+ and (bit != account_type):
+ self.assertIn(enum, (ldb.ERR_UNWILLING_TO_PERFORM,
+ ldb.ERR_OBJECT_CLASS_VIOLATION))
+ continue
+
+ elif (account_type == UF_WORKSTATION_TRUST_ACCOUNT) \
+ and (bit != UF_NORMAL_ACCOUNT) \
+ and (bit != account_type):
+ self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
+ continue
+
+ else:
+ self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=user)(cn=%s))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ if bit in ignored_bits:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD,
+ "Bit 0x%08x shouldn't stick" % bit)
+ else:
+ if bit in account_types:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ bit | UF_PASSWD_NOTREQD,
+ "Bit 0x%08x didn't stick" % bit)
+ else:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ bit | UF_NORMAL_ACCOUNT | UF_PASSWD_NOTREQD,
+ "Bit 0x%08x didn't stick" % bit)
+
+ try:
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(bit | UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.samdb.modify(m)
+
+ except LdbError as e2:
+ (enum, estr) = e2.args
+ self.fail("Unable to set userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=user)(cn=%s))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ if bit in account_types:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ bit | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
+ "bit 0X%08x should have been added (0X%08x vs 0X%08x)"
+ % (bit, int(res[0]["userAccountControl"][0]),
+ bit | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD))
+ elif bit in ignored_bits:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
+ "bit 0X%08x should have been added (0X%08x vs 0X%08x)"
+ % (bit, int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD))
+
+ else:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ bit | UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
+ "bit 0X%08x should have been added (0X%08x vs 0X%08x)"
+ % (bit, int(res[0]["userAccountControl"][0]),
+ bit | UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD))
+
+ try:
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["userAccountControl"] = ldb.MessageElement(str(UF_PASSWD_NOTREQD | UF_ACCOUNTDISABLE),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ self.samdb.modify(m)
+ if bit in priv_to_remove_bits:
+ self.fail("Should have been unable to remove userAccountControl bit 0x%08X on %s" % (bit, m.dn))
+
+ except LdbError as e3:
+ (enum, estr) = e3.args
+ if account_type == UF_WORKSTATION_TRUST_ACCOUNT:
+ # Because removing any bit would change the account back to a user, which is locked by objectclass
+ self.assertIn(enum, (ldb.ERR_OBJECT_CLASS_VIOLATION, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS))
+ elif bit in priv_to_remove_bits:
+ self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ else:
+ self.fail("Unexpectedly unable to remove userAccountControl bit 0x%08X on %s: %s" % (bit, m.dn, estr))
+
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=user)(cn=%s))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=["userAccountControl"])
+
+ if bit in priv_to_remove_bits:
+ if bit in account_types:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ bit | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
+ "bit 0X%08x should not have been removed" % bit)
+ else:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ bit | UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
+ "bit 0X%08x should not have been removed" % bit)
+ elif account_type != UF_WORKSTATION_TRUST_ACCOUNT:
+ self.assertEqual(int(res[0]["userAccountControl"][0]),
+ UF_NORMAL_ACCOUNT | UF_ACCOUNTDISABLE | UF_PASSWD_NOTREQD,
+ "bit 0X%08x should have been removed" % bit)
+
+ def _test_uac_bits_add_with_args(self, bit, bit_str):
+ computername = self.computernames[0]
+
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ mod = "(OA;;CC;bf967a86-0de6-11d0-a285-00aa003049e2;;%s)" % str(user_sid)
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+
+ invalid_bits = set([UF_TEMP_DUPLICATE_ACCOUNT])
+ # UF_NORMAL_ACCOUNT is invalid alone, needs UF_PASSWD_NOTREQD
+ unwilling_bits = set([UF_NORMAL_ACCOUNT])
+
+ # These bits are privileged, but authenticated users have that CAR by default, so this is a pain to test
+ priv_to_auth_users_bits = set([UF_PASSWD_NOTREQD, UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED,
+ UF_DONT_EXPIRE_PASSWD])
+
+ # These bits really are privileged
+ priv_bits = set([UF_INTERDOMAIN_TRUST_ACCOUNT, UF_SERVER_TRUST_ACCOUNT,
+ UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION,
+ UF_PARTIAL_SECRETS_ACCOUNT])
+
+ if bit not in account_types and ((bit & UF_NORMAL_ACCOUNT) == 0):
+ bit_add = bit|UF_WORKSTATION_TRUST_ACCOUNT
+ else:
+ bit_add = bit
+
+ try:
+
+ self.add_computer_ldap(computername, others={"userAccountControl": [str(bit_add)]})
+ delete_force(self.admin_samdb, "CN=%s,%s" % (computername, self.OU))
+ if bit in priv_bits:
+ self.fail("Unexpectedly able to set userAccountControl bit 0x%08X (%s) on %s"
+ % (bit, bit_str, computername))
+
+ except LdbError as e4:
+ (enum, estr) = e4.args
+ if bit in invalid_bits:
+ self.assertEqual(enum,
+ ldb.ERR_OTHER,
+ "Invalid bit 0x%08X (%s) was able to be set on %s"
+ % (bit,
+ bit_str,
+ computername))
+ elif bit in priv_bits:
+ if bit == UF_INTERDOMAIN_TRUST_ACCOUNT:
+ self.assertIn(enum, (ldb.ERR_OBJECT_CLASS_VIOLATION,
+ ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS))
+ else:
+ self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ elif bit in unwilling_bits:
+ # This can fail early as user in a computer is not permitted as non-admin
+ self.assertIn(enum, (ldb.ERR_UNWILLING_TO_PERFORM,
+ ldb.ERR_OBJECT_CLASS_VIOLATION))
+ elif bit & UF_NORMAL_ACCOUNT:
+ # This can fail early as user in a computer is not permitted as non-admin
+ self.assertIn(enum, (ldb.ERR_UNWILLING_TO_PERFORM,
+ ldb.ERR_OBJECT_CLASS_VIOLATION))
+ else:
+ self.fail("Unable to set userAccountControl bit 0x%08X (%s) on %s: %s"
+ % (bit,
+ bit_str,
+ computername,
+ estr))
+
+ def test_primarygroupID_cc_add(self):
+ computername = self.computernames[0]
+
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ ace_cc = f"(OA;;CC;{dsdb.DS_GUID_SCHEMA_CLASS_COMPUTER};;{user_sid})"
+ ace_wp_dnshostname = f"(OA;CI;WP;{dsdb.DS_GUID_SCHEMA_ATTR_DNS_HOST_NAME};;{user_sid})"
+ ace_wp_primarygroupid = f"(OA;CI;WP;{dsdb.DS_GUID_SCHEMA_ATTR_PRIMARY_GROUP_ID};;{user_sid})"
+ mod = ace_cc + ace_wp_dnshostname + ace_wp_primarygroupid
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+ try:
+ # When creating a new object, you can not ever set the primaryGroupID
+ self.add_computer_ldap(computername, others={"primaryGroupID": [str(security.DOMAIN_RID_ADMINS)]})
+ self.fail("Unexpectedly able to set primaryGruopID to be an admin on %s" % computername)
+ except LdbError as e13:
+ (enum, estr) = e13.args
+ self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
+
+ def test_primarygroupID_priv_DC_modify(self):
+ computername = self.computernames[0]
+
+ self.add_computer_ldap(computername,
+ others={"userAccountControl": [str(UF_SERVER_TRUST_ACCOUNT)]},
+ samdb=self.admin_samdb)
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=[""])
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid),
+ security.DOMAIN_RID_USERS))
+ m["member"] = ldb.MessageElement(
+ [str(res[0].dn)], ldb.FLAG_MOD_ADD,
+ "member")
+ self.admin_samdb.modify(m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["primaryGroupID"] = ldb.MessageElement(
+ [str(security.DOMAIN_RID_USERS)], ldb.FLAG_MOD_REPLACE,
+ "primaryGroupID")
+ try:
+ self.admin_samdb.modify(m)
+
+ # When creating a new object, you can not ever set the primaryGroupID
+ self.fail("Unexpectedly able to set primaryGroupID to be other than DCS on %s" % computername)
+ except LdbError as e14:
+ (enum, estr) = e14.args
+ self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
+
+ def test_primarygroupID_priv_member_modify(self):
+ computername = self.computernames[0]
+
+ self.add_computer_ldap(computername,
+ others={"userAccountControl": [str(UF_WORKSTATION_TRUST_ACCOUNT | UF_PARTIAL_SECRETS_ACCOUNT)]},
+ samdb=self.admin_samdb)
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=[""])
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid),
+ security.DOMAIN_RID_USERS))
+ m["member"] = ldb.MessageElement(
+ [str(res[0].dn)], ldb.FLAG_MOD_ADD,
+ "member")
+ self.admin_samdb.modify(m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["primaryGroupID"] = ldb.MessageElement(
+ [str(security.DOMAIN_RID_USERS)], ldb.FLAG_MOD_REPLACE,
+ "primaryGroupID")
+
+ self.assertRaisesLdbError(ldb.ERR_UNWILLING_TO_PERFORM,
+ f"Unexpectedly able to set primaryGroupID to be other than DCS on {m.dn}",
+ self.admin_samdb.modify, m)
+
+ def test_primarygroupID_priv_user_modify(self):
+ computername = self.computernames[0]
+
+ self.add_computer_ldap(computername,
+ others={"userAccountControl": [str(UF_WORKSTATION_TRUST_ACCOUNT)]},
+ samdb=self.admin_samdb)
+ res = self.admin_samdb.search("%s" % self.base_dn,
+ expression="(&(objectClass=computer)(samAccountName=%s$))" % computername,
+ scope=SCOPE_SUBTREE,
+ attrs=[""])
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.admin_samdb, "<SID=%s-%d>" % (str(self.domain_sid),
+ security.DOMAIN_RID_ADMINS))
+ m["member"] = ldb.MessageElement(
+ [str(res[0].dn)], ldb.FLAG_MOD_ADD,
+ "member")
+ self.admin_samdb.modify(m)
+
+ m = ldb.Message()
+ m.dn = res[0].dn
+ m["primaryGroupID"] = ldb.MessageElement(
+ [str(security.DOMAIN_RID_ADMINS)], ldb.FLAG_MOD_REPLACE,
+ "primaryGroupID")
+ self.admin_samdb.modify(m)
+
+ def _test_objectclass_uac_dollar_lock_with_args(self,
+ account_type,
+ objectclass,
+ name,
+ priv):
+ dn = "CN=%s,%s" % (name, self.OU)
+ msg_dict = {
+ "dn": dn,
+ "objectclass": objectclass,
+ "samAccountName": name,
+ "userAccountControl": str(account_type | UF_PASSWD_NOTREQD)}
+
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(account_type)
+
+ print(f"Adding account {name} as {account_type_str} with objectclass {objectclass}")
+
+ if priv:
+ samdb = self.admin_samdb
+ else:
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+ mod = "(OA;;CC;;;%s)" % str(user_sid)
+
+ self.sd_utils.dacl_add_ace(self.OU, mod)
+ samdb = self.samdb
+
+ enum = ldb.SUCCESS
+ try:
+ samdb.add(msg_dict)
+ except ldb.LdbError as e:
+ (enum, msg) = e.args
+
+ if (account_type == UF_SERVER_TRUST_ACCOUNT
+ and objectclass != "computer"):
+ self.assertEqual(enum, ldb.ERR_OBJECT_CLASS_VIOLATION)
+ return
+
+ if priv == False and account_type == UF_SERVER_TRUST_ACCOUNT:
+ self.assertEqual(enum, ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS)
+ return
+
+ if (objectclass == "user"
+ and account_type != UF_NORMAL_ACCOUNT):
+ self.assertEqual(enum, ldb.ERR_OBJECT_CLASS_VIOLATION)
+ return
+
+ if (not priv and objectclass == "computer"
+ and account_type == UF_NORMAL_ACCOUNT):
+ self.assertEqual(enum, ldb.ERR_OBJECT_CLASS_VIOLATION)
+ return
+
+ if priv and account_type == UF_NORMAL_ACCOUNT:
+ self.assertEqual(enum, 0)
+ return
+
+ if (priv == False and
+ account_type != UF_NORMAL_ACCOUNT and
+ name[-1] != '$'):
+ self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
+ return
+
+ self.assertEqual(enum, 0)
+
+ def _test_mod_lock_with_args(self,
+ objectclass,
+ objectclass2,
+ account_type,
+ account_type2,
+ name2,
+ priv):
+ name = "oc_uac_lock$"
+
+ dn = "CN=%s,%s" % (name, self.OU)
+ msg_dict = {
+ "dn": dn,
+ "objectclass": objectclass,
+ "samAccountName": name,
+ "userAccountControl": str(account_type | UF_PASSWD_NOTREQD)}
+
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(
+ account_type)
+
+ print(f"Adding account {name} as {account_type_str} "
+ f"with objectclass {objectclass}")
+
+ # Create the object as admin
+ self.admin_samdb.add(msg_dict)
+
+ if priv:
+ samdb = self.admin_samdb
+ else:
+ samdb = self.samdb
+
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+
+ # We want to test what the underlying rules for non-admins regardless
+ # of security descriptors are, so set this very, dangerously, broadly
+ mod = f"(OA;;WP;;;{user_sid})"
+
+ self.sd_utils.dacl_add_ace(dn, mod)
+
+ msg = "Modifying account"
+ if name2 is not None:
+ msg += f" to {name2}"
+ if account_type2 is not None:
+ account_type2_str = dsdb.user_account_control_flag_bit_to_string(
+ account_type2)
+ msg += f" as {account_type2_str}"
+ else:
+ account_type2_str = None
+ if objectclass2 is not None:
+ msg += f" with objectClass {objectclass2}"
+ print(msg)
+
+ msg = ldb.Message(ldb.Dn(samdb, dn))
+ if objectclass2 is not None:
+ msg["objectClass"] = ldb.MessageElement(objectclass2,
+ ldb.FLAG_MOD_REPLACE,
+ "objectClass")
+ if name2 is not None:
+ msg["sAMAccountName"] = ldb.MessageElement(name2,
+ ldb.FLAG_MOD_REPLACE,
+ "sAMAccountName")
+ if account_type2 is not None:
+ msg["userAccountControl"] = ldb.MessageElement(
+ str(account_type2 | UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_REPLACE,
+ "userAccountControl")
+ enum = ldb.SUCCESS
+ try:
+ samdb.modify(msg)
+ except ldb.LdbError as e:
+ enum, _ = e.args
+
+ # Setting userAccountControl to be an RODC is not allowed.
+ if account_type2 == UF_PARTIAL_SECRETS_ACCOUNT:
+ self.assertEqual(enum, ldb.ERR_OTHER)
+ return
+
+ # Unprivileged users cannot change userAccountControl. The exception is
+ # changing a non-normal account to UF_WORKSTATION_TRUST_ACCOUNT, which
+ # is allowed.
+ if (not priv
+ and account_type2 is not None
+ and account_type != account_type2
+ and (account_type == UF_NORMAL_ACCOUNT
+ or account_type2 != UF_WORKSTATION_TRUST_ACCOUNT)):
+ self.assertIn(enum, [ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ ldb.ERR_OBJECT_CLASS_VIOLATION])
+ return
+
+ # A non-computer account cannot have UF_SERVER_TRUST_ACCOUNT.
+ if objectclass == "user" and account_type2 == UF_SERVER_TRUST_ACCOUNT:
+ self.assertIn(enum, [ldb.ERR_UNWILLING_TO_PERFORM,
+ ldb.ERR_OBJECT_CLASS_VIOLATION])
+ return
+
+ # The objectClass is not allowed to change.
+ if objectclass2 is not None and objectclass != objectclass2:
+ self.assertIn(enum, [ldb.ERR_OBJECT_CLASS_VIOLATION,
+ ldb.ERR_UNWILLING_TO_PERFORM])
+ return
+
+ # Unprivileged users cannot remove the trailing dollar from a computer
+ # account.
+ if not priv and objectclass == "computer" and (
+ name2 is not None and name2[-1] != "$"):
+ self.assertEqual(enum, ldb.ERR_UNWILLING_TO_PERFORM)
+ return
+
+ self.assertEqual(enum, 0)
+ return
+
+ def _test_objectclass_uac_mod_lock_with_args(self,
+ account_type,
+ account_type2,
+ how,
+ priv):
+ name = "uac_mod_lock$"
+ dn = "CN=%s,%s" % (name, self.OU)
+ if account_type == UF_NORMAL_ACCOUNT:
+ objectclass = "user"
+ else:
+ objectclass = "computer"
+
+ msg_dict = {
+ "dn": dn,
+ "objectclass": objectclass,
+ "samAccountName": name,
+ "userAccountControl": str(account_type | UF_PASSWD_NOTREQD)}
+
+ account_type_str \
+ = dsdb.user_account_control_flag_bit_to_string(account_type)
+ account_type2_str \
+ = dsdb.user_account_control_flag_bit_to_string(account_type2)
+
+ print(f"Adding account {name} as {account_type_str} with objectclass {objectclass}")
+
+ if priv:
+ samdb = self.admin_samdb
+ else:
+ samdb = self.samdb
+
+ user_sid = self.sd_utils.get_object_sid(self.unpriv_user_dn)
+
+ # Create the object as admin
+ self.admin_samdb.add(msg_dict)
+
+ # We want to test what the underlying rules for non-admins
+ # regardless of security descriptors are, so set this very,
+ # dangerously, broadly
+ mod = "(OA;;WP;;;%s)" % str(user_sid)
+
+ self.sd_utils.dacl_add_ace(dn, mod)
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(samdb, dn)
+ if how == "replace":
+ m["userAccountControl"] = ldb.MessageElement(str(account_type2 | UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_REPLACE, "userAccountControl")
+ elif how == "deladd":
+ m["0userAccountControl"] = ldb.MessageElement([],
+ ldb.FLAG_MOD_DELETE, "userAccountControl")
+ m["1userAccountControl"] = ldb.MessageElement(str(account_type2 | UF_PASSWD_NOTREQD),
+ ldb.FLAG_MOD_ADD, "userAccountControl")
+ else:
+ raise ValueError(f"{how} was not a valid argument")
+
+ if (account_type == account_type2):
+ samdb.modify(m)
+ elif (account_type == UF_NORMAL_ACCOUNT) and \
+ (account_type2 == UF_SERVER_TRUST_ACCOUNT) and not priv:
+ self.assertRaisesLdbError([ldb.ERR_OBJECT_CLASS_VIOLATION,
+ ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS],
+ f"Should have been unable to change {account_type_str} to {account_type2_str}",
+ samdb.modify, m)
+ elif (account_type == UF_NORMAL_ACCOUNT) and \
+ (account_type2 == UF_SERVER_TRUST_ACCOUNT) and priv:
+ self.assertRaisesLdbError([ldb.ERR_OBJECT_CLASS_VIOLATION,
+ ldb.ERR_UNWILLING_TO_PERFORM],
+ f"Should have been unable to change {account_type_str} to {account_type2_str}",
+ samdb.modify, m)
+ elif (account_type == UF_WORKSTATION_TRUST_ACCOUNT) and \
+ (account_type2 == UF_SERVER_TRUST_ACCOUNT) and not priv:
+ self.assertRaisesLdbError(ldb.ERR_INSUFFICIENT_ACCESS_RIGHTS,
+ f"Should have been unable to change {account_type_str} to {account_type2_str}",
+ samdb.modify, m)
+ elif priv:
+ samdb.modify(m)
+ elif (account_type in [UF_SERVER_TRUST_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT]) and \
+ (account_type2 in [UF_SERVER_TRUST_ACCOUNT,
+ UF_WORKSTATION_TRUST_ACCOUNT]):
+ samdb.modify(m)
+ elif (account_type == account_type2):
+ samdb.modify(m)
+ else:
+ self.assertRaisesLdbError(ldb.ERR_OBJECT_CLASS_VIOLATION,
+ f"Should have been unable to change {account_type_str} to {account_type2_str}",
+ samdb.modify, m)
+
+ def _test_objectclass_mod_lock_with_args(self,
+ account_type,
+ objectclass,
+ how):
+ name = "uac_mod_lock$"
+ dn = "CN=%s,%s" % (name, self.OU)
+ if objectclass == "computer":
+ new_objectclass = ["top",
+ "person",
+ "organizationalPerson",
+ "user"]
+ elif objectclass == "user":
+ new_objectclass = ["top",
+ "person",
+ "organizationalPerson",
+ "user",
+ "computer"]
+
+ msg_dict = {
+ "dn": dn,
+ "objectclass": objectclass,
+ "samAccountName": name,
+ "userAccountControl": str(account_type | UF_PASSWD_NOTREQD)}
+
+ account_type_str = dsdb.user_account_control_flag_bit_to_string(account_type)
+
+ print(f"Adding account {name} as {account_type_str} with objectclass {objectclass}")
+
+ try:
+ self.admin_samdb.add(msg_dict)
+ if (objectclass == "user"
+ and account_type != UF_NORMAL_ACCOUNT):
+ self.fail(f"Able to create {account_type_str} on {objectclass}")
+ except LdbError as e:
+ (enum, estr) = e.args
+ self.assertEqual(enum, ldb.ERR_OBJECT_CLASS_VIOLATION)
+
+ if objectclass == "user" and account_type != UF_NORMAL_ACCOUNT:
+ return
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.admin_samdb, dn)
+ if how == "replace":
+ m["objectclass"] = ldb.MessageElement(new_objectclass,
+ ldb.FLAG_MOD_REPLACE, "objectclass")
+ elif how == "adddel":
+ m["0objectclass"] = ldb.MessageElement([],
+ ldb.FLAG_MOD_DELETE, "objectclass")
+ m["1objectclass"] = ldb.MessageElement(new_objectclass,
+ ldb.FLAG_MOD_ADD, "objectclass")
+
+ self.assertRaisesLdbError([ldb.ERR_OBJECT_CLASS_VIOLATION,
+ ldb.ERR_UNWILLING_TO_PERFORM],
+ f"Should have been unable to change objectclass of a {objectclass}",
+ self.admin_samdb.modify, m)
+
+runner = SubunitTestRunner()
+rc = 0
+if not runner.run(unittest.TestLoader().loadTestsFromTestCase(
+ UserAccountControlTests)).wasSuccessful():
+ rc = 1
+sys.exit(rc)
diff --git a/source4/dsdb/tests/python/vlv.py b/source4/dsdb/tests/python/vlv.py
new file mode 100644
index 0000000..13fccba
--- /dev/null
+++ b/source4/dsdb/tests/python/vlv.py
@@ -0,0 +1,1786 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Originally based on ./sam.py
+import optparse
+import sys
+import os
+import base64
+import random
+import re
+
+sys.path.insert(0, "bin/python")
+import samba
+from samba.tests.subunitrun import SubunitOptions, TestProgram
+
+import samba.getopt as options
+
+from samba.auth import system_session
+import ldb
+from samba.samdb import SamDB
+from samba.common import get_bytes
+from samba.common import get_string
+
+import time
+
+parser = optparse.OptionParser("vlv.py [options] <host>")
+sambaopts = options.SambaOptions(parser)
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+# use command line creds if available
+credopts = options.CredentialsOptions(parser)
+parser.add_option_group(credopts)
+subunitopts = SubunitOptions(parser)
+parser.add_option_group(subunitopts)
+
+parser.add_option('--elements', type='int', default=20,
+ help="use this many elements in the tests")
+
+parser.add_option('--delete-in-setup', action='store_true',
+ help="cleanup in next setup rather than teardown")
+
+parser.add_option('--skip-attr-regex',
+ help="ignore attributes matching this regex")
+
+opts, args = parser.parse_args()
+
+if len(args) < 1:
+ parser.print_usage()
+ sys.exit(1)
+
+host = args[0]
+
+lp = sambaopts.get_loadparm()
+creds = credopts.get_credentials(lp)
+
+N_ELEMENTS = opts.elements
+
+
+class VlvTestException(Exception):
+ pass
+
+
+def encode_vlv_control(critical=1,
+ before=0, after=0,
+ offset=None,
+ gte=None,
+ n=0, cookie=None):
+
+ s = "vlv:%d:%d:%d:" % (critical, before, after)
+
+ if offset is not None:
+ m = "%d:%d" % (offset, n)
+ elif b':' in gte or b'\x00' in gte:
+ gte = get_string(base64.b64encode(gte))
+ m = "base64>=%s" % gte
+ else:
+ m = ">=%s" % get_string(gte)
+
+ if cookie is None:
+ return s + m
+
+ return s + m + ':' + cookie
+
+
+def get_cookie(controls, expected_n=None):
+ """Get the cookie, STILL base64 encoded, or raise ValueError."""
+ for c in list(controls):
+ cstr = str(c)
+ if cstr.startswith('vlv_resp'):
+ head, n, _, cookie = cstr.rsplit(':', 3)
+ if expected_n is not None and int(n) != expected_n:
+ raise ValueError("Expected %s items, server said %s" %
+ (expected_n, n))
+ return cookie
+ raise ValueError("there is no VLV response")
+
+
+class TestsWithUserOU(samba.tests.TestCase):
+
+ def create_user(self, i, n, prefix='vlvtest', suffix='', attrs=None):
+ name = "%s%d%s" % (prefix, i, suffix)
+ user = {
+ 'cn': name,
+ "objectclass": "user",
+ 'givenName': "abcdefghijklmnopqrstuvwxyz"[i % 26],
+ "roomNumber": "%sbc" % (n - i),
+ "carLicense": "后来经",
+ "facsimileTelephoneNumber": name,
+ "employeeNumber": "%s%sx" % (abs(i * (99 - i)), '\n' * (i & 255)),
+ "accountExpires": "%s" % (10 ** 9 + 1000000 * i),
+ "msTSExpireDate4": "19%02d0101010000.0Z" % (i % 100),
+ "flags": str(i * (n - i)),
+ "serialNumber": "abc %s%s%s" % ('AaBb |-/'[i & 7],
+ ' 3z}'[i & 3],
+ '"@'[i & 1],),
+ }
+
+ # _user_broken_attrs tests are broken due to problems outside
+ # of VLV.
+ _user_broken_attrs = {
+ # Sort doesn't look past a NUL byte.
+ "photo": "\x00%d" % (n - i),
+ "audio": "%sn octet string %s%s ♫♬\x00lalala" % ('Aa'[i & 1],
+ chr(i & 255), i),
+ "displayNamePrintable": "%d\x00%c" % (i, i & 255),
+ "adminDisplayName": "%d\x00b" % (n - i),
+ "title": "%d%sb" % (n - i, '\x00' * i),
+ "comment": "Favourite colour is %d" % (n % (i + 1)),
+
+ # Names that vary only in case. Windows returns
+ # equivalent addresses in the order they were put
+ # in ('a st', 'A st',...).
+ "street": "%s st" % (chr(65 | (i & 14) | ((i & 1) * 32))),
+ }
+
+ if attrs is not None:
+ user.update(attrs)
+
+ user['dn'] = "cn=%s,%s" % (user['cn'], self.ou)
+
+ if opts.skip_attr_regex:
+ match = re.compile(opts.skip_attr_regex).search
+ for k in user.keys():
+ if match(k):
+ del user[k]
+
+ self.users.append(user)
+ self.ldb.add(user)
+ return user
+
+ def setUp(self):
+ super(TestsWithUserOU, self).setUp()
+ self.ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+ self.ldb_ro = self.ldb
+ self.base_dn = self.ldb.domain_dn()
+ self.tree_dn = "ou=vlvtesttree,%s" % self.base_dn
+ self.ou = "ou=vlvou,%s" % self.tree_dn
+ if opts.delete_in_setup:
+ try:
+ self.ldb.delete(self.tree_dn, ['tree_delete:1'])
+ except ldb.LdbError as e:
+ print("tried deleting %s, got error %s" % (self.tree_dn, e))
+ self.ldb.add({
+ "dn": self.tree_dn,
+ "objectclass": "organizationalUnit"})
+ self.ldb.add({
+ "dn": self.ou,
+ "objectclass": "organizationalUnit"})
+
+ self.users = []
+ for i in range(N_ELEMENTS):
+ self.create_user(i, N_ELEMENTS)
+
+ attrs = self.users[0].keys()
+ self.binary_sorted_keys = ['audio',
+ 'photo',
+ "msTSExpireDate4",
+ 'serialNumber',
+ "displayNamePrintable"]
+
+ self.numeric_sorted_keys = ['flags',
+ 'accountExpires']
+
+ self.timestamp_keys = ['msTSExpireDate4']
+
+ self.int64_keys = set(['accountExpires'])
+
+ self.locale_sorted_keys = [x for x in attrs if
+ x not in (self.binary_sorted_keys +
+ self.numeric_sorted_keys)]
+
+ # don't try spaces, etc in cn
+ self.delicate_keys = ['cn']
+
+ def tearDown(self):
+ super(TestsWithUserOU, self).tearDown()
+ if not opts.delete_in_setup:
+ self.ldb.delete(self.tree_dn, ['tree_delete:1'])
+
+
+class VLVTestsBase(TestsWithUserOU):
+
+ # Run a vlv search and return important fields of the response control
+ def vlv_search(self, attr, expr, cookie="", after_count=0, offset=1):
+ sort_ctrl = "server_sort:1:0:%s" % attr
+ ctrl = "vlv:1:0:%d:%d:0" % (after_count, offset)
+ if cookie:
+ ctrl += ":" + cookie
+
+ res = self.ldb_ro.search(self.ou,
+ expression=expr,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[ctrl, sort_ctrl])
+ results = [str(x[attr][0]) for x in res]
+
+ ctrls = [str(c) for c in res.controls if
+ str(c).startswith('vlv')]
+ self.assertEqual(len(ctrls), 1)
+
+ spl = ctrls[0].rsplit(':')
+ cookie = ""
+ if len(spl) == 6:
+ cookie = spl[-1]
+
+ return results, cookie
+
+
+class VLVTestsRO(VLVTestsBase):
+ def test_vlv_simple_double_run(self):
+ """Do the simplest possible VLV query to confirm if VLV
+ works at all. Useful for showing VLV as a whole works
+ on Global Catalog (for example)"""
+ attr = 'roomNumber'
+ expr = "(objectclass=user)"
+
+ # Start new search
+ full_results, cookie = self.vlv_search(attr, expr,
+ after_count=len(self.users))
+
+ results, cookie = self.vlv_search(attr, expr, cookie=cookie,
+ after_count=len(self.users))
+ expected_results = full_results
+ self.assertEqual(results, expected_results)
+
+
+class VLVTestsGC(VLVTestsRO):
+ def setUp(self):
+ super(VLVTestsRO, self).setUp()
+ self.ldb_ro = SamDB(host + ":3268", credentials=creds,
+ session_info=system_session(lp), lp=lp)
+
+
+class VLVTests(VLVTestsBase):
+ def get_full_list(self, attr, include_cn=False):
+ """Fetch the whole list sorted on the attribute, using the VLV.
+ This way you get a VLV cookie."""
+ n_users = len(self.users)
+ sort_control = "server_sort:1:0:%s" % attr
+ half_n = n_users // 2
+ vlv_search = "vlv:1:%d:%d:%d:0" % (half_n, half_n, half_n + 1)
+ attrs = [attr]
+ if include_cn:
+ attrs.append('cn')
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=attrs,
+ controls=[sort_control,
+ vlv_search])
+ if include_cn:
+ full_results = [(str(x[attr][0]), str(x['cn'][0])) for x in res]
+ else:
+ full_results = [str(x[attr][0]).lower() for x in res]
+ controls = res.controls
+ return full_results, controls, sort_control
+
+ def get_expected_order(self, attr, expression=None):
+ """Fetch the whole list sorted on the attribute, using sort only."""
+ sort_control = "server_sort:1:0:%s" % attr
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ expression=expression,
+ attrs=[attr],
+ controls=[sort_control])
+ results = [x[attr][0] for x in res]
+ return results
+
+ def delete_user(self, user):
+ self.ldb.delete(user['dn'])
+ del self.users[self.users.index(user)]
+
+ def get_gte_tests_and_order(self, attr, expression=None):
+ expected_order = self.get_expected_order(attr, expression=expression)
+ gte_users = []
+ if attr in self.delicate_keys:
+ gte_keys = [
+ '3',
+ 'abc',
+ '¹',
+ 'ŋđ¼³ŧ“«đð',
+ '桑巴',
+ ]
+ elif attr in self.timestamp_keys:
+ gte_keys = [
+ '18560101010000.0Z',
+ '19140103010000.0Z',
+ '19560101010010.0Z',
+ '19700101000000.0Z',
+ '19991231211234.3Z',
+ '20061111211234.0Z',
+ '20390901041234.0Z',
+ '25560101010000.0Z',
+ ]
+ elif attr not in self.numeric_sorted_keys:
+ gte_keys = [
+ '3',
+ 'abc',
+ ' ',
+ '!@#!@#!',
+ 'kōkako',
+ '¹',
+ 'ŋđ¼³ŧ“«đð',
+ '\n\t\t',
+ '桑巴',
+ 'zzzz',
+ ]
+ if expected_order:
+ gte_keys.append(expected_order[len(expected_order) // 2] + b' tail')
+
+ else:
+ # "numeric" means positive integers
+ # doesn't work with -1, 3.14, ' 3', '9' * 20
+ gte_keys = ['3',
+ '1' * 10,
+ '1',
+ '9' * 7,
+ '0']
+
+ if attr in self.int64_keys:
+ gte_keys += ['3' * 12, '71' * 8]
+
+ for i, x in enumerate(gte_keys):
+ user = self.create_user(i, N_ELEMENTS,
+ prefix='gte',
+ attrs={attr: x})
+ gte_users.append(user)
+
+ gte_order = self.get_expected_order(attr)
+ for user in gte_users:
+ self.delete_user(user)
+
+ # for sanity's sake
+ expected_order_2 = self.get_expected_order(attr, expression=expression)
+ self.assertEqual(expected_order, expected_order_2)
+
+ # Map gte tests to indexes in expected order. This will break
+ # if gte_order and expected_order are differently ordered (as
+ # it should).
+ gte_map = {}
+
+ # index to the first one with each value
+ index_map = {}
+ for i, k in enumerate(expected_order):
+ if k not in index_map:
+ index_map[k] = i
+
+ keys = []
+ for o in gte_order:
+ if o in index_map:
+ i = index_map[o]
+ gte_map[o] = i
+ for k in keys:
+ gte_map[k] = i
+ keys = []
+ else:
+ keys.append(o)
+
+ for k in keys:
+ gte_map[k] = len(expected_order)
+
+ if False:
+ print("gte_map:")
+ for k in gte_order:
+ print(" %10s => %10s" % (k, gte_map[k]))
+
+ return gte_order, expected_order, gte_map
+
+ def assertCorrectResults(self, results, expected_order,
+ offset, before, after):
+ """A helper to calculate offsets correctly and say as much as possible
+ when something goes wrong."""
+
+ start = max(offset - before - 1, 0)
+ end = offset + after
+ expected_results = expected_order[start: end]
+
+ # if it is a tuple with the cn, drop the cn
+ if expected_results and isinstance(expected_results[0], tuple):
+ expected_results = [x[0] for x in expected_results]
+
+ if expected_results == results:
+ return
+
+ if expected_order is not None:
+ print("expected order: %s" % expected_order[:20])
+ if len(expected_order) > 20:
+ print("... and %d more not shown" % (len(expected_order) - 20))
+
+ print("offset %d before %d after %d" % (offset, before, after))
+ print("start %d end %d" % (start, end))
+ print("expected: %s" % expected_results)
+ print("got : %s" % results)
+ self.assertEqual(expected_results, results)
+
+ def test_server_vlv_with_cookie(self):
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+ for attr in attrs:
+ expected_order = self.get_expected_order(attr)
+ sort_control = "server_sort:1:0:%s" % attr
+ res = None
+ n = len(self.users)
+ for before in [10, 0, 3, 1, 4, 5, 2]:
+ for after in [0, 3, 1, 4, 5, 2, 7]:
+ for offset in range(max(1, before - 2),
+ min(n - after + 2, n)):
+ if res is None:
+ vlv_search = "vlv:1:%d:%d:%d:0" % (before, after,
+ offset)
+ else:
+ cookie = get_cookie(res.controls, n)
+ vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
+ (before, after, offset, n,
+ cookie))
+
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+
+ results = [x[attr][0] for x in res]
+
+ self.assertCorrectResults(results, expected_order,
+ offset, before, after)
+
+ def run_index_tests_with_expressions(self, expressions):
+ # Here we don't test every before/after combination.
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+ for attr in attrs:
+ for expression in expressions:
+ expected_order = self.get_expected_order(attr, expression)
+ sort_control = "server_sort:1:0:%s" % attr
+ res = None
+ n = len(expected_order)
+ for before in range(0, 11):
+ after = before
+ for offset in range(max(1, before - 2),
+ min(n - after + 2, n)):
+ if res is None:
+ vlv_search = "vlv:1:%d:%d:%d:0" % (before, after,
+ offset)
+ else:
+ cookie = get_cookie(res.controls)
+ vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
+ (before, after, offset, n,
+ cookie))
+
+ res = self.ldb.search(self.ou,
+ expression=expression,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+
+ results = [x[attr][0] for x in res]
+
+ self.assertCorrectResults(results, expected_order,
+ offset, before, after)
+
+ def test_server_vlv_with_expression(self):
+ """What happens when we run the VLV with an expression?"""
+ expressions = ["(objectClass=*)",
+ "(cn=%s)" % self.users[-1]['cn'],
+ "(roomNumber=%s)" % self.users[0]['roomNumber'],
+ ]
+ self.run_index_tests_with_expressions(expressions)
+
+ def test_server_vlv_with_failing_expression(self):
+ """What happens when we run the VLV on an expression that matches
+ nothing?"""
+ expressions = ["(samaccountname=testferf)",
+ "(cn=hefalump)",
+ ]
+ self.run_index_tests_with_expressions(expressions)
+
+ def run_gte_tests_with_expressions(self, expressions):
+ # Here we don't test every before/after combination.
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+ for expression in expressions:
+ for attr in attrs:
+ gte_order, expected_order, gte_map = \
+ self.get_gte_tests_and_order(attr, expression)
+ # In case there is some order dependency, disorder tests
+ gte_tests = gte_order[:]
+ random.seed(2)
+ random.shuffle(gte_tests)
+ res = None
+ sort_control = "server_sort:1:0:%s" % attr
+ expected_order = self.get_expected_order(attr, expression)
+
+ for before in range(0, 11):
+ after = before
+ for gte in gte_tests:
+ if res is not None:
+ cookie = get_cookie(res.controls)
+ else:
+ cookie = None
+ vlv_search = encode_vlv_control(before=before,
+ after=after,
+ gte=get_bytes(gte),
+ cookie=cookie)
+
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ expression=expression,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+
+ results = [x[attr][0] for x in res]
+ offset = gte_map.get(gte, len(expected_order))
+
+ # here offset is 0-based
+ start = max(offset - before, 0)
+ end = offset + 1 + after
+
+ expected_results = expected_order[start: end]
+
+ self.assertEqual(expected_results, results)
+
+ def test_vlv_gte_with_expression(self):
+ """What happens when we run the VLV with an expression?"""
+ expressions = ["(objectClass=*)",
+ "(cn=%s)" % self.users[-1]['cn'],
+ "(roomNumber=%s)" % self.users[0]['roomNumber'],
+ ]
+ self.run_gte_tests_with_expressions(expressions)
+
+ def test_vlv_gte_with_failing_expression(self):
+ """What happens when we run the VLV on an expression that matches
+ nothing?"""
+ expressions = ["(samaccountname=testferf)",
+ "(cn=hefalump)",
+ ]
+ self.run_gte_tests_with_expressions(expressions)
+
+ def test_server_vlv_with_cookie_while_adding_and_deleting(self):
+ """What happens if we add or remove items in the middle of the VLV?
+
+ Nothing. The search and the sort is not repeated, and we only
+ deal with the objects originally found.
+ """
+ attrs = ['cn'] + [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+ user_number = 0
+ iteration = 0
+ for attr in attrs:
+ full_results, controls, sort_control = \
+ self.get_full_list(attr, True)
+ original_n = len(self.users)
+
+ expected_order = full_results
+ random.seed(1)
+
+ for before in list(range(0, 3)) + [6, 11, 19]:
+ for after in list(range(0, 3)) + [6, 11, 19]:
+ start = max(before - 1, 1)
+ end = max(start + 4, original_n - after + 2)
+ for offset in range(start, end):
+ # if iteration > 2076:
+ # return
+ cookie = get_cookie(controls, original_n)
+ vlv_search = encode_vlv_control(before=before,
+ after=after,
+ offset=offset,
+ n=original_n,
+ cookie=cookie)
+
+ iteration += 1
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+
+ controls = res.controls
+ results = [x[attr][0] for x in res]
+ real_offset = max(1, min(offset, len(expected_order)))
+
+ expected_results = []
+ skipped = 0
+ begin_offset = max(real_offset - before - 1, 0)
+ real_before = min(before, real_offset - 1)
+ real_after = min(after,
+ len(expected_order) - real_offset)
+
+ for x in expected_order[begin_offset:]:
+ if x is not None:
+ expected_results.append(get_bytes(x[0]))
+ if (len(expected_results) ==
+ real_before + real_after + 1):
+ break
+ else:
+ skipped += 1
+
+ if expected_results != results:
+ print("attr %s before %d after %d offset %d" %
+ (attr, before, after, offset))
+ self.assertEqual(expected_results, results)
+
+ n = len(self.users)
+ if random.random() < 0.1 + (n < 5) * 0.05:
+ if n == 0:
+ i = 0
+ else:
+ i = random.randrange(n)
+ user = self.create_user(i, n, suffix='-%s' %
+ user_number)
+ user_number += 1
+ if random.random() < 0.1 + (n > 50) * 0.02 and n:
+ index = random.randrange(n)
+ user = self.users.pop(index)
+
+ self.ldb.delete(user['dn'])
+
+ replaced = (user[attr], user['cn'])
+ if replaced in expected_order:
+ i = expected_order.index(replaced)
+ expected_order[i] = None
+
+ def test_server_vlv_with_cookie_while_changing(self):
+ """What happens if we modify items in the middle of the VLV?
+
+ The expected behaviour (as found on Windows) is the sort is
+ not repeated, but the changes in attributes are reflected.
+ """
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass', 'cn')]
+ for attr in attrs:
+ n_users = len(self.users)
+ expected_order = [x.upper() for x in self.get_expected_order(attr)]
+ sort_control = "server_sort:1:0:%s" % attr
+ res = None
+ i = 0
+
+ # First we'll fetch the whole list so we know the original
+ # sort order. This is necessary because we don't know how
+ # the server will order equivalent items. We are using the
+ # dn as a key.
+ half_n = n_users // 2
+ vlv_search = "vlv:1:%d:%d:%d:0" % (half_n, half_n, half_n + 1)
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=['dn', attr],
+ controls=[sort_control, vlv_search])
+
+ results = [x[attr][0].upper() for x in res]
+ #self.assertEqual(expected_order, results)
+
+ dn_order = [str(x['dn']) for x in res]
+ values = results[:]
+
+ for before in range(0, 3):
+ for after in range(0, 3):
+ for offset in range(1 + before, n_users - after):
+ cookie = get_cookie(res.controls, len(self.users))
+ vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
+ (before, after, offset, len(self.users),
+ cookie))
+
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=['dn', attr],
+ controls=[sort_control,
+ vlv_search])
+
+ dn_results = [str(x['dn']) for x in res]
+ dn_expected = dn_order[offset - before - 1:
+ offset + after]
+
+ self.assertEqual(dn_expected, dn_results)
+
+ results = [x[attr][0].upper() for x in res]
+
+ self.assertCorrectResults(results, values,
+ offset, before, after)
+
+ i += 1
+ if i % 3 == 2:
+ if (attr in self.locale_sorted_keys or
+ attr in self.binary_sorted_keys):
+ i1 = i % n_users
+ i2 = (i ^ 255) % n_users
+ dn1 = dn_order[i1]
+ dn2 = dn_order[i2]
+ v2 = values[i2]
+
+ if v2 in self.locale_sorted_keys:
+ v2 += '-%d' % i
+ cn1 = dn1.split(',', 1)[0][3:]
+ cn2 = dn2.split(',', 1)[0][3:]
+
+ values[i1] = v2
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.ldb, dn1)
+ m[attr] = ldb.MessageElement(v2,
+ ldb.FLAG_MOD_REPLACE,
+ attr)
+
+ self.ldb.modify(m)
+
+ def test_server_vlv_fractions_with_cookie(self):
+ """What happens when the count is set to an arbitrary number?
+
+ In that case the offset and the count form a fraction, and the
+ VLV should be centred at a point offset/count of the way
+ through. For example, if offset is 3 and count is 6, the VLV
+ should be looking around halfway. The actual algorithm is a
+ bit fiddlier than that, because of the one-basedness of VLV.
+ """
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+
+ n_users = len(self.users)
+
+ random.seed(4)
+
+ for attr in attrs:
+ full_results, controls, sort_control = self.get_full_list(attr)
+ self.assertEqual(len(full_results), n_users)
+ for before in range(0, 2):
+ for after in range(0, 2):
+ for denominator in range(1, 20):
+ for offset in range(1, denominator + 3):
+ cookie = get_cookie(controls, len(self.users))
+ vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
+ (before, after, offset,
+ denominator,
+ cookie))
+ try:
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+ except ldb.LdbError as e:
+ if offset != 0:
+ raise
+ print("offset %d denominator %d raised error "
+ "expected error %s\n"
+ "(offset zero is illegal unless "
+ "content count is zero)" %
+ (offset, denominator, e))
+ continue
+
+ results = [str(x[attr][0]).lower() for x in res]
+
+ if denominator == 0:
+ denominator = n_users
+ if offset == 0:
+ offset = denominator
+ elif denominator == 1:
+ # the offset can only be 1, but the 1/1 case
+ # means something special
+ if offset == 1:
+ real_offset = n_users
+ else:
+ real_offset = 1
+ else:
+ if offset > denominator:
+ offset = denominator
+ real_offset = (1 +
+ int(round((n_users - 1) *
+ (offset - 1) /
+ (denominator - 1.0)))
+ )
+
+ self.assertCorrectResults(results, full_results,
+ real_offset, before,
+ after)
+
+ controls = res.controls
+ if False:
+ for c in list(controls):
+ cstr = str(c)
+ if cstr.startswith('vlv_resp'):
+ bits = cstr.rsplit(':')
+ print("the answer is %s; we said %d" %
+ (bits[2], real_offset))
+ break
+
+ def test_server_vlv_no_cookie(self):
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+
+ for attr in attrs:
+ expected_order = self.get_expected_order(attr)
+ sort_control = "server_sort:1:0:%s" % attr
+ for before in range(0, 5):
+ for after in range(0, 7):
+ for offset in range(1 + before, len(self.users) - after):
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ "vlv:1:%d:%d:%d:0" %
+ (before, after,
+ offset)])
+ results = [x[attr][0] for x in res]
+ self.assertCorrectResults(results, expected_order,
+ offset, before, after)
+
+ def get_expected_order_showing_deleted(self, attr,
+ expression="(|(cn=vlvtest*)(cn=vlv-deleted*))",
+ base=None,
+ scope=ldb.SCOPE_SUBTREE
+ ):
+ """Fetch the whole list sorted on the attribute, using sort only,
+ searching in the entire tree, not just our OU. This is the
+ way to find deleted objects.
+ """
+ if base is None:
+ base = self.base_dn
+ sort_control = "server_sort:1:0:%s" % attr
+ controls = [sort_control, "show_deleted:1"]
+
+ res = self.ldb.search(base,
+ scope=scope,
+ expression=expression,
+ attrs=[attr],
+ controls=controls)
+ results = [x[attr][0] for x in res]
+ return results
+
+ def add_deleted_users(self, n):
+ deleted_users = [self.create_user(i, n, prefix='vlv-deleted')
+ for i in range(n)]
+
+ for user in deleted_users:
+ self.delete_user(user)
+
+ def test_server_vlv_no_cookie_show_deleted(self):
+ """What do we see with the show_deleted control?"""
+ attrs = ['objectGUID',
+ 'cn',
+ 'sAMAccountName',
+ 'objectSid',
+ 'name',
+ 'whenChanged',
+ 'usnChanged'
+ ]
+
+ # add some deleted users first, just in case there are none
+ self.add_deleted_users(6)
+ random.seed(22)
+ expression = "(|(cn=vlvtest*)(cn=vlv-deleted*))"
+
+ for attr in attrs:
+ show_deleted_control = "show_deleted:1"
+ expected_order = self.get_expected_order_showing_deleted(attr,
+ expression)
+ n = len(expected_order)
+ sort_control = "server_sort:1:0:%s" % attr
+ for before in [3, 1, 0]:
+ for after in [0, 2]:
+ # don't test every position, because there could be hundreds.
+ # jump back and forth instead
+ for i in range(20):
+ offset = random.randrange(max(1, before - 2),
+ min(n - after + 2, n))
+ res = self.ldb.search(self.base_dn,
+ expression=expression,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=[attr],
+ controls=[sort_control,
+ show_deleted_control,
+ "vlv:1:%d:%d:%d:0" %
+ (before, after,
+ offset)
+ ]
+ )
+ results = [x[attr][0] for x in res]
+ self.assertCorrectResults(results, expected_order,
+ offset, before, after)
+
+ def test_server_vlv_no_cookie_show_deleted_only(self):
+ """What do we see with the show_deleted control when we're not looking
+ at any non-deleted things"""
+ attrs = ['objectGUID',
+ 'cn',
+ 'sAMAccountName',
+ 'objectSid',
+ 'whenChanged',
+ ]
+
+ # add some deleted users first, just in case there are none
+ self.add_deleted_users(4)
+ base = 'CN=Deleted Objects,%s' % self.base_dn
+ expression = "(cn=vlv-deleted*)"
+ for attr in attrs:
+ show_deleted_control = "show_deleted:1"
+ expected_order = self.get_expected_order_showing_deleted(attr,
+ expression=expression,
+ base=base,
+ scope=ldb.SCOPE_ONELEVEL)
+ print("searching for attr %s amongst %d deleted objects" %
+ (attr, len(expected_order)))
+ sort_control = "server_sort:1:0:%s" % attr
+ step = max(len(expected_order) // 10, 1)
+ for before in [3, 0]:
+ for after in [0, 2]:
+ for offset in range(1 + before,
+ len(expected_order) - after,
+ step):
+ res = self.ldb.search(base,
+ expression=expression,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ show_deleted_control,
+ "vlv:1:%d:%d:%d:0" %
+ (before, after,
+ offset)])
+ results = [x[attr][0] for x in res]
+ self.assertCorrectResults(results, expected_order,
+ offset, before, after)
+
+ def test_server_vlv_with_cookie_show_deleted(self):
+ """What do we see with the show_deleted control?"""
+ attrs = ['objectGUID',
+ 'cn',
+ 'sAMAccountName',
+ 'objectSid',
+ 'name',
+ 'whenChanged',
+ 'usnChanged'
+ ]
+ self.add_deleted_users(6)
+ random.seed(23)
+ for attr in attrs:
+ expected_order = self.get_expected_order(attr)
+ sort_control = "server_sort:1:0:%s" % attr
+ res = None
+ show_deleted_control = "show_deleted:1"
+ expected_order = self.get_expected_order_showing_deleted(attr)
+ n = len(expected_order)
+ expression = "(|(cn=vlvtest*)(cn=vlv-deleted*))"
+ for before in [3, 2, 1, 0]:
+ after = before
+ for i in range(20):
+ offset = random.randrange(max(1, before - 2),
+ min(n - after + 2, n))
+ if res is None:
+ vlv_search = "vlv:1:%d:%d:%d:0" % (before, after,
+ offset)
+ else:
+ cookie = get_cookie(res.controls, n)
+ vlv_search = ("vlv:1:%d:%d:%d:%s:%s" %
+ (before, after, offset, n,
+ cookie))
+
+ res = self.ldb.search(self.base_dn,
+ expression=expression,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search,
+ show_deleted_control])
+
+ results = [x[attr][0] for x in res]
+
+ self.assertCorrectResults(results, expected_order,
+ offset, before, after)
+
+ def test_server_vlv_gte_with_cookie(self):
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+ for attr in attrs:
+ gte_order, expected_order, gte_map = \
+ self.get_gte_tests_and_order(attr)
+ # In case there is some order dependency, disorder tests
+ gte_tests = gte_order[:]
+ random.seed(1)
+ random.shuffle(gte_tests)
+ res = None
+ sort_control = "server_sort:1:0:%s" % attr
+ for before in [0, 1, 2, 4]:
+ for after in [0, 1, 3, 6]:
+ for gte in gte_tests:
+ if res is not None:
+ cookie = get_cookie(res.controls, len(self.users))
+ else:
+ cookie = None
+ vlv_search = encode_vlv_control(before=before,
+ after=after,
+ gte=get_bytes(gte),
+ cookie=cookie)
+
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+
+ results = [x[attr][0] for x in res]
+ offset = gte_map.get(gte, len(expected_order))
+
+ # here offset is 0-based
+ start = max(offset - before, 0)
+ end = offset + 1 + after
+
+ expected_results = expected_order[start: end]
+
+ self.assertEqual(expected_results, results)
+
+ def test_server_vlv_gte_no_cookie(self):
+ attrs = [x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')]
+ iteration = 0
+ for attr in attrs:
+ gte_order, expected_order, gte_map = \
+ self.get_gte_tests_and_order(attr)
+ # In case there is some order dependency, disorder tests
+ gte_tests = gte_order[:]
+ random.seed(1)
+ random.shuffle(gte_tests)
+
+ sort_control = "server_sort:1:0:%s" % attr
+ for before in [0, 1, 3]:
+ for after in [0, 4]:
+ for gte in gte_tests:
+ vlv_search = encode_vlv_control(before=before,
+ after=after,
+ gte=get_bytes(gte))
+
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ vlv_search])
+ results = [x[attr][0] for x in res]
+
+ # here offset is 0-based
+ offset = gte_map.get(gte, len(expected_order))
+ start = max(offset - before, 0)
+ end = offset + after + 1
+ expected_results = expected_order[start: end]
+ iteration += 1
+ if expected_results != results:
+ middle = expected_order[len(expected_order) // 2]
+ print(expected_results, results)
+ print(middle)
+ print(expected_order)
+ print()
+ print("\nattr %s offset %d before %d "
+ "after %d gte %s" %
+ (attr, offset, before, after, gte))
+ self.assertEqual(expected_results, results)
+
+ def test_multiple_searches(self):
+ """The maximum number of concurrent vlv searches per connection is
+ currently set at 3. That means if you open 4 VLV searches the
+ cookie on the first one should fail.
+ """
+ # Windows has a limit of 10 VLVs where there are low numbers
+ # of objects in each search.
+ attrs = ([x for x in self.users[0].keys() if x not in
+ ('dn', 'objectclass')] * 2)[:12]
+
+ vlv_cookies = []
+ for attr in attrs:
+ sort_control = "server_sort:1:0:%s" % attr
+
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ "vlv:1:1:1:1:0"])
+
+ cookie = get_cookie(res.controls, len(self.users))
+ vlv_cookies.append(cookie)
+ time.sleep(0.2)
+
+ # now this one should fail
+ self.assertRaises(ldb.LdbError,
+ self.ldb.search,
+ self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ "vlv:1:1:1:1:0:%s" % vlv_cookies[0]])
+
+ # and this one should succeed
+ res = self.ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ "vlv:1:1:1:1:0:%s" % vlv_cookies[-1]])
+
+ # this one should fail because it is a new connection and
+ # doesn't share cookies
+ new_ldb = SamDB(host, credentials=creds,
+ session_info=system_session(lp), lp=lp)
+
+ self.assertRaises(ldb.LdbError,
+ new_ldb.search, self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ "vlv:1:1:1:1:0:%s" % vlv_cookies[-1]])
+
+ # but now without the critical flag it just does no VLV.
+ new_ldb.search(self.ou,
+ scope=ldb.SCOPE_ONELEVEL,
+ attrs=[attr],
+ controls=[sort_control,
+ "vlv:0:1:1:1:0:%s" % vlv_cookies[-1]])
+
+ def test_vlv_modify_during_view(self):
+ attr = 'roomNumber'
+ expr = "(objectclass=user)"
+
+ # Start new search
+ full_results, cookie = self.vlv_search(attr, expr,
+ after_count=len(self.users))
+
+ # Edit a user
+ edit_index = len(self.users)//2
+ edit_attr = full_results[edit_index]
+ users_with_attr = [u for u in self.users if u[attr] == edit_attr]
+ self.assertEqual(len(users_with_attr), 1)
+ edit_user = users_with_attr[0]
+
+ # Put z at the front of the val so it comes last in ordering
+ edit_val = "z_" + edit_user[attr]
+
+ m = ldb.Message()
+ m.dn = ldb.Dn(self.ldb, edit_user['dn'])
+ m[attr] = ldb.MessageElement(edit_val, ldb.FLAG_MOD_REPLACE, attr)
+ self.ldb.modify(m)
+
+ results, cookie = self.vlv_search(attr, expr, cookie=cookie,
+ after_count=len(self.users))
+
+ # Make expected_results by copying and editing full_results
+ expected_results = full_results[:]
+ expected_results[edit_index] = edit_val
+ self.assertEqual(results, expected_results)
+
+ # Test changing the search expression in a request on an initialised view
+ # Expected failure on samba, passes on windows
+ def test_vlv_change_search_expr(self):
+ attr = 'roomNumber'
+ expr = "(objectclass=user)"
+
+ # Start new search
+ full_results, cookie = self.vlv_search(attr, expr,
+ after_count=len(self.users))
+
+ middle_index = len(full_results)//2
+ # Search that excludes the old value but includes the new one
+ expr = "%s>=%s" % (attr, full_results[middle_index])
+ results, cookie = self.vlv_search(attr, expr, cookie=cookie,
+ after_count=len(self.users))
+ self.assertEqual(results, full_results[middle_index:])
+
+ # Check you can't add a value to a vlv view
+ def test_vlv_add_during_view(self):
+ attr = 'roomNumber'
+ expr = "(objectclass=user)"
+
+ # Start new search
+ full_results, cookie = self.vlv_search(attr, expr,
+ after_count=len(self.users))
+
+ # Add a user at the end of the sort order
+ add_val = "z_addedval"
+ user = {'cn': add_val, "objectclass": "user", attr: add_val}
+ user['dn'] = "cn=%s,%s" % (user['cn'], self.ou)
+ self.ldb.add(user)
+
+ results, cookie = self.vlv_search(attr, expr, cookie=cookie,
+ after_count=len(self.users)+1)
+ self.assertEqual(results, full_results)
+
+ def test_vlv_delete_during_view(self):
+ attr = 'roomNumber'
+ expr = "(objectclass=user)"
+
+ # Start new search
+ full_results, cookie = self.vlv_search(attr, expr,
+ after_count=len(self.users))
+
+ # Delete one of the users
+ del_index = len(self.users)//2
+ del_user = self.users[del_index]
+ self.ldb.delete(del_user['dn'])
+
+ results, cookie = self.vlv_search(attr, expr, cookie=cookie,
+ after_count=len(self.users))
+ expected_results = [r for r in full_results if r != del_user[attr]]
+ self.assertEqual(results, expected_results)
+
+ def test_vlv_change_during_search(self):
+ attr = 'facsimileTelephoneNumber'
+ prefix = "change_during_search_"
+ expr = "(&(objectClass=user)(cn=%s*))" % (prefix)
+ num_users = 3
+ users = [self.create_user(i, num_users, prefix=prefix)
+ for i in range(num_users)]
+ expr = "(&(objectClass=user)(facsimileTelephoneNumber=%s*))" % (prefix)
+
+ # Start the VLV, change the searched attribute and try the
+ # cookie.
+ results, cookie = self.vlv_search(attr, expr)
+
+ for u in users:
+ self.ldb.modify_ldif("dn: %s\n"
+ "changetype: modify\n"
+ "replace: facsimileTelephoneNumber\n"
+ "facsimileTelephoneNumber: 123" % u['dn'])
+
+ for i in range(2):
+ results, cookie = self.vlv_search(attr, expr, cookie=cookie,
+ offset=i+1)
+
+
+
+class PagedResultsTests(TestsWithUserOU):
+
+ def paged_search(self, expr, cookie="", page_size=0, extra_ctrls=None,
+ attrs=None, ou=None, subtree=False, sort=True):
+ ou = ou or self.ou
+ if cookie:
+ cookie = ":" + cookie
+ ctrl = "paged_results:1:" + str(page_size) + cookie
+ controls = [ctrl]
+
+ # If extra controls are provided then add them, else default to
+ # sort control on 'cn' attribute
+ if extra_ctrls is not None:
+ controls += extra_ctrls
+ elif sort:
+ sort_ctrl = "server_sort:1:0:cn"
+ controls.append(sort_ctrl)
+
+ kwargs = {}
+ if attrs is not None:
+ kwargs = {"attrs": attrs}
+
+ scope = ldb.SCOPE_ONELEVEL
+ if subtree:
+ scope = ldb.SCOPE_SUBTREE
+
+ res = self.ldb_ro.search(ou,
+ expression=expr,
+ scope=scope,
+ controls=controls,
+ **kwargs)
+ results = [str(r['cn'][0]) for r in res]
+
+ ctrls = [str(c) for c in res.controls if
+ str(c).startswith("paged_results")]
+ assert len(ctrls) == 1, "no paged_results response"
+
+ spl = ctrls[0].rsplit(':', 3)
+ cookie = ""
+ if len(spl) == 3:
+ cookie = spl[-1]
+ return results, cookie
+
+
+class PagedResultsTestsRO(PagedResultsTests):
+
+ def test_paged_search_lockstep(self):
+ expr = "(objectClass=*)"
+ ps = 3
+
+ all_results, _ = self.paged_search(expr, page_size=len(self.users)+1)
+
+ # Run two different but overlapping paged searches simultaneously.
+ set_1_index = int((len(all_results))//3)
+ set_2_index = int((2*len(all_results))//3)
+ set_1 = all_results[set_1_index:]
+ set_2 = all_results[:set_2_index+1]
+ set_1_expr = "(cn>=%s)" % (all_results[set_1_index])
+ set_2_expr = "(cn<=%s)" % (all_results[set_2_index])
+
+ results, cookie1 = self.paged_search(set_1_expr, page_size=ps)
+ self.assertEqual(results, set_1[:ps])
+ results, cookie2 = self.paged_search(set_2_expr, page_size=ps)
+ self.assertEqual(results, set_2[:ps])
+
+ results, cookie1 = self.paged_search(set_1_expr, cookie=cookie1,
+ page_size=ps)
+ self.assertEqual(results, set_1[ps:ps*2])
+ results, cookie2 = self.paged_search(set_2_expr, cookie=cookie2,
+ page_size=ps)
+ self.assertEqual(results, set_2[ps:ps*2])
+
+ results, _ = self.paged_search(set_1_expr, cookie=cookie1,
+ page_size=len(self.users))
+ self.assertEqual(results, set_1[ps*2:])
+ results, _ = self.paged_search(set_2_expr, cookie=cookie2,
+ page_size=len(self.users))
+ self.assertEqual(results, set_2[ps*2:])
+
+
+class PagedResultsTestsGC(PagedResultsTestsRO):
+
+ def setUp(self):
+ super(PagedResultsTestsRO, self).setUp()
+ self.ldb_ro = SamDB(host + ":3268", credentials=creds,
+ session_info=system_session(lp), lp=lp)
+
+
+class PagedResultsTestsRW(PagedResultsTests):
+
+ def test_paged_delete_during_search(self, sort=True):
+ expr = "(objectClass=*)"
+
+ # Start new search
+ first_page_size = 3
+ results, cookie = self.paged_search(expr, sort=sort,
+ page_size=first_page_size)
+
+ # Run normal search to get expected results
+ unedited_results, _ = self.paged_search(expr, sort=sort,
+ page_size=len(self.users))
+
+ # Get remaining users not returned by the search above
+ unreturned_users = [u for u in self.users if u['cn'] not in results]
+
+ # Delete one of the users
+ del_index = len(self.users)//2
+ del_user = unreturned_users[del_index]
+ self.ldb.delete(del_user['dn'])
+
+ # Run test
+ results, _ = self.paged_search(expr, cookie=cookie, sort=sort,
+ page_size=len(self.users))
+ expected_results = [r for r in unedited_results[first_page_size:]
+ if r != del_user['cn']]
+ self.assertEqual(results, expected_results)
+
+ def test_paged_delete_during_search_unsorted(self):
+ self.test_paged_delete_during_search(sort=False)
+
+ def test_paged_show_deleted(self):
+ unique = time.strftime("%s", time.gmtime())[-5:]
+ prefix = "show_deleted_test_%s_" % (unique)
+ expr = "(&(objectClass=user)(cn=%s*))" % (prefix)
+ del_ctrl = "show_deleted:1"
+
+ num_users = 10
+ users = []
+ for i in range(num_users):
+ user = self.create_user(i, num_users, prefix=prefix)
+ users.append(user)
+
+ first_user = users[0]
+ self.ldb.delete(first_user['dn'])
+
+ # Start new search
+ first_page_size = 3
+ results, cookie = self.paged_search(expr, page_size=first_page_size,
+ extra_ctrls=[del_ctrl],
+ ou=self.base_dn,
+ subtree=True)
+
+ # Get remaining users not returned by the search above
+ unreturned_users = [u for u in users if u['cn'] not in results]
+
+ # Delete one of the users
+ del_index = len(users)//2
+ del_user = unreturned_users[del_index]
+ self.ldb.delete(del_user['dn'])
+
+ results2, _ = self.paged_search(expr, cookie=cookie,
+ page_size=len(users)*2,
+ extra_ctrls=[del_ctrl],
+ ou=self.base_dn,
+ subtree=True)
+
+ user_cns = {str(u['cn']) for u in users}
+ deleted_cns = {first_user['cn'], del_user['cn']}
+
+ all_results = results + results2
+ normal_results = {r for r in all_results if "DEL:" not in r}
+ self.assertEqual(normal_results, user_cns - deleted_cns)
+
+ # Deleted results get "\nDEL:<GUID>" added to the CN, so cut it out.
+ deleted_results = {r[:r.index('\n')] for r in all_results
+ if "DEL:" in r}
+ self.assertEqual(deleted_results, deleted_cns)
+
+ def test_paged_add_during_search(self, sort=True):
+ expr = "(objectClass=*)"
+
+ # Start new search
+ first_page_size = 3
+ results, cookie = self.paged_search(expr, sort=sort,
+ page_size=first_page_size)
+
+ unedited_results, _ = self.paged_search(expr, sort=sort,
+ page_size=len(self.users)+1)
+
+ # Get remaining users not returned by the search above
+ unwalked_users = [cn for cn in unedited_results if cn not in results]
+
+ # Add a user in the middle of the sort order
+ middle_index = len(unwalked_users)//2
+ middle_user = unwalked_users[middle_index]
+
+ user = {'cn': middle_user + '_2', "objectclass": "user"}
+ user['dn'] = "cn=%s,%s" % (user['cn'], self.ou)
+ self.ldb.add(user)
+
+ results, _ = self.paged_search(expr, sort=sort, cookie=cookie,
+ page_size=len(self.users)+1)
+ expected_results = unwalked_users[:]
+
+ # Uncomment this line to assert that adding worked.
+ # expected_results.insert(middle_index+1, user['cn'])
+
+ self.assertEqual(results, expected_results)
+
+ # On Windows, when server_sort ctrl is NOT provided in the initial search,
+ # adding a record during the search will cause the modified record to
+ # be returned in a future page if it belongs there in the ordering.
+ # When server_sort IS provided, the added record will not be returned.
+ # Samba implements the latter behaviour. This test confirms Samba's
+ # implementation and will fail on Windows.
+ def test_paged_add_during_search_unsorted(self):
+ self.test_paged_add_during_search(sort=False)
+
+ def test_paged_modify_during_search(self, sort=True):
+ expr = "(objectClass=*)"
+
+ # Start new search
+ first_page_size = 3
+ results, cookie = self.paged_search(expr, sort=sort,
+ page_size=first_page_size)
+
+ unedited_results, _ = self.paged_search(expr, sort=sort,
+ page_size=len(self.users)+1)
+
+ # Modify user in the middle of the remaining sort order
+ unwalked_users = [cn for cn in unedited_results if cn not in results]
+ middle_index = len(unwalked_users)//2
+ middle_cn = unwalked_users[middle_index]
+
+ # Find user object
+ users_with_middle_cn = [u for u in self.users if u['cn'] == middle_cn]
+ self.assertEqual(len(users_with_middle_cn), 1)
+ middle_user = users_with_middle_cn[0]
+
+ # Rename object
+ edit_cn = "z_" + middle_cn
+ new_dn = middle_user['dn'].replace(middle_cn, edit_cn)
+ self.ldb.rename(middle_user['dn'], new_dn)
+
+ results, _ = self.paged_search(expr, cookie=cookie, sort=sort,
+ page_size=len(self.users)+1)
+ expected_results = unwalked_users[:]
+ expected_results[middle_index] = edit_cn
+ self.assertEqual(results, expected_results)
+
+ # On Windows, when server_sort ctrl is NOT provided in the initial search,
+ # modifying a record during the search will cause the modified record to
+ # be returned in its new place in a CN ordering.
+ # When server_sort IS provided, the record will be returned its old place
+ # in the control-specified ordering.
+ # Samba implements the latter behaviour. This test confirms Samba's
+ # implementation and will fail on Windows.
+ def test_paged_modify_during_search_unsorted(self):
+ self.test_paged_modify_during_search(sort=False)
+
+ def test_paged_modify_object_scope(self):
+ expr = "(objectClass=*)"
+
+ ou2 = "OU=vlvtestou2,%s" % (self.tree_dn)
+ self.ldb.add({"dn": ou2, "objectclass": "organizationalUnit"})
+
+ # Do a separate, full search to get all results
+ unedited_results, _ = self.paged_search(expr,
+ page_size=len(self.users)+1)
+
+ # Rename before starting a search
+ first_cn = self.users[0]['cn']
+ new_dn = "CN=%s,%s" % (first_cn, ou2)
+ self.ldb.rename(self.users[0]['dn'], new_dn)
+
+ # Start new search under the original OU
+ first_page_size = 3
+ results, cookie = self.paged_search(expr, page_size=first_page_size)
+ self.assertEqual(results, unedited_results[1:1+first_page_size])
+
+ # Get one of the users that is yet to be returned
+ unwalked_users = [cn for cn in unedited_results if cn not in results]
+ middle_index = len(unwalked_users)//2
+ middle_cn = unwalked_users[middle_index]
+
+ # Find user object
+ users_with_middle_cn = [u for u in self.users if u['cn'] == middle_cn]
+ self.assertEqual(len(users_with_middle_cn), 1)
+ middle_user = users_with_middle_cn[0]
+
+ # Rename
+ new_dn = "CN=%s,%s" % (middle_cn, ou2)
+ self.ldb.rename(middle_user['dn'], new_dn)
+
+ results, _ = self.paged_search(expr, cookie=cookie,
+ page_size=len(self.users)+1)
+
+ expected_results = unwalked_users[:]
+
+ # We should really expect that the object renamed into a different
+ # OU should vanish from the results, but turns out Windows does return
+ # the object in this case. Our module matches the Windows behaviour.
+
+ # If behaviour changes, this line inverts the test's expectations to
+ # what you might expect.
+ # del expected_results[middle_index]
+
+ # But still expect the user we removed before the search to be gone
+ del expected_results[0]
+
+ self.assertEqual(results, expected_results)
+
+ def test_paged_modify_one_during_search(self):
+ prefix = "change_during_search_"
+ num_users = 5
+ users = [self.create_user(i, num_users, prefix=prefix)
+ for i in range(num_users)]
+ expr = "(&(objectClass=user)(facsimileTelephoneNumber=%s*))" % (prefix)
+
+ # Get the first page, then change the searched attribute and
+ # try for the second page.
+ results, cookie = self.paged_search(expr, page_size=1)
+ self.assertEqual(len(results), 1)
+ unwalked_users = [u for u in users if u['cn'] != results[0]]
+ self.assertEqual(len(unwalked_users), num_users-1)
+
+ mod_dn = unwalked_users[0]['dn']
+ self.ldb.modify_ldif("dn: %s\n"
+ "changetype: modify\n"
+ "replace: facsimileTelephoneNumber\n"
+ "facsimileTelephoneNumber: 123" % mod_dn)
+
+ results, _ = self.paged_search(expr, cookie=cookie,
+ page_size=len(self.users))
+ expected_cns = {u['cn'] for u in unwalked_users if u['dn'] != mod_dn}
+ self.assertEqual(set(results), expected_cns)
+
+ def test_paged_modify_all_during_search(self):
+ prefix = "change_during_search_"
+ num_users = 5
+ users = [self.create_user(i, num_users, prefix=prefix)
+ for i in range(num_users)]
+ expr = "(&(objectClass=user)(facsimileTelephoneNumber=%s*))" % (prefix)
+
+ # Get the first page, then change the searched attribute and
+ # try for the second page.
+ results, cookie = self.paged_search(expr, page_size=1)
+ unwalked_users = [u for u in users if u['cn'] != results[0]]
+
+ for u in users:
+ self.ldb.modify_ldif("dn: %s\n"
+ "changetype: modify\n"
+ "replace: facsimileTelephoneNumber\n"
+ "facsimileTelephoneNumber: 123" % u['dn'])
+
+ results, _ = self.paged_search(expr, cookie=cookie,
+ page_size=len(self.users))
+ self.assertEqual(results, [])
+
+ def assertPagedSearchRaises(self, err_num, expr, cookie, attrs=None,
+ extra_ctrls=None):
+ try:
+ results, _ = self.paged_search(expr, cookie=cookie,
+ page_size=2,
+ extra_ctrls=extra_ctrls,
+ attrs=attrs)
+ except ldb.LdbError as e:
+ self.assertEqual(e.args[0], err_num)
+ return
+
+ self.fail("No error raised by invalid search")
+
+ def test_paged_changed_expr(self):
+ # Initiate search then use a different expr in subsequent req
+ expr = "(objectClass=*)"
+ results, cookie = self.paged_search(expr, page_size=3)
+ expr = "cn>=a"
+ expected_error_num = 12
+ self.assertPagedSearchRaises(expected_error_num, expr, cookie)
+
+ def test_paged_changed_controls(self):
+ expr = "(objectClass=*)"
+ sort_ctrl = "server_sort:1:0:cn"
+ del_ctrl = "show_deleted:1"
+ expected_error_num = 12
+ ps = 3
+
+ # Initiate search with a sort control then remove in subsequent req
+ results, cookie = self.paged_search(expr, page_size=ps,
+ extra_ctrls=[sort_ctrl])
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, extra_ctrls=[])
+
+ # Initiate search with no sort control then add one in subsequent req
+ results, cookie = self.paged_search(expr, page_size=ps,
+ extra_ctrls=[])
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, extra_ctrls=[sort_ctrl])
+
+ # Initiate search with show-deleted control then
+ # remove it in subsequent req
+ results, cookie = self.paged_search(expr, page_size=ps,
+ extra_ctrls=[del_ctrl])
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, extra_ctrls=[])
+
+ # Initiate normal search then add show-deleted control
+ # in subsequent req
+ results, cookie = self.paged_search(expr, page_size=ps,
+ extra_ctrls=[])
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, extra_ctrls=[del_ctrl])
+
+ # Changing order of controls shouldn't break the search
+ results, cookie = self.paged_search(expr, page_size=ps,
+ extra_ctrls=[del_ctrl, sort_ctrl])
+ try:
+ results, cookie = self.paged_search(expr, page_size=ps,
+ extra_ctrls=[sort_ctrl,
+ del_ctrl])
+ except ldb.LdbError as e:
+ self.fail(e)
+
+ def test_paged_cant_change_controls_data(self):
+ # Some defaults for the rest of the tests
+ expr = "(objectClass=*)"
+ sort_ctrl = "server_sort:1:0:cn"
+ expected_error_num = 12
+
+ # Initiate search with sort control then change it in subsequent req
+ results, cookie = self.paged_search(expr, page_size=3,
+ extra_ctrls=[sort_ctrl])
+ changed_sort_ctrl = "server_sort:1:0:roomNumber"
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, extra_ctrls=[changed_sort_ctrl])
+
+ # Initiate search with a control with crit=1, then use crit=0
+ results, cookie = self.paged_search(expr, page_size=3,
+ extra_ctrls=[sort_ctrl])
+ changed_sort_ctrl = "server_sort:0:0:cn"
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, extra_ctrls=[changed_sort_ctrl])
+
+ def test_paged_search_referrals(self):
+ expr = "(objectClass=*)"
+ paged_ctrl = "paged_results:1:5"
+ res = self.ldb.search(self.base_dn,
+ expression=expr,
+ attrs=['cn'],
+ scope=ldb.SCOPE_SUBTREE,
+ controls=[paged_ctrl])
+
+ # Do a paged search walk over the whole database and save a list
+ # of all the referrals returned by each search.
+ referral_lists = []
+
+ while True:
+ referral_lists.append(res.referals)
+
+ ctrls = [str(c) for c in res.controls if
+ str(c).startswith("paged_results")]
+ self.assertEqual(len(ctrls), 1)
+ spl = ctrls[0].rsplit(':')
+ if len(spl) != 3:
+ break
+
+ cookie = spl[-1]
+ res = self.ldb.search(self.base_dn,
+ expression=expr,
+ attrs=['cn'],
+ scope=ldb.SCOPE_SUBTREE,
+ controls=[paged_ctrl + ":" + cookie])
+
+ ref_list = referral_lists[0]
+
+ # Sanity check to make sure the search actually did something
+ self.assertGreater(len(referral_lists), 2)
+
+ # Check the first referral set contains stuff
+ self.assertGreater(len(ref_list), 0)
+
+ # Check the others don't
+ self.assertTrue(all([len(l) == 0 for l in referral_lists[1:]]))
+
+ # Check the entries in the first referral list look like referrals
+ self.assertTrue(all([s.startswith('ldap://') for s in ref_list]))
+
+ def test_paged_change_attrs(self):
+ expr = "(objectClass=*)"
+ attrs = ['cn']
+ expected_error_num = 12
+
+ results, cookie = self.paged_search(expr, page_size=3, attrs=attrs)
+ results, cookie = self.paged_search(expr, cookie=cookie, page_size=3,
+ attrs=attrs)
+
+ changed_attrs = attrs + ['roomNumber']
+ self.assertPagedSearchRaises(expected_error_num, expr,
+ cookie, attrs=changed_attrs,
+ extra_ctrls=[])
+
+ def test_vlv_paged(self):
+ """Testing behaviour with VLV and paged_results set.
+
+ A strange combination, certainly
+
+ Thankfully combining both of these gives
+ unavailable-critical-extension against Windows 1709
+
+ """
+ sort_control = "server_sort:1:0:cn"
+
+ try:
+ msgs = self.ldb.search(base=self.base_dn,
+ scope=ldb.SCOPE_SUBTREE,
+ attrs=["objectGUID", "cn", "member"],
+ controls=["vlv:1:20:20:11:0",
+ sort_control,
+ "paged_results:1:1024"])
+ self.fail("should have failed with LDAP_UNAVAILABLE_CRITICAL_EXTENSION")
+ except ldb.LdbError as e:
+ (enum, estr) = e.args
+ self.assertEqual(enum, ldb.ERR_UNSUPPORTED_CRITICAL_EXTENSION)
+
+ def test_anr_paged(self):
+ """Testing behaviour with anr= searches and paged_results set.
+
+ A problematic combination, as anr involves filter rewriting
+
+ """
+ prefix = "anr"
+ num_users = 5
+ users = [self.create_user(i, num_users, prefix=prefix)
+ for i in range(num_users)]
+ expr = f"(|(anr={prefix})(&(objectClass=user)(facsimileTelephoneNumber={prefix}*)))"
+
+ results, cookie = self.paged_search(expr, page_size=1)
+ self.assertEqual(len(results), 1)
+
+ results, cookie = self.paged_search(expr, page_size=2, cookie=cookie)
+ self.assertEqual(len(results), 2)
+
+ results, cookie = self.paged_search(expr, page_size=2, cookie=cookie)
+ self.assertEqual(len(results), 2)
+
+
+if "://" not in host:
+ if os.path.isfile(host):
+ host = "tdb://%s" % host
+ else:
+ host = "ldap://%s" % host
+
+
+TestProgram(module=__name__, opts=subunitopts)
diff --git a/source4/dsdb/wscript_build b/source4/dsdb/wscript_build
new file mode 100644
index 0000000..d0d6439
--- /dev/null
+++ b/source4/dsdb/wscript_build
@@ -0,0 +1,83 @@
+#!/usr/bin/env python
+
+bld.RECURSE('samdb/ldb_modules')
+
+bld.SAMBA_LIBRARY('samdb',
+ source='samdb/samdb.c samdb/samdb_privilege.c samdb/cracknames.c repl/replicated_objects.c',
+ pc_files='samdb.pc',
+ autoproto='samdb/samdb_proto.h',
+ public_deps='krb5',
+ public_headers='',
+ vnum='0.0.1',
+ deps='ndr NDR_DRSUAPI NDR_DRSBLOBS auth_system_session LIBCLI_AUTH ndr SAMDB_SCHEMA ldbsamba samdb-common LIBCLI_DRSUAPI cli-ldap-common samba-util com_err authkrb5 samba-credentials ldbwrap samba-errors krb5samba ldb',
+ )
+
+bld.SAMBA_LIBRARY('samdb-common',
+ source='common/util.c common/util_trusts.c common/util_groups.c common/util_samr.c common/dsdb_dn.c common/dsdb_access.c common/util_links.c common/rodc_helper.c',
+ autoproto='common/proto.h',
+ private_library=True,
+ deps='ldb NDR_DRSBLOBS util_ldb LIBCLI_AUTH samba-hostconfig samba_socket cli-ldap-common flag_mapping UTIL_RUNCMD SAMBA_VERSION samba-security'
+ )
+
+
+bld.SAMBA_SUBSYSTEM('SAMDB_SCHEMA',
+ source='schema/schema_init.c schema/schema_set.c schema/schema_query.c schema/schema_syntax.c schema/schema_description.c schema/schema_convert_to_ol.c schema/schema_inferiors.c schema/schema_prefixmap.c schema/schema_info_attr.c schema/schema_filtered.c schema/dsdb_dn.c',
+ autoproto='schema/proto.h',
+ deps='samdb-common NDR_DRSUAPI NDR_DRSBLOBS ldbsamba tevent'
+ )
+
+
+bld.SAMBA_MODULE('service_drepl',
+ source='repl/drepl_service.c repl/drepl_periodic.c repl/drepl_partitions.c repl/drepl_out_pull.c repl/drepl_out_helpers.c repl/drepl_notify.c repl/drepl_ridalloc.c repl/drepl_extended.c repl/drepl_fsmo.c repl/drepl_secret.c repl/drepl_replica.c',
+ autoproto='repl/drepl_service_proto.h',
+ subsystem='service',
+ init_function='server_service_drepl_init',
+ deps='samdb process_model RPC_NDR_DRSUAPI',
+ internal_module=False,
+ enabled=bld.AD_DC_BUILD_IS_ENABLED()
+ )
+
+bld.SAMBA_LIBRARY('dsdb_garbage_collect_tombstones',
+ source='kcc/garbage_collect_tombstones.c',
+ deps='samdb RPC_NDR_DRSUAPI',
+ private_library=True,
+ enabled=bld.AD_DC_BUILD_IS_ENABLED())
+
+bld.SAMBA_LIBRARY('scavenge_dns_records',
+ source='kcc/scavenge_dns_records.c',
+ deps='samdb RPC_NDR_DRSUAPI dnsserver_common',
+ private_library=True,
+ enabled=bld.AD_DC_BUILD_IS_ENABLED())
+
+bld.SAMBA_MODULE('service_kcc',
+ source='kcc/kcc_service.c kcc/kcc_connection.c kcc/kcc_periodic.c kcc/kcc_drs_replica_info.c',
+ autoproto='kcc/kcc_service_proto.h',
+ subsystem='service',
+ init_function='server_service_kcc_init',
+ deps='samdb process_model RPC_NDR_IRPC RPC_NDR_DRSUAPI UTIL_RUNCMD dsdb_garbage_collect_tombstones scavenge_dns_records',
+ internal_module=False,
+ enabled=bld.AD_DC_BUILD_IS_ENABLED()
+ )
+
+
+bld.SAMBA_MODULE('service_dns_update',
+ source='dns/dns_update.c',
+ subsystem='service',
+ init_function='server_service_dnsupdate_init',
+ deps='samdb UTIL_RUNCMD samba-util ldb samdb-common samba-errors talloc auth_system_session samba-hostconfig',
+ internal_module=False,
+ enabled=bld.AD_DC_BUILD_IS_ENABLED()
+ )
+
+pyldb_util = bld.pyembed_libname('pyldb-util')
+pyrpc_util = bld.pyembed_libname('pyrpc_util')
+pyparam_util = bld.pyembed_libname('pyparam_util')
+bld.SAMBA_PYTHON('python_dsdb',
+ source='pydsdb.c',
+ # the dependency on dcerpc here is because gensec
+ # depends on dcerpc but the waf circular dependency finder
+ # removes it so we end up with unresolved symbols.
+ deps='samdb %s dcerpc com_err %s %s dsdb_garbage_collect_tombstones scavenge_dns_records' %\
+ (pyldb_util, pyrpc_util, pyparam_util),
+ realname='samba/dsdb.so'
+ )