diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 17:20:00 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 17:20:00 +0000 |
commit | 8daa83a594a2e98f39d764422bfbdbc62c9efd44 (patch) | |
tree | 4099e8021376c7d8c05bdf8503093d80e9c7bad0 /source4/torture/drs | |
parent | Initial commit. (diff) | |
download | samba-8daa83a594a2e98f39d764422bfbdbc62c9efd44.tar.xz samba-8daa83a594a2e98f39d764422bfbdbc62c9efd44.zip |
Adding upstream version 2:4.20.0+dfsg.upstream/2%4.20.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'source4/torture/drs')
28 files changed, 16343 insertions, 0 deletions
diff --git a/source4/torture/drs/drs_init.c b/source4/torture/drs/drs_init.c new file mode 100644 index 0000000..bbe246d --- /dev/null +++ b/source4/torture/drs/drs_init.c @@ -0,0 +1,80 @@ +/* + Unix SMB/CIFS implementation. + + DRSUAPI utility functions to be used in torture tests + + 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 "torture/smbtorture.h" +#include "torture/rpc/drsuapi.h" +#include "dsdb/samdb/samdb.h" +#include "torture/drs/proto.h" + +/** + * DRSUAPI tests to be executed remotely + */ +static struct torture_suite * torture_drs_rpc_suite(TALLOC_CTX *mem_ctx, + const char *suite_name) +{ + struct torture_suite *suite = torture_suite_create(mem_ctx, suite_name); + + torture_drs_rpc_dssync_tcase(suite); + torture_drs_rpc_dsintid_tcase(suite); + + suite->description = talloc_strdup(suite, + "DRSUAPI RPC Tests Suite"); + + return suite; +} + +/** + * DRSUAPI tests to be executed remotely + */ +static struct torture_suite * torture_drs_unit_suite(TALLOC_CTX *mem_ctx, + const char *suite_name) +{ + struct torture_suite *suite = torture_suite_create(mem_ctx, suite_name); + + torture_drs_unit_prefixmap(suite); + torture_drs_unit_schemainfo(suite); + + suite->description = talloc_strdup(suite, + "DRSUAPI Unit Tests Suite"); + + return suite; +} + +/** + * DRSUAPI torture module initialization + */ +NTSTATUS torture_drs_init(TALLOC_CTX *ctx) +{ + struct torture_suite *suite; + + /* register RPC related test cases */ + suite = torture_drs_rpc_suite(ctx, "drs.rpc"); + if (!suite) return NT_STATUS_NO_MEMORY; + torture_register_suite(ctx, suite); + + /* register DRS Unit test cases */ + suite = torture_drs_unit_suite(ctx, "drs.unit"); + if (!suite) return NT_STATUS_NO_MEMORY; + torture_register_suite(ctx, suite); + + return NT_STATUS_OK; +} diff --git a/source4/torture/drs/drs_util.c b/source4/torture/drs/drs_util.c new file mode 100644 index 0000000..c43836e --- /dev/null +++ b/source4/torture/drs/drs_util.c @@ -0,0 +1,167 @@ +/* + Unix SMB/CIFS implementation. + + DRSUAPI utility functions to be used in torture tests + + 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 "torture/torture.h" +#include "dsdb/samdb/samdb.h" +#include "torture/rpc/drsuapi.h" +#include "../lib/util/asn1.h" +#include "torture/drs/proto.h" + +/** + * Decode Attribute OID based on MS documentation + * See MS-DRSR.pdf - 5.16.4 + * + * On success returns decoded OID and + * corresponding prefix_map index (if requested) + */ +bool drs_util_oid_from_attid(struct torture_context *tctx, + const struct drsuapi_DsReplicaOIDMapping_Ctr *prefix_map, + uint32_t attid, + const char **_oid, + int *map_idx) +{ + uint32_t i, hi_word, lo_word; + DATA_BLOB bin_oid = {NULL, 0}; + char *oid; + struct drsuapi_DsReplicaOIDMapping *map_entry = NULL; + TALLOC_CTX *mem_ctx = talloc_named(tctx, 0, "util_drsuapi_oid_from_attid"); + + /* crack attid value */ + hi_word = attid >> 16; + lo_word = attid & 0xFFFF; + + /* check last entry in the prefix map is the special one */ + map_entry = &prefix_map->mappings[prefix_map->num_mappings-1]; + torture_assert(tctx, + (map_entry->id_prefix == 0) + && (*map_entry->oid.binary_oid == 0xFF), + "Last entry in Prefix Map is not the special one!"); + + /* locate corresponding prefixMap entry */ + map_entry = NULL; + for (i = 0; i < prefix_map->num_mappings - 1; i++) { + + if (hi_word == prefix_map->mappings[i].id_prefix) { + map_entry = &prefix_map->mappings[i]; + if (map_idx) *map_idx = i; + break; + } + } + + torture_assert(tctx, map_entry, "Unable to locate corresponding Prefix Map entry"); + + /* copy partial oid making enough room */ + bin_oid.length = map_entry->oid.length + 2; + bin_oid.data = talloc_array(mem_ctx, uint8_t, bin_oid.length); + torture_assert(tctx, bin_oid.data, "Not enough memory"); + memcpy(bin_oid.data, map_entry->oid.binary_oid, map_entry->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] = ((lo_word / 128) % 128) + 128; /* (0x80 | ((lo_word>>7) & 0x7f)) */ + bin_oid.data[bin_oid.length-1] = lo_word % 128; /* lo_word & 0x7f */ + } + + torture_assert(tctx, + ber_read_OID_String(tctx, bin_oid, &oid), + "Failed to decode binary OID"); + talloc_free(mem_ctx); + + *_oid = oid; + + return true; +} + + +/** + * Loads dsdb_schema from ldb connection using remote prefixMap. + * Schema will be loaded only if: + * - ldb has no attached schema + * - reload_schema is true + * + * This function is to be used in tests that use GetNCChanges() function + */ +bool drs_util_dsdb_schema_load_ldb(struct torture_context *tctx, + struct ldb_context *ldb, + const struct drsuapi_DsReplicaOIDMapping_Ctr *mapping_ctr, + bool reload_schema) +{ + int ret; + WERROR werr; + char *err_msg; + struct ldb_result *res; + struct ldb_dn *schema_dn; + struct dsdb_schema *ldap_schema; + + ldap_schema = dsdb_get_schema(ldb, NULL); + if (ldap_schema && !reload_schema) { + return true; + } + + schema_dn = ldb_get_schema_basedn(ldb); + torture_assert(tctx, schema_dn != NULL, + talloc_asprintf(tctx, "ldb_get_schema_basedn() failed: %s", ldb_errstring(ldb))); + + ldap_schema = dsdb_new_schema(ldb); + torture_assert(tctx, ldap_schema != NULL, "dsdb_new_schema() failed!"); + + werr = dsdb_load_prefixmap_from_drsuapi(ldap_schema, mapping_ctr); + torture_assert_werr_ok(tctx, werr, + "Failed to construct prefixMap from drsuapi data"); + + /* + * load the attribute and objectClass definitions + */ + ret = ldb_search(ldb, ldap_schema, &res, + schema_dn, LDB_SCOPE_ONELEVEL, NULL, + "(|(objectClass=attributeSchema)(objectClass=classSchema))"); + if (ret != LDB_SUCCESS) { + err_msg = talloc_asprintf(tctx, + "failed to search attributeSchema or classSchema objects: %s", + ldb_errstring(ldb)); + torture_fail(tctx, err_msg); + } + + ret = dsdb_load_ldb_results_into_schema(tctx, ldb, ldap_schema, res, &err_msg); + if (ret != LDB_SUCCESS) { + err_msg = talloc_asprintf(tctx, + "dsdb_load_ldb_results_into_schema failed: %s", + err_msg); + torture_fail(tctx, err_msg); + } + + talloc_free(res); + + ret = dsdb_set_schema(ldb, ldap_schema, SCHEMA_WRITE); + if (ret != LDB_SUCCESS) { + torture_fail(tctx, + talloc_asprintf(tctx, "dsdb_set_schema() failed: %s", ldb_strerror(ret))); + } + + return true; +} diff --git a/source4/torture/drs/python/cracknames.py b/source4/torture/drs/python/cracknames.py new file mode 100644 index 0000000..f244605 --- /dev/null +++ b/source4/torture/drs/python/cracknames.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# 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 samba.tests +import ldb +import drs_base + +from samba.dcerpc import drsuapi + + +class DrsCracknamesTestCase(drs_base.DrsBaseTestCase): + def setUp(self): + super(DrsCracknamesTestCase, self).setUp() + (self.drs, self.drs_handle) = self._ds_bind(self.dnsname_dc1) + + self.ou = "ou=Cracknames_ou,%s" % self.ldb_dc1.get_default_basedn() + self.username = "Cracknames_user" + self.user = "cn=%s,%s" % (self.username, self.ou) + + self.ldb_dc1.add({ + "dn": self.ou, + "objectclass": "organizationalUnit"}) + + self.user_record = { + "dn": self.user, + "objectclass": "user", + "sAMAccountName": self.username, + "userPrincipalName": "test@test.com", + "servicePrincipalName": "test/%s" % self.ldb_dc1.get_default_basedn(), + "displayName": "test"} + + self.ldb_dc1.add(self.user_record) + self.ldb_dc1.delete(self.user_record["dn"]) + self.ldb_dc1.add(self.user_record) + + # The formats specified in MS-DRSR 4.1.4.13; DS_NAME_FORMAT + # We don't support any of the ones specified in 4.1.4.1.2. + self.formats = { + drsuapi.DRSUAPI_DS_NAME_FORMAT_FQDN_1779, + drsuapi.DRSUAPI_DS_NAME_FORMAT_NT4_ACCOUNT, + drsuapi.DRSUAPI_DS_NAME_FORMAT_DISPLAY, + drsuapi.DRSUAPI_DS_NAME_FORMAT_GUID, + drsuapi.DRSUAPI_DS_NAME_FORMAT_CANONICAL, + drsuapi.DRSUAPI_DS_NAME_FORMAT_USER_PRINCIPAL, + drsuapi.DRSUAPI_DS_NAME_FORMAT_CANONICAL_EX, + drsuapi.DRSUAPI_DS_NAME_FORMAT_SERVICE_PRINCIPAL, + drsuapi.DRSUAPI_DS_NAME_FORMAT_SID_OR_SID_HISTORY, + # This format is not supported by Windows (or us) + # drsuapi.DRSUAPI_DS_NAME_FORMAT_DNS_DOMAIN, + } + + def tearDown(self): + self.ldb_dc1.delete(self.user) + self.ldb_dc1.delete(self.ou) + super(DrsCracknamesTestCase, self).tearDown() + + def test_Cracknames(self): + """ + Verifies that we can cracknames any of the standard formats + (DS_NAME_FORMAT) to a GUID, and that we can cracknames a + GUID to any of the standard formats. + + GUID was chosen just so that we don't have to do an n^2 loop. + """ + (result, ctr) = self._do_cracknames(self.user, + drsuapi.DRSUAPI_DS_NAME_FORMAT_FQDN_1779, + drsuapi.DRSUAPI_DS_NAME_FORMAT_GUID) + + self.assertEqual(ctr.count, 1) + self.assertEqual(ctr.array[0].status, + drsuapi.DRSUAPI_DS_NAME_STATUS_OK) + + user_guid = ctr.array[0].result_name + + for name_format in self.formats: + (result, ctr) = self._do_cracknames(user_guid, + drsuapi.DRSUAPI_DS_NAME_FORMAT_GUID, + name_format) + + self.assertEqual(ctr.count, 1) + self.assertEqual(ctr.array[0].status, + drsuapi.DRSUAPI_DS_NAME_STATUS_OK, + "Expected 0, got %s, desired format is %s" + % (ctr.array[0].status, name_format)) + + (result, ctr) = self._do_cracknames(ctr.array[0].result_name, + name_format, + drsuapi.DRSUAPI_DS_NAME_FORMAT_GUID) + + self.assertEqual(ctr.count, 1) + self.assertEqual(ctr.array[0].status, + drsuapi.DRSUAPI_DS_NAME_STATUS_OK, + "Expected 0, got %s, offered format is %s" + % (ctr.array[0].status, name_format)) + + def test_MultiValuedAttribute(self): + """ + Verifies that, if we try and cracknames with the desired output + being a multi-valued attribute, it returns + DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE. + """ + username = "Cracknames_user_MVA" + user = "cn=%s,%s" % (username, self.ou) + + user_record = { + "dn": user, + "objectclass": "user", + "sAMAccountName": username, + "userPrincipalName": "test2@test.com", + "servicePrincipalName": ["test2/%s" % self.ldb_dc1.get_default_basedn(), + "test3/%s" % self.ldb_dc1.get_default_basedn()], + "displayName": "test2"} + + self.ldb_dc1.add(user_record) + + (result, ctr) = self._do_cracknames(user, + drsuapi.DRSUAPI_DS_NAME_FORMAT_FQDN_1779, + drsuapi.DRSUAPI_DS_NAME_FORMAT_GUID) + + self.assertEqual(ctr.count, 1) + self.assertEqual(ctr.array[0].status, + drsuapi.DRSUAPI_DS_NAME_STATUS_OK) + + user_guid = ctr.array[0].result_name + + (result, ctr) = self._do_cracknames(user_guid, + drsuapi.DRSUAPI_DS_NAME_FORMAT_GUID, + drsuapi.DRSUAPI_DS_NAME_FORMAT_SERVICE_PRINCIPAL) + + self.assertEqual(ctr.count, 1) + self.assertEqual(ctr.array[0].status, + drsuapi.DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE) + + self.ldb_dc1.delete(user) + + def test_NoSPNAttribute(self): + """ + Verifies that, if we try and cracknames with the desired output + being an SPN, it returns + DRSUAPI_DS_NAME_STATUS_NOT_UNIQUE. + """ + username = "Cracknames_no_SPN" + user = "cn=%s,%s" % (username, self.ou) + + user_record = { + "dn": user, + "objectclass": "user", + "sAMAccountName" : username, + "userPrincipalName" : "test4@test.com", + "displayName" : "test4"} + + self.ldb_dc1.add(user_record) + + (result, ctr) = self._do_cracknames(user, + drsuapi.DRSUAPI_DS_NAME_FORMAT_FQDN_1779, + drsuapi.DRSUAPI_DS_NAME_FORMAT_GUID) + + self.assertEqual(ctr.count, 1) + self.assertEqual(ctr.array[0].status, + drsuapi.DRSUAPI_DS_NAME_STATUS_OK) + + user_guid = ctr.array[0].result_name + + (result, ctr) = self._do_cracknames(user_guid, + drsuapi.DRSUAPI_DS_NAME_FORMAT_GUID, + drsuapi.DRSUAPI_DS_NAME_FORMAT_SERVICE_PRINCIPAL) + + self.assertEqual(ctr.count, 1) + self.assertEqual(ctr.array[0].status, + drsuapi.DRSUAPI_DS_NAME_STATUS_NOT_FOUND) + + self.ldb_dc1.delete(user) + + def _do_cracknames(self, name, format_offered, format_desired): + req = drsuapi.DsNameRequest1() + names = drsuapi.DsNameString() + names.str = name + + req.codepage = 1252 # German, but it doesn't really matter here + req.language = 1033 + req.format_flags = 0 + req.format_offered = format_offered + req.format_desired = format_desired + req.count = 1 + req.names = [names] + + (result, ctr) = self.drs.DsCrackNames(self.drs_handle, 1, req) + return (result, ctr) diff --git a/source4/torture/drs/python/delete_object.py b/source4/torture/drs/python/delete_object.py new file mode 100644 index 0000000..5f2f703 --- /dev/null +++ b/source4/torture/drs/python/delete_object.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python3 +# -*- 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 DC1=dc1_dns_name +# export DC2=dc2_dns_name +# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun +# PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN delete_object -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD" +# + +import time + + +from ldb import ( + SCOPE_SUBTREE, +) + +import drs_base +import ldb + + +class DrsDeleteObjectTestCase(drs_base.DrsBaseTestCase): + + def setUp(self): + super(DrsDeleteObjectTestCase, self).setUp() + # disable automatic replication temporary + self._disable_all_repl(self.dnsname_dc1) + self._disable_all_repl(self.dnsname_dc2) + # make sure DCs are synchronized before the test + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + + def tearDown(self): + self._enable_all_repl(self.dnsname_dc1) + self._enable_all_repl(self.dnsname_dc2) + super(DrsDeleteObjectTestCase, self).tearDown() + + def _make_username(self): + return "DrsDelObjUser_" + time.strftime("%s", time.gmtime()) + + # now also used to check the group + def _check_obj(self, sam_ldb, obj_orig, is_deleted): + # search the user by guid as it may be deleted + guid_str = self._GUID_string(obj_orig["objectGUID"][0]) + expression = "(objectGUID=%s)" % guid_str + res = sam_ldb.search(base=self.domain_dn, + expression=expression, + controls=["show_deleted:1"]) + self.assertEqual(len(res), 1) + user_cur = res[0] + # Deleted Object base DN + dodn = self._deleted_objects_dn(sam_ldb) + # now check properties of the user + cn_orig = str(obj_orig["cn"][0]) + cn_cur = str(user_cur["cn"][0]) + name_orig = str(obj_orig["name"][0]) + name_cur = str(user_cur["name"][0]) + if is_deleted: + self.assertEqual(str(user_cur["isDeleted"][0]), "TRUE") + self.assertFalse("objectCategory" in user_cur) + self.assertFalse("sAMAccountType" in user_cur) + self.assertFalse("description" in user_cur) + self.assertFalse("memberOf" in user_cur) + self.assertFalse("member" in user_cur) + self.assertTrue(dodn in str(user_cur["dn"]), + "User %s is deleted but it is not located under %s (found at %s)!" % (name_orig, dodn, user_cur["dn"])) + self.assertEqual(name_cur, name_orig + "\nDEL:" + guid_str) + self.assertEqual(name_cur, user_cur.dn.get_rdn_value()) + self.assertEqual(cn_cur, cn_orig + "\nDEL:" + guid_str) + self.assertEqual(name_cur, cn_cur) + else: + self.assertFalse("isDeleted" in user_cur) + self.assertEqual(name_cur, name_orig) + self.assertEqual(name_cur, user_cur.dn.get_rdn_value()) + self.assertEqual(cn_cur, cn_orig) + self.assertEqual(name_cur, cn_cur) + self.assertEqual(obj_orig["dn"], user_cur["dn"]) + self.assertTrue(dodn not in str(user_cur["dn"])) + return user_cur + + def test_ReplicateDeletedObject1(self): + """Verifies how a deleted-object is replicated between two DCs. + This test should verify that: + - deleted-object is replicated properly + - We verify that after replication, + object's state to conform to a tombstone-object state + - This test replicates the object modifications to + the server with the user deleted first + + TODO: It will also be great if check replPropertyMetaData. + TODO: Check for deleted-object state, depending on DC's features + when recycle-bin is enabled + """ + # work-out unique username to test with + username = self._make_username() + + # create user on DC1 + self.ldb_dc1.newuser(username=username, password="P@sswOrd!") + ldb_res = self.ldb_dc1.search(base=self.domain_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (username, self._GUID_string(user_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=False) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + # delete user on DC1 + self.ldb_dc1.delete(user_dn) + # check user info on DC1 - should be deleted + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=True) + # check user info on DC2 - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_orig, is_deleted=False) + + # The user should not have a description or memberOf yet + self.assertFalse("description" in user_cur) + self.assertFalse("memberOf" in user_cur) + + self.ldb_dc2.newgroup("group_%s" % username) + + self.ldb_dc2.newgroup("group2_%s" % username) + + ldb_res = self.ldb_dc2.search(base=self.domain_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=group_%s)" % username) + self.assertTrue(len(ldb_res) == 1) + self.assertTrue("sAMAccountName" in ldb_res[0]) + group_orig = ldb_res[0] + group_dn = ldb_res[0]["dn"] + + # modify user on DC2 to have a description and be a member of the group + m = ldb.Message() + m.dn = user_dn + m["description"] = ldb.MessageElement("a description", + ldb.FLAG_MOD_ADD, "description") + self.ldb_dc2.modify(m) + m = ldb.Message() + m.dn = group_dn + m["member"] = ldb.MessageElement(str(user_dn), + ldb.FLAG_MOD_ADD, "member") + self.ldb_dc2.modify(m) + + ldb_res = self.ldb_dc2.search(base=self.domain_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=group2_%s)" % username) + self.assertTrue(len(ldb_res) == 1) + self.assertTrue("sAMAccountName" in ldb_res[0]) + group2_dn = ldb_res[0]["dn"] + group2_orig = ldb_res[0] + + m = ldb.Message() + m.dn = group2_dn + m["member"] = ldb.MessageElement(str(group_dn), + ldb.FLAG_MOD_ADD, "member") + self.ldb_dc2.modify(m) + + # check user info on DC2 - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_orig, is_deleted=False) + + # The user should not have a description yet + self.assertTrue("description" in user_cur) + self.assertTrue("memberOf" in user_cur) + + ldb_res = self.ldb_dc2.search(base=self.domain_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=group_%s)" % username) + self.assertTrue(len(ldb_res) == 1) + + # This group is a member of another group + self.assertTrue("memberOf" in ldb_res[0]) + + # The user was deleted on DC1, but check the modify we just did on DC2 + self.assertTrue("member" in ldb_res[0]) + + # trigger replication from DC2 to DC1 + # to check if deleted object gets restored + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + # check user info on DC1 - should be deleted + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=True) + # check user info on DC2 - should be valid user + self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_orig, is_deleted=False) + + ldb_res = self.ldb_dc1.search(base=self.domain_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=group_%s)" % username) + self.assertTrue(len(ldb_res) == 1) + + # This group is a member of another group + self.assertTrue("memberOf" in ldb_res[0]) + + # The user was deleted on DC1, but the modify we did on DC2, check it never replicated in + self.assertFalse("member" in ldb_res[0]) + + # trigger replication from DC1 to DC2 + # to check if deleted object is replicated + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC1 - should be deleted + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=True) + # check user info on DC2 - should be deleted + self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_orig, is_deleted=True) + + # delete group on DC1 + self.ldb_dc1.delete(group_dn) + + # trigger replication from DC1 to DC2 + # to check if deleted object is replicated + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + # check group info on DC1 - should be deleted + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=group_orig, is_deleted=True) + # check group info on DC2 - should be deleted + self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=group_orig, is_deleted=True) + + ldb_res = self.ldb_dc2.search(base=self.domain_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=group2_%s)" % username) + self.assertTrue(len(ldb_res) == 1) + self.assertFalse("member" in ldb_res[0]) + + # delete group on DC1 + self.ldb_dc1.delete(group2_dn) + + # trigger replication from DC1 to DC2 + # to check if deleted object is replicated + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + # check group info on DC1 - should be deleted + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=group2_orig, is_deleted=True) + # check group info on DC2 - should be deleted + self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=group2_orig, is_deleted=True) + + def test_ReplicateDeletedObject2(self): + """Verifies how a deleted-object is replicated between two DCs. + This test should verify that: + - deleted-object is replicated properly + - We verify that after replication, + object's state to conform to a tombstone-object state + - This test replicates the delete to the server with the + object modifications first + + TODO: It will also be great if check replPropertyMetaData. + TODO: Check for deleted-object state, depending on DC's features + when recycle-bin is enabled + """ + # work-out unique username to test with + username = self._make_username() + + # create user on DC1 + self.ldb_dc1.newuser(username=username, password="P@sswOrd!") + ldb_res = self.ldb_dc1.search(base=self.domain_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (username, self._GUID_string(user_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=False) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + # delete user on DC1 + self.ldb_dc1.delete(user_dn) + # check user info on DC1 - should be deleted + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=True) + # check user info on DC2 - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_orig, is_deleted=False) + + # The user should not have a description or memberOf yet + self.assertFalse("description" in user_cur) + self.assertFalse("memberOf" in user_cur) + + self.ldb_dc2.newgroup("group_%s" % username) + + self.ldb_dc2.newgroup("group2_%s" % username) + + ldb_res = self.ldb_dc2.search(base=self.domain_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=group_%s)" % username) + self.assertTrue(len(ldb_res) == 1) + self.assertTrue("sAMAccountName" in ldb_res[0]) + group_dn = ldb_res[0]["dn"] + + # modify user on DC2 to have a description and be a member of the group + m = ldb.Message() + m.dn = user_dn + m["description"] = ldb.MessageElement("a description", + ldb.FLAG_MOD_ADD, "description") + self.ldb_dc2.modify(m) + m = ldb.Message() + m.dn = group_dn + m["member"] = ldb.MessageElement(str(user_dn), + ldb.FLAG_MOD_ADD, "member") + self.ldb_dc2.modify(m) + + ldb_res = self.ldb_dc2.search(base=self.domain_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=group2_%s)" % username) + self.assertTrue(len(ldb_res) == 1) + self.assertTrue("sAMAccountName" in ldb_res[0]) + group2_dn = ldb_res[0]["dn"] + + m = ldb.Message() + m.dn = group2_dn + m["member"] = ldb.MessageElement(str(group_dn), + ldb.FLAG_MOD_ADD, "member") + self.ldb_dc2.modify(m) + + # check user info on DC2 - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_orig, is_deleted=False) + + # The user should not have a description yet + self.assertTrue("description" in user_cur) + self.assertTrue("memberOf" in user_cur) + + # trigger replication from DC1 to DC2 + # to check if deleted object gets restored + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC1 - should be deleted + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=True) + # check user info on DC2 - should be deleted + self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_orig, is_deleted=True) + + ldb_res = self.ldb_dc2.search(base=self.domain_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=group_%s)" % username) + self.assertTrue(len(ldb_res) == 1) + self.assertTrue("memberOf" in ldb_res[0]) + self.assertFalse("member" in ldb_res[0]) + + # trigger replication from DC2 to DC1 + # to check if deleted object is replicated + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + # check user info on DC1 - should be deleted + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=True) + # check user info on DC2 - should be deleted + self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_orig, is_deleted=True) + + ldb_res = self.ldb_dc1.search(base=self.domain_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=group_%s)" % username) + self.assertTrue(len(ldb_res) == 1) + self.assertTrue("memberOf" in ldb_res[0]) + self.assertFalse("member" in ldb_res[0]) + + # delete group on DC1 + self.ldb_dc1.delete(group_dn) + self.ldb_dc1.delete(group2_dn) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) diff --git a/source4/torture/drs/python/drs_base.py b/source4/torture/drs/python/drs_base.py new file mode 100644 index 0000000..bf98e59 --- /dev/null +++ b/source4/torture/drs/python/drs_base.py @@ -0,0 +1,632 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Unix SMB/CIFS implementation. +# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2011 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2016 +# Copyright (C) Catalyst IT Ltd. 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 sys +import time +import os +import ldb + +sys.path.insert(0, "bin/python") +import samba.tests +from samba.tests.samba_tool.base import SambaToolCmdTest +from samba import dsdb +from samba.dcerpc import drsuapi, misc, drsblobs, security +from samba.ndr import ndr_unpack, ndr_pack +from samba.drs_utils import drs_DsBind +from samba import gensec +from ldb import ( + SCOPE_BASE, + Message, + FLAG_MOD_REPLACE, +) +from samba.common import cmp +from samba.common import get_string + + +class DrsBaseTestCase(SambaToolCmdTest): + """Base class implementation for all DRS python tests. + It is intended to provide common initialization and + and functionality used by all DRS tests in drs/python + test package. For instance, DC1 and DC2 are always used + to pass URLs for DCs to test against""" + + def setUp(self): + super(DrsBaseTestCase, self).setUp() + creds = self.get_credentials() + creds.set_gensec_features(creds.get_gensec_features() | gensec.FEATURE_SEAL) + + # connect to DCs + self.url_dc1 = samba.tests.env_get_var_value("DC1") + (self.ldb_dc1, self.info_dc1) = samba.tests.connect_samdb_ex(self.url_dc1, + ldap_only=True) + self.url_dc2 = samba.tests.env_get_var_value("DC2") + (self.ldb_dc2, self.info_dc2) = samba.tests.connect_samdb_ex(self.url_dc2, + ldap_only=True) + self.test_ldb_dc = self.ldb_dc1 + + # cache some of RootDSE props + self.schema_dn = str(self.info_dc1["schemaNamingContext"][0]) + self.domain_dn = str(self.info_dc1["defaultNamingContext"][0]) + self.config_dn = str(self.info_dc1["configurationNamingContext"][0]) + self.forest_level = int(self.info_dc1["forestFunctionality"][0]) + + # we will need DCs DNS names for 'samba-tool drs' command + self.dnsname_dc1 = str(self.info_dc1["dnsHostName"][0]) + self.dnsname_dc2 = str(self.info_dc2["dnsHostName"][0]) + + # for debugging the test code + self._debug = False + + def tearDown(self): + super(DrsBaseTestCase, self).tearDown() + + def set_test_ldb_dc(self, ldb_dc): + """Sets which DC's LDB we perform operations on during the test""" + self.test_ldb_dc = ldb_dc + + def _GUID_string(self, guid): + return get_string(self.test_ldb_dc.schema_format_value("objectGUID", guid)) + + def _ldap_schemaUpdateNow(self, sam_db): + rec = {"dn": "", + "schemaUpdateNow": "1"} + m = Message.from_dict(sam_db, rec, FLAG_MOD_REPLACE) + sam_db.modify(m) + + def _deleted_objects_dn(self, sam_ldb): + wkdn = "<WKGUID=18E2EA80684F11D2B9AA00C04F79F805,%s>" % self.domain_dn + res = sam_ldb.search(base=wkdn, + scope=SCOPE_BASE, + controls=["show_deleted:1"]) + self.assertEqual(len(res), 1) + return str(res[0]["dn"]) + + def _lost_and_found_dn(self, sam_ldb, nc): + wkdn = "<WKGUID=%s,%s>" % (dsdb.DS_GUID_LOSTANDFOUND_CONTAINER, nc) + res = sam_ldb.search(base=wkdn, + scope=SCOPE_BASE) + self.assertEqual(len(res), 1) + return str(res[0]["dn"]) + + def _make_obj_name(self, prefix): + return prefix + time.strftime("%s", time.gmtime()) + + def _samba_tool_cmd_list(self, drs_command): + # make command line credentials string + + # If test runs on windows then it can provide its own auth string + if hasattr(self, 'cmdline_auth'): + cmdline_auth = self.cmdline_auth + else: + ccache_name = self.get_creds_ccache_name() + + # Tunnel the command line credentials down to the + # subcommand to avoid a new kinit + cmdline_auth = "--use-krb5-ccache=%s" % ccache_name + + # bin/samba-tool drs <drs_command> <cmdline_auth> + return ["drs", drs_command, cmdline_auth] + + def _net_drs_replicate(self, DC, fromDC, nc_dn=None, forced=True, + local=False, full_sync=False, single=False): + if nc_dn is None: + nc_dn = self.domain_dn + # make base command line + samba_tool_cmdline = self._samba_tool_cmd_list("replicate") + # bin/samba-tool drs replicate <Dest_DC_NAME> <Src_DC_NAME> <Naming Context> + samba_tool_cmdline += [DC, fromDC, nc_dn] + + if forced: + samba_tool_cmdline += ["--sync-forced"] + if local: + samba_tool_cmdline += ["--local"] + if full_sync: + samba_tool_cmdline += ["--full-sync"] + if single: + samba_tool_cmdline += ["--single-object"] + + (result, out, err) = self.runsubcmd(*samba_tool_cmdline) + self.assertCmdSuccess(result, out, err) + self.assertEqual(err, "", "Shouldn't be any error messages") + + def _enable_inbound_repl(self, DC): + # make base command line + samba_tool_cmd = self._samba_tool_cmd_list("options") + # disable replication + samba_tool_cmd += [DC, "--dsa-option=-DISABLE_INBOUND_REPL"] + (result, out, err) = self.runsubcmd(*samba_tool_cmd) + self.assertCmdSuccess(result, out, err) + self.assertEqual(err, "", "Shouldn't be any error messages") + + def _disable_inbound_repl(self, DC): + # make base command line + samba_tool_cmd = self._samba_tool_cmd_list("options") + # disable replication + samba_tool_cmd += [DC, "--dsa-option=+DISABLE_INBOUND_REPL"] + (result, out, err) = self.runsubcmd(*samba_tool_cmd) + self.assertCmdSuccess(result, out, err) + self.assertEqual(err, "", "Shouldn't be any error messages") + + def _enable_all_repl(self, DC): + self._enable_inbound_repl(DC) + # make base command line + samba_tool_cmd = self._samba_tool_cmd_list("options") + # enable replication + samba_tool_cmd += [DC, "--dsa-option=-DISABLE_OUTBOUND_REPL"] + (result, out, err) = self.runsubcmd(*samba_tool_cmd) + self.assertCmdSuccess(result, out, err) + self.assertEqual(err, "", "Shouldn't be any error messages") + + def _disable_all_repl(self, DC): + self._disable_inbound_repl(DC) + # make base command line + samba_tool_cmd = self._samba_tool_cmd_list("options") + # disable replication + samba_tool_cmd += [DC, "--dsa-option=+DISABLE_OUTBOUND_REPL"] + (result, out, err) = self.runsubcmd(*samba_tool_cmd) + self.assertCmdSuccess(result, out, err) + self.assertEqual(err, "", "Shouldn't be any error messages") + + def _get_highest_hwm_utdv(self, ldb_conn): + res = ldb_conn.search("", scope=ldb.SCOPE_BASE, attrs=["highestCommittedUSN"]) + hwm = drsuapi.DsReplicaHighWaterMark() + hwm.tmp_highest_usn = int(res[0]["highestCommittedUSN"][0]) + hwm.reserved_usn = 0 + hwm.highest_usn = hwm.tmp_highest_usn + + utdv = drsuapi.DsReplicaCursorCtrEx() + cursors = [] + c1 = drsuapi.DsReplicaCursor() + c1.source_dsa_invocation_id = misc.GUID(ldb_conn.get_invocation_id()) + c1.highest_usn = hwm.highest_usn + cursors.append(c1) + utdv.count = len(cursors) + utdv.cursors = cursors + return (hwm, utdv) + + def _get_identifier(self, ldb_conn, dn): + res = ldb_conn.search(dn, scope=ldb.SCOPE_BASE, + attrs=["objectGUID", "objectSid"]) + id = drsuapi.DsReplicaObjectIdentifier() + id.guid = ndr_unpack(misc.GUID, res[0]['objectGUID'][0]) + if "objectSid" in res[0]: + id.sid = ndr_unpack(security.dom_sid, res[0]['objectSid'][0]) + id.dn = str(res[0].dn) + return id + + def _get_ctr6_links(self, ctr6): + """ + Unpacks the linked attributes from a DsGetNCChanges response + and returns them as a list. + """ + ctr6_links = [] + for lidx in range(0, ctr6.linked_attributes_count): + l = ctr6.linked_attributes[lidx] + try: + target = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3, + l.value.blob) + except: + target = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3Binary, + l.value.blob) + al = AbstractLink(l.attid, l.flags, + l.identifier.guid, + target.guid, target.dn) + ctr6_links.append(al) + + return ctr6_links + + def _get_ctr6_object_guids(self, ctr6): + """Returns all the object GUIDs in a GetNCChanges response""" + guid_list = [] + + obj = ctr6.first_object + for i in range(0, ctr6.object_count): + guid_list.append(str(obj.object.identifier.guid)) + obj = obj.next_object + + return guid_list + + def _ctr6_debug(self, ctr6): + """ + Displays basic info contained in a DsGetNCChanges response. + Having this debug code allows us to see the difference in behaviour + between Samba and Windows easier. Turn on the self._debug flag to see it. + """ + + if self._debug: + print("------------ recvd CTR6 -------------") + + next_object = ctr6.first_object + for i in range(0, ctr6.object_count): + print("Obj %d: %s %s" % (i, next_object.object.identifier.dn[:25], + next_object.object.identifier.guid)) + next_object = next_object.next_object + + print("Linked Attributes: %d" % ctr6.linked_attributes_count) + for lidx in range(0, ctr6.linked_attributes_count): + l = ctr6.linked_attributes[lidx] + try: + target = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3, + l.value.blob) + except: + target = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3Binary, + l.value.blob) + + print("Link Tgt %s... <-- Src %s" + % (target.dn[:25], l.identifier.guid)) + state = "Del" + if l.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE: + state = "Act" + print(" v%u %s changed %u" % (l.meta_data.version, state, + l.meta_data.originating_change_time)) + + print("HWM: %d" % (ctr6.new_highwatermark.highest_usn)) + print("Tmp HWM: %d" % (ctr6.new_highwatermark.tmp_highest_usn)) + print("More data: %d" % (ctr6.more_data)) + + def _get_replication(self, replica_flags, + drs_error=drsuapi.DRSUAPI_EXOP_ERR_NONE, drs=None, drs_handle=None, + highwatermark=None, uptodateness_vector=None, + more_flags=0, max_objects=133, exop=0, + dest_dsa=drsuapi.DRSUAPI_DS_BIND_GUID_W2K3, + source_dsa=None, invocation_id=None, nc_dn_str=None): + """ + Builds a DsGetNCChanges request based on the information provided + and returns the response received from the DC. + """ + if source_dsa is None: + source_dsa = self.test_ldb_dc.get_ntds_GUID() + if invocation_id is None: + invocation_id = self.test_ldb_dc.get_invocation_id() + if nc_dn_str is None: + nc_dn_str = self.test_ldb_dc.domain_dn() + + if highwatermark is None: + if self.default_hwm is None: + (highwatermark, _) = self._get_highest_hwm_utdv(self.test_ldb_dc) + else: + highwatermark = self.default_hwm + + if drs is None: + drs = self.drs + if drs_handle is None: + drs_handle = self.drs_handle + + req10 = self._getnc_req10(dest_dsa=dest_dsa, + invocation_id=invocation_id, + nc_dn_str=nc_dn_str, + exop=exop, + max_objects=max_objects, + replica_flags=replica_flags, + more_flags=more_flags) + req10.highwatermark = highwatermark + if uptodateness_vector is not None: + uptodateness_vector_v1 = drsuapi.DsReplicaCursorCtrEx() + cursors = [] + for i in range(0, uptodateness_vector.count): + c = uptodateness_vector.cursors[i] + c1 = drsuapi.DsReplicaCursor() + c1.source_dsa_invocation_id = c.source_dsa_invocation_id + c1.highest_usn = c.highest_usn + cursors.append(c1) + uptodateness_vector_v1.count = len(cursors) + uptodateness_vector_v1.cursors = cursors + req10.uptodateness_vector = uptodateness_vector_v1 + (level, ctr) = drs.DsGetNCChanges(drs_handle, 10, req10) + self._ctr6_debug(ctr) + + self.assertEqual(level, 6, "expected level 6 response!") + self.assertEqual(ctr.source_dsa_guid, misc.GUID(source_dsa)) + self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(invocation_id)) + self.assertEqual(ctr.extended_ret, drs_error) + + return ctr + + def _check_replication(self, expected_dns, replica_flags, expected_links=None, + drs_error=drsuapi.DRSUAPI_EXOP_ERR_NONE, drs=None, drs_handle=None, + highwatermark=None, uptodateness_vector=None, + more_flags=0, more_data=False, + dn_ordered=True, links_ordered=True, + max_objects=133, exop=0, + dest_dsa=drsuapi.DRSUAPI_DS_BIND_GUID_W2K3, + source_dsa=None, invocation_id=None, nc_dn_str=None, + nc_object_count=0, nc_linked_attributes_count=0): + """ + Makes sure that replication returns the specific error given. + """ + if expected_links is None: + expected_links = [] + + # send a DsGetNCChanges to the DC + ctr6 = self._get_replication(replica_flags, + drs_error, drs, drs_handle, + highwatermark, uptodateness_vector, + more_flags, max_objects, exop, dest_dsa, + source_dsa, invocation_id, nc_dn_str) + + # check the response is what we expect + self._check_ctr6(ctr6, expected_dns, expected_links, + nc_object_count=nc_object_count, more_data=more_data, + dn_ordered=dn_ordered) + return (ctr6.new_highwatermark, ctr6.uptodateness_vector) + + def _get_ctr6_dn_list(self, ctr6): + """ + Returns the DNs contained in a DsGetNCChanges response. + """ + dn_list = [] + next_object = ctr6.first_object + for i in range(0, ctr6.object_count): + dn_list.append(next_object.object.identifier.dn) + next_object = next_object.next_object + self.assertEqual(next_object, None) + + return dn_list + + def _check_ctr6(self, ctr6, expected_dns=None, expected_links=None, + dn_ordered=True, links_ordered=True, + more_data=False, nc_object_count=0, + nc_linked_attributes_count=0, drs_error=0): + """ + Check that a ctr6 matches the specified parameters. + """ + if expected_dns is None: + expected_dns = [] + + if expected_links is None: + expected_links = [] + + ctr6_raw_dns = self._get_ctr6_dn_list(ctr6) + + # filter out changes to the RID Set objects, as these can happen + # intermittently and mess up the test assertions + ctr6_dns = [] + for dn in ctr6_raw_dns: + if "CN=RID Set," in dn or "CN=RID Manager$," in dn: + print("Removing {0} from GetNCChanges reply".format(dn)) + else: + ctr6_dns.append(dn) + + self.assertEqual(len(ctr6_dns), len(expected_dns), + "Received unexpected objects (%s)" % ctr6_dns) + self.assertEqual(ctr6.object_count, len(ctr6_raw_dns)) + self.assertEqual(ctr6.linked_attributes_count, len(expected_links)) + self.assertEqual(ctr6.more_data, more_data) + self.assertEqual(ctr6.nc_object_count, nc_object_count) + self.assertEqual(ctr6.nc_linked_attributes_count, nc_linked_attributes_count) + self.assertEqual(ctr6.drs_error[0], drs_error) + + i = 0 + for dn in expected_dns: + # Expect them back in the exact same order as specified. + if dn_ordered: + self.assertNotEqual(ctr6_dns[i], None) + self.assertEqual(ctr6_dns[i], dn) + i = i + 1 + # Don't care what order + else: + self.assertTrue(dn in ctr6_dns, "Couldn't find DN '%s' anywhere in ctr6 response." % dn) + + # Extract the links from the response + ctr6_links = self._get_ctr6_links(ctr6) + expected_links.sort() + + lidx = 0 + for el in expected_links: + if links_ordered: + self.assertEqual(el, ctr6_links[lidx]) + lidx += 1 + else: + self.assertTrue(el in ctr6_links, "Couldn't find link '%s' anywhere in ctr6 response." % el) + + def _exop_req8(self, dest_dsa, invocation_id, nc_dn_str, exop, + replica_flags=0, max_objects=0, partial_attribute_set=None, + partial_attribute_set_ex=None, mapping_ctr=None, nc_guid=None): + req8 = drsuapi.DsGetNCChangesRequest8() + + req8.destination_dsa_guid = misc.GUID(dest_dsa) if dest_dsa else misc.GUID() + req8.source_dsa_invocation_id = misc.GUID(invocation_id) + req8.naming_context = drsuapi.DsReplicaObjectIdentifier() + req8.naming_context.dn = str(nc_dn_str) + if nc_guid is not None: + req8.naming_context.guid = nc_guid + req8.highwatermark = drsuapi.DsReplicaHighWaterMark() + req8.highwatermark.tmp_highest_usn = 0 + req8.highwatermark.reserved_usn = 0 + req8.highwatermark.highest_usn = 0 + req8.uptodateness_vector = None + req8.replica_flags = replica_flags + req8.max_object_count = max_objects + req8.max_ndr_size = 402116 + req8.extended_op = exop + req8.fsmo_info = 0 + req8.partial_attribute_set = partial_attribute_set + req8.partial_attribute_set_ex = partial_attribute_set_ex + if mapping_ctr: + req8.mapping_ctr = mapping_ctr + else: + req8.mapping_ctr.num_mappings = 0 + req8.mapping_ctr.mappings = None + + return req8 + + def _getnc_req10(self, dest_dsa, invocation_id, nc_dn_str, exop, + replica_flags=0, max_objects=0, partial_attribute_set=None, + partial_attribute_set_ex=None, mapping_ctr=None, + more_flags=0, nc_guid=None): + req10 = drsuapi.DsGetNCChangesRequest10() + + req10.destination_dsa_guid = misc.GUID(dest_dsa) if dest_dsa else misc.GUID() + req10.source_dsa_invocation_id = misc.GUID(invocation_id) + req10.naming_context = drsuapi.DsReplicaObjectIdentifier() + req10.naming_context.dn = str(nc_dn_str) + if nc_guid is not None: + req10.naming_context.guid = nc_guid + req10.highwatermark = drsuapi.DsReplicaHighWaterMark() + req10.highwatermark.tmp_highest_usn = 0 + req10.highwatermark.reserved_usn = 0 + req10.highwatermark.highest_usn = 0 + req10.uptodateness_vector = None + req10.replica_flags = replica_flags + req10.max_object_count = max_objects + req10.max_ndr_size = 402116 + req10.extended_op = exop + req10.fsmo_info = 0 + req10.partial_attribute_set = partial_attribute_set + req10.partial_attribute_set_ex = partial_attribute_set_ex + if mapping_ctr: + req10.mapping_ctr = mapping_ctr + else: + req10.mapping_ctr.num_mappings = 0 + req10.mapping_ctr.mappings = None + req10.more_flags = more_flags + + return req10 + + def _ds_bind(self, server_name, creds=None, ip=None): + if ip is None: + binding_str = f"ncacn_ip_tcp:{server_name}[seal]" + else: + binding_str = f"ncacn_ip_tcp:{ip}[seal,target_hostname={server_name}]" + + if creds is None: + creds = self.get_credentials() + drs = drsuapi.drsuapi(binding_str, self.get_loadparm(), creds) + (drs_handle, supported_extensions) = drs_DsBind(drs) + return (drs, drs_handle) + + def get_partial_attribute_set(self, attids=None): + if attids is None: + attids = [drsuapi.DRSUAPI_ATTID_objectClass] + partial_attribute_set = drsuapi.DsPartialAttributeSet() + partial_attribute_set.attids = attids + partial_attribute_set.num_attids = len(attids) + return partial_attribute_set + + +class AbstractLink: + def __init__(self, attid, flags, identifier, targetGUID, + targetDN=""): + self.attid = attid + self.flags = flags + self.identifier = str(identifier) + self.selfGUID_blob = ndr_pack(identifier) + self.targetGUID = str(targetGUID) + self.targetGUID_blob = ndr_pack(targetGUID) + self.targetDN = targetDN + + def __repr__(self): + return "AbstractLink(0x%08x, 0x%08x, %s, %s)" % ( + self.attid, self.flags, self.identifier, self.targetGUID) + + def __internal_cmp__(self, other, verbose=False): + """See CompareLinks() in MS-DRSR section 4.1.10.5.17""" + if not isinstance(other, AbstractLink): + if verbose: + print("AbstractLink.__internal_cmp__(%r, %r) => wrong type" % (self, other)) + return NotImplemented + + c = cmp(self.selfGUID_blob, other.selfGUID_blob) + if c != 0: + if verbose: + print("AbstractLink.__internal_cmp__(%r, %r) => %d different identifier" % (self, other, c)) + return c + + c = other.attid - self.attid + if c != 0: + if verbose: + print("AbstractLink.__internal_cmp__(%r, %r) => %d different attid" % (self, other, c)) + return c + + self_active = self.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE + other_active = other.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE + + c = self_active - other_active + if c != 0: + if verbose: + print("AbstractLink.__internal_cmp__(%r, %r) => %d different FLAG_ACTIVE" % (self, other, c)) + return c + + c = cmp(self.targetGUID_blob, other.targetGUID_blob) + if c != 0: + if verbose: + print("AbstractLink.__internal_cmp__(%r, %r) => %d different target" % (self, other, c)) + return c + + c = self.flags - other.flags + if c != 0: + if verbose: + print("AbstractLink.__internal_cmp__(%r, %r) => %d different flags" % (self, other, c)) + return c + + return 0 + + def __lt__(self, other): + c = self.__internal_cmp__(other) + if c == NotImplemented: + return NotImplemented + if c < 0: + return True + return False + + def __le__(self, other): + c = self.__internal_cmp__(other) + if c == NotImplemented: + return NotImplemented + if c <= 0: + return True + return False + + def __eq__(self, other): + c = self.__internal_cmp__(other, verbose=True) + if c == NotImplemented: + return NotImplemented + if c == 0: + return True + return False + + def __ne__(self, other): + c = self.__internal_cmp__(other) + if c == NotImplemented: + return NotImplemented + if c != 0: + return True + return False + + def __gt__(self, other): + c = self.__internal_cmp__(other) + if c == NotImplemented: + return NotImplemented + if c > 0: + return True + return False + + def __ge__(self, other): + c = self.__internal_cmp__(other) + if c == NotImplemented: + return NotImplemented + if c >= 0: + return True + return False + + def __hash__(self): + return hash((self.attid, self.flags, self.identifier, self.targetGUID)) diff --git a/source4/torture/drs/python/fsmo.py b/source4/torture/drs/python/fsmo.py new file mode 100644 index 0000000..55805b9 --- /dev/null +++ b/source4/torture/drs/python/fsmo.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Unix SMB/CIFS implementation. +# Copyright (C) Anatoliy Atanasov <anatoliy.atanasov@postpath.com> 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 DC1=dc1_dns_name +# export DC2=dc2_dns_name +# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun +# PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN fsmo -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD" +# + +import sys +import time +import os + +sys.path.insert(0, "bin/python") + +from ldb import SCOPE_BASE + +import drs_base + + +class DrsFsmoTestCase(drs_base.DrsBaseTestCase): + + def setUp(self): + super(DrsFsmoTestCase, self).setUp() + + # we have to wait for the replication before we make the check + self.fsmo_wait_max_time = 20 + self.fsmo_wait_sleep_time = 0.2 + + # cache some of RootDSE props + self.dsServiceName_dc1 = self.info_dc1["dsServiceName"][0] + self.dsServiceName_dc2 = self.info_dc2["dsServiceName"][0] + self.infrastructure_dn = "CN=Infrastructure," + self.domain_dn + self.naming_dn = "CN=Partitions," + self.config_dn + self.rid_dn = "CN=RID Manager$,CN=System," + self.domain_dn + self.domain_dns_dn = ( + "CN=Infrastructure,DC=DomainDnsZones, %s" % self.domain_dn ) + self.forest_dns_dn = ( + "CN=Infrastructure,DC=ForestDnsZones, %s" % self.domain_dn ) + + def tearDown(self): + super(DrsFsmoTestCase, self).tearDown() + + def _net_fsmo_role_transfer(self, DC, role, noop=False): + # make command line credentials string + ccache_name = self.get_creds_ccache_name() + cmd_line_auth = "--use-krb5-ccache=%s" % ccache_name + (result, out, err) = self.runsubcmd("fsmo", "transfer", + "--role=%s" % role, + "-H", "ldap://%s:389" % DC, + cmd_line_auth) + + self.assertCmdSuccess(result, out, err) + self.assertEqual(err, "", "Shouldn't be any error messages") + if not noop: + self.assertTrue("FSMO transfer of '%s' role successful" % role in out) + else: + self.assertTrue("This DC already has the '%s' FSMO role" % role in out) + + def _wait_for_role_transfer(self, ldb_dc, role_dn, master): + """Wait for role transfer for certain amount of time + + :return: (Result=True|False, CurrentMasterDnsName) tuple + """ + cur_master = '' + retries = int(self.fsmo_wait_max_time / self.fsmo_wait_sleep_time) + 1 + for i in range(0, retries): + # check if master has been transferred + res = ldb_dc.search(role_dn, + scope=SCOPE_BASE, attrs=["fSMORoleOwner"]) + assert len(res) == 1, "Only one fSMORoleOwner value expected!" + cur_master = res[0]["fSMORoleOwner"][0] + if master == cur_master: + return (True, cur_master) + # skip last sleep, if no need to wait anymore + if i != (retries - 1): + # wait a little bit before next retry + time.sleep(self.fsmo_wait_sleep_time) + return (False, cur_master) + + def _role_transfer(self, role, role_dn): + """Triggers transfer of role from DC1 to DC2 + and vice versa so the role goes back to the original dc""" + # dc2 gets the role from dc1 + print("Testing for %s role transfer from %s to %s" % (role, self.dnsname_dc1, self.dnsname_dc2)) + + self._net_fsmo_role_transfer(DC=self.dnsname_dc2, role=role) + # check if the role is transferred + (res, master) = self._wait_for_role_transfer(ldb_dc=self.ldb_dc2, + role_dn=role_dn, + master=self.dsServiceName_dc2) + self.assertTrue(res, + "Transferring %s role to %s has failed, master is: %s!" % (role, self.dsServiceName_dc2, master)) + + # dc1 gets back the role from dc2 + print("Testing for %s role transfer from %s to %s" % (role, self.dnsname_dc2, self.dnsname_dc1)) + self._net_fsmo_role_transfer(DC=self.dnsname_dc1, role=role) + # check if the role is transferred + (res, master) = self._wait_for_role_transfer(ldb_dc=self.ldb_dc1, + role_dn=role_dn, + master=self.dsServiceName_dc1) + self.assertTrue(res, + "Transferring %s role to %s has failed, master is: %s!" % (role, self.dsServiceName_dc1, master)) + + # dc1 keeps the role + print("Testing for no-op %s role transfer from %s to %s" % (role, self.dnsname_dc2, self.dnsname_dc1)) + self._net_fsmo_role_transfer(DC=self.dnsname_dc1, role=role, noop=True) + # check if the role is transferred + (res, master) = self._wait_for_role_transfer(ldb_dc=self.ldb_dc1, + role_dn=role_dn, + master=self.dsServiceName_dc1) + self.assertTrue(res, + "Transferring %s role to %s has failed, master is: %s!" % (role, self.dsServiceName_dc1, master)) + + def test_SchemaMasterTransfer(self): + self._role_transfer(role="schema", role_dn=self.schema_dn) + + def test_InfrastructureMasterTransfer(self): + self._role_transfer(role="infrastructure", role_dn=self.infrastructure_dn) + + def test_PDCMasterTransfer(self): + self._role_transfer(role="pdc", role_dn=self.domain_dn) + + def test_RIDMasterTransfer(self): + self._role_transfer(role="rid", role_dn=self.rid_dn) + + def test_NamingMasterTransfer(self): + self._role_transfer(role="naming", role_dn=self.naming_dn) + + def test_DomainDnsZonesMasterTransfer(self): + self._role_transfer(role="domaindns", role_dn=self.domain_dns_dn) + + def test_ForestDnsZonesMasterTransfer(self): + self._role_transfer(role="forestdns", role_dn=self.forest_dns_dn) diff --git a/source4/torture/drs/python/getnc_exop.py b/source4/torture/drs/python/getnc_exop.py new file mode 100644 index 0000000..0f12d9b --- /dev/null +++ b/source4/torture/drs/python/getnc_exop.py @@ -0,0 +1,1304 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Tests various schema replication scenarios +# +# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2011 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 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/>. +# + +# +# Usage: +# export DC1=dc1_dns_name +# export DC2=dc2_dns_name +# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun +# PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN getnc_exop -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD" +# + +import random + +import drs_base +from drs_base import AbstractLink + +import samba.tests +from samba import werror, WERRORError + +import ldb +from ldb import SCOPE_BASE + +from samba.dcerpc import drsuapi, misc, drsblobs +from samba.drs_utils import drs_DsBind +from samba.ndr import ndr_unpack, ndr_pack +from functools import cmp_to_key +from samba.common import cmp + + +def _linked_attribute_compare(la1, la2): + """See CompareLinks() in MS-DRSR section 4.1.10.5.17""" + la1, la1_target = la1 + la2, la2_target = la2 + + # Ascending host object GUID + c = cmp(ndr_pack(la1.identifier.guid), ndr_pack(la2.identifier.guid)) + if c != 0: + return c + + # Ascending attribute ID + if la1.attid != la2.attid: + return -1 if la1.attid < la2.attid else 1 + + la1_active = la1.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE + la2_active = la2.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE + + # Ascending 'is present' + if la1_active != la2_active: + return 1 if la1_active else -1 + + # Ascending target object GUID + return cmp(ndr_pack(la1_target), ndr_pack(la2_target)) + + +class DrsReplicaSyncTestCase(drs_base.DrsBaseTestCase): + """Intended as a semi-black box test case for DsGetNCChanges + implementation for extended operations. It should be testing + how DsGetNCChanges handles different input params (mostly invalid). + Final goal is to make DsGetNCChanges as binary compatible to + Windows implementation as possible""" + + def setUp(self): + super(DrsReplicaSyncTestCase, self).setUp() + self.base_dn = self.ldb_dc1.get_default_basedn() + self.ou = "OU=test_getncchanges%d,%s" % (random.randint(0, 4294967295), + self.base_dn) + self.ldb_dc1.add({ + "dn": self.ou, + "objectclass": "organizationalUnit"}) + (self.drs, self.drs_handle) = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + (self.default_hwm, self.default_utdv) = self._get_highest_hwm_utdv(self.ldb_dc1) + + def tearDown(self): + try: + self.ldb_dc1.delete(self.ou, ["tree_delete:1"]) + except ldb.LdbError as e: + (enum, string) = e.args + if enum == ldb.ERR_NO_SUCH_OBJECT: + pass + super(DrsReplicaSyncTestCase, self).tearDown() + + def _determine_fSMORoleOwner(self, fsmo_obj_dn): + """Returns (owner, not_owner) pair where: + owner: dns name for FSMO owner + not_owner: dns name for DC not owning the FSMO""" + # collect info to return later + fsmo_info_1 = {"dns_name": self.dnsname_dc1, + "invocation_id": self.ldb_dc1.get_invocation_id(), + "ntds_guid": self.ldb_dc1.get_ntds_GUID(), + "server_dn": self.ldb_dc1.get_serverName()} + fsmo_info_2 = {"dns_name": self.dnsname_dc2, + "invocation_id": self.ldb_dc2.get_invocation_id(), + "ntds_guid": self.ldb_dc2.get_ntds_GUID(), + "server_dn": self.ldb_dc2.get_serverName()} + + msgs = self.ldb_dc1.search(scope=ldb.SCOPE_BASE, base=fsmo_info_1["server_dn"], attrs=["serverReference"]) + fsmo_info_1["server_acct_dn"] = ldb.Dn(self.ldb_dc1, msgs[0]["serverReference"][0].decode('utf8')) + fsmo_info_1["rid_set_dn"] = ldb.Dn(self.ldb_dc1, "CN=RID Set") + fsmo_info_1["server_acct_dn"] + + msgs = self.ldb_dc2.search(scope=ldb.SCOPE_BASE, base=fsmo_info_2["server_dn"], attrs=["serverReference"]) + fsmo_info_2["server_acct_dn"] = ldb.Dn(self.ldb_dc2, msgs[0]["serverReference"][0].decode('utf8')) + fsmo_info_2["rid_set_dn"] = ldb.Dn(self.ldb_dc2, "CN=RID Set") + fsmo_info_2["server_acct_dn"] + + # determine the owner dc + res = self.ldb_dc1.search(fsmo_obj_dn, + scope=SCOPE_BASE, attrs=["fSMORoleOwner"]) + assert len(res) == 1, "Only one fSMORoleOwner value expected for %s!" % fsmo_obj_dn + fsmo_owner = res[0]["fSMORoleOwner"][0] + if fsmo_owner == self.info_dc1["dsServiceName"][0]: + return (fsmo_info_1, fsmo_info_2) + return (fsmo_info_2, fsmo_info_1) + + def _check_exop_failed(self, ctr6, expected_failure): + self.assertEqual(ctr6.extended_ret, expected_failure) + #self.assertEqual(ctr6.object_count, 0) + #self.assertEqual(ctr6.first_object, None) + self.assertEqual(ctr6.more_data, False) + self.assertEqual(ctr6.nc_object_count, 0) + self.assertEqual(ctr6.nc_linked_attributes_count, 0) + self.assertEqual(ctr6.linked_attributes_count, 0) + self.assertEqual(ctr6.linked_attributes, []) + self.assertEqual(ctr6.drs_error[0], 0) + + def test_do_single_repl(self): + """ + Make sure that DRSUAPI_EXOP_REPL_OBJ never replicates more than + one object, even when we use DRS_GET_ANC/GET_TGT. + """ + + ou1 = "OU=get_anc1,%s" % self.ou + self.ldb_dc1.add({ + "dn": ou1, + "objectclass": "organizationalUnit" + }) + ou1_id = self._get_identifier(self.ldb_dc1, ou1) + ou2 = "OU=get_anc2,%s" % ou1 + self.ldb_dc1.add({ + "dn": ou2, + "objectclass": "organizationalUnit" + }) + ou2_id = self._get_identifier(self.ldb_dc1, ou2) + dc3 = "CN=test_anc_dc_%u,%s" % (random.randint(0, 4294967295), ou2) + self.ldb_dc1.add({ + "dn": dc3, + "objectclass": "computer", + "userAccountControl": "%d" % (samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_SERVER_TRUST_ACCOUNT) + }) + dc3_id = self._get_identifier(self.ldb_dc1, dc3) + + # Add some linked attributes (for checking GET_TGT behaviour) + m = ldb.Message() + m.dn = ldb.Dn(self.ldb_dc2, ou1) + m["managedBy"] = ldb.MessageElement(ou2, ldb.FLAG_MOD_ADD, "managedBy") + self.ldb_dc1.modify(m) + ou1_link = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + ou1_id.guid, ou2_id.guid) + + m.dn = ldb.Dn(self.ldb_dc2, dc3) + m["managedBy"] = ldb.MessageElement(ou2, ldb.FLAG_MOD_ADD, "managedBy") + self.ldb_dc1.modify(m) + dc3_link = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + dc3_id.guid, ou2_id.guid) + + req = self._getnc_req10(dest_dsa=None, + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=ou1, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP, + more_flags=drsuapi.DRSUAPI_DRS_GET_TGT) + (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req) + self._check_ctr6(ctr, [ou1], expected_links=[ou1_link]) + + # DRSUAPI_DRS_WRIT_REP means that we should only replicate the dn we give (dc3). + # DRSUAPI_DRS_GET_ANC means that we should also replicate its ancestors, but + # Windows doesn't do this if we use both. + req = self._getnc_req10(dest_dsa=None, + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=dc3, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC, + more_flags=drsuapi.DRSUAPI_DRS_GET_TGT) + (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req) + self._check_ctr6(ctr, [dc3], expected_links=[dc3_link]) + + # Even though the ancestor of ou2 (ou1) has changed since last hwm, and we're + # sending DRSUAPI_DRS_GET_ANC, the expected response is that it will only try + # and replicate the single object still. + req = self._getnc_req10(dest_dsa=None, + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=ou2, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + replica_flags=drsuapi.DRSUAPI_DRS_CRITICAL_ONLY | + drsuapi.DRSUAPI_DRS_GET_ANC, + more_flags=drsuapi.DRSUAPI_DRS_GET_TGT) + (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req) + self._check_ctr6(ctr, [ou2]) + + def test_do_full_repl_on_ou(self): + """ + Make sure that a full replication on a not-an-nc fails with + the right error code + """ + + non_nc_ou = "OU=not-an-NC,%s" % self.ou + self.ldb_dc1.add({ + "dn": non_nc_ou, + "objectclass": "organizationalUnit" + }) + req8 = self._exop_req8(dest_dsa=None, + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=non_nc_ou, + exop=drsuapi.DRSUAPI_EXOP_NONE, + replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP) + + try: + (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 8, req8) + self.fail("Expected DsGetNCChanges to fail with WERR_DS_CANT_FIND_EXPECTED_NC") + except WERRORError as e1: + (enum, estr) = e1.args + self.assertEqual(enum, werror.WERR_DS_CANT_FIND_EXPECTED_NC) + + def test_InvalidNC_DummyDN_InvalidGUID_REPL_OBJ(self): + """Test single object replication on a totally invalid GUID fails with the right error code""" + fsmo_dn = self.ldb_dc1.get_schema_basedn() + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + req8 = self._exop_req8(dest_dsa="9c637462-5b8c-4467-aef2-bdb1f57bc4ef", + invocation_id=fsmo_owner["invocation_id"], + nc_dn_str="DummyDN", + nc_guid=misc.GUID("c2d2f745-1610-4e93-964b-d4ba73eb32f8"), + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ) + + (drs, drs_handle) = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + try: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + except WERRORError as e1: + (enum, estr) = e1.args + self.assertEqual(enum, werror.WERR_DS_DRA_BAD_DN) + + def test_InvalidNC_DummyDN_InvalidGUID_REPL_SECRET(self): + """Test single object replication on a totally invalid GUID fails with the right error code""" + fsmo_dn = self.ldb_dc1.get_schema_basedn() + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + req8 = self._exop_req8(dest_dsa="9c637462-5b8c-4467-aef2-bdb1f57bc4ef", + invocation_id=fsmo_owner["invocation_id"], + nc_dn_str="DummyDN", + nc_guid=misc.GUID("c2d2f745-1610-4e93-964b-d4ba73eb32f8"), + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ) + + (drs, drs_handle) = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + try: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + except WERRORError as e1: + (enum, estr) = e1.args + self.assertEqual(enum, werror.WERR_DS_DRA_BAD_DN) + + def test_InvalidNC_DummyDN_InvalidGUID_RID_ALLOC(self): + """Test RID Allocation on a totally invalid GUID fails with the right error code""" + fsmo_dn = self.ldb_dc1.get_schema_basedn() + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + req8 = self._exop_req8(dest_dsa="9c637462-5b8c-4467-aef2-bdb1f57bc4ef", + invocation_id=fsmo_owner["invocation_id"], + nc_dn_str="DummyDN", + nc_guid=misc.GUID("c2d2f745-1610-4e93-964b-d4ba73eb32f8"), + exop=drsuapi.DRSUAPI_EXOP_FSMO_RID_ALLOC) + + (drs, drs_handle) = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + try: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + except WERRORError as e1: + (enum, estr) = e1.args + self.assertEqual(enum, werror.WERR_DS_DRA_BAD_NC) + + def test_valid_GUID_only_REPL_OBJ(self): + dc_guid_1 = self.ldb_dc1.get_invocation_id() + drs, drs_handle = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + + res = self.ldb_dc1.search(base=self.ou, scope=SCOPE_BASE, + attrs=["objectGUID"]) + + guid = misc.GUID(res[0]["objectGUID"][0]) + + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str="", + nc_guid=guid, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ) + + try: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + except WERRORError as e1: + (enum, estr) = e1.args + self.fail(f"Failed to call GetNCChanges with EXOP_REPL_OBJ and a GUID: {estr}") + + self.assertEqual(ctr.first_object.object.identifier.guid, guid) + + def test_DummyDN_valid_GUID_REPL_OBJ(self): + dc_guid_1 = self.ldb_dc1.get_invocation_id() + drs, drs_handle = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + + res = self.ldb_dc1.search(base=self.ou, scope=SCOPE_BASE, + attrs=["objectGUID"]) + + guid = misc.GUID(res[0]["objectGUID"][0]) + + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str="DummyDN", + nc_guid=guid, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ) + + try: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + except WERRORError as e1: + (enum, estr) = e1.args + self.fail(f"Failed to call GetNCChanges with EXOP_REPL_OBJ, DummyDN and a GUID: {estr}") + + self.assertEqual(ctr.first_object.object.identifier.guid, guid) + + def test_DummyDN_valid_GUID_REPL_SECRET(self): + dc_guid_1 = self.ldb_dc1.get_invocation_id() + drs, drs_handle = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + + res = self.ldb_dc1.search(base=self.ou, scope=SCOPE_BASE, + attrs=["objectGUID"]) + + guid = misc.GUID(res[0]["objectGUID"][0]) + + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str="DummyDN", + nc_guid=guid, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET) + + try: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + except WERRORError as e1: + (enum, estr) = e1.args + + # We expect to get as far as failing on the missing dest_dsa + self.assertEqual(enum, werror.WERR_DS_DRA_DB_ERROR) + + def test_link_utdv_hwm(self): + """Test verify the DRS_GET_ANC behavior.""" + + ou1 = "OU=get_anc1,%s" % self.ou + self.ldb_dc1.add({ + "dn": ou1, + "objectclass": "organizationalUnit" + }) + ou1_id = self._get_identifier(self.ldb_dc1, ou1) + ou2 = "OU=get_anc2,%s" % ou1 + self.ldb_dc1.add({ + "dn": ou2, + "objectclass": "organizationalUnit" + }) + ou2_id = self._get_identifier(self.ldb_dc1, ou2) + dc3 = "CN=test_anc_dc_%u,%s" % (random.randint(0, 4294967295), ou2) + self.ldb_dc1.add({ + "dn": dc3, + "objectclass": "computer", + "userAccountControl": "%d" % (samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_SERVER_TRUST_ACCOUNT) + }) + dc3_id = self._get_identifier(self.ldb_dc1, dc3) + + (hwm1, utdv1) = self._check_replication([ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_WRIT_REP) + + self._check_replication([ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC) + + self._check_replication([dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY) + + self._check_replication([ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY | + drsuapi.DRSUAPI_DRS_GET_ANC) + + m = ldb.Message() + m.dn = ldb.Dn(self.ldb_dc1, ou1) + m["displayName"] = ldb.MessageElement("OU1", ldb.FLAG_MOD_ADD, "displayName") + self.ldb_dc1.modify(m) + + (hwm2, utdv2) = self._check_replication([ou2, dc3, ou1], + drsuapi.DRSUAPI_DRS_WRIT_REP) + + self._check_replication([ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC) + + self._check_replication([dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY) + + self._check_replication([ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY | + drsuapi.DRSUAPI_DRS_GET_ANC) + + self._check_replication([ou1], + drsuapi.DRSUAPI_DRS_WRIT_REP, + highwatermark=hwm1) + + self._check_replication([ou1], + drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC, + highwatermark=hwm1) + + self._check_replication([ou1], + drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC, + uptodateness_vector=utdv1) + + m = ldb.Message() + m.dn = ldb.Dn(self.ldb_dc1, ou2) + m["displayName"] = ldb.MessageElement("OU2", ldb.FLAG_MOD_ADD, "displayName") + self.ldb_dc1.modify(m) + + (hwm3, utdv3) = self._check_replication([dc3, ou1, ou2], + drsuapi.DRSUAPI_DRS_WRIT_REP) + + self._check_replication([ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC) + + self._check_replication([dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY) + + self._check_replication([ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY | + drsuapi.DRSUAPI_DRS_GET_ANC) + + self._check_replication([ou1, ou2], + drsuapi.DRSUAPI_DRS_WRIT_REP, + highwatermark=hwm1) + + self._check_replication([ou1, ou2], + drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC, + highwatermark=hwm1) + + self._check_replication([ou1, ou2], + drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC, + uptodateness_vector=utdv1) + + m = ldb.Message() + m.dn = ldb.Dn(self.ldb_dc1, self.ou) + m["displayName"] = ldb.MessageElement("OU", ldb.FLAG_MOD_ADD, "displayName") + self.ldb_dc1.modify(m) + + (hwm4, utdv4) = self._check_replication([dc3, ou1, ou2, self.ou], + drsuapi.DRSUAPI_DRS_WRIT_REP) + + self._check_replication([self.ou, ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC) + + self._check_replication([dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY) + + self._check_replication([self.ou, ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY | + drsuapi.DRSUAPI_DRS_GET_ANC) + + self._check_replication([self.ou, ou2], + drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC, + uptodateness_vector=utdv2) + + cn3 = "CN=get_anc3,%s" % ou2 + self.ldb_dc1.add({ + "dn": cn3, + "objectclass": "container", + }) + + (hwm5, utdv5) = self._check_replication([dc3, ou1, ou2, self.ou, cn3], + drsuapi.DRSUAPI_DRS_WRIT_REP) + + self._check_replication([self.ou, ou1, ou2, dc3, cn3], + drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC) + + self._check_replication([dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY) + + self._check_replication([self.ou, ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY | + drsuapi.DRSUAPI_DRS_GET_ANC) + + m = ldb.Message() + m.dn = ldb.Dn(self.ldb_dc1, ou2) + m["managedBy"] = ldb.MessageElement(dc3, ldb.FLAG_MOD_ADD, "managedBy") + self.ldb_dc1.modify(m) + ou2_managedBy_dc3 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + ou2_id.guid, dc3_id.guid) + + (hwm6, utdv6) = self._check_replication([dc3, ou1, self.ou, cn3, ou2], + drsuapi.DRSUAPI_DRS_WRIT_REP, + expected_links=[ou2_managedBy_dc3]) + + # Can fail against Windows due to equal precedence of dc3, cn3 + self._check_replication([self.ou, ou1, ou2, dc3, cn3], + drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC, + expected_links=[ou2_managedBy_dc3]) + + self._check_replication([dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY) + + self._check_replication([self.ou, ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY | + drsuapi.DRSUAPI_DRS_GET_ANC) + + self._check_replication([], + drsuapi.DRSUAPI_DRS_WRIT_REP, + uptodateness_vector=utdv5, + expected_links=[ou2_managedBy_dc3]) + + self._check_replication([], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY, + uptodateness_vector=utdv5) + + self._check_replication([], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY, + uptodateness_vector=utdv5) + + m = ldb.Message() + m.dn = ldb.Dn(self.ldb_dc1, dc3) + m["managedBy"] = ldb.MessageElement(ou1, ldb.FLAG_MOD_ADD, "managedBy") + self.ldb_dc1.modify(m) + dc3_managedBy_ou1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + dc3_id.guid, ou1_id.guid) + + (hwm7, utdv7) = self._check_replication([ou1, self.ou, cn3, ou2, dc3], + drsuapi.DRSUAPI_DRS_WRIT_REP, + expected_links=[ou2_managedBy_dc3, dc3_managedBy_ou1]) + + # Can fail against Windows due to equal precedence of dc3, cn3 + # self._check_replication([self.ou,ou1,ou2,dc3,cn3], + # drsuapi.DRSUAPI_DRS_WRIT_REP| + # drsuapi.DRSUAPI_DRS_GET_ANC, + # expected_links=[ou2_managedBy_dc3,dc3_managedBy_ou1]) + + self._check_replication([dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY, + expected_links=[dc3_managedBy_ou1]) + + self._check_replication([dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY, + expected_links=[dc3_managedBy_ou1]) + + self._check_replication([self.ou, ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY | + drsuapi.DRSUAPI_DRS_GET_ANC, + expected_links=[dc3_managedBy_ou1]) + + # GET_TGT seems to override DRS_CRITICAL_ONLY and also returns any + # object(s) that relate to the linked attributes (similar to GET_ANC) + self._check_replication([ou1, dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY, + more_flags=drsuapi.DRSUAPI_DRS_GET_TGT, + expected_links=[dc3_managedBy_ou1], dn_ordered=False) + + # Change DC3's managedBy to OU2 instead of OU1 + # Note that the OU1 managedBy linked attribute will still exist as + # a tombstone object (and so will be returned in the replication still) + m = ldb.Message() + m.dn = ldb.Dn(self.ldb_dc1, dc3) + m["managedBy"] = ldb.MessageElement(ou2, ldb.FLAG_MOD_REPLACE, "managedBy") + self.ldb_dc1.modify(m) + dc3_managedBy_ou1.flags &= ~drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE + dc3_managedBy_ou2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + dc3_id.guid, ou2_id.guid) + + (hwm8, utdv8) = self._check_replication([ou1, self.ou, cn3, ou2, dc3], + drsuapi.DRSUAPI_DRS_WRIT_REP, + expected_links=[ou2_managedBy_dc3, dc3_managedBy_ou1, dc3_managedBy_ou2]) + + # Can fail against Windows due to equal precedence of dc3, cn3 + # self._check_replication([self.ou,ou1,ou2,dc3,cn3], + # drsuapi.DRSUAPI_DRS_WRIT_REP| + # drsuapi.DRSUAPI_DRS_GET_ANC, + # expected_links=[ou2_managedBy_dc3,dc3_managedBy_ou1,dc3_managedBy_ou2]) + + self._check_replication([dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY, + expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2]) + + self._check_replication([self.ou, ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY | + drsuapi.DRSUAPI_DRS_GET_ANC, + expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2]) + + # GET_TGT will also return any DNs referenced by the linked attributes + # (including the Tombstone attribute) + self._check_replication([ou1, ou2, dc3], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY, + more_flags=drsuapi.DRSUAPI_DRS_GET_TGT, + expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2], dn_ordered=False) + + # Use the highwater-mark prior to changing ManagedBy - this should + # only return the old/Tombstone and new linked attributes (we already + # know all the DNs) + self._check_replication([], + drsuapi.DRSUAPI_DRS_WRIT_REP, + expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2], + highwatermark=hwm7) + + self._check_replication([], + drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC, + expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2], + highwatermark=hwm7) + + self._check_replication([], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY, + expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2], + highwatermark=hwm7) + + self._check_replication([], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY | + drsuapi.DRSUAPI_DRS_GET_ANC, + expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2], + highwatermark=hwm7) + + self._check_replication([], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY, + more_flags=drsuapi.DRSUAPI_DRS_GET_TGT, + expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2], + highwatermark=hwm7) + + # Repeat the above set of tests using the uptodateness_vector + # instead of the highwater-mark + self._check_replication([], + drsuapi.DRSUAPI_DRS_WRIT_REP, + expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2], + uptodateness_vector=utdv7) + + self._check_replication([], + drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC, + expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2], + uptodateness_vector=utdv7) + + self._check_replication([], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY, + expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2], + uptodateness_vector=utdv7) + + self._check_replication([], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY | + drsuapi.DRSUAPI_DRS_GET_ANC, + expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2], + uptodateness_vector=utdv7) + + self._check_replication([], + drsuapi.DRSUAPI_DRS_CRITICAL_ONLY, + more_flags=drsuapi.DRSUAPI_DRS_GET_TGT, + expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2], + uptodateness_vector=utdv7) + + def test_FSMONotOwner(self): + """Test role transfer with against DC not owner of the role""" + fsmo_dn = self.ldb_dc1.get_schema_basedn() + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + req8 = self._exop_req8(dest_dsa=fsmo_owner["ntds_guid"], + invocation_id=fsmo_not_owner["invocation_id"], + nc_dn_str=fsmo_dn, + exop=drsuapi.DRSUAPI_EXOP_FSMO_REQ_ROLE) + + (drs, drs_handle) = self._ds_bind(fsmo_not_owner["dns_name"]) + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + self.assertEqual(level, 6, "Expected level 6 response!") + self._check_exop_failed(ctr, drsuapi.DRSUAPI_EXOP_ERR_FSMO_NOT_OWNER) + self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_not_owner["ntds_guid"])) + self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_not_owner["invocation_id"])) + + def test_InvalidDestDSA(self): + """Test role transfer with invalid destination DSA guid""" + fsmo_dn = self.ldb_dc1.get_schema_basedn() + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + req8 = self._exop_req8(dest_dsa="9c637462-5b8c-4467-aef2-bdb1f57bc4ef", + invocation_id=fsmo_owner["invocation_id"], + nc_dn_str=fsmo_dn, + exop=drsuapi.DRSUAPI_EXOP_FSMO_REQ_ROLE) + + (drs, drs_handle) = self._ds_bind(fsmo_owner["dns_name"]) + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + self.assertEqual(level, 6, "Expected level 6 response!") + self._check_exop_failed(ctr, drsuapi.DRSUAPI_EXOP_ERR_UNKNOWN_CALLER) + self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_owner["ntds_guid"])) + self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_owner["invocation_id"])) + + def test_InvalidDestDSA_and_GUID(self): + """Test role transfer with invalid destination DSA guid""" + fsmo_dn = self.ldb_dc1.get_schema_basedn() + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + req8 = self._exop_req8(dest_dsa="9c637462-5b8c-4467-aef2-bdb1f57bc4ef", + invocation_id=fsmo_owner["invocation_id"], + nc_dn_str="DummyDN", + nc_guid=misc.GUID("c2d2f745-1610-4e93-964b-d4ba73eb32f8"), + exop=drsuapi.DRSUAPI_EXOP_FSMO_REQ_ROLE) + + (drs, drs_handle) = self._ds_bind(fsmo_owner["dns_name"]) + try: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + except WERRORError as e1: + (enum, estr) = e1.args + self.fail(f"DsGetNCChanges failed with {estr}") + self.assertEqual(level, 6, "Expected level 6 response!") + self._check_exop_failed(ctr, drsuapi.DRSUAPI_EXOP_ERR_UNKNOWN_CALLER) + self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_owner["ntds_guid"])) + self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_owner["invocation_id"])) + + def test_InvalidDestDSA_and_GUID_RID_ALLOC(self): + """Test role transfer with invalid destination DSA guid""" + fsmo_dn = self.ldb_dc1.get_schema_basedn() + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + req8 = self._exop_req8(dest_dsa="9c637462-5b8c-4467-aef2-bdb1f57bc4ef", + invocation_id=fsmo_owner["invocation_id"], + nc_dn_str="DummyDN", + nc_guid=misc.GUID("c2d2f745-1610-4e93-964b-d4ba73eb32f8"), + exop=drsuapi.DRSUAPI_EXOP_FSMO_RID_ALLOC) + + (drs, drs_handle) = self._ds_bind(fsmo_owner["dns_name"]) + try: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + except WERRORError as e1: + (enum, estr) = e1.args + self.fail(f"DsGetNCChanges failed with {estr}") + self.assertEqual(level, 6, "Expected level 6 response!") + self._check_exop_failed(ctr, drsuapi.DRSUAPI_EXOP_ERR_UNKNOWN_CALLER) + self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_owner["ntds_guid"])) + self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_owner["invocation_id"])) + + +class DrsReplicaPrefixMapTestCase(drs_base.DrsBaseTestCase): + def setUp(self): + super(DrsReplicaPrefixMapTestCase, self).setUp() + self.base_dn = self.ldb_dc1.get_default_basedn() + self.ou = "ou=pfm_exop%d,%s" % (random.randint(0, 4294967295), + self.base_dn) + self.ldb_dc1.add({ + "dn": self.ou, + "objectclass": "organizationalUnit"}) + self.user = "cn=testuser,%s" % self.ou + self.ldb_dc1.add({ + "dn": self.user, + "objectclass": "user"}) + + def tearDown(self): + super(DrsReplicaPrefixMapTestCase, self).tearDown() + try: + self.ldb_dc1.delete(self.ou, ["tree_delete:1"]) + except ldb.LdbError as e2: + (enum, string) = e2.args + if enum == ldb.ERR_NO_SUCH_OBJECT: + pass + + def test_missing_prefix_map_dsa(self): + partial_attribute_set = self.get_partial_attribute_set() + + dc_guid_1 = self.ldb_dc1.get_invocation_id() + + drs, drs_handle = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.user, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + partial_attribute_set=partial_attribute_set) + + try: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + self.assertEqual(ctr.extended_ret, drsuapi.DRSUAPI_EXOP_ERR_SUCCESS) + except RuntimeError: + self.fail("Missing prefixmap shouldn't have triggered an error") + + def test_invalid_prefix_map_attid(self): + # Request for invalid attid + partial_attribute_set = self.get_partial_attribute_set([99999]) + + dc_guid_1 = self.ldb_dc1.get_invocation_id() + drs, drs_handle = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + + try: + pfm = self._samdb_fetch_pfm_and_schi() + except KeyError: + # On Windows, prefixMap isn't available over LDAP + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.user, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ) + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + pfm = ctr.mapping_ctr + + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.user, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + partial_attribute_set=partial_attribute_set, + mapping_ctr=pfm) + + try: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + self.fail("Invalid attid (99999) should have triggered an error") + except RuntimeError as e3: + (ecode, emsg) = e3.args + self.assertEqual(ecode, 0x000020E2, "Error code should have been " + "WERR_DS_DRA_SCHEMA_MISMATCH") + + def test_secret_prefix_map_attid(self): + # Request for a secret attid + partial_attribute_set = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_unicodePwd]) + + dc_guid_1 = self.ldb_dc1.get_invocation_id() + drs, drs_handle = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + + try: + pfm = self._samdb_fetch_pfm_and_schi() + except KeyError: + # On Windows, prefixMap isn't available over LDAP + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.user, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ) + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + pfm = ctr.mapping_ctr + + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.user, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + partial_attribute_set=partial_attribute_set, + mapping_ctr=pfm) + + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + + found = False + for attr in ctr.first_object.object.attribute_ctr.attributes: + if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd: + found = True + break + + self.assertTrue(found, "Ensure we get the unicodePwd attribute back") + + for i, mapping in enumerate(pfm.mappings): + # OID: 2.5.4.* + # objectClass: 2.5.4.0 + if mapping.oid.binary_oid == [85, 4]: + idx1 = i + # OID: 1.2.840.113556.1.4.* + # unicodePwd: 1.2.840.113556.1.4.90 + elif mapping.oid.binary_oid == [42, 134, 72, 134, 247, 20, 1, 4]: + idx2 = i + + (pfm.mappings[idx1].id_prefix, + pfm.mappings[idx2].id_prefix) = (pfm.mappings[idx2].id_prefix, + pfm.mappings[idx1].id_prefix) + + tmp = pfm.mappings + tmp[idx1], tmp[idx2] = tmp[idx2], tmp[idx1] + pfm.mappings = tmp + + # 90 for unicodePwd (with new prefix = 0) + # 589824, 589827 for objectClass and CN + # Use of three ensures sorting is correct + partial_attribute_set = self.get_partial_attribute_set([90, 589824, 589827]) + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.user, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + partial_attribute_set=partial_attribute_set, + mapping_ctr=pfm) + + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + + found = False + for attr in ctr.first_object.object.attribute_ctr.attributes: + if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd: + found = True + break + + self.assertTrue(found, "Ensure we get the unicodePwd attribute back") + + def test_regular_prefix_map_attid(self): + # Request for a regular (non-secret) attid + partial_attribute_set = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_name]) + + dc_guid_1 = self.ldb_dc1.get_invocation_id() + drs, drs_handle = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + + try: + pfm = self._samdb_fetch_pfm_and_schi() + except KeyError: + # On Windows, prefixMap isn't available over LDAP + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.user, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ) + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + pfm = ctr.mapping_ctr + + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.user, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + partial_attribute_set=partial_attribute_set, + mapping_ctr=pfm) + + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + + found = False + for attr in ctr.first_object.object.attribute_ctr.attributes: + if attr.attid == drsuapi.DRSUAPI_ATTID_name: + found = True + break + + self.assertTrue(found, "Ensure we get the name attribute back") + + for i, mapping in enumerate(pfm.mappings): + # OID: 2.5.4.* + # objectClass: 2.5.4.0 + if mapping.oid.binary_oid == [85, 4]: + idx1 = i + # OID: 1.2.840.113556.1.4.* + # name: 1.2.840.113556.1.4.1 + elif mapping.oid.binary_oid == [42, 134, 72, 134, 247, 20, 1, 4]: + idx2 = i + + (pfm.mappings[idx1].id_prefix, + pfm.mappings[idx2].id_prefix) = (pfm.mappings[idx2].id_prefix, + pfm.mappings[idx1].id_prefix) + + tmp = pfm.mappings + tmp[idx1], tmp[idx2] = tmp[idx2], tmp[idx1] + pfm.mappings = tmp + + # 1 for name (with new prefix = 0) + partial_attribute_set = self.get_partial_attribute_set([1]) + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.user, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + partial_attribute_set=partial_attribute_set, + mapping_ctr=pfm) + + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + + found = False + for attr in ctr.first_object.object.attribute_ctr.attributes: + if attr.attid == drsuapi.DRSUAPI_ATTID_name: + found = True + break + + self.assertTrue(found, "Ensure we get the name attribute back") + + def test_regular_prefix_map_ex_attid(self): + # Request for a regular (non-secret) attid + partial_attribute_set = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_name]) + partial_attribute_set_ex = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_unicodePwd]) + + dc_guid_1 = self.ldb_dc1.get_invocation_id() + drs, drs_handle = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + + try: + pfm = self._samdb_fetch_pfm_and_schi() + except KeyError: + # On Windows, prefixMap isn't available over LDAP + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.user, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ) + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + pfm = ctr.mapping_ctr + + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.user, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + partial_attribute_set=partial_attribute_set, + partial_attribute_set_ex=partial_attribute_set_ex, + mapping_ctr=pfm) + + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + + found = False + for attr in ctr.first_object.object.attribute_ctr.attributes: + if attr.attid == drsuapi.DRSUAPI_ATTID_name: + found = True + break + + self.assertTrue(found, "Ensure we get the name attribute back") + + found = False + for attr in ctr.first_object.object.attribute_ctr.attributes: + if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd: + found = True + break + + self.assertTrue(found, "Ensure we get the unicodePwd attribute back") + + for i, mapping in enumerate(pfm.mappings): + # OID: 2.5.4.* + # objectClass: 2.5.4.0 + if mapping.oid.binary_oid == [85, 4]: + idx1 = i + # OID: 1.2.840.113556.1.4.* + # name: 1.2.840.113556.1.4.1 + # unicodePwd: 1.2.840.113556.1.4.90 + elif mapping.oid.binary_oid == [42, 134, 72, 134, 247, 20, 1, 4]: + idx2 = i + + (pfm.mappings[idx1].id_prefix, + pfm.mappings[idx2].id_prefix) = (pfm.mappings[idx2].id_prefix, + pfm.mappings[idx1].id_prefix) + + tmp = pfm.mappings + tmp[idx1], tmp[idx2] = tmp[idx2], tmp[idx1] + pfm.mappings = tmp + + # 1 for name (with new prefix = 0) + partial_attribute_set = self.get_partial_attribute_set([1]) + # 90 for unicodePwd (with new prefix = 0) + # HOWEVER: Windows doesn't seem to respect incoming maps for PartialAttrSetEx + partial_attribute_set_ex = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_unicodePwd]) + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.user, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + partial_attribute_set=partial_attribute_set, + partial_attribute_set_ex=partial_attribute_set_ex, + mapping_ctr=pfm) + + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + + found = False + for attr in ctr.first_object.object.attribute_ctr.attributes: + if attr.attid == drsuapi.DRSUAPI_ATTID_name: + found = True + break + + self.assertTrue(found, "Ensure we get the name attribute back") + + found = False + for attr in ctr.first_object.object.attribute_ctr.attributes: + if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd: + found = True + break + + self.assertTrue(found, "Ensure we get the unicodePwd attribute back") + + def _samdb_fetch_pfm_and_schi(self): + """Fetch prefixMap and schemaInfo stored in SamDB using LDB connection""" + samdb = self.ldb_dc1 + res = samdb.search(base=samdb.get_schema_basedn(), scope=SCOPE_BASE, + attrs=["prefixMap", "schemaInfo"]) + + pfm = ndr_unpack(drsblobs.prefixMapBlob, + res[0]['prefixMap'][0]) + + schi = drsuapi.DsReplicaOIDMapping() + schi.id_prefix = 0 + if 'schemaInfo' in res[0]: + binary_oid = [x if isinstance(x, int) else ord(x) for x in res[0]['schemaInfo'][0]] + schi.oid.length = len(binary_oid) + schi.oid.binary_oid = binary_oid + else: + schema_info = drsblobs.schemaInfoBlob() + schema_info.revision = 0 + schema_info.marker = 0xFF + schema_info.invocation_id = misc.GUID(samdb.get_invocation_id()) + + binary_oid = [x if isinstance(x, int) else ord(x) for x in ndr_pack(schema_info)] + # you have to set the length before setting binary_oid + schi.oid.length = len(binary_oid) + schi.oid.binary_oid = binary_oid + + pfm.ctr.mappings = pfm.ctr.mappings + [schi] + pfm.ctr.num_mappings += 1 + return pfm.ctr + + +class DrsReplicaSyncSortTestCase(drs_base.DrsBaseTestCase): + def setUp(self): + super(DrsReplicaSyncSortTestCase, self).setUp() + self.base_dn = self.ldb_dc1.get_default_basedn() + self.ou = "ou=sort_exop%d,%s" % (random.randint(0, 4294967295), + self.base_dn) + self.ldb_dc1.add({ + "dn": self.ou, + "objectclass": "organizationalUnit"}) + + def tearDown(self): + super(DrsReplicaSyncSortTestCase, self).tearDown() + # tidyup groups and users + try: + self.ldb_dc1.delete(self.ou, ["tree_delete:1"]) + except ldb.LdbError as e4: + (enum, string) = e4.args + if enum == ldb.ERR_NO_SUCH_OBJECT: + pass + + def add_linked_attribute(self, src, dest, attr='member'): + m = ldb.Message() + m.dn = ldb.Dn(self.ldb_dc1, src) + m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_ADD, attr) + self.ldb_dc1.modify(m) + + def remove_linked_attribute(self, src, dest, attr='member'): + m = ldb.Message() + m.dn = ldb.Dn(self.ldb_dc1, src) + m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_DELETE, attr) + self.ldb_dc1.modify(m) + + def test_sort_behaviour_single_object(self): + """Testing sorting behaviour on single objects""" + + user1_dn = "cn=test_user1,%s" % self.ou + user2_dn = "cn=test_user2,%s" % self.ou + user3_dn = "cn=test_user3,%s" % self.ou + group_dn = "cn=test_group,%s" % self.ou + + self.ldb_dc1.add({"dn": user1_dn, "objectclass": "user"}) + self.ldb_dc1.add({"dn": user2_dn, "objectclass": "user"}) + self.ldb_dc1.add({"dn": user3_dn, "objectclass": "user"}) + self.ldb_dc1.add({"dn": group_dn, "objectclass": "group"}) + + u1_guid = misc.GUID(self.ldb_dc1.search(base=user1_dn, + attrs=["objectGUID"])[0]['objectGUID'][0]) + u2_guid = misc.GUID(self.ldb_dc1.search(base=user2_dn, + attrs=["objectGUID"])[0]['objectGUID'][0]) + u3_guid = misc.GUID(self.ldb_dc1.search(base=user3_dn, + attrs=["objectGUID"])[0]['objectGUID'][0]) + g_guid = misc.GUID(self.ldb_dc1.search(base=group_dn, + attrs=["objectGUID"])[0]['objectGUID'][0]) + + self.add_linked_attribute(group_dn, user1_dn, + attr='member') + self.add_linked_attribute(group_dn, user2_dn, + attr='member') + self.add_linked_attribute(group_dn, user3_dn, + attr='member') + self.add_linked_attribute(group_dn, user1_dn, + attr='managedby') + self.add_linked_attribute(group_dn, user2_dn, + attr='nonSecurityMember') + self.add_linked_attribute(group_dn, user3_dn, + attr='nonSecurityMember') + + set_inactive = AbstractLink(drsuapi.DRSUAPI_ATTID_nonSecurityMember, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + g_guid, u3_guid) + + expected_links = set([set_inactive, + AbstractLink(drsuapi.DRSUAPI_ATTID_member, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + g_guid, + u1_guid), + AbstractLink(drsuapi.DRSUAPI_ATTID_member, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + g_guid, + u2_guid), + AbstractLink(drsuapi.DRSUAPI_ATTID_member, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + g_guid, + u3_guid), + AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + g_guid, + u1_guid), + AbstractLink(drsuapi.DRSUAPI_ATTID_nonSecurityMember, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + g_guid, + u2_guid), + ]) + + dc_guid_1 = self.ldb_dc1.get_invocation_id() + + drs, drs_handle = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=group_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ) + + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + + no_inactive = [] + for link in ctr.linked_attributes: + target_guid = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3, + link.value.blob).guid + no_inactive.append((link, target_guid)) + self.assertTrue(AbstractLink(link.attid, link.flags, + link.identifier.guid, + target_guid) in expected_links) + + no_inactive.sort(key=cmp_to_key(_linked_attribute_compare)) + + # assert the two arrays are the same + self.assertEqual(len(expected_links), ctr.linked_attributes_count) + self.assertEqual([x[0] for x in no_inactive], ctr.linked_attributes) + + self.remove_linked_attribute(group_dn, user3_dn, + attr='nonSecurityMember') + + # Set the link inactive + expected_links.remove(set_inactive) + set_inactive.flags = 0 + expected_links.add(set_inactive) + + has_inactive = [] + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + for link in ctr.linked_attributes: + target_guid = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3, + link.value.blob).guid + has_inactive.append((link, target_guid)) + self.assertTrue(AbstractLink(link.attid, link.flags, + link.identifier.guid, + target_guid) in expected_links) + + has_inactive.sort(key=cmp_to_key(_linked_attribute_compare)) + + # assert the two arrays are the same + self.assertEqual(len(expected_links), ctr.linked_attributes_count) + self.assertEqual([x[0] for x in has_inactive], ctr.linked_attributes) + + def test_sort_behaviour_ncchanges(self): + """Testing sorting behaviour on a group of objects.""" + user1_dn = "cn=test_user1,%s" % self.ou + group_dn = "cn=test_group,%s" % self.ou + self.ldb_dc1.add({"dn": user1_dn, "objectclass": "user"}) + self.ldb_dc1.add({"dn": group_dn, "objectclass": "group"}) + + self.add_linked_attribute(group_dn, user1_dn, + attr='member') + + dc_guid_1 = self.ldb_dc1.get_invocation_id() + + drs, drs_handle = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + + # Make sure the max objects count is high enough + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.base_dn, + replica_flags=0, + max_objects=100, + exop=drsuapi.DRSUAPI_EXOP_NONE) + + # Loop until we get linked attributes, or we get to the end. + # Samba sends linked attributes at the end, unlike Windows. + while True: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + if ctr.more_data == 0 or ctr.linked_attributes_count != 0: + break + req8.highwatermark = ctr.new_highwatermark + + self.assertTrue(ctr.linked_attributes_count != 0) + + no_inactive = [] + for link in ctr.linked_attributes: + try: + target_guid = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3, + link.value.blob).guid + except: + target_guid = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3Binary, + link.value.blob).guid + no_inactive.append((link, target_guid)) + + no_inactive.sort(key=cmp_to_key(_linked_attribute_compare)) + + # assert the two arrays are the same + self.assertEqual([x[0] for x in no_inactive], ctr.linked_attributes) diff --git a/source4/torture/drs/python/getnc_schema.py b/source4/torture/drs/python/getnc_schema.py new file mode 100644 index 0000000..60062f9 --- /dev/null +++ b/source4/torture/drs/python/getnc_schema.py @@ -0,0 +1,304 @@ +import drs_base +import ldb +import time +import random +import os + +break_me = os.getenv("PLEASE_BREAK_MY_WINDOWS") == "1" +assert break_me, ("This test breaks Windows active directory after " + "a few runs. Set PLEASE_BREAK_MY_WINDOWS=1 to run.") + +# This test runs against Windows. To run, set up two Windows AD DCs, join one +# to the other, and make sure the passwords are the same. SMB_CONF_PATH must +# also be set to any smb.conf file. Set DC1 to the PDC's hostname, and DC2 to +# the join'd DC's hostname. Example: +# PLEASE_BREAK_MY_WINDOWS=1 +# DC1=pdc DC2=joindc +# SMB_CONF_PATH=st/ad_dc/etc/smb.conf +# PYTHONPATH=$PYTHONPATH:./source4/torture/drs/python +# python3 ./source4/scripting/bin/subunitrun getnc_schema +# -UAdministrator%Password + +class SchemaReplicationTests(drs_base.DrsBaseTestCase): + + def setUp(self): + super(SchemaReplicationTests, self).setUp() + self.creds = self.get_credentials() + self.cmdline_auth = "-U{}%{}".format(self.creds.get_username(), + self.creds.get_password()) + + self.from_ldb = self.ldb_dc1 + self.dest_ldb = self.ldb_dc2 + self._disable_inbound_repl(self.url_dc1) + self._disable_all_repl(self.url_dc1) + self.free_offset = 0 + + def tearDown(self): + self._enable_inbound_repl(self.url_dc1) + self._enable_all_repl(self.url_dc1) + + def do_repl(self, partition_dn): + self._enable_inbound_repl(self.url_dc1) + self._enable_all_repl(self.url_dc1) + + samba_tool_cmd = ["drs", "replicate", self.url_dc2, self.url_dc1] + samba_tool_cmd += [partition_dn] + username = self.creds.get_username() + password = self.creds.get_password() + samba_tool_cmd += ["-U{0}%{1}".format(username, password)] + + (result, out, err) = self.runsubcmd(*samba_tool_cmd) + + try: + self.assertCmdSuccess(result, out, err) + except AssertionError: + print("Failed repl, retrying in 10s") + time.sleep(10) + (result, out, err) = self.runsubcmd(*samba_tool_cmd) + + self._disable_inbound_repl(self.url_dc1) + self._disable_all_repl(self.url_dc1) + + self.assertCmdSuccess(result, out, err) + + # Get a unique prefix for some search expression like "([att]=[pref]{i}*)" + def get_unique(self, expr_templ): + found = True + while found: + i = random.randint(0, 65535) + res = self.from_ldb.search(base=self.schema_dn, + scope=ldb.SCOPE_SUBTREE, + expression=expr_templ.format(i=i)) + found = len(res) > 0 + + return str(i) + + def unique_gov_id_prefix(self): + prefix = "1.3.6.1.4.1.7165.4.6.2.8." + return prefix + self.get_unique("(governsId=" + prefix + "{i}.*)") + + def unique_cn_prefix(self, prefix="testobj"): + return prefix + self.get_unique("(cn=" + prefix + "{i}x*)") + "x" + + # Make test schema classes linked to each other in a line, then modify + # them in reverse order so when we repl, a link crosses the chunk + # boundary. Chunk size is 133 by default so we do 150. + def test_poss_superiors_across_chunk(self): + num_schema_objects_to_add = 150 + class_name = self.unique_cn_prefix() + + ldif_template = """ +dn: CN={class_name}{i},{schema_dn} +objectClass: top +objectClass: classSchema +adminDescription: {class_name}{i} +adminDisplayName: {class_name}{i} +cn: {class_name}{i} +governsId: {gov_id}.{i} +instanceType: 4 +objectClassCategory: 1 +systemFlags: 16 +systemOnly: FALSE +""" + + ldif_kwargs = {'class_name': class_name, + 'schema_dn': self.schema_dn} + gov_id = self.unique_gov_id_prefix() + ldif = ldif_template.format(i=0, gov_id=gov_id, **ldif_kwargs) + self.from_ldb.add_ldif(ldif) + + ldif_template += "systemPossSuperiors: {possSup}\n" + + ids = list(range(num_schema_objects_to_add)) + got_no_such_attrib = False + for i in ids[1:]: + last_class_name = class_name + str(i-1) + ldif = ldif_template.format(i=i, gov_id=gov_id, + possSup=last_class_name, + **ldif_kwargs) + + try: + self.from_ldb.add_ldif(ldif) + if got_no_such_attrib: + self.from_ldb.set_schema_update_now() + except ldb.LdbError as e: + if e.args[0] != ldb.ERR_NO_SUCH_ATTRIBUTE: + self.fail(e) + if got_no_such_attrib: + self.fail(("got NO_SUCH_ATTRIB even after " + "setting schemaUpdateNow", str(e))) + print("got NO_SUCH_ATTRIB, trying schemaUpdateNow") + got_no_such_attrib = True + self.from_ldb.set_schema_update_now() + self.from_ldb.add_ldif(ldif) + self.from_ldb.set_schema_update_now() + + ldif_template = """ +dn: CN={class_name}{i},{schema_dn} +changetype: modify +replace: adminDescription +adminDescription: new_description +""" + + for i in reversed(ids): + ldif = ldif_template.format(i=i, **ldif_kwargs) + self.from_ldb.modify_ldif(ldif) + + self.do_repl(self.schema_dn) + + dn_templ = "CN={class_name}{i},{schema_dn}" + for i in ids: + dn = dn_templ.format(i=i, **ldif_kwargs) + res = self.dest_ldb.search(base=dn, scope=ldb.SCOPE_BASE) + self.assertEqual(len(res), 1) + + # Test for method of adding linked attributes in schema partition + # required by other tests. + def test_create_linked_attribute_in_schema(self): + # Make an object outside of the schema partition that we can link to + user_name = self.unique_cn_prefix("user") + user_dn = "CN={},CN=Users,{}".format(user_name, self.domain_dn) + + ldif_template = """ +dn: {user_dn} +objectClass: person +objectClass: user""" + ldif = ldif_template.format(user_dn=user_dn) + self.from_ldb.add_ldif(ldif) + + # Make test class name unique so test can run multiple times + class_name = self.unique_cn_prefix("class") + + kwargs = {'class_name': class_name, + 'schema_dn': self.schema_dn, + 'user_dn': user_dn} + + # Add an auxiliary schemaClass (cat 3) class and give it managedBy + # so we can create schema objects with linked attributes. + ldif_template = """ +dn: CN={class_name},{schema_dn} +objectClass: classSchema +governsId: {gov_id}.0 +instanceType: 4 +systemFlags: 16 +systemOnly: FALSE +objectClassCategory: 3 +mayContain: managedBy +""" + + gov_id = self.unique_gov_id_prefix() + ldif = ldif_template.format(gov_id=gov_id, **kwargs) + self.from_ldb.add_ldif(ldif) + + # Now make an instance that points back to the user with managedBy, + # thus creating an object in the schema with a linked attribute + ldif_template = """ +dn: CN=link{class_name},{schema_dn} +objectClass: classSchema +objectClass: {class_name} +instanceType: 4 +governsId: {gov_id}.0 +systemFlags: 16 +managedBy: {user_dn} +""" + + gov_id = self.unique_gov_id_prefix() + ldif = ldif_template.format(gov_id=gov_id, **kwargs) + self.from_ldb.add_ldif(ldif) + + # Check link exists on test schema object + dn_templ = "CN=link{class_name},{schema_dn}" + dn = dn_templ.format(**kwargs) + res = self.from_ldb.search(base=dn, scope=ldb.SCOPE_BASE) + self.assertEqual(len(res), 1) + self.assertIsNotNone(res[0].get("managedBy")) + self.assertEqual(str(res[0].get("managedBy")[0]), user_dn) + + # Check backlink on user object + res = self.from_ldb.search(base=user_dn, scope=ldb.SCOPE_BASE) + self.assertEqual(len(res), 1) + managed_objs = res[0].get("managedObjects") + self.assertEqual(len(managed_objs), 1) + managed_objs = [str(o) for o in managed_objs] + self.assertEqual(managed_objs, [dn_templ.format(**kwargs)]) + + def test_schema_linked_attributes(self): + num_test_objects = 9 + + # Make an object outside of the schema partition that we can link to + user_name = self.unique_cn_prefix("user") + user_dn = "CN={},CN=Users,{}".format(user_name, self.domain_dn) + + ldif_template = """ +dn: {user_dn} +objectClass: person +objectClass: user""" + ldif = ldif_template.format(user_dn=user_dn) + self.from_ldb.add_ldif(ldif) + + self.do_repl(self.domain_dn) + + # Make test object name prefixes unique so test can run multiple times + # in a single testenv (can't delete schema objects) + class_name = self.unique_cn_prefix("class") + link_class_name = self.unique_cn_prefix("linkClass") + + kwargs = {'class_name': class_name, + 'schema_dn': self.schema_dn, + 'link_class_name': link_class_name, + 'user_dn': user_dn} + + # Add an auxiliary schemaClass (cat 3) class and give it managedBy + # so we can create schema objects with linked attributes. + ldif_template = """ +dn: CN={class_name},{schema_dn} +objectClass: classSchema +governsId: {gov_id}.0 +instanceType: 4 +systemFlags: 16 +systemOnly: FALSE +objectClassCategory: 3 +mayContain: managedBy +""" + + gov_id = self.unique_gov_id_prefix() + ldif = ldif_template.format(gov_id=gov_id, **kwargs) + self.from_ldb.add_ldif(ldif) + + # Now make instances that point back to the user with managedBy, + # thus creating objects in the schema with linked attributes + ldif_template = """ +dn: CN={link_class_name}{i},{schema_dn} +objectClass: classSchema +objectClass: {class_name} +instanceType: 4 +governsId: {gov_id}.0 +systemFlags: 16 +managedBy: {user_dn} +""" + + id_range = list(range(num_test_objects)) + for i in id_range: + gov_id = self.unique_gov_id_prefix() + ldif = ldif_template.format(i=i, gov_id=gov_id, **kwargs) + self.from_ldb.add_ldif(ldif) + + self.do_repl(self.schema_dn) + + # Check link exists in each test schema objects at destination DC + dn_templ = "CN={link_class_name}{i},{schema_dn}" + for i in id_range: + dn = dn_templ.format(i=i, **kwargs) + res = self.dest_ldb.search(base=dn, scope=ldb.SCOPE_BASE) + self.assertEqual(len(res), 1) + self.assertIsNotNone(res[0].get("managedBy")) + self.assertEqual(str(res[0].get("managedBy")[0]), user_dn) + + # Check backlinks list on user object contains DNs of test objects. + res = self.dest_ldb.search(base=user_dn, scope=ldb.SCOPE_BASE) + self.assertEqual(len(res), 1) + managed_objs = res[0].get("managedObjects") + self.assertIsNotNone(managed_objs) + managed_objs_set = {str(el) for el in managed_objs} + expected = {dn_templ.format(i=i, **kwargs) for i in id_range} + self.assertEqual(managed_objs_set, expected) diff --git a/source4/torture/drs/python/getnc_unpriv.py b/source4/torture/drs/python/getnc_unpriv.py new file mode 100644 index 0000000..c53906a --- /dev/null +++ b/source4/torture/drs/python/getnc_unpriv.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Tests replication scenarios with different user privileges. +# We want to test every replication scenario we can think of against: +# - users with only GET_CHANGES privileges +# - users with only GET_ALL_CHANGES privileges +# - users with both GET_CHANGES and GET_ALL_CHANGES privileges +# - users with no privileges +# +# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2011 +# 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/>. +# + +# +# Usage: +# export DC1=dc1_dns_name +# export DC2=dc2_dns_name +# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun +# PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN getnc_unpriv -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD" +# + +import drs_base +import samba.tests +from samba import werror, WERRORError + +from samba import sd_utils +import ldb +from ldb import SCOPE_BASE +import random + +from samba.dcerpc import drsuapi, security +from samba.credentials import DONT_USE_KERBEROS + + +class DrsReplicaSyncUnprivTestCase(drs_base.DrsBaseTestCase): + """Confirm the behaviour of DsGetNCChanges for unprivileged users""" + + def setUp(self): + super(DrsReplicaSyncUnprivTestCase, self).setUp() + self.get_changes_user = "get-changes-user" + self.base_dn = self.ldb_dc1.get_default_basedn() + self.user_pass = samba.generate_random_password(12, 16) + + # add some randomness to the test OU. (Deletion of the last test's + # objects can be slow to replicate out. So the OU created by a previous + # testenv may still exist at this point). + rand = random.randint(1, 10000000) + test_ou = "OU=test_getnc_unpriv%d" % rand + self.ou = "%s,%s" % (test_ou, self.base_dn) + self.ldb_dc1.add({ + "dn": self.ou, + "objectclass": "organizationalUnit"}) + self.ldb_dc1.newuser(self.get_changes_user, self.user_pass, + userou=test_ou) + (self.drs, self.drs_handle) = self._ds_bind(self.dnsname_dc1) + + self.sd_utils = sd_utils.SDUtils(self.ldb_dc1) + self.user_dn = "cn=%s,%s" % (self.get_changes_user, self.ou) + user_sid = self.sd_utils.get_object_sid(self.user_dn) + self.acl_mod_get_changes = "(OA;;CR;%s;;%s)" % (security.GUID_DRS_GET_CHANGES, + str(user_sid)) + self.acl_mod_get_all_changes = "(OA;;CR;%s;;%s)" % (security.GUID_DRS_GET_ALL_CHANGES, + str(user_sid)) + self.desc_sddl = self.sd_utils.get_sd_as_sddl(self.base_dn) + + # We set DONT_USE_KERBEROS to avoid a race with getting the + # user replicated to our selected KDC + self.user_creds = self.insta_creds(template=self.get_credentials(), + username=self.get_changes_user, + userpass=self.user_pass, + kerberos_state=DONT_USE_KERBEROS) + (self.user_drs, self.user_drs_handle) = self._ds_bind(self.dnsname_dc1, + self.user_creds) + + def tearDown(self): + self.sd_utils.modify_sd_on_dn(self.base_dn, self.desc_sddl) + try: + self.ldb_dc1.delete(self.ou, ["tree_delete:1"]) + except ldb.LdbError as e1: + (enum, string) = e1.args + if enum == ldb.ERR_NO_SUCH_OBJECT: + pass + super(DrsReplicaSyncUnprivTestCase, self).tearDown() + + def _test_repl_exop(self, exop, repl_obj, expected_error, dest_dsa=None, + partial_attribute_set=None): + """ + Common function to send a replication request and check the result + matches what's expected. + """ + req8 = self._exop_req8(dest_dsa=dest_dsa, + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=repl_obj, + exop=exop, + replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP, + partial_attribute_set=partial_attribute_set) + + if expected_error is None: + # user is OK, request should be accepted without throwing an error + (level, ctr) = self.user_drs.DsGetNCChanges(self.user_drs_handle, + 8, req8) + else: + # check the request is rejected (with the error we're expecting) + try: + (level, ctr) = self.user_drs.DsGetNCChanges(self.user_drs_handle, + 8, req8) + self.fail("Should have failed with user denied access") + except WERRORError as e: + (enum, estr) = e.args + self.assertTrue(enum in expected_error, + "Got unexpected error: %s" % estr) + + def _test_repl_single_obj(self, repl_obj, expected_error, + partial_attribute_set=None): + """ + Checks that replication on a single object either succeeds or fails as + expected (based on the user's access rights) + """ + self._test_repl_exop(exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + repl_obj=repl_obj, + expected_error=expected_error, + partial_attribute_set=partial_attribute_set) + + def _test_repl_secret(self, repl_obj, expected_error, dest_dsa=None): + """ + Checks that REPL_SECRET on an object either succeeds or fails as + expected (based on the user's access rights) + """ + self._test_repl_exop(exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, + repl_obj=repl_obj, + expected_error=expected_error, + dest_dsa=dest_dsa) + + def _test_repl_full(self, expected_error, partial_attribute_set=None): + """ + Checks that a full replication either succeeds or fails as expected + (based on the user's access rights) + """ + self._test_repl_exop(exop=drsuapi.DRSUAPI_EXOP_NONE, + repl_obj=self.ldb_dc1.get_default_basedn(), + expected_error=expected_error, + partial_attribute_set=partial_attribute_set) + + def _test_repl_full_on_ou(self, repl_obj, expected_error): + """ + Full replication on a specific OU should always fail (it should be done + against a base NC). The error may vary based on the user's access rights + """ + # Just try against the OU created in the test setup + self._test_repl_exop(exop=drsuapi.DRSUAPI_EXOP_NONE, + repl_obj=repl_obj, + expected_error=expected_error) + + def test_repl_getchanges_userpriv(self): + """ + Tests various replication requests made by a user with only GET_CHANGES + rights. Some requests will be accepted, but most will be rejected. + """ + + # Assign the user GET_CHANGES rights + self.sd_utils.dacl_add_ace(self.base_dn, self.acl_mod_get_changes) + + self._test_repl_single_obj(repl_obj=self.ou, + expected_error=[werror.WERR_DS_DRA_ACCESS_DENIED]) + bad_ou = "OU=bad_obj,%s" % self.ou + self._test_repl_single_obj(repl_obj=bad_ou, + expected_error=[werror.WERR_DS_DRA_BAD_DN, + werror.WERR_DS_DRA_ACCESS_DENIED]) + + self._test_repl_secret(repl_obj=self.ou, + expected_error=[werror.WERR_DS_DRA_ACCESS_DENIED]) + self._test_repl_secret(repl_obj=self.user_dn, + expected_error=[werror.WERR_DS_DRA_ACCESS_DENIED]) + self._test_repl_secret(repl_obj=self.user_dn, + dest_dsa=self.ldb_dc1.get_ntds_GUID(), + expected_error=[werror.WERR_DS_DRA_ACCESS_DENIED]) + self._test_repl_secret(repl_obj=bad_ou, + expected_error=[werror.WERR_DS_DRA_BAD_DN]) + + self._test_repl_full(expected_error=[werror.WERR_DS_DRA_ACCESS_DENIED]) + self._test_repl_full_on_ou(repl_obj=self.ou, + expected_error=[werror.WERR_DS_CANT_FIND_EXPECTED_NC, + werror.WERR_DS_DRA_ACCESS_DENIED]) + self._test_repl_full_on_ou(repl_obj=bad_ou, + expected_error=[werror.WERR_DS_DRA_BAD_NC, + werror.WERR_DS_DRA_ACCESS_DENIED]) + + # Partial Attribute Sets don't require GET_ALL_CHANGES rights, so we + # expect the following to succeed + self._test_repl_single_obj(repl_obj=self.ou, + expected_error=None, + partial_attribute_set=self.get_partial_attribute_set()) + self._test_repl_full(expected_error=None, + partial_attribute_set=self.get_partial_attribute_set()) + + def test_repl_getallchanges_userpriv(self): + """ + Tests various replication requests made by a user with only + GET_ALL_CHANGES rights. Note that assigning these rights is possible, + but doesn't make a lot of sense. We test it anyway for consistency. + """ + + # Assign the user GET_ALL_CHANGES rights + self.sd_utils.dacl_add_ace(self.base_dn, self.acl_mod_get_all_changes) + + # We can expect to get the same responses as an unprivileged user, + # i.e. we have permission to see the results, but don't have permission + # to ask + self.test_repl_no_userpriv() + + def test_repl_both_userpriv(self): + """ + Tests various replication requests made by a privileged user (i.e. has + both GET_CHANGES and GET_ALL_CHANGES). We expect any valid requests + to be accepted. + """ + + # Assign the user both GET_CHANGES and GET_ALL_CHANGES rights + both_rights = self.acl_mod_get_changes + self.acl_mod_get_all_changes + self.sd_utils.dacl_add_ace(self.base_dn, both_rights) + + self._test_repl_single_obj(repl_obj=self.ou, + expected_error=None) + bad_ou = "OU=bad_obj,%s" % self.ou + self._test_repl_single_obj(repl_obj=bad_ou, + expected_error=[werror.WERR_DS_DRA_BAD_DN]) + + # Microsoft returns DB_ERROR, Samba returns ACCESS_DENIED + self._test_repl_secret(repl_obj=self.ou, + expected_error=[werror.WERR_DS_DRA_DB_ERROR, + werror.WERR_DS_DRA_ACCESS_DENIED]) + self._test_repl_secret(repl_obj=self.user_dn, + expected_error=[werror.WERR_DS_DRA_DB_ERROR, + werror.WERR_DS_DRA_ACCESS_DENIED]) + # Note that Windows accepts this but Samba rejects it + self._test_repl_secret(repl_obj=self.user_dn, + dest_dsa=self.ldb_dc1.get_ntds_GUID(), + expected_error=[werror.WERR_DS_DRA_ACCESS_DENIED]) + + self._test_repl_secret(repl_obj=bad_ou, + expected_error=[werror.WERR_DS_DRA_BAD_DN]) + + self._test_repl_full(expected_error=None) + self._test_repl_full_on_ou(repl_obj=self.ou, + expected_error=[werror.WERR_DS_CANT_FIND_EXPECTED_NC]) + self._test_repl_full_on_ou(repl_obj=bad_ou, + expected_error=[werror.WERR_DS_DRA_BAD_NC, + werror.WERR_DS_DRA_BAD_DN]) + + self._test_repl_single_obj(repl_obj=self.ou, + expected_error=None, + partial_attribute_set=self.get_partial_attribute_set()) + self._test_repl_full(expected_error=None, + partial_attribute_set=self.get_partial_attribute_set()) + + def test_repl_no_userpriv(self): + """ + Tests various replication requests made by a unprivileged user. + We expect all these requests to be rejected. + """ + + # Microsoft usually returns BAD_DN, Samba returns ACCESS_DENIED + usual_error = [werror.WERR_DS_DRA_BAD_DN, werror.WERR_DS_DRA_ACCESS_DENIED] + + self._test_repl_single_obj(repl_obj=self.ou, + expected_error=usual_error) + bad_ou = "OU=bad_obj,%s" % self.ou + self._test_repl_single_obj(repl_obj=bad_ou, + expected_error=usual_error) + + self._test_repl_secret(repl_obj=self.ou, + expected_error=usual_error) + self._test_repl_secret(repl_obj=self.user_dn, + expected_error=usual_error) + self._test_repl_secret(repl_obj=self.user_dn, + dest_dsa=self.ldb_dc1.get_ntds_GUID(), + expected_error=usual_error) + self._test_repl_secret(repl_obj=bad_ou, + expected_error=usual_error) + + self._test_repl_full(expected_error=[werror.WERR_DS_DRA_ACCESS_DENIED]) + self._test_repl_full_on_ou(repl_obj=self.ou, + expected_error=usual_error) + self._test_repl_full_on_ou(repl_obj=bad_ou, + expected_error=[werror.WERR_DS_DRA_BAD_NC, + werror.WERR_DS_DRA_ACCESS_DENIED]) + + self._test_repl_single_obj(repl_obj=self.ou, + expected_error=usual_error, + partial_attribute_set=self.get_partial_attribute_set()) + self._test_repl_full(expected_error=[werror.WERR_DS_DRA_ACCESS_DENIED], + partial_attribute_set=self.get_partial_attribute_set()) diff --git a/source4/torture/drs/python/getncchanges.py b/source4/torture/drs/python/getncchanges.py new file mode 100644 index 0000000..6b5456a --- /dev/null +++ b/source4/torture/drs/python/getncchanges.py @@ -0,0 +1,1427 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Tests various schema replication scenarios +# +# 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/>. +# + +# +# Usage: +# export DC1=dc1_dns_name +# export DC2=dc2_dns_name +# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun +# PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN \ +# getncchanges -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD" +# + +import drs_base +import samba.tests +import ldb +from ldb import SCOPE_BASE +import random + +from samba.dcerpc import drsuapi, misc +from samba import WERRORError +from samba import werror + +class DrsReplicaSyncIntegrityTestCase(drs_base.DrsBaseTestCase): + def setUp(self): + super(DrsReplicaSyncIntegrityTestCase, self).setUp() + + self.init_test_state() + + # Note that DC2 is the DC with the testenv-specific quirks (e.g. it's + # the vampire_dc), so we point this test directly at that DC + self.set_test_ldb_dc(self.ldb_dc2) + + self.ou = str(samba.tests.create_test_ou(self.test_ldb_dc, + "getncchanges." + self.id().rsplit(".", 1)[1])) + + self.addCleanup(self.ldb_dc2.delete, self.ou, ["tree_delete:1"]) + + self.base_dn = self.test_ldb_dc.get_default_basedn() + + self.default_conn = DcConnection(self, self.ldb_dc2, self.dnsname_dc2) + self.set_dc_connection(self.default_conn) + + def init_test_state(self): + self.rxd_dn_list = [] + self.rxd_links = [] + self.rxd_guids = [] + self.last_ctr = None + + # 100 is the minimum max_objects that Microsoft seems to honour + # (the max honoured is 400ish), so we use that in these tests + self.max_objects = 100 + + # store whether we used GET_TGT/GET_ANC flags in the requests + self.used_get_tgt = False + self.used_get_anc = False + + def add_object(self, dn, objectclass="organizationalunit"): + """Adds an OU object""" + self.test_ldb_dc.add({"dn": dn, "objectclass": objectclass}) + res = self.test_ldb_dc.search(base=dn, scope=SCOPE_BASE) + self.assertEqual(len(res), 1) + + def modify_object(self, dn, attr, value): + """Modifies an object's USN by adding an attribute value to it""" + m = ldb.Message() + m.dn = ldb.Dn(self.test_ldb_dc, dn) + m[attr] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, attr) + self.test_ldb_dc.modify(m) + + def delete_attribute(self, dn, attr, value): + """Deletes an attribute from an object""" + m = ldb.Message() + m.dn = ldb.Dn(self.test_ldb_dc, dn) + m[attr] = ldb.MessageElement(value, ldb.FLAG_MOD_DELETE, attr) + self.test_ldb_dc.modify(m) + + def start_new_repl_cycle(self): + """Resets enough state info to start a new replication cycle""" + # reset rxd_links, but leave rxd_guids and rxd_dn_list alone so we know + # whether a parent/target is unknown and needs GET_ANC/GET_TGT to + # resolve + self.rxd_links = [] + + self.used_get_tgt = False + self.used_get_anc = False + # mostly preserve self.last_ctr, so that we use the last HWM + if self.last_ctr is not None: + self.last_ctr.more_data = True + + def create_object_range(self, start, end, prefix="", + children=None, parent_list=None): + """ + Creates a block of objects. Object names are numbered sequentially, + using the optional prefix supplied. If the children parameter is + supplied it will create a parent-child hierarchy and return the + top-level parents separately. + """ + dn_list = [] + + # Use dummy/empty lists if we're not creating a parent/child hierarchy + if children is None: + children = [] + + if parent_list is None: + parent_list = [] + + # Create the parents first, then the children. + # This makes it easier to see in debug when GET_ANC takes effect + # because the parent/children become interleaved (by default, + # this approach means the objects are organized into blocks of + # parents and blocks of children together) + for x in range(start, end): + ou = "OU=test_ou_%s%d,%s" % (prefix, x, self.ou) + self.add_object(ou) + dn_list.append(ou) + + # keep track of the top-level parents (if needed) + parent_list.append(ou) + + # create the block of children (if needed) + for x in range(start, end): + for child in children: + ou = "OU=test_ou_child%s%d,%s" % (child, x, parent_list[x]) + self.add_object(ou) + dn_list.append(ou) + + return dn_list + + def assert_expected_data(self, expected_list): + """ + Asserts that we received all the DNs that we expected and + none are missing. + """ + received_list = self.rxd_dn_list + + # Note that with GET_ANC Windows can end up sending the same parent + # object multiple times, so this might be noteworthy but doesn't + # warrant failing the test + num_received = len(received_list) + num_expected = len(expected_list) + if num_received != num_expected: + print("Note: received %d objects but expected %d" % (num_received, + num_expected)) + + # Check that we received every object that we were expecting + for dn in expected_list: + self.assertTrue(dn in received_list, + "DN '%s' missing from replication." % dn) + + def test_repl_integrity(self): + """ + Modify the objects being replicated while the replication is still + in progress and check that no object loss occurs. + """ + + # The server behaviour differs between samba and Windows. Samba returns + # the objects in the original order (up to the pre-modify HWM). Windows + # incorporates the modified objects and returns them in the new order + # (i.e. modified objects last), up to the post-modify HWM. The + # Microsoft docs state the Windows behaviour is optional. + + # Create a range of objects to replicate. + expected_dn_list = self.create_object_range(0, 400) + (orig_hwm, unused) = self._get_highest_hwm_utdv(self.test_ldb_dc) + + # We ask for the first page of 100 objects. + # For this test, we don't care what order we receive the objects in, + # so long as by the end we've received everything + self.repl_get_next() + + # Modify some of the second page of objects. This should bump the + # highwatermark + for x in range(100, 200): + self.modify_object(expected_dn_list[x], "displayName", "OU%d" % x) + + (post_modify_hwm, _) = self._get_highest_hwm_utdv(self.test_ldb_dc) + self.assertTrue(post_modify_hwm.highest_usn > orig_hwm.highest_usn) + + # Get the remaining blocks of data + while not self.replication_complete(): + self.repl_get_next() + + # Check we still receive all the objects we're expecting + self.assert_expected_data(expected_dn_list) + + def is_parent_known(self, dn, known_dn_list): + """ + Returns True if the parent of the dn specified is in known_dn_list + """ + + # we can sometimes get system objects like the RID Manager returned. + # Ignore anything that is not under the test OU we created + if self.ou not in dn: + return True + + # Remove the child portion from the name to get the parent's DN + name_substrings = dn.split(",") + del name_substrings[0] + + parent_dn = ",".join(name_substrings) + + # check either this object is a parent (it's parent is the top-level + # test object), or its parent has been seen previously + return parent_dn == self.ou or parent_dn in known_dn_list + + def _repl_send_request(self, get_anc=False, get_tgt=False): + """ + Sends a GetNCChanges request for the next block of replication data. + """ + + # we're just trying to mimic regular client behaviour here, so just + # use the highwatermark in the last response we received + if self.last_ctr: + highwatermark = self.last_ctr.new_highwatermark + uptodateness_vector = self.last_ctr.uptodateness_vector + else: + # this is the first replication chunk + highwatermark = None + uptodateness_vector = None + + # Ask for the next block of replication data + replica_flags = drsuapi.DRSUAPI_DRS_WRIT_REP + more_flags = 0 + + if get_anc: + replica_flags |= drsuapi.DRSUAPI_DRS_GET_ANC + self.used_get_anc = True + + if get_tgt: + more_flags = drsuapi.DRSUAPI_DRS_GET_TGT + self.used_get_tgt = True + + # return the response from the DC + return self._get_replication(replica_flags, + max_objects=self.max_objects, + highwatermark=highwatermark, + uptodateness_vector=uptodateness_vector, + + more_flags=more_flags) + + def repl_get_next(self, get_anc=False, get_tgt=False, assert_links=False): + """ + Requests the next block of replication data. This tries to simulate + client behaviour - if we receive a replicated object that we don't know + the parent of, then re-request the block with the GET_ANC flag set. + If we don't know the target object for a linked attribute, then + re-request with GET_TGT. + """ + + # send a request to the DC and get the response + ctr6 = self._repl_send_request(get_anc=get_anc, get_tgt=get_tgt) + + # extract the object DNs and their GUIDs from the response + rxd_dn_list = self._get_ctr6_dn_list(ctr6) + rxd_guid_list = self._get_ctr6_object_guids(ctr6) + + # we'll add new objects as we discover them, so take a copy of the + # ones we already know about, so we can modify these lists safely + known_objects = self.rxd_dn_list[:] + known_guids = self.rxd_guids[:] + + # check that we know the parent for every object received + for i in range(0, len(rxd_dn_list)): + + dn = rxd_dn_list[i] + guid = rxd_guid_list[i] + + if self.is_parent_known(dn, known_objects): + + # the new DN is now known so add it to the list. + # It may be the parent of another child in this block + known_objects.append(dn) + known_guids.append(guid) + else: + # If we've already set the GET_ANC flag then it should mean + # we receive the parents before the child + self.assertFalse(get_anc, "Unknown parent for object %s" % dn) + + print("Unknown parent for %s - try GET_ANC" % dn) + + # try the same thing again with the GET_ANC flag set this time + return self.repl_get_next(get_anc=True, get_tgt=get_tgt, + assert_links=assert_links) + + # check we know about references to any objects in the linked attrs + received_links = self._get_ctr6_links(ctr6) + + # This is so that older versions of Samba fail - we want the links to + # be sent roughly with the objects, rather than getting all links at + # the end + if assert_links: + self.assertTrue(len(received_links) > 0, + "Links were expected in the GetNCChanges response") + + for link in received_links: + + # skip any links that aren't part of the test + if self.ou not in link.targetDN: + continue + + # check the source object is known (Windows can actually send links + # where we don't know the source object yet). Samba shouldn't ever + # hit this case because it gets the links based on the source + if link.identifier not in known_guids: + + # If we've already set the GET_ANC flag then it should mean + # this case doesn't happen + self.assertFalse(get_anc, "Unknown source object for GUID %s" + % link.identifier) + + print("Unknown source GUID %s - try GET_ANC" % link.identifier) + + # try the same thing again with the GET_ANC flag set this time + return self.repl_get_next(get_anc=True, get_tgt=get_tgt, + assert_links=assert_links) + + # check we know the target object + if link.targetGUID not in known_guids: + + # If we've already set the GET_TGT flag then we should have + # already received any objects we need to know about + self.assertFalse(get_tgt, "Unknown linked target for object %s" + % link.targetDN) + + print("Unknown target for %s - try GET_TGT" % link.targetDN) + + # try the same thing again with the GET_TGT flag set this time + return self.repl_get_next(get_anc=get_anc, get_tgt=True, + assert_links=assert_links) + + # store the last successful result so we know what HWM to request next + self.last_ctr = ctr6 + + # store the objects, GUIDs, and links we received + self.rxd_dn_list += self._get_ctr6_dn_list(ctr6) + self.rxd_links += self._get_ctr6_links(ctr6) + self.rxd_guids += self._get_ctr6_object_guids(ctr6) + + return ctr6 + + def replication_complete(self): + """Returns True if the current/last replication cycle is complete""" + + if self.last_ctr is None or self.last_ctr.more_data: + return False + else: + return True + + def test_repl_integrity_get_anc(self): + """ + Modify the parent objects being replicated while the replication is + still in progress (using GET_ANC) and check that no object loss occurs. + """ + + # Note that GET_ANC behaviour varies between Windows and Samba. + # On Samba GET_ANC results in the replication restarting from the very + # beginning. After that, Samba remembers GET_ANC and also sends the + # parents in subsequent requests (regardless of whether GET_ANC is + # specified in the later request). + # Windows only sends the parents if GET_ANC was specified in the last + # request. It will also resend a parent, even if it's already sent the + # parent in a previous response (whereas Samba doesn't). + + # Create a small block of 50 parents, each with 2 children (A and B) + # This is so that we receive some children in the first block, so we + # can resend with GET_ANC before we learn too many parents + parent_dn_list = [] + expected_dn_list = self.create_object_range(0, 50, prefix="parent", + children=("A", "B"), + parent_list=parent_dn_list) + + # create the remaining parents and children + expected_dn_list += self.create_object_range(50, 150, prefix="parent", + children=("A", "B"), + parent_list=parent_dn_list) + + # We've now got objects in the following order: + # [50 parents][100 children][100 parents][200 children] + + # Modify the first parent so that it's now ordered last by USN + # This means we set the GET_ANC flag pretty much straight away + # because we receive the first child before the first parent + self.modify_object(parent_dn_list[0], "displayName", "OU0") + + # modify a later block of parents so they also get reordered + for x in range(50, 100): + self.modify_object(parent_dn_list[x], "displayName", "OU%d" % x) + + # Get the first block of objects - this should resend the request with + # GET_ANC set because we won't know about the first child's parent. + # On samba GET_ANC essentially starts the sync from scratch again, so + # we get this over with early before we learn too many parents + self.repl_get_next() + + # modify the last chunk of parents. They should now have a USN higher + # than the highwater-mark for the replication cycle + for x in range(100, 150): + self.modify_object(parent_dn_list[x], "displayName", "OU%d" % x) + + # Get the remaining blocks of data - this will resend the request with + # GET_ANC if it encounters an object it doesn't have the parent for. + while not self.replication_complete(): + self.repl_get_next() + + # The way the test objects have been created should force + # self.repl_get_next() to use the GET_ANC flag. If this doesn't + # actually happen, then the test isn't doing its job properly + self.assertTrue(self.used_get_anc, + "Test didn't use the GET_ANC flag as expected") + + # Check we get all the objects we're expecting + self.assert_expected_data(expected_dn_list) + + def assert_expected_links(self, objects_with_links, link_attr="managedBy", + num_expected=None): + """ + Asserts that a GetNCChanges response contains any expected links + for the objects it contains. + """ + received_links = self.rxd_links + + if num_expected is None: + num_expected = len(objects_with_links) + + self.assertTrue(len(received_links) == num_expected, + "Received %d links but expected %d" + % (len(received_links), num_expected)) + + for dn in objects_with_links: + self.assert_object_has_link(dn, link_attr, received_links) + + def assert_object_has_link(self, dn, link_attr, received_links): + """ + Queries the object in the DB and asserts there is a link in the + GetNCChanges response that matches. + """ + + # Look up the link attribute in the DB + # The extended_dn option will dump the GUID info for the link + # attribute (as a hex blob) + res = self.test_ldb_dc.search(ldb.Dn(self.test_ldb_dc, dn), + attrs=[link_attr], + controls=['extended_dn:1:0'], + scope=ldb.SCOPE_BASE) + + # We didn't find the expected link attribute in the DB for the object. + # Something has gone wrong somewhere... + self.assertTrue(link_attr in res[0], + "%s in DB doesn't have attribute %s" % (dn, link_attr)) + + # find the received link in the list and assert that the target and + # source GUIDs match what's in the DB + for val in [str(val) for val in res[0][link_attr]]: + # Work out the expected source and target GUIDs for the DB link + target_dn = ldb.Dn(self.test_ldb_dc, val) + targetGUID_blob = target_dn.get_extended_component("GUID") + sourceGUID_blob = res[0].dn.get_extended_component("GUID") + + found = False + + for link in received_links: + if link.selfGUID_blob == sourceGUID_blob and \ + link.targetGUID_blob == targetGUID_blob: + + found = True + + if self._debug: + print("Link %s --> %s" % (dn[:25], link.targetDN[:25])) + break + + self.assertTrue(found, + "Did not receive expected link for DN %s" % dn) + + def test_repl_get_tgt(self): + """ + Creates a scenario where we should receive the linked attribute before + we know about the target object, and therefore need to use GET_TGT. + Note: Samba currently avoids this problem by sending all its links last + """ + + # create the test objects + reportees = self.create_object_range(0, 100, prefix="reportee") + managers = self.create_object_range(0, 100, prefix="manager") + all_objects = managers + reportees + expected_links = reportees + + # add a link attribute to each reportee object that points to the + # corresponding manager object as the target + for i in range(0, 100): + self.modify_object(reportees[i], "managedBy", managers[i]) + + # touch the managers (the link-target objects) again to make sure the + # reportees (link source objects) get returned first by the replication + for i in range(0, 100): + self.modify_object(managers[i], "displayName", "OU%d" % i) + + links_expected = True + + # Get all the replication data - this code should resend the requests + # with GET_TGT + while not self.replication_complete(): + + # get the next block of replication data (this sets GET_TGT + # if needed) + self.repl_get_next(assert_links=links_expected) + links_expected = len(self.rxd_links) < len(expected_links) + + # The way the test objects have been created should force + # self.repl_get_next() to use the GET_TGT flag. If this doesn't + # actually happen, then the test isn't doing its job properly + self.assertTrue(self.used_get_tgt, + "Test didn't use the GET_TGT flag as expected") + + # Check we get all the objects we're expecting + self.assert_expected_data(all_objects) + + # Check we received links for all the reportees + self.assert_expected_links(expected_links) + + def test_repl_get_tgt_chain(self): + """ + Tests the behaviour of GET_TGT with a more complicated scenario. + Here we create a chain of objects linked together, so if we follow + the link target, then we'd traverse ~200 objects each time. + """ + + # create the test objects + objectsA = self.create_object_range(0, 100, prefix="AAA") + objectsB = self.create_object_range(0, 100, prefix="BBB") + objectsC = self.create_object_range(0, 100, prefix="CCC") + + # create a complex set of object links: + # A0-->B0-->C1-->B2-->C3-->B4-->and so on... + # Basically each object-A should link to a circular chain of 200 B/C + # objects. We create the links in separate chunks here, as it makes it + # clearer what happens with the USN (links on Windows have their own + # USN, so this approach means the A->B/B->C links aren't interleaved) + for i in range(0, 100): + self.modify_object(objectsA[i], "managedBy", objectsB[i]) + + for i in range(0, 100): + self.modify_object(objectsB[i], "managedBy", + objectsC[(i + 1) % 100]) + + for i in range(0, 100): + self.modify_object(objectsC[i], "managedBy", + objectsB[(i + 1) % 100]) + + all_objects = objectsA + objectsB + objectsC + expected_links = all_objects + + # the default order the objects now get returned in should be: + # [A0-A99][B0-B99][C0-C99] + + links_expected = True + + # Get all the replication data - this code should resend the requests + # with GET_TGT + while not self.replication_complete(): + + # get the next block of replication data (this sets GET_TGT + # if needed) + self.repl_get_next(assert_links=links_expected) + links_expected = len(self.rxd_links) < len(expected_links) + + # The way the test objects have been created should force + # self.repl_get_next() to use the GET_TGT flag. If this doesn't + # actually happen, then the test isn't doing its job properly + self.assertTrue(self.used_get_tgt, + "Test didn't use the GET_TGT flag as expected") + + # Check we get all the objects we're expecting + self.assert_expected_data(all_objects) + + # Check we received links for all the reportees + self.assert_expected_links(expected_links) + + def test_repl_integrity_link_attr(self): + """ + Tests adding links to new objects while a replication is in progress. + """ + + # create some source objects for the linked attributes, sandwiched + # between 2 blocks of filler objects + filler = self.create_object_range(0, 100, prefix="filler") + reportees = self.create_object_range(0, 100, prefix="reportee") + filler += self.create_object_range(100, 200, prefix="filler") + + # Start the replication and get the first block of filler objects + # (We're being mean here and setting the GET_TGT flag right from the + # start. On earlier Samba versions, if the client encountered an + # unknown target object and retried with GET_TGT, it would restart the + # replication cycle from scratch, which avoids the problem). + self.repl_get_next(get_tgt=True) + + # create the target objects and add the links. These objects should be + # outside the scope of the Samba replication cycle, but the links + # should still get sent with the source object + managers = self.create_object_range(0, 100, prefix="manager") + + for i in range(0, 100): + self.modify_object(reportees[i], "managedBy", managers[i]) + + expected_objects = managers + reportees + filler + expected_links = reportees + + # complete the replication + while not self.replication_complete(): + self.repl_get_next(get_tgt=True) + + # If we didn't receive the most recently created objects in the last + # replication cycle, then kick off another replication to get them + if len(self.rxd_dn_list) < len(expected_objects): + self.repl_get_next() + + while not self.replication_complete(): + self.repl_get_next() + + # Check we get all the objects we're expecting + self.assert_expected_data(expected_objects) + + # Check we received links for all the parents + self.assert_expected_links(expected_links) + + def test_repl_get_anc_link_attr(self): + """ + A basic GET_ANC test where the parents have linked attributes + """ + + # Create a block of 100 parents and 100 children + parent_dn_list = [] + expected_dn_list = self.create_object_range(0, 100, prefix="parent", + children=("A"), + parent_list=parent_dn_list) + + # Add links from the parents to the children + for x in range(0, 100): + self.modify_object(parent_dn_list[x], "managedBy", + expected_dn_list[x + 100]) + + # add some filler objects at the end. This allows us to easily see + # which chunk the links get sent in + expected_dn_list += self.create_object_range(0, 100, prefix="filler") + + # We've now got objects in the following order: + # [100 x children][100 x parents][100 x filler] + + # Get the replication data - because the block of children come first, + # this should retry the request with GET_ANC + while not self.replication_complete(): + self.repl_get_next() + + self.assertTrue(self.used_get_anc, + "Test didn't use the GET_ANC flag as expected") + + # Check we get all the objects we're expecting + self.assert_expected_data(expected_dn_list) + + # Check we received links for all the parents + self.assert_expected_links(parent_dn_list) + + def test_repl_get_tgt_and_anc(self): + """ + Check we can resolve an unknown ancestor when fetching the link target, + i.e. tests using GET_TGT and GET_ANC in combination + """ + + # Create some parent/child objects (the child will be the link target) + parents = [] + all_objects = self.create_object_range(0, 100, prefix="parent", + children=["la_tgt"], + parent_list=parents) + + children = [item for item in all_objects if item not in parents] + + # create the link source objects and link them to the child/target + la_sources = self.create_object_range(0, 100, prefix="la_src") + all_objects += la_sources + + for i in range(0, 100): + self.modify_object(la_sources[i], "managedBy", children[i]) + + expected_links = la_sources + + # modify the children/targets so they come after the link source + for x in range(0, 100): + self.modify_object(children[x], "displayName", "OU%d" % x) + + # modify the parents, so they now come last in the replication + for x in range(0, 100): + self.modify_object(parents[x], "displayName", "OU%d" % x) + + # We've now got objects in the following order: + # [100 la_source][100 la_target][100 parents (of la_target)] + + links_expected = True + + # Get all the replication data - this code should resend the requests + # with GET_TGT and GET_ANC + while not self.replication_complete(): + + # get the next block of replication data (this sets + # GET_TGT/GET_ANC) + self.repl_get_next(assert_links=links_expected) + links_expected = len(self.rxd_links) < len(expected_links) + + # The way the test objects have been created should force + # self.repl_get_next() to use the GET_TGT/GET_ANC flags. If this + # doesn't actually happen, then the test isn't doing its job properly + self.assertTrue(self.used_get_tgt, + "Test didn't use the GET_TGT flag as expected") + self.assertTrue(self.used_get_anc, + "Test didn't use the GET_ANC flag as expected") + + # Check we get all the objects we're expecting + self.assert_expected_data(all_objects) + + # Check we received links for all the link sources + self.assert_expected_links(expected_links) + + # Second part of test. Add some extra objects and kick off another + # replication. The test code will use the HWM from the last replication + # so we'll only receive the objects we modify below + self.start_new_repl_cycle() + + # add an extra level of grandchildren that hang off a child + # that got created last time + new_parent = "OU=test_new_parent,%s" % children[0] + self.add_object(new_parent) + new_children = [] + + for x in range(0, 50): + dn = "OU=test_new_la_tgt%d,%s" % (x, new_parent) + self.add_object(dn) + new_children.append(dn) + + # replace half of the links to point to the new children + for x in range(0, 50): + self.delete_attribute(la_sources[x], "managedBy", children[x]) + self.modify_object(la_sources[x], "managedBy", new_children[x]) + + # add some filler objects to fill up the 1st chunk + filler = self.create_object_range(0, 100, prefix="filler") + + # modify the new children/targets so they come after the link source + for x in range(0, 50): + self.modify_object(new_children[x], "displayName", "OU-%d" % x) + + # modify the parent, so it now comes last in the replication + self.modify_object(new_parent, "displayName", "OU%d" % x) + + # We should now get the modified objects in the following order: + # [50 links (x 2)][100 filler][50 new children][new parent] + # Note that the link sources aren't actually sent (their new linked + # attributes are sent, but apart from that, nothing has changed) + all_objects = filler + new_children + [new_parent] + expected_links = la_sources[:50] + + links_expected = True + + while not self.replication_complete(): + self.repl_get_next(assert_links=links_expected) + links_expected = len(self.rxd_links) < len(expected_links) + + self.assertTrue(self.used_get_tgt, + "Test didn't use the GET_TGT flag as expected") + self.assertTrue(self.used_get_anc, + "Test didn't use the GET_ANC flag as expected") + + # Check we get all the objects we're expecting + self.assert_expected_data(all_objects) + + # Check we received links (50 deleted links and 50 new) + self.assert_expected_links(expected_links, num_expected=100) + + def _repl_integrity_obj_deletion(self, delete_link_source=True): + """ + Tests deleting link objects while a replication is in progress. + """ + + # create some objects and link them together, with some filler + # object in between the link sources + la_sources = self.create_object_range(0, 100, prefix="la_source") + la_targets = self.create_object_range(0, 100, prefix="la_targets") + + for i in range(0, 50): + self.modify_object(la_sources[i], "managedBy", la_targets[i]) + + filler = self.create_object_range(0, 100, prefix="filler") + + for i in range(50, 100): + self.modify_object(la_sources[i], "managedBy", la_targets[i]) + + # touch the targets so that the sources get replicated first + for i in range(0, 100): + self.modify_object(la_targets[i], "displayName", "OU%d" % i) + + # objects should now be in the following USN order: + # [50 la_source][100 filler][50 la_source][100 la_target] + + # Get the first block containing 50 link sources + self.repl_get_next() + + # delete either the link targets or link source objects + if delete_link_source: + objects_to_delete = la_sources + # in GET_TGT testenvs we only receive the first 50 source objects + expected_objects = la_sources[:50] + la_targets + filler + else: + objects_to_delete = la_targets + expected_objects = la_sources + filler + + for obj in objects_to_delete: + self.ldb_dc2.delete(obj) + + # complete the replication + while not self.replication_complete(): + self.repl_get_next() + + # Check we get all the objects we're expecting + self.assert_expected_data(expected_objects) + + # we can't use assert_expected_links() here because it tries to check + # against the deleted objects on the DC. (Although we receive some + # links from the first block processed, the Samba client should end up + # deleting these, as the source/target object involved is deleted) + self.assertTrue(len(self.rxd_links) == 50, + "Expected 50 links, not %d" % len(self.rxd_links)) + + def test_repl_integrity_src_obj_deletion(self): + self._repl_integrity_obj_deletion(delete_link_source=True) + + def test_repl_integrity_tgt_obj_deletion(self): + self._repl_integrity_obj_deletion(delete_link_source=False) + + def restore_deleted_object(self, guid, new_dn): + """Re-animates a deleted object""" + + guid_str = self._GUID_string(guid) + res = self.test_ldb_dc.search(base="<GUID=%s>" % guid_str, + attrs=["isDeleted"], + controls=['show_deleted:1'], + scope=ldb.SCOPE_BASE) + if len(res) != 1: + return + + msg = ldb.Message() + msg.dn = res[0].dn + msg["isDeleted"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, + "isDeleted") + msg["distinguishedName"] = ldb.MessageElement([new_dn], + ldb.FLAG_MOD_REPLACE, + "distinguishedName") + self.test_ldb_dc.modify(msg, ["show_deleted:1"]) + + def sync_DCs(self, nc_dn=None): + # make sure DC1 has all the changes we've made to DC2 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, + nc_dn=nc_dn) + + def get_object_guid(self, dn): + res = self.test_ldb_dc.search(base=dn, attrs=["objectGUID"], + scope=ldb.SCOPE_BASE) + return res[0]['objectGUID'][0] + + def set_dc_connection(self, conn): + """ + Switches over the connection state info that the underlying drs_base + class uses so that we replicate with a different DC. + """ + self.default_hwm = conn.default_hwm + self.default_utdv = conn.default_utdv + self.drs = conn.drs + self.drs_handle = conn.drs_handle + self.set_test_ldb_dc(conn.ldb_dc) + + def assert_DCs_replication_is_consistent(self, peer_conn, all_objects, + expected_links): + """ + Replicates against both the primary and secondary DCs in the testenv + and checks that both return the expected results. + """ + print("Checking replication against primary test DC...") + + # get the replication data from the test DC first + while not self.replication_complete(): + self.repl_get_next() + + # Check we get all the objects and links we're expecting + self.assert_expected_data(all_objects) + self.assert_expected_links(expected_links) + + # switch over the DC state info so we now talk to the peer DC + self.set_dc_connection(peer_conn) + self.init_test_state() + + print("Checking replication against secondary test DC...") + + # check that we get the same information from the 2nd DC + while not self.replication_complete(): + self.repl_get_next() + + self.assert_expected_data(all_objects) + self.assert_expected_links(expected_links) + + # switch back to using the default connection + self.set_dc_connection(self.default_conn) + + def test_repl_integrity_obj_reanimation(self): + """ + Checks receiving links for a re-animated object doesn't lose links. + We test this against the peer DC to make sure it doesn't drop links. + """ + + # This test is a little different in that we're particularly interested + # in exercising the replmd client code on the second DC. + # First, make sure the peer DC has the base OU, then connect to it (so + # we store its initial HWM) + self.sync_DCs() + peer_conn = DcConnection(self, self.ldb_dc1, self.dnsname_dc1) + + # create the link source/target objects + la_sources = self.create_object_range(0, 100, prefix="la_src") + la_targets = self.create_object_range(0, 100, prefix="la_tgt") + + # store the target object's GUIDs (we need to know these to + # reanimate them) + target_guids = [] + + for dn in la_targets: + target_guids.append(self.get_object_guid(dn)) + + # delete the link target + for x in range(0, 100): + self.ldb_dc2.delete(la_targets[x]) + + # sync the DCs, then disable replication. We want the peer DC to get + # all the following changes in a single replication cycle + self.sync_DCs() + self._disable_all_repl(self.dnsname_dc2) + + # restore the target objects for the linked attributes again + for x in range(0, 100): + self.restore_deleted_object(target_guids[x], la_targets[x]) + + # add the links + for x in range(0, 100): + self.modify_object(la_sources[x], "managedBy", la_targets[x]) + + # create some additional filler objects + filler = self.create_object_range(0, 100, prefix="filler") + + # modify the targets so they now come last + for x in range(0, 100): + self.modify_object(la_targets[x], "displayName", "OU-%d" % x) + + # the objects should now be sent in the following order: + # [la sources + links][filler][la targets] + all_objects = la_sources + la_targets + filler + expected_links = la_sources + + # Enable replication again make sure the 2 DCs are back in sync + self._enable_all_repl(self.dnsname_dc2) + self.sync_DCs() + + # Get the replication data from each DC in turn. + # Check that both give us all the objects and links we're expecting, + # i.e. no links were lost + self.assert_DCs_replication_is_consistent(peer_conn, all_objects, + expected_links) + + def _test_repl_integrity_cross_partition_links(self, get_tgt=False): + """ + Checks that a cross-partition link to an unknown target object does + not result in missing links. + """ + + # check the peer DC is up-to-date, then connect (storing its HWM) + self.sync_DCs() + peer_conn = DcConnection(self, self.ldb_dc1, self.dnsname_dc1) + + # stop replication so the peer gets the following objects in one go + self._disable_all_repl(self.dnsname_dc2) + + # optionally force the client-side to use GET_TGT locally, by adding a + # one-way link to a missing/deleted target object + if get_tgt: + missing_target = "OU=missing_tgt,%s" % self.ou + self.add_object(missing_target) + get_tgt_source = "CN=get_tgt_src,%s" % self.ou + self.add_object(get_tgt_source, + objectclass="msExchConfigurationContainer") + self.modify_object(get_tgt_source, "addressBookRoots2", + missing_target) + self.test_ldb_dc.delete(missing_target) + + # create a link source object in the main NC + la_source = "OU=cross_nc_src,%s" % self.ou + self.add_object(la_source) + + # create the link target (a server object) in the config NC + sites_dn = "CN=Sites,%s" % self.config_dn + servers_dn = "CN=Servers,CN=Default-First-Site-Name,%s" % sites_dn + rand = random.randint(1, 10000000) + la_target = "CN=getncchanges-%d,%s" % (rand, servers_dn) + self.add_object(la_target, objectclass="server") + + # add a cross-partition link between the two + self.modify_object(la_source, "managedBy", la_target) + + # First, sync to the peer the NC containing the link source object + self.sync_DCs() + + # Now, before the peer has received the partition containing the target + # object, try replicating from the peer. It will only know about half + # of the link at this point, but it should be a valid scenario + self.set_dc_connection(peer_conn) + + while not self.replication_complete(): + # pretend we've received other link targets out of order and that's + # forced us to use GET_TGT. This checks the peer doesn't fail + # trying to fetch a cross-partition target object that doesn't + # exist + self.repl_get_next(get_tgt=True) + + self.set_dc_connection(self.default_conn) + + # delete the GET_TGT test object. We're not interested in asserting its + # links - it was just there to make the client use GET_TGT (and it + # creates an inconsistency because one DC correctly ignores the link, + # because it points to a deleted object) + if get_tgt: + self.test_ldb_dc.delete(get_tgt_source) + + self.init_test_state() + + # Now sync across the partition containing the link target object + self.sync_DCs(nc_dn=self.config_dn) + self._enable_all_repl(self.dnsname_dc2) + + # Get the replication data from each DC in turn. + # Check that both return the cross-partition link (note we're not + # checking the config domain NC here for simplicity) + self.assert_DCs_replication_is_consistent(peer_conn, + all_objects=[la_source], + expected_links=[la_source]) + + # the cross-partition linked attribute has a missing backlink. Check + # that we can still delete it successfully + self.delete_attribute(la_source, "managedBy", la_target) + self.sync_DCs() + + res = self.test_ldb_dc.search(ldb.Dn(self.ldb_dc1, la_source), + attrs=["managedBy"], + controls=['extended_dn:1:0'], + scope=ldb.SCOPE_BASE) + self.assertFalse("managedBy" in res[0], + "%s in DB still has managedBy attribute" % la_source) + res = self.test_ldb_dc.search(ldb.Dn(self.ldb_dc2, la_source), + attrs=["managedBy"], + controls=['extended_dn:1:0'], + scope=ldb.SCOPE_BASE) + self.assertFalse("managedBy" in res[0], + "%s in DB still has managedBy attribute" % la_source) + + # Check receiving a cross-partition link to a deleted target. + # Delete the target and make sure the deletion is sync'd between DCs + target_guid = self.get_object_guid(la_target) + self.test_ldb_dc.delete(la_target) + self.sync_DCs(nc_dn=self.config_dn) + self._disable_all_repl(self.dnsname_dc2) + + # re-animate the target + self.restore_deleted_object(target_guid, la_target) + self.modify_object(la_source, "managedBy", la_target) + + # now sync the link - because the target is in another partition, the + # peer DC receives a link for a deleted target, which it should accept + self.sync_DCs() + res = self.test_ldb_dc.search(ldb.Dn(self.ldb_dc1, la_source), + attrs=["managedBy"], + controls=['extended_dn:1:0'], + scope=ldb.SCOPE_BASE) + self.assertTrue("managedBy" in res[0], + "%s in DB missing managedBy attribute" % la_source) + + # cleanup the server object we created in the Configuration partition + self.test_ldb_dc.delete(la_target) + self._enable_all_repl(self.dnsname_dc2) + + def test_repl_integrity_cross_partition_links(self): + self._test_repl_integrity_cross_partition_links(get_tgt=False) + + def test_repl_integrity_cross_partition_links_with_tgt(self): + self._test_repl_integrity_cross_partition_links(get_tgt=True) + + def test_repl_get_tgt_multivalued_links(self): + """Tests replication with multi-valued link attributes.""" + + # create the target/source objects and link them together + la_targets = self.create_object_range(0, 500, prefix="la_tgt") + la_source = "CN=la_src,%s" % self.ou + self.add_object(la_source, objectclass="msExchConfigurationContainer") + + for tgt in la_targets: + self.modify_object(la_source, "addressBookRoots2", tgt) + + filler = self.create_object_range(0, 100, prefix="filler") + + # We should receive the objects/links in the following order: + # [500 targets + 1 source][500 links][100 filler] + expected_objects = la_targets + [la_source] + filler + link_only_chunk = False + + # First do the replication without needing GET_TGT + while not self.replication_complete(): + ctr6 = self.repl_get_next() + + if ctr6.object_count == 0 and ctr6.linked_attributes_count != 0: + link_only_chunk = True + + # we should receive one chunk that contains only links + self.assertTrue(link_only_chunk, + "Expected to receive a chunk containing only links") + + # check we received all the expected objects/links + self.assert_expected_data(expected_objects) + self.assert_expected_links([la_source], link_attr="addressBookRoots2", + num_expected=500) + + # Do the replication again, forcing the use of GET_TGT this time + self.init_test_state() + + for x in range(0, 500): + self.modify_object(la_targets[x], "displayName", "OU-%d" % x) + + # The objects/links should get sent in the following order: + # [1 source][500 targets][500 links][100 filler] + + while not self.replication_complete(): + ctr6 = self.repl_get_next() + + self.assertTrue(self.used_get_tgt, + "Test didn't use the GET_TGT flag as expected") + + # check we received all the expected objects/links + self.assert_expected_data(expected_objects) + self.assert_expected_links([la_source], link_attr="addressBookRoots2", + num_expected=500) + + + def test_InvalidNC_DummyDN_InvalidGUID_full_repl(self): + """Test full replication on a totally invalid GUID fails with the right error code""" + dc_guid_1 = self.ldb_dc1.get_invocation_id() + drs, drs_handle = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + + req8 = self._exop_req8(dest_dsa="9c637462-5b8c-4467-aef2-bdb1f57bc4ef", + invocation_id=dc_guid_1, + nc_dn_str="DummyDN", + nc_guid=misc.GUID("c2d2f745-1610-4e93-964b-d4ba73eb32f8"), + exop=drsuapi.DRSUAPI_EXOP_NONE, + max_objects=1) + + (drs, drs_handle) = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + try: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + except WERRORError as e1: + (enum, estr) = e1.args + self.assertEqual(enum, werror.WERR_DS_DRA_BAD_NC) + + def test_DummyDN_valid_GUID_full_repl(self): + dc_guid_1 = self.ldb_dc1.get_invocation_id() + drs, drs_handle = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + + res = self.ldb_dc1.search(base=self.base_dn, scope=SCOPE_BASE, + attrs=["objectGUID"]) + + guid = misc.GUID(res[0]["objectGUID"][0]) + + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str="DummyDN", + nc_guid=guid, + replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP | + drsuapi.DRSUAPI_DRS_GET_ANC, + exop=drsuapi.DRSUAPI_EXOP_NONE, + max_objects=1) + + try: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + except WERRORError as e1: + (enum, estr) = e1.args + self.fail(f"Failed to call GetNCChanges with DummyDN and a GUID: {estr}") + + # The NC should be the first object returned due to GET_ANC + self.assertEqual(ctr.first_object.object.identifier.guid, guid) + + def _test_do_full_repl_no_overlap(self, mix=True, get_anc=False): + self.default_hwm = drsuapi.DsReplicaHighWaterMark() + + # We set get_anc=True so we can assert the BASE DN will be the + # first object + ctr6 = self._repl_send_request(get_anc=get_anc) + guid_list_1 = self._get_ctr6_object_guids(ctr6) + + if mix: + dc_guid_1 = self.ldb_dc1.get_invocation_id() + + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=self.ldb_dc1.get_default_basedn(), + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ) + + (level, ctr_repl_obj) = self.drs.DsGetNCChanges(self.drs_handle, 8, req8) + + self.assertEqual(ctr_repl_obj.extended_ret, drsuapi.DRSUAPI_EXOP_ERR_SUCCESS) + + repl_obj_guid_list = self._get_ctr6_object_guids(ctr_repl_obj) + + self.assertEqual(len(repl_obj_guid_list), 1) + + # This should be the first object in the main replication due + # to get_anc=True above in one case, and a rule that the NC must be first regardless otherwise + self.assertEqual(repl_obj_guid_list[0], guid_list_1[0]) + + self.last_ctr = ctr6 + ctr6 = self._repl_send_request(get_anc=True) + guid_list_2 = self._get_ctr6_object_guids(ctr6) + + self.assertNotEqual(guid_list_1, guid_list_2) + + def test_do_full_repl_no_overlap_get_anc(self): + """ + Make sure that a full replication on an nc succeeds to the goal despite needing multiple passes + """ + self._test_do_full_repl_no_overlap(mix=False, get_anc=True) + + def test_do_full_repl_no_overlap(self): + """ + Make sure that a full replication on an nc succeeds to the goal despite needing multiple passes + """ + self._test_do_full_repl_no_overlap(mix=False) + + def test_do_full_repl_mix_no_overlap(self): + """ + Make sure that a full replication on an nc succeeds to the goal despite needing multiple passes + + Assert this is true even if we do a REPL_OBJ in between the replications + + """ + self._test_do_full_repl_no_overlap(mix=True) + + def nc_change(self): + old_base_msg = self.default_conn.ldb_dc.search(base=self.base_dn, + scope=SCOPE_BASE, + attrs=["oEMInformation"]) + rec_cleanup = {"dn": self.base_dn, + "oEMInformation": old_base_msg[0]["oEMInformation"][0]} + m_cleanup = ldb.Message.from_dict(self.default_conn.ldb_dc, + rec_cleanup, + ldb.FLAG_MOD_REPLACE) + + self.addCleanup(self.default_conn.ldb_dc.modify, m_cleanup) + + rec = {"dn": self.base_dn, + "oEMInformation": f"Tortured by Samba's getncchanges.py {self.id()} against {self.default_conn.dnsname_dc}"} + m = ldb.Message.from_dict(self.default_conn.ldb_dc, rec, ldb.FLAG_MOD_REPLACE) + self.default_conn.ldb_dc.modify(m) + + def _test_repl_nc_is_first(self, start_at_zero=True, nc_change=True, ou_change=True, mid_change=False): + """Tests that the NC is always replicated first, but does not move the + tmp_highest_usn at that point, just like 'early' GET_ANC objects. + """ + + # create objects, twice more than the page size of 133 + objs = self.create_object_range(0, 300, prefix="obj") + + if nc_change: + self.nc_change() + + if mid_change: + # create even more objects + objs = self.create_object_range(301, 450, prefix="obj2") + + base_msg = self.default_conn.ldb_dc.search(base=self.base_dn, + scope=SCOPE_BASE, + attrs=["uSNChanged", + "objectGUID"]) + + base_guid = misc.GUID(base_msg[0]["objectGUID"][0]) + base_usn = int(base_msg[0]["uSNChanged"][0]) + + if ou_change: + # Make one more modification. We want to assert we have + # caught up to the base DN, but Windows both promotes the NC + # to the front and skips including it in the tmp_highest_usn, + # so we make a later modification that will be to show we get + # this change. + rec = {"dn": self.ou, + "postalCode": "0"} + m = ldb.Message.from_dict(self.default_conn.ldb_dc, rec, ldb.FLAG_MOD_REPLACE) + self.default_conn.ldb_dc.modify(m) + + ou_msg = self.default_conn.ldb_dc.search(base=self.ou, + scope=SCOPE_BASE, + attrs=["uSNChanged", + "objectGUID"]) + + ou_guid = misc.GUID(ou_msg[0]["objectGUID"][0]) + ou_usn = int(ou_msg[0]["uSNChanged"][0]) + + # Check some predicates about USN ordering that the below tests will rely on + if ou_change and nc_change: + self.assertGreater(ou_usn, base_usn) + elif not ou_change and nc_change: + self.assertGreater(base_usn, ou_usn) + + ctr6 = self.repl_get_next() + + guid_list_1 = self._get_ctr6_object_guids(ctr6) + if nc_change or start_at_zero: + self.assertEqual(base_guid, misc.GUID(guid_list_1[0])) + self.assertIn(str(base_guid), guid_list_1) + self.assertNotIn(str(base_guid), guid_list_1[1:]) + else: + self.assertNotEqual(base_guid, misc.GUID(guid_list_1[0])) + self.assertNotIn(str(base_guid), guid_list_1) + + self.assertTrue(ctr6.more_data) + + if not ou_change and nc_change: + self.assertLess(ctr6.new_highwatermark.tmp_highest_usn, base_usn) + + i = 0 + while not self.replication_complete(): + i = i + 1 + last_tmp_highest_usn = ctr6.new_highwatermark.tmp_highest_usn + ctr6 = self.repl_get_next() + guid_list_2 = self._get_ctr6_object_guids(ctr6) + if len(guid_list_2) > 0: + self.assertNotEqual(last_tmp_highest_usn, ctr6.new_highwatermark.tmp_highest_usn) + + if (nc_change or start_at_zero) and base_usn > last_tmp_highest_usn: + self.assertEqual(base_guid, misc.GUID(guid_list_2[0]), + f"pass={i} more_data={ctr6.more_data} base_usn={base_usn} tmp_highest_usn={ctr6.new_highwatermark.tmp_highest_usn} last_tmp_highest_usn={last_tmp_highest_usn}") + self.assertIn(str(base_guid), guid_list_2, + f"pass {i}·more_data={ctr6.more_data} base_usn={base_usn} tmp_highest_usn={ctr6.new_highwatermark.tmp_highest_usn} last_tmp_highest_usn={last_tmp_highest_usn}") + else: + self.assertNotIn(str(base_guid), guid_list_2, + f"pass {i}·more_data={ctr6.more_data} base_usn={base_usn} tmp_highest_usn={ctr6.new_highwatermark.tmp_highest_usn} last_tmp_highest_usn={last_tmp_highest_usn}") + + if ou_change: + # The modification to the base OU should be in the final chunk + self.assertIn(str(ou_guid), guid_list_2) + self.assertGreaterEqual(ctr6.new_highwatermark.highest_usn, + ou_usn) + else: + # Show that the NC root change does not show up in the + # highest_usn. We either get the change before or after + # it. + self.assertNotEqual(ctr6.new_highwatermark.highest_usn, + base_usn) + self.assertEqual(ctr6.new_highwatermark.highest_usn, + ctr6.new_highwatermark.tmp_highest_usn) + + self.assertFalse(ctr6.more_data) + + def test_repl_nc_is_first_start_zero_nc_change(self): + self.default_hwm = drsuapi.DsReplicaHighWaterMark() + self._test_repl_nc_is_first(start_at_zero=True, nc_change=True, ou_change=True) + + def test_repl_nc_is_first_start_zero(self): + # Get the NC change in the middle of the replication stream, certainly not at the start or end + self.nc_change() + self.default_hwm = drsuapi.DsReplicaHighWaterMark() + self._test_repl_nc_is_first(start_at_zero=True, nc_change=False, ou_change=False) + + def test_repl_nc_is_first_mid(self): + # This is a modification of the next test, that Samba + # will pass as it will always include the NC in the + # tmp_highest_usn at the point where it belongs + self._test_repl_nc_is_first(start_at_zero=False, + nc_change=True, + ou_change=True, + mid_change=True) + + def test_repl_nc_is_first(self): + # This is a modification of the next test, that Samba + # will pass as it will always include the NC in the + # tmp_highest_usn at the point where it belongs + self._test_repl_nc_is_first(start_at_zero=False, nc_change=True, ou_change=True) + + def test_repl_nc_is_first_nc_change_only(self): + # This shows that the NC change is not reflected in the tmp_highest_usn + self._test_repl_nc_is_first(start_at_zero=False, nc_change=True, ou_change=False) + + def test_repl_nc_is_first_no_change(self): + # The NC should not be present in this replication + self._test_repl_nc_is_first(start_at_zero=False, nc_change=False, ou_change=False) + +class DcConnection: + """Helper class to track a connection to another DC""" + + def __init__(self, drs_base, ldb_dc, dnsname_dc): + self.ldb_dc = ldb_dc + (self.drs, self.drs_handle) = drs_base._ds_bind(dnsname_dc) + (self.default_hwm, utdv) = drs_base._get_highest_hwm_utdv(ldb_dc) + self.default_utdv = utdv + self.dnsname_dc = dnsname_dc diff --git a/source4/torture/drs/python/link_conflicts.py b/source4/torture/drs/python/link_conflicts.py new file mode 100644 index 0000000..d344b7e --- /dev/null +++ b/source4/torture/drs/python/link_conflicts.py @@ -0,0 +1,763 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Tests replication scenarios that involve conflicting linked attribute +# information between the 2 DCs. +# +# 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/>. +# + +# +# Usage: +# export DC1=dc1_dns_name +# export DC2=dc2_dns_name +# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun +# PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN \ +# link_conflicts -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD" +# + +import drs_base +import samba.tests +import ldb +from ldb import SCOPE_BASE +import random +import time + +from drs_base import AbstractLink +from samba.dcerpc import drsuapi, misc +from samba.dcerpc.drsuapi import DRSUAPI_EXOP_ERR_SUCCESS + +# specifies the order to sync DCs in +DC1_TO_DC2 = 1 +DC2_TO_DC1 = 2 + + +class DrsReplicaLinkConflictTestCase(drs_base.DrsBaseTestCase): + def setUp(self): + super(DrsReplicaLinkConflictTestCase, self).setUp() + + self.ou = samba.tests.create_test_ou(self.ldb_dc1, + "test_link_conflict") + self.base_dn = self.ldb_dc1.get_default_basedn() + + (self.drs, self.drs_handle) = self._ds_bind(self.dnsname_dc1) + (self.drs2, self.drs2_handle) = self._ds_bind(self.dnsname_dc2) + + # disable replication for the tests so we can control at what point + # the DCs try to replicate + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + def tearDown(self): + # re-enable replication + self._enable_inbound_repl(self.dnsname_dc1) + self._enable_inbound_repl(self.dnsname_dc2) + self.ldb_dc1.delete(self.ou, ["tree_delete:1"]) + super(DrsReplicaLinkConflictTestCase, self).tearDown() + + def get_guid(self, samdb, dn): + """Returns an object's GUID (in string format)""" + res = samdb.search(base=dn, attrs=["objectGUID"], scope=ldb.SCOPE_BASE) + return self._GUID_string(res[0]['objectGUID'][0]) + + def add_object(self, samdb, dn, objectclass="organizationalunit"): + """Adds an object""" + samdb.add({"dn": dn, "objectclass": objectclass}) + return self.get_guid(samdb, dn) + + def modify_object(self, samdb, dn, attr, value): + """Modifies an attribute for an object""" + m = ldb.Message() + m.dn = ldb.Dn(samdb, dn) + m[attr] = ldb.MessageElement(value, ldb.FLAG_MOD_ADD, attr) + samdb.modify(m) + + def add_link_attr(self, samdb, source_dn, attr, target_dn): + """Adds a linked attribute between 2 objects""" + # add the specified attribute to the source object + self.modify_object(samdb, source_dn, attr, target_dn) + + def del_link_attr(self, samdb, src, attr, target): + m = ldb.Message() + m.dn = ldb.Dn(samdb, src) + m[attr] = ldb.MessageElement(target, ldb.FLAG_MOD_DELETE, attr) + samdb.modify(m) + + def sync_DCs(self, sync_order=DC1_TO_DC2): + """Manually syncs the 2 DCs to ensure they're in sync""" + if sync_order == DC1_TO_DC2: + # sync DC1-->DC2, then DC2-->DC1 + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1) + self._net_drs_replicate(DC=self.dnsname_dc1, + fromDC=self.dnsname_dc2) + else: + # sync DC2-->DC1, then DC1-->DC2 + self._net_drs_replicate(DC=self.dnsname_dc1, + fromDC=self.dnsname_dc2) + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1) + + def ensure_unique_timestamp(self): + """Waits a second to ensure a unique timestamp between 2 objects""" + time.sleep(1) + + def unique_dn(self, obj_name): + """Returns a unique object DN""" + # Because we run each test case twice, we need to create a unique DN so + # that the 2nd run doesn't hit objects that already exist. Add some + # randomness to the object DN to make it unique + rand = random.randint(1, 10000000) + return "%s-%d,%s" % (obj_name, rand, self.ou) + + def assert_attrs_match(self, res1, res2, attr, expected_count): + """ + Asserts that the search results contain the expected number of + attributes and the results match on both DCs + """ + actual_len = len(res1[0][attr]) + self.assertTrue(actual_len == expected_count, + "Expected %u %s attributes, got %u" % (expected_count, + attr, + actual_len)) + actual_len = len(res2[0][attr]) + self.assertTrue(actual_len == expected_count, + "Expected %u %s attributes, got %u" % (expected_count, + attr, + actual_len)) + + # check DCs both agree on the same linked attributes + for val in res1[0][attr]: + self.assertTrue(val in res2[0][attr], + "%s '%s' not found on DC2" % (attr, val)) + + def zero_highwatermark(self): + """Returns a zeroed highwatermark so that all DRS data gets returned""" + hwm = drsuapi.DsReplicaHighWaterMark() + hwm.tmp_highest_usn = 0 + hwm.reserved_usn = 0 + hwm.highest_usn = 0 + return hwm + + def _check_replicated_links(self, src_obj_dn, expected_links): + """Checks that replication sends back the expected linked attributes""" + self._check_replication([src_obj_dn], + drsuapi.DRSUAPI_DRS_WRIT_REP, + dest_dsa=None, + drs_error=drsuapi.DRSUAPI_EXOP_ERR_SUCCESS, + nc_dn_str=src_obj_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + expected_links=expected_links, + highwatermark=self.zero_highwatermark()) + + # Check DC2 as well + self.set_test_ldb_dc(self.ldb_dc2) + + self._check_replication([src_obj_dn], + drsuapi.DRSUAPI_DRS_WRIT_REP, + dest_dsa=None, + drs_error=drsuapi.DRSUAPI_EXOP_ERR_SUCCESS, + nc_dn_str=src_obj_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + expected_links=expected_links, + highwatermark=self.zero_highwatermark(), + drs=self.drs2, drs_handle=self.drs2_handle) + self.set_test_ldb_dc(self.ldb_dc1) + + def _test_conflict_single_valued_link(self, sync_order): + """ + Tests a simple single-value link conflict, i.e. each DC adds a link to + the same source object but linking to different targets. + """ + src_ou = self.unique_dn("OU=src") + src_guid = self.add_object(self.ldb_dc1, src_ou) + self.sync_DCs() + + # create a unique target on each DC + target1_ou = self.unique_dn("OU=target1") + target2_ou = self.unique_dn("OU=target2") + + target1_guid = self.add_object(self.ldb_dc1, target1_ou) + target2_guid = self.add_object(self.ldb_dc2, target2_ou) + + # link the test OU to the respective targets created + self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou) + self.ensure_unique_timestamp() + self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou) + + # sync the 2 DCs + self.sync_DCs(sync_order=sync_order) + + res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["managedBy"]) + res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["managedBy"]) + + # check the object has only have one occurrence of the single-valued + # attribute and it matches on both DCs + self.assert_attrs_match(res1, res2, "managedBy", 1) + + self.assertTrue(str(res1[0]["managedBy"][0]) == target2_ou, + "Expected most recent update to win conflict") + + # we can't query the deleted links over LDAP, but we can check DRS + # to make sure the DC kept a copy of the conflicting link + link1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, 0, + misc.GUID(src_guid), misc.GUID(target1_guid)) + link2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + misc.GUID(src_guid), misc.GUID(target2_guid)) + self._check_replicated_links(src_ou, [link1, link2]) + + def test_conflict_single_valued_link(self): + # repeat the test twice, to give each DC a chance to resolve + # the conflict + self._test_conflict_single_valued_link(sync_order=DC1_TO_DC2) + self._test_conflict_single_valued_link(sync_order=DC2_TO_DC1) + + def _test_duplicate_single_valued_link(self, sync_order): + """ + Adds the same single-valued link on 2 DCs and checks we don't end up + with 2 copies of the link. + """ + # create unique objects for the link + target_ou = self.unique_dn("OU=target") + self.add_object(self.ldb_dc1, target_ou) + src_ou = self.unique_dn("OU=src") + src_guid = self.add_object(self.ldb_dc1, src_ou) + self.sync_DCs() + + # link the same test OU to the same target on both DCs + self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target_ou) + self.ensure_unique_timestamp() + self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target_ou) + + # sync the 2 DCs + self.sync_DCs(sync_order=sync_order) + + res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["managedBy"]) + res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["managedBy"]) + + # check the object has only have one occurrence of the single-valued + # attribute and it matches on both DCs + self.assert_attrs_match(res1, res2, "managedBy", 1) + + def test_duplicate_single_valued_link(self): + # repeat the test twice, to give each DC a chance to resolve + # the conflict + self._test_duplicate_single_valued_link(sync_order=DC1_TO_DC2) + self._test_duplicate_single_valued_link(sync_order=DC2_TO_DC1) + + def _test_conflict_multi_valued_link(self, sync_order): + """ + Tests a simple multi-valued link conflict. This adds 2 objects with the + same username on 2 different DCs and checks their group membership is + preserved after the conflict is resolved. + """ + + # create a common link source + src_dn = self.unique_dn("CN=src") + src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group") + self.sync_DCs() + + # create the same user (link target) on each DC. + # Note that the GUIDs will differ between the DCs + target_dn = self.unique_dn("CN=target") + target1_guid = self.add_object(self.ldb_dc1, target_dn, + objectclass="user") + self.ensure_unique_timestamp() + target2_guid = self.add_object(self.ldb_dc2, target_dn, + objectclass="user") + + # link the src group to the respective target created + self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn) + self.ensure_unique_timestamp() + self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn) + + # sync the 2 DCs. We expect the more recent target2 object to win + self.sync_DCs(sync_order=sync_order) + + res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["member"]) + res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["member"]) + target1_conflict = False + + # we expect exactly 2 members in our test group (both DCs should agree) + self.assert_attrs_match(res1, res2, "member", 2) + + for val in [str(val) for val in res1[0]["member"]]: + # check the expected conflicting object was renamed + self.assertFalse("CNF:%s" % target2_guid in val) + if "CNF:%s" % target1_guid in val: + target1_conflict = True + + self.assertTrue(target1_conflict, + "Expected link to conflicting target object not found") + + def test_conflict_multi_valued_link(self): + # repeat the test twice, to give each DC a chance to resolve + # the conflict + self._test_conflict_multi_valued_link(sync_order=DC1_TO_DC2) + self._test_conflict_multi_valued_link(sync_order=DC2_TO_DC1) + + def _test_duplicate_multi_valued_link(self, sync_order): + """ + Adds the same multivalued link on 2 DCs and checks we don't end up + with 2 copies of the link. + """ + + # create the link source/target objects + src_dn = self.unique_dn("CN=src") + src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group") + target_dn = self.unique_dn("CN=target") + self.add_object(self.ldb_dc1, target_dn, objectclass="user") + self.sync_DCs() + + # link the src group to the same target user separately on each DC + self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn) + self.ensure_unique_timestamp() + self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn) + + self.sync_DCs(sync_order=sync_order) + + res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["member"]) + res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["member"]) + + # we expect to still have only 1 member in our test group + self.assert_attrs_match(res1, res2, "member", 1) + + def test_duplicate_multi_valued_link(self): + # repeat the test twice, to give each DC a chance to resolve + # the conflict + self._test_duplicate_multi_valued_link(sync_order=DC1_TO_DC2) + self._test_duplicate_multi_valued_link(sync_order=DC2_TO_DC1) + + def _test_conflict_backlinks(self, sync_order): + """ + Tests that resolving a source object conflict fixes up any backlinks, + e.g. the same user is added to a conflicting group. + """ + + # create a common link target + target_dn = self.unique_dn("CN=target") + target_guid = self.add_object(self.ldb_dc1, target_dn, + objectclass="user") + self.sync_DCs() + + # create the same group (link source) on each DC. + # Note that the GUIDs will differ between the DCs + src_dn = self.unique_dn("CN=src") + src1_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group") + self.ensure_unique_timestamp() + src2_guid = self.add_object(self.ldb_dc2, src_dn, objectclass="group") + + # link the src group to the respective target created + self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn) + self.ensure_unique_timestamp() + self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn) + + # sync the 2 DCs. We expect the more recent src2 object to win + self.sync_DCs(sync_order=sync_order) + + res1 = self.ldb_dc1.search(base="<GUID=%s>" % target_guid, + scope=SCOPE_BASE, attrs=["memberOf"]) + res2 = self.ldb_dc2.search(base="<GUID=%s>" % target_guid, + scope=SCOPE_BASE, attrs=["memberOf"]) + src1_backlink = False + + # our test user should still be a member of 2 groups (check both + # DCs agree) + self.assert_attrs_match(res1, res2, "memberOf", 2) + + for val in [str(val) for val in res1[0]["memberOf"]]: + # check the conflicting object was renamed + self.assertFalse("CNF:%s" % src2_guid in val) + if "CNF:%s" % src1_guid in val: + src1_backlink = True + + self.assertTrue(src1_backlink, + "Backlink to conflicting source object not found") + + def test_conflict_backlinks(self): + # repeat the test twice, to give each DC a chance to resolve + # the conflict + self._test_conflict_backlinks(sync_order=DC1_TO_DC2) + self._test_conflict_backlinks(sync_order=DC2_TO_DC1) + + def _test_link_deletion_conflict(self, sync_order): + """ + Checks that a deleted link conflicting with an active link is + resolved correctly. + """ + + # Add the link objects + target_dn = self.unique_dn("CN=target") + self.add_object(self.ldb_dc1, target_dn, objectclass="user") + src_dn = self.unique_dn("CN=src") + src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group") + self.sync_DCs() + + # add the same link on both DCs, and resolve any conflict + self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn) + self.ensure_unique_timestamp() + self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn) + self.sync_DCs(sync_order=sync_order) + + # delete and re-add the link on one DC + self.del_link_attr(self.ldb_dc1, src_dn, "member", target_dn) + self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn) + + # just delete it on the other DC + self.ensure_unique_timestamp() + self.del_link_attr(self.ldb_dc2, src_dn, "member", target_dn) + # sanity-check the link is gone on this DC + res1 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["member"]) + self.assertFalse("member" in res1[0], "Couldn't delete member attr") + + # sync the 2 DCs. We expect the more older DC1 attribute to win + # because it has a higher version number (even though it's older) + self.sync_DCs(sync_order=sync_order) + + res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["member"]) + res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["member"]) + + # our test user should still be a member of the group (check both + # DCs agree) + self.assertTrue("member" in res1[0], + "Expected member attribute missing") + self.assert_attrs_match(res1, res2, "member", 1) + + def test_link_deletion_conflict(self): + # repeat the test twice, to give each DC a chance to resolve + # the conflict + self._test_link_deletion_conflict(sync_order=DC1_TO_DC2) + self._test_link_deletion_conflict(sync_order=DC2_TO_DC1) + + def _test_obj_deletion_conflict(self, sync_order, del_target): + """ + Checks that a receiving a new link for a deleted object gets + resolved correctly. + """ + + target_dn = self.unique_dn("CN=target") + target_guid = self.add_object(self.ldb_dc1, target_dn, + objectclass="user") + src_dn = self.unique_dn("CN=src") + src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group") + + self.sync_DCs() + + # delete the object on one DC + if del_target: + search_guid = src_guid + self.ldb_dc2.delete(target_dn) + else: + search_guid = target_guid + self.ldb_dc2.delete(src_dn) + + # add a link on the other DC + self.ensure_unique_timestamp() + self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn) + + self.sync_DCs(sync_order=sync_order) + + # the object deletion should trump the link addition. + # Check the link no longer exists on the remaining object + res1 = self.ldb_dc1.search(base="<GUID=%s>" % search_guid, + scope=SCOPE_BASE, + attrs=["member", "memberOf"]) + res2 = self.ldb_dc2.search(base="<GUID=%s>" % search_guid, + scope=SCOPE_BASE, + attrs=["member", "memberOf"]) + + self.assertFalse("member" in res1[0], "member attr shouldn't exist") + self.assertFalse("member" in res2[0], "member attr shouldn't exist") + self.assertFalse("memberOf" in res1[0], "member attr shouldn't exist") + self.assertFalse("memberOf" in res2[0], "member attr shouldn't exist") + + def test_obj_deletion_conflict(self): + # repeat the test twice, to give each DC a chance to resolve + # the conflict + self._test_obj_deletion_conflict(sync_order=DC1_TO_DC2, + del_target=True) + self._test_obj_deletion_conflict(sync_order=DC2_TO_DC1, + del_target=True) + + # and also try deleting the source object instead of the link target + self._test_obj_deletion_conflict(sync_order=DC1_TO_DC2, + del_target=False) + self._test_obj_deletion_conflict(sync_order=DC2_TO_DC1, + del_target=False) + + def _test_full_sync_link_conflict(self, sync_order): + """ + Checks that doing a full sync doesn't affect how conflicts get resolved + """ + + # create the objects for the linked attribute + src_dn = self.unique_dn("CN=src") + src_guid = self.add_object(self.ldb_dc1, src_dn, objectclass="group") + target_dn = self.unique_dn("CN=target") + self.add_object(self.ldb_dc1, target_dn, objectclass="user") + self.sync_DCs() + + # add the same link on both DCs + self.add_link_attr(self.ldb_dc2, src_dn, "member", target_dn) + self.ensure_unique_timestamp() + self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn) + + # Do a couple of full syncs which should resolve the conflict + # (but only for one DC) + if sync_order == DC1_TO_DC2: + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + full_sync=True) + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + full_sync=True) + else: + self._net_drs_replicate(DC=self.dnsname_dc1, + fromDC=self.dnsname_dc2, + full_sync=True) + self._net_drs_replicate(DC=self.dnsname_dc1, + fromDC=self.dnsname_dc2, + full_sync=True) + + # delete and re-add the link on one DC + self.del_link_attr(self.ldb_dc1, src_dn, "member", target_dn) + self.ensure_unique_timestamp() + self.add_link_attr(self.ldb_dc1, src_dn, "member", target_dn) + + # just delete the link on the 2nd DC + self.ensure_unique_timestamp() + self.del_link_attr(self.ldb_dc2, src_dn, "member", target_dn) + + # sync the 2 DCs. We expect DC1 to win based on version number + self.sync_DCs(sync_order=sync_order) + + res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["member"]) + res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["member"]) + + # check the membership still exits (and both DCs agree) + self.assertTrue("member" in res1[0], + "Expected member attribute missing") + self.assert_attrs_match(res1, res2, "member", 1) + + def test_full_sync_link_conflict(self): + # repeat the test twice, to give each DC a chance to resolve + # the conflict + self._test_full_sync_link_conflict(sync_order=DC1_TO_DC2) + self._test_full_sync_link_conflict(sync_order=DC2_TO_DC1) + + def _singleval_link_conflict_deleted_winner(self, sync_order): + """ + Tests a single-value link conflict where the more-up-to-date link value + is deleted. + """ + src_ou = self.unique_dn("OU=src") + src_guid = self.add_object(self.ldb_dc1, src_ou) + self.sync_DCs() + + # create a unique target on each DC + target1_ou = self.unique_dn("OU=target1") + target2_ou = self.unique_dn("OU=target2") + + target1_guid = self.add_object(self.ldb_dc1, target1_ou) + target2_guid = self.add_object(self.ldb_dc2, target2_ou) + + # add the links for the respective targets, and delete one of the links + self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou) + self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou) + self.ensure_unique_timestamp() + self.del_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou) + + # sync the 2 DCs + self.sync_DCs(sync_order=sync_order) + + res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["managedBy"]) + res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["managedBy"]) + + # Although the more up-to-date link value is deleted, this shouldn't + # trump DC1's active link + self.assert_attrs_match(res1, res2, "managedBy", 1) + + self.assertTrue(str(res1[0]["managedBy"][0]) == target2_ou, + "Expected active link win conflict") + + # we can't query the deleted links over LDAP, but we can check that + # the deleted links exist using DRS + link1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, 0, + misc.GUID(src_guid), misc.GUID(target1_guid)) + link2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + misc.GUID(src_guid), misc.GUID(target2_guid)) + self._check_replicated_links(src_ou, [link1, link2]) + + def test_conflict_single_valued_link_deleted_winner(self): + # repeat the test twice, to give each DC a chance to resolve + # the conflict + self._singleval_link_conflict_deleted_winner(sync_order=DC1_TO_DC2) + self._singleval_link_conflict_deleted_winner(sync_order=DC2_TO_DC1) + + def _singleval_link_conflict_deleted_loser(self, sync_order): + """ + Tests a single-valued link conflict, where the losing link value is + deleted. + """ + src_ou = self.unique_dn("OU=src") + src_guid = self.add_object(self.ldb_dc1, src_ou) + self.sync_DCs() + + # create a unique target on each DC + target1_ou = self.unique_dn("OU=target1") + target2_ou = self.unique_dn("OU=target2") + + target1_guid = self.add_object(self.ldb_dc1, target1_ou) + target2_guid = self.add_object(self.ldb_dc2, target2_ou) + + # add the links - we want the link to end up deleted on DC2, but active + # on DC1. DC1 has the better version and DC2 has the better timestamp - + # the better version should win + self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou) + self.del_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou) + self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou) + self.ensure_unique_timestamp() + self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou) + self.del_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou) + + self.sync_DCs(sync_order=sync_order) + + res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["managedBy"]) + res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["managedBy"]) + + # check the object has only have one occurrence of the single-valued + # attribute and it matches on both DCs + self.assert_attrs_match(res1, res2, "managedBy", 1) + + self.assertTrue(str(res1[0]["managedBy"][0]) == target1_ou, + "Expected most recent update to win conflict") + + # we can't query the deleted links over LDAP, but we can check DRS + # to make sure the DC kept a copy of the conflicting link + link1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + misc.GUID(src_guid), misc.GUID(target1_guid)) + link2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, 0, + misc.GUID(src_guid), misc.GUID(target2_guid)) + self._check_replicated_links(src_ou, [link1, link2]) + + def test_conflict_single_valued_link_deleted_loser(self): + # repeat the test twice, to give each DC a chance to resolve + # the conflict + self._singleval_link_conflict_deleted_loser(sync_order=DC1_TO_DC2) + self._singleval_link_conflict_deleted_loser(sync_order=DC2_TO_DC1) + + def _test_conflict_existing_single_valued_link(self, sync_order): + """ + Tests a single-valued link conflict, where the conflicting link value + already exists (as inactive) on both DCs. + """ + # create the link objects + src_ou = self.unique_dn("OU=src") + src_guid = self.add_object(self.ldb_dc1, src_ou) + + target1_ou = self.unique_dn("OU=target1") + target2_ou = self.unique_dn("OU=target2") + target1_guid = self.add_object(self.ldb_dc1, target1_ou) + target2_guid = self.add_object(self.ldb_dc1, target2_ou) + + # add the links, but then delete them + self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou) + self.del_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou) + self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target2_ou) + self.del_link_attr(self.ldb_dc1, src_ou, "managedBy", target2_ou) + self.sync_DCs() + + # re-add the links independently on each DC + self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou) + self.ensure_unique_timestamp() + self.add_link_attr(self.ldb_dc2, src_ou, "managedBy", target2_ou) + + # try to sync the 2 DCs + self.sync_DCs(sync_order=sync_order) + + res1 = self.ldb_dc1.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["managedBy"]) + res2 = self.ldb_dc2.search(base="<GUID=%s>" % src_guid, + scope=SCOPE_BASE, attrs=["managedBy"]) + + # check the object has only have one occurrence of the single-valued + # attribute and it matches on both DCs + self.assert_attrs_match(res1, res2, "managedBy", 1) + + # here we expect DC2 to win because it has the more recent link + self.assertTrue(str(res1[0]["managedBy"][0]) == target2_ou, + "Expected most recent update to win conflict") + + # we can't query the deleted links over LDAP, but we can check DRS + # to make sure the DC kept a copy of the conflicting link + link1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, 0, + misc.GUID(src_guid), misc.GUID(target1_guid)) + link2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy, + drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE, + misc.GUID(src_guid), misc.GUID(target2_guid)) + self._check_replicated_links(src_ou, [link1, link2]) + + def test_conflict_existing_single_valued_link(self): + # repeat the test twice, to give each DC a chance to resolve + # the conflict + self._test_conflict_existing_single_valued_link(sync_order=DC1_TO_DC2) + self._test_conflict_existing_single_valued_link(sync_order=DC2_TO_DC1) + + def test_link_attr_version(self): + """ + Checks the link attribute version starts from the correct value + """ + # create some objects and add a link + src_ou = self.unique_dn("OU=src") + self.add_object(self.ldb_dc1, src_ou) + target1_ou = self.unique_dn("OU=target1") + self.add_object(self.ldb_dc1, target1_ou) + self.add_link_attr(self.ldb_dc1, src_ou, "managedBy", target1_ou) + + # get the link info via replication + ctr6 = self._get_replication(drsuapi.DRSUAPI_DRS_WRIT_REP, + dest_dsa=None, + drs_error=DRSUAPI_EXOP_ERR_SUCCESS, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + highwatermark=self.zero_highwatermark(), + nc_dn_str=src_ou) + + self.assertTrue(ctr6.linked_attributes_count == 1, + "DRS didn't return a link") + link = ctr6.linked_attributes[0] + rcvd_version = link.meta_data.version + self.assertTrue(rcvd_version == 1, + "Link version started from %u, not 1" % rcvd_version) diff --git a/source4/torture/drs/python/linked_attributes_drs.py b/source4/torture/drs/python/linked_attributes_drs.py new file mode 100644 index 0000000..93ad313 --- /dev/null +++ b/source4/torture/drs/python/linked_attributes_drs.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Originally based on ./sam.py +import sys + +sys.path.insert(0, "bin/python") +import ldb + +from samba.dcerpc import drsuapi, misc +from samba.ndr import ndr_unpack, ndr_pack + +import drs_base + + +class LATestException(Exception): + pass + + +class LATests(drs_base.DrsBaseTestCase): + + def setUp(self): + super(LATests, self).setUp() + # DrsBaseTestCase sets up self.ldb_dc1, self.ldb_dc2 + # we're only using one + self.samdb = self.ldb_dc1 + + self.base_dn = self.samdb.domain_dn() + self.ou = "OU=la,%s" % self.base_dn + if True: + try: + self.samdb.delete(self.ou, ['tree_delete:1']) + except ldb.LdbError as e: + pass + self.samdb.add({'objectclass': 'organizationalUnit', + 'dn': self.ou}) + + self.dc_guid = self.samdb.get_invocation_id() + self.drs, self.drs_handle = self._ds_bind(self.dnsname_dc1) + + def tearDown(self): + super(LATests, self).tearDown() + try: + self.samdb.delete(self.ou, ['tree_delete:1']) + except ldb.LdbError as e: + pass + + def delete_user(self, user): + self.samdb.delete(user['dn']) + del self.users[self.users.index(user)] + + def add_object(self, cn, objectclass): + dn = "CN=%s,%s" % (cn, self.ou) + self.samdb.add({'cn': cn, + 'objectclass': objectclass, + 'dn': dn}) + + return dn + + def add_objects(self, n, objectclass, prefix=None): + if prefix is None: + prefix = objectclass + dns = [] + for i in range(n): + dns.append(self.add_object("%s%d" % (prefix, i + 1), + objectclass)) + return dns + + def add_linked_attribute(self, src, dest, attr='member'): + m = ldb.Message() + m.dn = ldb.Dn(self.samdb, src) + m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_ADD, attr) + self.samdb.modify(m) + + def remove_linked_attribute(self, src, dest, attr='member'): + m = ldb.Message() + m.dn = ldb.Dn(self.samdb, src) + m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_DELETE, attr) + self.samdb.modify(m) + + def attr_search(self, obj, expected, attr, scope=ldb.SCOPE_BASE): + + req8 = self._exop_req8(dest_dsa=None, + invocation_id=self.dc_guid, + nc_dn_str=obj, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ) + + level, ctr = self.drs.DsGetNCChanges(self.drs_handle, 8, req8) + expected_attid = getattr(drsuapi, 'DRSUAPI_ATTID_' + attr) + + links = [] + for link in ctr.linked_attributes: + if link.attid == expected_attid: + unpacked = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3, + link.value.blob) + active = link.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE + links.append((str(unpacked.dn), bool(active))) + + return links + + def assert_forward_links(self, obj, expected, attr='member'): + results = self.attr_search(obj, expected, attr) + self.assertEqual(len(results), len(expected)) + + for k, v in results: + self.assertTrue(k in expected) + self.assertEqual(expected[k], v, "%s active flag should be %d, not %d" % + (k, expected[k], v)) + + 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_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_forward_links(g1, {u1: True}) + res = self.samdb.search('<GUID=%s>' % g2guid, + scope=ldb.SCOPE_BASE, + controls=['show_deleted:1']) + new_dn = res[0].dn + self.assert_forward_links(new_dn, {}) + + 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') + + 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(g1, {u1: True}) + self.assert_forward_links(g2, {u1: False, u2: True}) + + self.add_linked_attribute(g2, u1) + self.remove_linked_attribute(g2, u2) + self.assert_forward_links(g2, {u1: True, u2: False}) + self.remove_linked_attribute(g2, u1) + self.assert_forward_links(g2, {u1: False, u2: False}) + + 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) + + self.samdb.delete(u1) + + self.assert_forward_links(g1, {}) + self.assert_forward_links(g2, {u2: True}) diff --git a/source4/torture/drs/python/repl_move.py b/source4/torture/drs/python/repl_move.py new file mode 100644 index 0000000..c206ab8 --- /dev/null +++ b/source4/torture/drs/python/repl_move.py @@ -0,0 +1,2608 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Unix SMB/CIFS implementation. +# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2010 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 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/>. +# + +# +# Usage: +# export DC1=dc1_dns_name +# export DC2=dc2_dns_name +# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun +# PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN repl_move -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD" +# + +import time +import samba.tests + +from samba.ndr import ndr_unpack +from samba.dcerpc import drsblobs +from samba.dcerpc import misc +from samba.drs_utils import drs_DsBind + +from ldb import ( + SCOPE_BASE, + SCOPE_SUBTREE, +) + +import drs_base +import ldb +from samba.dcerpc.drsuapi import ( + drsuapi, + DRSUAPI_ATTID_accountExpires, + DRSUAPI_ATTID_cn, + DRSUAPI_ATTID_codePage, + DRSUAPI_ATTID_countryCode, + DRSUAPI_ATTID_dBCSPwd, + DRSUAPI_ATTID_description, + DRSUAPI_ATTID_instanceType, + DRSUAPI_ATTID_isDeleted, + DRSUAPI_ATTID_isRecycled, + DRSUAPI_ATTID_lastKnownParent, + DRSUAPI_ATTID_lmPwdHistory, + DRSUAPI_ATTID_logonHours, + DRSUAPI_ATTID_name, + DRSUAPI_ATTID_ntPwdHistory, + DRSUAPI_ATTID_ntSecurityDescriptor, + DRSUAPI_ATTID_objectCategory, + DRSUAPI_ATTID_objectClass, + DRSUAPI_ATTID_objectSid, + DRSUAPI_ATTID_ou, + DRSUAPI_ATTID_primaryGroupID, + DRSUAPI_ATTID_pwdLastSet, + DRSUAPI_ATTID_sAMAccountName, + DRSUAPI_ATTID_sAMAccountType, + DRSUAPI_ATTID_unicodePwd, + DRSUAPI_ATTID_userAccountControl, + DRSUAPI_ATTID_userPrincipalName, + DRSUAPI_ATTID_whenCreated, + DRSUAPI_DRS_SYNC_FORCED, + DRSUAPI_EXOP_REPL_OBJ, + DsGetNCChangesRequest8, + DsReplicaHighWaterMark, + DsReplicaObjectIdentifier) + + +class DrsMoveObjectTestCase(drs_base.DrsBaseTestCase): + + def setUp(self): + super(DrsMoveObjectTestCase, self).setUp() + # disable automatic replication temporary + self._disable_all_repl(self.dnsname_dc1) + self._disable_all_repl(self.dnsname_dc2) + + # make sure DCs are synchronized before the test + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + + self.top_ou = samba.tests.create_test_ou(self.ldb_dc1, + "replica_move") + + self.ou1_dn = ldb.Dn(self.ldb_dc1, "OU=DrsOU1") + self.ou1_dn.add_base(self.top_ou) + ou1 = {} + ou1["dn"] = self.ou1_dn + ou1["objectclass"] = "organizationalUnit" + ou1["ou"] = self.ou1_dn.get_component_value(0) + self.ldb_dc1.add(ou1) + + self.ou2_dn = ldb.Dn(self.ldb_dc1, "OU=DrsOU2") + self.ou2_dn.add_base(self.top_ou) + ou2 = {} + ou2["dn"] = self.ou2_dn + ou2["objectclass"] = "organizationalUnit" + ou2["ou"] = self.ou2_dn.get_component_value(0) + self.ldb_dc1.add(ou2) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + self.dc1_guid = self.ldb_dc1.get_invocation_id() + self.dc2_guid = self.ldb_dc2.get_invocation_id() + + self.drs_dc1 = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + self.drs_dc2 = self._ds_bind(self.dnsname_dc2, ip=self.url_dc2) + + def tearDown(self): + try: + self.ldb_dc1.delete(self.top_ou, ["tree_delete:1"]) + except ldb.LdbError as e: + (enum, string) = e.args + if enum == ldb.ERR_NO_SUCH_OBJECT: + pass + + self._enable_all_repl(self.dnsname_dc1) + self._enable_all_repl(self.dnsname_dc2) + super(DrsMoveObjectTestCase, self).tearDown() + + def _make_username(self): + return "DrsMoveU_" + time.strftime("%s", time.gmtime()) + + def _check_metadata(self, user_dn, sam_ldb, drs, metadata, expected): + repl = ndr_unpack(drsblobs.replPropertyMetaDataBlob, metadata[0]) + + self.assertEqual(len(repl.ctr.array), len(expected)) + + i = 0 + for o in repl.ctr.array: + e = expected[i] + (attid, orig_dsa, version) = e + self.assertEqual(attid, o.attid, + "(LDAP) Wrong attid " + "for expected value %d, wanted 0x%08x got 0x%08x" + % (i, attid, o.attid)) + self.assertEqual(o.originating_invocation_id, + misc.GUID(orig_dsa), + "(LDAP) Wrong originating_invocation_id " + "for expected value %d, attid 0x%08x, wanted %s got %s" + % (i, o.attid, + misc.GUID(orig_dsa), + o.originating_invocation_id)) + # 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" + % (i, o.attid, + version, o.version)) + i = i + 1 + + if drs is None: + return + + req8 = DsGetNCChangesRequest8() + + req8.source_dsa_invocation_id = misc.GUID(sam_ldb.get_invocation_id()) + req8.naming_context = DsReplicaObjectIdentifier() + req8.naming_context.dn = str(user_dn) + req8.highwatermark = DsReplicaHighWaterMark() + req8.highwatermark.tmp_highest_usn = 0 + req8.highwatermark.reserved_usn = 0 + req8.highwatermark.highest_usn = 0 + req8.uptodateness_vector = None + req8.replica_flags = DRSUAPI_DRS_SYNC_FORCED + req8.max_object_count = 1 + req8.max_ndr_size = 402116 + req8.extended_op = DRSUAPI_EXOP_REPL_OBJ + req8.fsmo_info = 0 + req8.partial_attribute_set = None + req8.partial_attribute_set_ex = None + req8.mapping_ctr.num_mappings = 0 + req8.mapping_ctr.mappings = None + + (drs_conn, drs_handle) = drs + + (level, drs_ctr) = drs_conn.DsGetNCChanges(drs_handle, 8, req8) + self.assertEqual(level, 6) + self.assertEqual(drs_ctr.object_count, 1) + + self.assertEqual(len(drs_ctr.first_object.meta_data_ctr.meta_data), len(expected) - 1) + att_idx = 0 + for o in drs_ctr.first_object.meta_data_ctr.meta_data: + i = 0 + drs_attid = drs_ctr.first_object.object.attribute_ctr.attributes[att_idx] + e = expected[i] + (attid, orig_dsa, version) = e + + # Skip the RDN from the expected set, it is not sent over DRS + if (user_dn.get_rdn_name().upper() == "CN" + and attid == DRSUAPI_ATTID_cn) \ + or (user_dn.get_rdn_name().upper() == "OU" + and attid == DRSUAPI_ATTID_ou): + i = i + 1 + e = expected[i] + (attid, orig_dsa, version) = e + + self.assertEqual(attid, drs_attid.attid, + "(DRS) Wrong attid " + "for expected value %d, wanted 0x%08x got 0x%08x" + % (i, attid, drs_attid.attid)) + + self.assertEqual(o.originating_invocation_id, + misc.GUID(orig_dsa), + "(DRS) Wrong originating_invocation_id " + "for expected value %d, attid 0x%08x, wanted %s got %s" + % (i, attid, + misc.GUID(orig_dsa), + o.originating_invocation_id)) + # Allow version to be skipped when it does not matter + if version is not None: + self.assertEqual(o.version, version, + "(DRS) Wrong version for expected value %d, " + "attid 0x%08x, " + "wanted %d got %d" + % (i, attid, version, o.version)) + break + i = i + 1 + att_idx = att_idx + 1 + + # now also used to check the group + def _check_obj(self, sam_ldb, obj_orig, is_deleted, expected_metadata=None, drs=None): + # search the user by guid as it may be deleted + guid_str = self._GUID_string(obj_orig["objectGUID"][0]) + res = sam_ldb.search(base='<GUID=%s>' % guid_str, + controls=["show_deleted:1"], + attrs=["*", "parentGUID", + "replPropertyMetaData"]) + self.assertEqual(len(res), 1) + user_cur = res[0] + rdn_orig = str(obj_orig[user_cur.dn.get_rdn_name()][0]) + rdn_cur = str(user_cur[user_cur.dn.get_rdn_name()][0]) + name_orig = str(obj_orig["name"][0]) + name_cur = str(user_cur["name"][0]) + dn_orig = obj_orig["dn"] + dn_cur = user_cur["dn"] + # now check properties of the user + if is_deleted: + self.assertTrue("isDeleted" in user_cur) + self.assertEqual(rdn_cur.split('\n')[0], rdn_orig) + self.assertEqual(name_cur.split('\n')[0], name_orig) + self.assertEqual(dn_cur.get_rdn_value().split('\n')[0], + dn_orig.get_rdn_value()) + self.assertEqual(name_cur, rdn_cur) + else: + self.assertFalse("isDeleted" in user_cur) + self.assertEqual(rdn_cur, rdn_orig) + self.assertEqual(name_cur, name_orig) + self.assertEqual(dn_cur, dn_orig) + self.assertEqual(name_cur, rdn_cur) + parent_cur = user_cur["parentGUID"][0] + try: + parent_orig = obj_orig["parentGUID"][0] + self.assertEqual(parent_orig, parent_cur) + except KeyError: + pass + self.assertEqual(name_cur, user_cur.dn.get_rdn_value()) + + if expected_metadata is not None: + self._check_metadata(dn_cur, sam_ldb, drs, user_cur["replPropertyMetaData"], + expected_metadata) + + return user_cur + + def test_ReplicateMoveObject1(self): + """Verifies how a moved container with a user inside is replicated between two DCs. + This test should verify that: + - the OU is replicated properly + - the OU is renamed + - We verify that after replication, + that the user has the correct DN (under OU2) + - the OU is deleted + - the OU is modified on DC2 + - We verify that after replication, + that the user has the correct DN (deleted) and has not description + + """ + # work-out unique username to test with + username = self._make_username() + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (username, self._GUID_string(user_orig["objectGUID"][0]))) + initial_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 1), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_orig, is_deleted=False, + expected_metadata=initial_metadata) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(user_dn, new_dn) + ldb_res = self.ldb_dc1.search(base=self.ou2_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + + user_moved_orig = ldb_res[0] + + moved_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 2), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + # check user info on DC1 after rename - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=False, + expected_metadata=moved_metadata) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + moved_metadata_dc2 = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc2_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 2), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + # check user info on DC2 - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, drs=self.drs_dc2, + obj_orig=user_moved_orig, + is_deleted=False, + expected_metadata=moved_metadata_dc2) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + deleted_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 2), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_isDeleted, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 3), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 2), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 2), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 2), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 2), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 2), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 2), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 2), + (DRSUAPI_ATTID_lastKnownParent, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 2), + (DRSUAPI_ATTID_isRecycled, self.dc1_guid, 1)] + + user_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_moved_orig, is_deleted=True, expected_metadata=deleted_metadata) + + # Modify description on DC2. This triggers a replication, but + # not of 'name' and so a bug in Samba regarding the DN. + msg = ldb.Message() + msg.dn = new_dn + msg["description"] = ldb.MessageElement("User Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc2.modify(msg) + + modified_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc2_guid, 1), + (DRSUAPI_ATTID_description, self.dc2_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 2), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, drs=self.drs_dc2, + obj_orig=user_moved_orig, + is_deleted=False, + expected_metadata=modified_metadata) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + deleted_modified_metadata_dc2 = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc2_guid, 2), + (DRSUAPI_ATTID_description, self.dc2_guid, 2), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_isDeleted, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 3), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 2), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 2), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 2), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 2), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 2), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 2), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 2), + (DRSUAPI_ATTID_lastKnownParent, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 2), + (DRSUAPI_ATTID_isRecycled, self.dc1_guid, 1)] + + # check user info on DC2 - should be deleted user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, drs=self.drs_dc2, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_modified_metadata_dc2) + self.assertFalse("description" in user_cur) + + # trigger replication from DC2 to DC1, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + + deleted_modified_metadata_dc1 = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 2), + (DRSUAPI_ATTID_description, self.dc2_guid, 2), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_isDeleted, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 3), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 2), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 2), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 2), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 2), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 2), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 2), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 2), + (DRSUAPI_ATTID_lastKnownParent, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 2), + (DRSUAPI_ATTID_isRecycled, self.dc1_guid, 1)] + + # check user info on DC1 - should be deleted user + user_cur = self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_modified_metadata_dc1) + self.assertFalse("description" in user_cur) + + def test_ReplicateMoveObject2(self): + """Verifies how a moved container with a user inside is not + replicated between two DCs as no replication is triggered + This test should verify that: + - the OU is not replicated + - the user is not replicated + + """ + # work-out unique username to test with + username = self._make_username() + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (username, self._GUID_string(user_orig["objectGUID"][0]))) + initial_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 1), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_orig, is_deleted=False, + expected_metadata=initial_metadata) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(user_dn, new_dn) + ldb_res = self.ldb_dc1.search(base=self.ou2_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_moved_orig = ldb_res[0] + + moved_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 2), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + # check user info on DC1 after rename - should be valid user + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=False, + expected_metadata=moved_metadata) + + # check user info on DC2 - should not be there, we have not done replication + ldb_res = self.ldb_dc2.search(base=self.ou2_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 0) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + + deleted_metadata_dc1 = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 2), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_isDeleted, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 3), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 2), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 2), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 2), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 2), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 2), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 2), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 2), + (DRSUAPI_ATTID_lastKnownParent, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 2), + (DRSUAPI_ATTID_isRecycled, self.dc1_guid, 1)] + + # check user info on DC1 - should be deleted user + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_metadata_dc1) + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + deleted_metadata_dc2 = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc2_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_isDeleted, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 3), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 2), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 2), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 2), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 2), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 2), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 2), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 2), + (DRSUAPI_ATTID_lastKnownParent, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 2), + (DRSUAPI_ATTID_isRecycled, self.dc1_guid, 1)] + + # check user info on DC2 - should be deleted user + self._check_obj(sam_ldb=self.ldb_dc2, drs=self.drs_dc2, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_metadata_dc2) + + # trigger replication from DC2 to DC1, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + + # check user info on DC1 - should be deleted user + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_metadata_dc1) + + def test_ReplicateMoveObject3(self): + """Verifies how a moved container with a user inside is replicated between two DCs. + This test should verify that: + - the OU is created on DC1 + - the OU is renamed on DC1 + - We verify that after replication, + that the user has the correct DN (under OU2). + + """ + # work-out unique username to test with + username = self._make_username() + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (username, self._GUID_string(user_orig["objectGUID"][0]))) + initial_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 1), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_orig, is_deleted=False, + expected_metadata=initial_metadata) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(user_dn, new_dn) + ldb_res = self.ldb_dc1.search(base=self.ou2_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + + user_moved_orig = ldb_res[0] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + moved_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 2), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + # check user info on DC1 after rename - should be valid user + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=False, + expected_metadata=moved_metadata) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + deleted_metadata_dc1 = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 2), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_isDeleted, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 3), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 2), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 2), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 2), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 2), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 2), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 2), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 2), + (DRSUAPI_ATTID_lastKnownParent, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 2), + (DRSUAPI_ATTID_isRecycled, self.dc1_guid, 1)] + + # check user info on DC1 - should be deleted user + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_metadata_dc1) + + # trigger replication from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + + # check user info on DC1 - should be deleted user + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_metadata_dc1) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + deleted_metadata_dc2 = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc2_guid, 2), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_isDeleted, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 3), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 2), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 2), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 2), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 2), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 2), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 2), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 2), + (DRSUAPI_ATTID_lastKnownParent, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 2), + (DRSUAPI_ATTID_isRecycled, self.dc1_guid, 1)] + + # check user info on DC2 - should be deleted user + self._check_obj(sam_ldb=self.ldb_dc2, drs=self.drs_dc2, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_metadata_dc2) + + def test_ReplicateMoveObject3b(self): + """Verifies how a moved container with a user inside is replicated between two DCs. + This test should verify that: + - the OU is created on DC1 + - the OU is renamed on DC1 + - We verify that after replication, + that the user has the correct DN (under OU2). + + """ + # work-out unique username to test with + username = self._make_username() + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (username, self._GUID_string(user_orig["objectGUID"][0]))) + initial_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 1), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_orig, is_deleted=False, + expected_metadata=initial_metadata) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(user_dn, new_dn) + ldb_res = self.ldb_dc1.search(base=self.ou2_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + + user_moved_orig = ldb_res[0] + + # trigger replication from DC2 (Which has never seen the object) to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + moved_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 2), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + # check user info on DC1 after rename - should be valid user + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=False, + expected_metadata=moved_metadata) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + deleted_metadata_dc1 = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 2), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_isDeleted, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 3), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 2), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 2), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 2), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 2), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 2), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 2), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 2), + (DRSUAPI_ATTID_lastKnownParent, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 2), + (DRSUAPI_ATTID_isRecycled, self.dc1_guid, 1)] + + # check user info on DC1 - should be deleted user + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_metadata_dc1) + + # trigger replication from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + + # check user info on DC1 - should be deleted user + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_metadata_dc1) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + deleted_metadata_dc2 = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc2_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_isDeleted, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 3), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 2), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 2), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 2), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 2), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 2), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 2), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 2), + (DRSUAPI_ATTID_lastKnownParent, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 2), + (DRSUAPI_ATTID_isRecycled, self.dc1_guid, 1)] + + # check user info on DC2 - should be deleted user + self._check_obj(sam_ldb=self.ldb_dc2, drs=self.drs_dc2, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_metadata_dc2) + + def test_ReplicateMoveObject4(self): + """Verifies how a moved container with a user inside is replicated between two DCs. + This test should verify that: + - the OU is replicated properly + - the user is modified on DC2 + - the OU is renamed on DC1 + - We verify that after replication DC1 -> DC2, + that the user has the correct DN (under OU2), and the description + + """ + # work-out unique username to test with + username = self._make_username() + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (username, self._GUID_string(user_orig["objectGUID"][0]))) + initial_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 1), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_orig, is_deleted=False, + expected_metadata=initial_metadata) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + initial_metadata_dc2 = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc2_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 1), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + # check user info on DC2 - should still be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, drs=self.drs_dc2, + obj_orig=user_orig, is_deleted=False, + expected_metadata=initial_metadata_dc2) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(user_dn, new_dn) + ldb_res = self.ldb_dc1.search(base=self.ou2_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + + user_moved_orig = ldb_res[0] + + moved_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 2), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + # check user info on DC1 after rename - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=False, + expected_metadata=moved_metadata) + + # Modify description on DC2. This triggers a replication, but + # not of 'name' and so a bug in Samba regarding the DN. + msg = ldb.Message() + msg.dn = user_dn + msg["description"] = ldb.MessageElement("User Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc2.modify(msg) + + modified_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc2_guid, 1), + (DRSUAPI_ATTID_description, self.dc2_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 1), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, drs=self.drs_dc2, + obj_orig=user_orig, + is_deleted=False, + expected_metadata=modified_metadata) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + modified_renamed_metadata = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc2_guid, 2), + (DRSUAPI_ATTID_description, self.dc2_guid, 1), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 2), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 1), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 1), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 1), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 1), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 1), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 1)] + + # check user info on DC2 - should still be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, drs=self.drs_dc2, + obj_orig=user_moved_orig, + is_deleted=False, + expected_metadata=modified_renamed_metadata) + + self.assertTrue("description" in user_cur) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + deleted_metadata_dc1 = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 2), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_isDeleted, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 3), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 2), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 2), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 2), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 2), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 2), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 2), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 2), + (DRSUAPI_ATTID_lastKnownParent, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 2), + (DRSUAPI_ATTID_isRecycled, self.dc1_guid, 1)] + + # check user info on DC1 - should be deleted user + user_cur = self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_metadata_dc1) + + # trigger replication from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + # check user info on DC2 - should still be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, drs=self.drs_dc2, + obj_orig=user_moved_orig, + is_deleted=False, + expected_metadata=modified_renamed_metadata) + + self.assertTrue("description" in user_cur) + + deleted_metadata_dc1 = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc1_guid, 2), + (DRSUAPI_ATTID_description, self.dc1_guid, 2), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_isDeleted, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 3), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 2), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 2), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 2), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 2), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 2), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 2), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 2), + (DRSUAPI_ATTID_lastKnownParent, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 2), + (DRSUAPI_ATTID_isRecycled, self.dc1_guid, 1)] + + # check user info on DC1 - should be deleted user + user_cur = self._check_obj(sam_ldb=self.ldb_dc1, drs=self.drs_dc1, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_metadata_dc1) + + self.assertFalse("description" in user_cur) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + deleted_metadata_dc2 = [ + (DRSUAPI_ATTID_objectClass, self.dc1_guid, 1), + (DRSUAPI_ATTID_cn, self.dc2_guid, 3), + (DRSUAPI_ATTID_description, self.dc1_guid, 2), + (DRSUAPI_ATTID_instanceType, self.dc1_guid, 1), + (DRSUAPI_ATTID_whenCreated, self.dc1_guid, 1), + (DRSUAPI_ATTID_isDeleted, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntSecurityDescriptor, self.dc1_guid, 1), + (DRSUAPI_ATTID_name, self.dc1_guid, 3), + (DRSUAPI_ATTID_userAccountControl, self.dc1_guid, None), + (DRSUAPI_ATTID_codePage, self.dc1_guid, 2), + (DRSUAPI_ATTID_countryCode, self.dc1_guid, 2), + (DRSUAPI_ATTID_dBCSPwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_logonHours, self.dc1_guid, 1), + (DRSUAPI_ATTID_unicodePwd, self.dc1_guid, 1), + (DRSUAPI_ATTID_ntPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_pwdLastSet, self.dc1_guid, 2), + (DRSUAPI_ATTID_primaryGroupID, self.dc1_guid, 2), + (DRSUAPI_ATTID_objectSid, self.dc1_guid, 1), + (DRSUAPI_ATTID_accountExpires, self.dc1_guid, 2), + (DRSUAPI_ATTID_lmPwdHistory, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountName, self.dc1_guid, 1), + (DRSUAPI_ATTID_sAMAccountType, self.dc1_guid, 2), + (DRSUAPI_ATTID_userPrincipalName, self.dc1_guid, 2), + (DRSUAPI_ATTID_lastKnownParent, self.dc1_guid, 1), + (DRSUAPI_ATTID_objectCategory, self.dc1_guid, 2), + (DRSUAPI_ATTID_isRecycled, self.dc1_guid, 1)] + + # check user info on DC2 - should be deleted user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, drs=self.drs_dc2, + obj_orig=user_moved_orig, + is_deleted=True, + expected_metadata=deleted_metadata_dc2) + + self.assertFalse("description" in user_cur) + + def test_ReplicateMoveObject5(self): + """Verifies how a moved container with a user inside is replicated between two DCs. + This test should verify that: + - the OU is replicated properly + - the user is modified on DC2 + - the OU is renamed on DC1 + - We verify that after replication DC2 -> DC1, + that the user has the correct DN (under OU2), and the description + + """ + # work-out unique username to test with + username = self._make_username() + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should still be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=False) + + # check user info on DC1 + print("Testing for %s with GUID %s" % (username, self._GUID_string(user_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=False) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(user_dn, new_dn) + ldb_res = self.ldb_dc1.search(base=self.ou2_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + + user_moved_orig = ldb_res[0] + + # Modify description on DC2. This triggers a replication, but + # not of 'name' and so a bug in Samba regarding the DN. + msg = ldb.Message() + msg.dn = user_dn + msg["description"] = ldb.MessageElement("User Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc2.modify(msg) + + # trigger replication from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + # check user info on DC1 - should still be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_moved_orig, is_deleted=False) + self.assertTrue("description" in user_cur) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_moved_orig, is_deleted=False) + self.assertTrue("description" in user_cur) + + # delete user on DC2 + self.ldb_dc2.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + # trigger replication from DC2 to DC1 for cleanup + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + + # check user info on DC1 - should be deleted user + user_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_moved_orig, is_deleted=True) + self.assertFalse("description" in user_cur) + + def test_ReplicateMoveObject6(self): + """Verifies how a moved container is replicated between two DCs. + This test should verify that: + - the OU1 is replicated properly + - the OU1 is modified on DC2 + - the OU1 is renamed on DC1 + - We verify that after replication DC1 -> DC2, + that the OU1 has the correct DN (under OU2), and the description + + """ + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_BASE, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + ou_orig = ldb_res[0] + ou_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (self.ou1_dn, self._GUID_string(ou_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=False) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should still be valid user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=False) + + new_dn = ldb.Dn(self.ldb_dc1, "OU=%s" % self.ou1_dn.get_component_value(0)) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(ou_dn, new_dn) + ldb_res = self.ldb_dc1.search(base=new_dn, + scope=SCOPE_BASE, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + + ou_moved_orig = ldb_res[0] + + # Modify description on DC2. This triggers a replication, but + # not of 'name' and so a bug in Samba regarding the DN. + msg = ldb.Message() + msg.dn = ou_dn + msg["description"] = ldb.MessageElement("OU Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc2.modify(msg) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should still be valid user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=ou_moved_orig, is_deleted=False) + self.assertTrue("description" in ou_cur) + + # delete OU on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(ou_orig["objectGUID"][0])) + # trigger replication from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + # check user info on DC2 - should be deleted user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_moved_orig, is_deleted=True) + self.assertFalse("description" in ou_cur) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + # check user info on DC2 - should be deleted user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=ou_moved_orig, is_deleted=True) + self.assertFalse("description" in ou_cur) + + def test_ReplicateMoveObject7(self): + """Verifies how a moved container is replicated between two DCs. + This test should verify that: + - the OU1 is replicated properly + - the OU1 is modified on DC2 + - the OU1 is renamed on DC1 to be under OU2 + - We verify that after replication DC2 -> DC1, + that the OU1 has the correct DN (under OU2), and the description + + """ + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_BASE, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + ou_orig = ldb_res[0] + ou_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (self.ou1_dn, self._GUID_string(ou_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=False) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should still be valid user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=False) + + new_dn = ldb.Dn(self.ldb_dc1, "OU=%s" % self.ou1_dn.get_component_value(0)) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(ou_dn, new_dn) + ldb_res = self.ldb_dc1.search(base=new_dn, + scope=SCOPE_BASE, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + + ou_moved_orig = ldb_res[0] + + # Modify description on DC2. This triggers a replication, but + # not of 'name' and so a bug in Samba regarding the DN. + msg = ldb.Message() + msg.dn = ou_dn + msg["description"] = ldb.MessageElement("OU Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc2.modify(msg) + + # trigger replication from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + # check user info on DC1 - should still be valid user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_moved_orig, is_deleted=False) + self.assertTrue("description" in ou_cur) + + # delete OU on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(ou_orig["objectGUID"][0])) + # trigger replication from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + # check user info on DC2 - should be deleted user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_moved_orig, is_deleted=True) + self.assertFalse("description" in ou_cur) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + # check user info on DC2 - should be deleted user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=ou_moved_orig, is_deleted=True) + self.assertFalse("description" in ou_cur) + + def test_ReplicateMoveObject8(self): + """Verifies how a moved container is replicated between two DCs. + This test should verify that: + - the OU1 is replicated properly + - the OU1 is modified on DC2 + - the OU1 is renamed on DC1 to OU1-renamed + - We verify that after replication DC1 -> DC2, + that the OU1 has the correct DN (OU1-renamed), and the description + + """ + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_BASE, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + ou_orig = ldb_res[0] + ou_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (self.ou1_dn, self._GUID_string(ou_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=False) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should still be valid user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=False) + + new_dn = ldb.Dn(self.ldb_dc1, "OU=%s-renamed" % self.ou1_dn.get_component_value(0)) + new_dn.add_base(self.ou1_dn.parent()) + self.ldb_dc1.rename(ou_dn, new_dn) + ldb_res = self.ldb_dc1.search(base=new_dn, + scope=SCOPE_BASE, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + + ou_moved_orig = ldb_res[0] + + # Modify description on DC2. This triggers a replication, but + # not of 'name' and so a bug in Samba regarding the DN. + msg = ldb.Message() + msg.dn = ou_dn + msg["description"] = ldb.MessageElement("OU Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc2.modify(msg) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should still be valid user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=ou_moved_orig, is_deleted=False) + self.assertTrue("description" in ou_cur) + + # delete OU on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(ou_orig["objectGUID"][0])) + # trigger replication from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + # check user info on DC2 - should be deleted user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_moved_orig, is_deleted=True) + self.assertFalse("description" in ou_cur) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + # check user info on DC2 - should be deleted user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=ou_moved_orig, is_deleted=True) + self.assertFalse("description" in ou_cur) + + def test_ReplicateMoveObject9(self): + """Verifies how a moved container is replicated between two DCs. + This test should verify that: + - the OU1 is replicated properly + - the OU1 is modified on DC2 + - the OU1 is renamed on DC1 to be under OU2 + - the OU1 is renamed on DC1 to OU1-renamed + - We verify that after replication DC1 -> DC2, + that the OU1 has the correct DN (OU1-renamed), and the description + + """ + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_BASE, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + ou_orig = ldb_res[0] + ou_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (self.ou1_dn, self._GUID_string(ou_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=False) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should still be valid user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=False) + + new_dn = ldb.Dn(self.ldb_dc1, "OU=%s-renamed" % self.ou1_dn.get_component_value(0)) + new_dn.add_base(self.ou1_dn.parent()) + self.ldb_dc1.rename(ou_dn, new_dn) + ldb_res = self.ldb_dc1.search(base=new_dn, + scope=SCOPE_BASE, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + + ou_moved_orig = ldb_res[0] + + # Modify description on DC2. This triggers a replication, but + # not of 'name' and so a bug in Samba regarding the DN. + msg = ldb.Message() + msg.dn = ou_dn + msg["description"] = ldb.MessageElement("OU Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc2.modify(msg) + + # trigger replication from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + # check user info on DC1 - should still be valid user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_moved_orig, is_deleted=False) + self.assertTrue("description" in ou_cur) + + # delete OU on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(ou_orig["objectGUID"][0])) + # trigger replication from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + # check user info on DC2 - should be deleted user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_moved_orig, is_deleted=True) + self.assertFalse("description" in ou_cur) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + # check user info on DC2 - should be deleted user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=ou_moved_orig, is_deleted=True) + self.assertFalse("description" in ou_cur) + + def test_ReplicateMoveObject10(self): + """Verifies how a moved container is replicated between two DCs. + This test should verify that: + - the OU1 is replicated properly + - the OU1 is modified on DC2 + - the OU1 is deleted on DC1 + - We verify that after replication DC1 -> DC2, + that the OU1 is deleted, and the description has gone away + + """ + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_BASE, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + ou_orig = ldb_res[0] + ou_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (self.ou1_dn, self._GUID_string(ou_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=False) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should still be valid user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=False) + + # Modify description on DC2. This triggers a replication, but + # not of 'name' and so a bug in Samba regarding the DN. + msg = ldb.Message() + msg.dn = ou_dn + msg["description"] = ldb.MessageElement("OU Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc2.modify(msg) + + # delete OU on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(ou_orig["objectGUID"][0])) + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be deleted OU + ou_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=ou_orig, is_deleted=True) + self.assertFalse("description" in ou_cur) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + + # check user info on DC2 - should be deleted OU + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=True) + self.assertFalse("description" in ou_cur) + + def test_ReplicateMoveObject11(self): + """Verifies how a moved container is replicated between two DCs. + This test should verify that: + - the OU1 is replicated properly + - the OU1 is modified on DC2 + - the OU1 is deleted on DC1 + - We verify that after replication DC2 -> DC1, + that the OU1 is deleted, and the description has gone away + + """ + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_BASE, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + ou_orig = ldb_res[0] + ou_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (self.ou1_dn, self._GUID_string(ou_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=False) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should still be valid user + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=False) + + # Modify description on DC2. This triggers a replication, but + # not of 'name' and so a bug in Samba regarding the DN. + msg = ldb.Message() + msg.dn = ou_dn + msg["description"] = ldb.MessageElement("OU Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc2.modify(msg) + + # delete OU on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(ou_orig["objectGUID"][0])) + # trigger replication from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + # check user info on DC2 - should be deleted OU + ou_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=ou_orig, is_deleted=True) + self.assertFalse("description" in ou_cur) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + # check user info on DC2 - should be deleted OU + ou_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=ou_orig, is_deleted=True) + self.assertFalse("description" in ou_cur) + + +class DrsMoveBetweenTreeOfObjectTestCase(drs_base.DrsBaseTestCase): + + def setUp(self): + super(DrsMoveBetweenTreeOfObjectTestCase, self).setUp() + # disable automatic replication temporary + self._disable_all_repl(self.dnsname_dc1) + self._disable_all_repl(self.dnsname_dc2) + + self.top_ou = samba.tests.create_test_ou(self.ldb_dc1, + "replica_move") + + # make sure DCs are synchronized before the test + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + + self.ou1_dn = ldb.Dn(self.ldb_dc1, "OU=DrsOU1") + self.ou1_dn.add_base(self.top_ou) + self.ou1 = {} + self.ou1["dn"] = self.ou1_dn + self.ou1["objectclass"] = "organizationalUnit" + self.ou1["ou"] = self.ou1_dn.get_component_value(0) + + self.ou2_dn = ldb.Dn(self.ldb_dc1, "OU=DrsOU2,OU=DrsOU1") + self.ou2_dn.add_base(self.top_ou) + self.ou2 = {} + self.ou2["dn"] = self.ou2_dn + self.ou2["objectclass"] = "organizationalUnit" + self.ou2["ou"] = self.ou2_dn.get_component_value(0) + + self.ou2b_dn = ldb.Dn(self.ldb_dc1, "OU=DrsOU2B,OU=DrsOU1") + self.ou2b_dn.add_base(self.top_ou) + self.ou2b = {} + self.ou2b["dn"] = self.ou2b_dn + self.ou2b["objectclass"] = "organizationalUnit" + self.ou2b["ou"] = self.ou2b_dn.get_component_value(0) + + self.ou2c_dn = ldb.Dn(self.ldb_dc1, "OU=DrsOU2C,OU=DrsOU1") + self.ou2c_dn.add_base(self.top_ou) + + self.ou3_dn = ldb.Dn(self.ldb_dc1, "OU=DrsOU3,OU=DrsOU2,OU=DrsOU1") + self.ou3_dn.add_base(self.top_ou) + self.ou3 = {} + self.ou3["dn"] = self.ou3_dn + self.ou3["objectclass"] = "organizationalUnit" + self.ou3["ou"] = self.ou3_dn.get_component_value(0) + + self.ou4_dn = ldb.Dn(self.ldb_dc1, "OU=DrsOU4,OU=DrsOU3,OU=DrsOU2,OU=DrsOU1") + self.ou4_dn.add_base(self.top_ou) + self.ou4 = {} + self.ou4["dn"] = self.ou4_dn + self.ou4["objectclass"] = "organizationalUnit" + self.ou4["ou"] = self.ou4_dn.get_component_value(0) + + self.ou5_dn = ldb.Dn(self.ldb_dc1, "OU=DrsOU5,OU=DrsOU4,OU=DrsOU3,OU=DrsOU2,OU=DrsOU1") + self.ou5_dn.add_base(self.top_ou) + self.ou5 = {} + self.ou5["dn"] = self.ou5_dn + self.ou5["objectclass"] = "organizationalUnit" + self.ou5["ou"] = self.ou5_dn.get_component_value(0) + + self.ou6_dn = ldb.Dn(self.ldb_dc1, "OU=DrsOU6,OU=DrsOU5,OU=DrsOU4,OU=DrsOU3,OU=DrsOU2,OU=DrsOU1") + self.ou6_dn.add_base(self.top_ou) + self.ou6 = {} + self.ou6["dn"] = self.ou6_dn + self.ou6["objectclass"] = "organizationalUnit" + self.ou6["ou"] = self.ou6_dn.get_component_value(0) + + def tearDown(self): + self.ldb_dc1.delete(self.top_ou, ["tree_delete:1"]) + self._enable_all_repl(self.dnsname_dc1) + self._enable_all_repl(self.dnsname_dc2) + super(DrsMoveBetweenTreeOfObjectTestCase, self).tearDown() + + def _make_username(self): + return "DrsTreeU_" + time.strftime("%s", time.gmtime()) + + # now also used to check the group + def _check_obj(self, sam_ldb, obj_orig, is_deleted): + # search the user by guid as it may be deleted + guid_str = self._GUID_string(obj_orig["objectGUID"][0]) + res = sam_ldb.search(base='<GUID=%s>' % guid_str, + controls=["show_deleted:1"], + attrs=["*", "parentGUID"]) + self.assertEqual(len(res), 1) + user_cur = res[0] + cn_orig = str(obj_orig["cn"][0]) + cn_cur = str(user_cur["cn"][0]) + name_orig = str(obj_orig["name"][0]) + name_cur = str(user_cur["name"][0]) + dn_orig = obj_orig["dn"] + dn_cur = user_cur["dn"] + # now check properties of the user + if is_deleted: + self.assertTrue("isDeleted" in user_cur) + self.assertEqual(cn_cur.split('\n')[0], cn_orig) + self.assertEqual(name_cur.split('\n')[0], name_orig) + self.assertEqual(dn_cur.get_rdn_value().split('\n')[0], + dn_orig.get_rdn_value()) + self.assertEqual(name_cur, cn_cur) + else: + self.assertFalse("isDeleted" in user_cur) + self.assertEqual(cn_cur, cn_orig) + self.assertEqual(name_cur, name_orig) + self.assertEqual(dn_cur, dn_orig) + self.assertEqual(name_cur, cn_cur) + self.assertEqual(name_cur, user_cur.dn.get_rdn_value()) + + return user_cur + + def test_ReplicateMoveInTree1(self): + """Verifies how an object is replicated between two DCs. + This test should verify that: + - a complex OU tree can be replicated correctly + - the user is in the correct spot (renamed into) within the tree + on both DCs + """ + # work-out unique username to test with + username = self._make_username() + + self.ldb_dc1.add(self.ou1) + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (username, self._GUID_string(user_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=False) + + self.ldb_dc1.add(self.ou2) + self.ldb_dc1.add(self.ou3) + self.ldb_dc1.add(self.ou4) + self.ldb_dc1.add(self.ou5) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou5_dn) + self.ldb_dc1.rename(user_dn, new_dn) + ldb_res = self.ldb_dc1.search(base=self.ou2_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username) + self.assertEqual(len(ldb_res), 1) + + user_moved_orig = ldb_res[0] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be valid user + self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_moved_orig, is_deleted=False) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + def test_ReplicateMoveInTree2(self): + """Verifies how an object is replicated between two DCs. + This test should verify that: + - a complex OU tree can be replicated correctly + - the user is in the correct spot (renamed into) within the tree + on both DCs + - that a rename back works correctly, and is replicated + """ + # work-out unique username to test with + username = self._make_username() + + self.ldb_dc1.add(self.ou1) + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (username, self._GUID_string(user_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=False) + + self.ldb_dc1.add(self.ou2) + self.ldb_dc1.add(self.ou2b) + self.ldb_dc1.add(self.ou3) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou3_dn) + self.ldb_dc1.rename(user_dn, new_dn) + + new_dn3 = ldb.Dn(self.ldb_dc1, "OU=%s" % self.ou3_dn.get_component_value(0)) + new_dn3.add_base(self.ou2b_dn) + self.ldb_dc1.rename(self.ou3_dn, new_dn3) + + ldb_res = self.ldb_dc1.search(base=new_dn3, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username) + self.assertEqual(len(ldb_res), 1) + + user_moved_orig = ldb_res[0] + user_moved_dn = ldb_res[0]["dn"] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_moved_orig, is_deleted=False) + + # Rename on DC1 + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou1_dn) + self.ldb_dc1.rename(user_moved_dn, new_dn) + + # Modify description on DC2 + msg = ldb.Message() + msg.dn = user_moved_dn + msg["description"] = ldb.MessageElement("User Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc2.modify(msg) + + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username) + self.assertEqual(len(ldb_res), 1) + + user_moved_orig = ldb_res[0] + user_moved_dn = ldb_res[0]["dn"] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_moved_orig, is_deleted=False) + self.assertTrue("description" in user_cur) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + + # trigger replication from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + # check user info on DC1 - should be deleted user + user_cur = self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_moved_orig, is_deleted=True) + self.assertFalse("description" in user_cur) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be deleted user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_moved_orig, is_deleted=True) + self.assertFalse("description" in user_cur) + + def test_ReplicateMoveInTree3(self): + """Verifies how an object is replicated between two DCs. + This test should verify that: + - a complex OU tree can be replicated correctly + - the user is in the correct spot (renamed into) within the tree + on both DCs + - that a rename back works correctly, and is replicated + """ + # work-out unique username to test with + username = self._make_username() + + self.ldb_dc1.add(self.ou1) + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (username, self._GUID_string(user_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=False) + + self.ldb_dc1.add(self.ou2) + self.ldb_dc1.add(self.ou2b) + self.ldb_dc1.add(self.ou3) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou3_dn) + self.ldb_dc1.rename(user_dn, new_dn) + + new_dn3 = ldb.Dn(self.ldb_dc1, "OU=%s" % self.ou3_dn.get_component_value(0)) + new_dn3.add_base(self.ou2b_dn) + self.ldb_dc1.rename(self.ou3_dn, new_dn3) + + ldb_res = self.ldb_dc1.search(base=new_dn3, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username) + self.assertEqual(len(ldb_res), 1) + + user_moved_orig = ldb_res[0] + user_moved_dn = ldb_res[0]["dn"] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_moved_orig, is_deleted=False) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(user_moved_dn, new_dn) + + self.ldb_dc1.rename(self.ou2_dn, self.ou2c_dn) + self.ldb_dc1.rename(self.ou2b_dn, self.ou2_dn) + self.ldb_dc1.rename(self.ou2c_dn, self.ou2b_dn) + + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + + user_moved_orig = ldb_res[0] + user_moved_dn = ldb_res[0]["dn"] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_moved_orig, is_deleted=False) + + self.assertEqual(user_cur["parentGUID"], user_moved_orig["parentGUID"]) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + def test_ReplicateMoveInTree3b(self): + """Verifies how an object is replicated between two DCs. + This test should verify that: + - a complex OU tree can be replicated correctly + - the user is in the correct spot (renamed into) within the tree + on both DCs + - that a rename back works correctly, and is replicated + - that a complex rename suffle, combined with unrelated changes to the object, + is replicated correctly. The aim here is the send the objects out-of-order + when sorted by usnChanged. + - confirm that the OU tree and (in particular the user DN) is identical between + the DCs once this has been replicated. + """ + # work-out unique username to test with + username = self._make_username() + + self.ldb_dc1.add(self.ou1) + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (username, self._GUID_string(user_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=False) + + self.ldb_dc1.add(self.ou2) + self.ldb_dc1.add(self.ou2b) + self.ldb_dc1.add(self.ou3) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(user_dn, new_dn) + + ldb_res = self.ldb_dc1.search(base=self.ou2_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username) + self.assertEqual(len(ldb_res), 1) + + user_moved_orig = ldb_res[0] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_moved_orig, is_deleted=False) + + msg = ldb.Message() + msg.dn = new_dn + msg["description"] = ldb.MessageElement("User Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc1.modify(msg) + + # The sleep(1) calls here ensure that the name objects get a + # new 1-sec based timestamp, and so we select how the conflict + # resolution resolves. + self.ldb_dc1.rename(self.ou2_dn, self.ou2c_dn) + time.sleep(1) + self.ldb_dc1.rename(self.ou2b_dn, self.ou2_dn) + time.sleep(1) + self.ldb_dc1.rename(self.ou2c_dn, self.ou2b_dn) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0]), new_dn) + + msg = ldb.Message() + msg.dn = self.ou2_dn + msg["description"] = ldb.MessageElement("OU2 Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc1.modify(msg) + + msg = ldb.Message() + msg.dn = self.ou2b_dn + msg["description"] = ldb.MessageElement("OU2b Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc1.modify(msg) + + ldb_res = self.ldb_dc1.search(base=self.ou2_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + + user_moved_orig = ldb_res[0] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_moved_orig, is_deleted=False) + self.assertEqual(user_cur["parentGUID"][0], user_moved_orig["parentGUID"][0]) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + def test_ReplicateMoveInTree4(self): + """Verifies how an object is replicated between two DCs. + This test should verify that: + - an OU and user can be replicated correctly, even after a rename + - The creation and rename of the OU has been combined with unrelated changes to the object, + The aim here is the send the objects out-of-order when sorted by usnChanged. + - That is, the OU will be sorted by usnChanged after the user that is within that OU. + - That will cause the client to need to get the OU first, by use of the GET_ANC flag + """ + # work-out unique username to test with + username = self._make_username() + + self.ldb_dc1.add(self.ou1) + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # check user info on DC1 + print("Testing for %s with GUID %s" % (username, self._GUID_string(user_orig["objectGUID"][0]))) + self._check_obj(sam_ldb=self.ldb_dc1, obj_orig=user_orig, is_deleted=False) + + self.ldb_dc1.add(self.ou2) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(user_dn, new_dn) + + msg = ldb.Message() + msg.dn = self.ou2_dn + msg["description"] = ldb.MessageElement("OU2 Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc1.modify(msg) + + ldb_res = self.ldb_dc1.search(base=self.ou2_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username) + self.assertEqual(len(ldb_res), 1) + + user_moved_orig = ldb_res[0] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be valid user + self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_moved_orig, is_deleted=False) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + def test_ReplicateAddInOU(self): + """Verifies how an object is replicated between two DCs. + This test should verify that: + - an OU and user can be replicated correctly + - The creation of the OU has been combined with unrelated changes to the object, + The aim here is the send the objects out-of-order when sorted by usnChanged. + - That is, the OU will be sorted by usnChanged after the user that is within that OU. + - That will cause the client to need to get the OU first, by use of the GET_ANC flag + """ + # work-out unique username to test with + username = self._make_username() + + self.ldb_dc1.add(self.ou1) + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + + msg = ldb.Message() + msg.dn = self.ou1_dn + msg["description"] = ldb.MessageElement("OU1 Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc1.modify(msg) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_orig, is_deleted=False) + + self.assertEqual(user_cur["parentGUID"], user_orig["parentGUID"]) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + def test_ReplicateAddInMovedOU(self): + """Verifies how an object is replicated between two DCs. + This test should verify that: + - an OU and user can be replicated correctly + - The creation of the OU has been combined with unrelated changes to the object, + The aim here is the send the objects out-of-order when sorted by usnChanged. + - That is, the OU will be sorted by usnChanged after the user that is within that OU. + - That will cause the client to need to get the OU first, by use of the GET_ANC flag + """ + # work-out unique username to test with + username = self._make_username() + + self.ldb_dc1.add(self.ou1) + self.ldb_dc1.add(self.ou2) + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(user_dn, new_dn) + + self.ldb_dc1.rename(self.ou2_dn, self.ou2b_dn) + + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_moved = ldb_res[0] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be valid user + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_moved, is_deleted=False) + + self.assertEqual(user_cur["parentGUID"], user_moved["parentGUID"]) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + def test_ReplicateAddInConflictOU_time(self): + """Verifies how an object is replicated between two DCs, when created in an ambiguous location + This test should verify that: + - Without replication, two conflicting objects can be created + - force the conflict resolution algorithm so we know which copy will win + (by sleeping while creating the objects, therefore increasing that timestamp on 'name') + - confirm that the user object, created on DC1, ends up in the right place on DC2 + - therefore confirm that the conflict algorithm worked correctly, and that parentGUID was used. + + """ + # work-out unique username to test with + username = self._make_username() + + self.ldb_dc1.add(self.ou1) + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + # Now create two, conflicting objects. This gives the user + # object something to be under on both DCs. + + # We sleep between the two adds so that DC1 adds second, and + # so wins the conflict resolution due to a later creation time + # (modification timestamp on the name attribute). + self.ldb_dc2.add(self.ou2) + time.sleep(1) + self.ldb_dc1.add(self.ou2) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(user_dn, new_dn) + + # Now that we have renamed the user (and so bumped the + # usnChanged), bump the value on the OUs. + msg = ldb.Message() + msg.dn = self.ou2_dn + msg["description"] = ldb.MessageElement("OU2 Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc1.modify(msg) + + msg = ldb.Message() + msg.dn = self.ou2_dn + msg["description"] = ldb.MessageElement("OU2 Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc2.modify(msg) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_moved = ldb_res[0] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be under the OU2 from DC1 + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_moved, is_deleted=False) + + self.assertEqual(user_cur["parentGUID"], user_moved["parentGUID"]) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + def test_ReplicateAddInConflictOU2(self): + """Verifies how an object is replicated between two DCs, when created in an ambiguous location + This test should verify that: + - Without replication, two conflicting objects can be created + - force the conflict resolution algorithm so we know which copy will win + (by changing the description twice, therefore increasing that version count) + - confirm that the user object, created on DC1, ends up in the right place on DC2 + - therefore confirm that the conflict algorithm worked correctly, and that parentGUID was used. + """ + # work-out unique username to test with + username = self._make_username() + + self.ldb_dc1.add(self.ou1) + + # create user on DC1 + self.ldb_dc1.newuser(username=username, + userou="ou=%s,ou=%s" + % (self.ou1_dn.get_component_value(0), + self.top_ou.get_component_value(0)), + password=None, setpassword=False) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_orig = ldb_res[0] + user_dn = ldb_res[0]["dn"] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + # Now create two, conflicting objects. This gives the user + # object something to be under on both DCs. We create it on + # DC1 1sec later so that it will win the conflict resolution. + + self.ldb_dc2.add(self.ou2) + time.sleep(1) + self.ldb_dc1.add(self.ou2) + + new_dn = ldb.Dn(self.ldb_dc1, "CN=%s" % username) + new_dn.add_base(self.ou2_dn) + self.ldb_dc1.rename(user_dn, new_dn) + + # Now that we have renamed the user (and so bumped the + # usnChanged), bump the value on the OUs. + msg = ldb.Message() + msg.dn = self.ou2_dn + msg["description"] = ldb.MessageElement("OU2 Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc1.modify(msg) + + msg = ldb.Message() + msg.dn = self.ou2_dn + msg["description"] = ldb.MessageElement("OU2 Description", ldb.FLAG_MOD_REPLACE, "description") + self.ldb_dc2.modify(msg) + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + ldb_res = self.ldb_dc1.search(base=self.ou1_dn, + scope=SCOPE_SUBTREE, + expression="(samAccountName=%s)" % username, + attrs=["*", "parentGUID"]) + self.assertEqual(len(ldb_res), 1) + user_moved = ldb_res[0] + + # trigger replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + # check user info on DC2 - should be under the OU2 from DC1 + user_cur = self._check_obj(sam_ldb=self.ldb_dc2, obj_orig=user_moved, is_deleted=False) + + self.assertEqual(user_cur["parentGUID"], user_moved["parentGUID"]) + + # delete user on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self._GUID_string(user_orig["objectGUID"][0])) + + # trigger replication from DC1 to DC2, for cleanup + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) diff --git a/source4/torture/drs/python/repl_rodc.py b/source4/torture/drs/python/repl_rodc.py new file mode 100644 index 0000000..ab3d6fa --- /dev/null +++ b/source4/torture/drs/python/repl_rodc.py @@ -0,0 +1,735 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Test replication scenarios involving an RODC +# +# 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/>. +# + +# +# Usage: +# export DC1=dc1_dns_name +# export DC2=dc1_dns_name [this is unused for the test, but it'll still try to connect] +# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun +# PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN repl_rodc -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD" +# + +import drs_base +import samba.tests +import ldb + +from samba import WERRORError +from samba.join import DCJoinContext +from samba.dcerpc import drsuapi, misc, drsblobs, security +from samba.ndr import ndr_unpack, ndr_pack +from samba.samdb import dsdb_Dn +from samba.credentials import Credentials + +import random +import time + + +def drs_get_rodc_partial_attribute_set(samdb, samdb1, exceptions=None): + '''get a list of attributes for RODC replication''' + if exceptions is None: + exceptions = [] + + partial_attribute_set = drsuapi.DsPartialAttributeSet() + partial_attribute_set.version = 1 + + attids = [] + + # the exact list of attids we send is quite critical. Note that + # we do ask for the secret attributes, but set SPECIAL_SECRET_PROCESSING + # to zero them out + schema_dn = samdb.get_schema_basedn() + res = samdb.search(base=schema_dn, scope=ldb.SCOPE_SUBTREE, + expression="objectClass=attributeSchema", + attrs=["lDAPDisplayName", "systemFlags", + "searchFlags"]) + + for r in res: + ldap_display_name = str(r["lDAPDisplayName"][0]) + if "systemFlags" in r: + system_flags = str(r["systemFlags"][0]) + if (int(system_flags) & (samba.dsdb.DS_FLAG_ATTR_NOT_REPLICATED | + samba.dsdb.DS_FLAG_ATTR_IS_CONSTRUCTED)): + continue + if "searchFlags" in r: + search_flags = str(r["searchFlags"][0]) + if (int(search_flags) & samba.dsdb.SEARCH_FLAG_RODC_ATTRIBUTE): + continue + try: + attid = samdb1.get_attid_from_lDAPDisplayName(ldap_display_name) + if attid not in exceptions: + attids.append(int(attid)) + except: + pass + + # the attids do need to be sorted, or windows doesn't return + # all the attributes we need + attids.sort() + partial_attribute_set.attids = attids + partial_attribute_set.num_attids = len(attids) + return partial_attribute_set + + +class DrsRodcTestCase(drs_base.DrsBaseTestCase): + """Intended as a semi-black box test case for replication involving + an RODC.""" + + def setUp(self): + super(DrsRodcTestCase, self).setUp() + self.base_dn = self.ldb_dc1.get_default_basedn() + + self.ou = samba.tests.create_test_ou(self.ldb_dc1, "test_drs_rodc") + self.allowed_group = "CN=Allowed RODC Password Replication Group,CN=Users,%s" % self.base_dn + + self.site = self.ldb_dc1.server_site_name() + self.rodc_name = "TESTRODCDRS%s" % random.randint(1, 10000000) + self.rodc_pass = "password12#" + self.computer_dn = "CN=%s,OU=Domain Controllers,%s" % (self.rodc_name, self.base_dn) + + self.rodc_ctx = DCJoinContext(server=self.ldb_dc1.host_dns_name(), + creds=self.get_credentials(), + lp=self.get_loadparm(), site=self.site, + netbios_name=self.rodc_name, + targetdir=None, domain=None, + machinepass=self.rodc_pass) + self._create_rodc(self.rodc_ctx) + self.rodc_ctx.create_tmp_samdb() + self.tmp_samdb = self.rodc_ctx.tmp_samdb + + rodc_creds = Credentials() + rodc_creds.guess(self.rodc_ctx.lp) + rodc_creds.set_username(self.rodc_name + '$') + rodc_creds.set_password(self.rodc_pass) + self.rodc_creds = rodc_creds + + (self.drs, self.drs_handle) = self._ds_bind(self.dnsname_dc1) + (self.rodc_drs, self.rodc_drs_handle) = self._ds_bind(self.dnsname_dc1, rodc_creds) + + def tearDown(self): + self.rodc_ctx.cleanup_old_join() + super(DrsRodcTestCase, self).tearDown() + + def test_admin_repl_secrets(self): + """ + When a secret attribute is set to be replicated to an RODC with the + admin credentials, it should always replicate regardless of whether + or not it's in the Allowed RODC Password Replication Group. + """ + rand = random.randint(1, 10000000) + expected_user_attributes = [drsuapi.DRSUAPI_ATTID_lmPwdHistory, + drsuapi.DRSUAPI_ATTID_supplementalCredentials, + drsuapi.DRSUAPI_ATTID_ntPwdHistory, + drsuapi.DRSUAPI_ATTID_unicodePwd, + drsuapi.DRSUAPI_ATTID_dBCSPwd] + + user_name = "test_rodcA_%s" % rand + user_dn = "CN=%s,%s" % (user_name, self.ou) + self.ldb_dc1.add({ + "dn": user_dn, + "objectclass": "user", + "sAMAccountName": user_name + }) + + # Store some secret on this user + self.ldb_dc1.setpassword("(sAMAccountName=%s)" % user_name, 'penguin12#', False, user_name) + + req10 = self._getnc_req10(dest_dsa=str(self.rodc_ctx.ntds_guid), + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=user_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, + partial_attribute_set=drs_get_rodc_partial_attribute_set(self.ldb_dc1, self.tmp_samdb), + max_objects=133, + replica_flags=0) + (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req10) + + # Check that the user has been added to msDSRevealedUsers + self._assert_in_revealed_users(user_dn, expected_user_attributes) + + def test_admin_repl_secrets_DummyDN_GUID(self): + """ + When a secret attribute is set to be replicated to an RODC with the + admin credentials, it should always replicate regardless of whether + or not it's in the Allowed RODC Password Replication Group. + """ + rand = random.randint(1, 10000000) + expected_user_attributes = [drsuapi.DRSUAPI_ATTID_lmPwdHistory, + drsuapi.DRSUAPI_ATTID_supplementalCredentials, + drsuapi.DRSUAPI_ATTID_ntPwdHistory, + drsuapi.DRSUAPI_ATTID_unicodePwd, + drsuapi.DRSUAPI_ATTID_dBCSPwd] + + user_name = "test_rodcA_%s" % rand + user_dn = "CN=%s,%s" % (user_name, self.ou) + self.ldb_dc1.add({ + "dn": user_dn, + "objectclass": "user", + "sAMAccountName": user_name + }) + + res = self.ldb_dc1.search(base=user_dn, scope=ldb.SCOPE_BASE, + attrs=["objectGUID"]) + + user_guid = misc.GUID(res[0]["objectGUID"][0]) + + # Store some secret on this user + self.ldb_dc1.setpassword("(sAMAccountName=%s)" % user_name, 'penguin12#', False, user_name) + + req10 = self._getnc_req10(dest_dsa=str(self.rodc_ctx.ntds_guid), + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str="DummyDN", + nc_guid=user_guid, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, + partial_attribute_set=drs_get_rodc_partial_attribute_set(self.ldb_dc1, self.tmp_samdb), + max_objects=133, + replica_flags=0) + try: + (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req10) + except WERRORError as e1: + (enum, estr) = e1.args + self.fail(f"DsGetNCChanges failed with {estr}") + + # Check that the user has been added to msDSRevealedUsers + self._assert_in_revealed_users(user_dn, expected_user_attributes) + + def test_rodc_repl_secrets(self): + """ + When a secret attribute is set to be replicated to an RODC with + the RODC account credentials, it should not replicate if it's in + the Allowed RODC Password Replication Group. Once it is added to + the group, it should replicate. + """ + rand = random.randint(1, 10000000) + expected_user_attributes = [drsuapi.DRSUAPI_ATTID_lmPwdHistory, + drsuapi.DRSUAPI_ATTID_supplementalCredentials, + drsuapi.DRSUAPI_ATTID_ntPwdHistory, + drsuapi.DRSUAPI_ATTID_unicodePwd, + drsuapi.DRSUAPI_ATTID_dBCSPwd] + + user_name = "test_rodcB_%s" % rand + user_dn = "CN=%s,%s" % (user_name, self.ou) + self.ldb_dc1.add({ + "dn": user_dn, + "objectclass": "user", + "sAMAccountName": user_name + }) + + # Store some secret on this user + self.ldb_dc1.setpassword("(sAMAccountName=%s)" % user_name, 'penguin12#', False, user_name) + + req10 = self._getnc_req10(dest_dsa=str(self.rodc_ctx.ntds_guid), + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=user_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, + partial_attribute_set=drs_get_rodc_partial_attribute_set(self.ldb_dc1, self.tmp_samdb), + max_objects=133, + replica_flags=0) + + try: + (level, ctr) = self.rodc_drs.DsGetNCChanges(self.rodc_drs_handle, 10, req10) + self.fail("Successfully replicated secrets to an RODC that shouldn't have been replicated.") + except WERRORError as e: + (enum, estr) = e.args + self.assertEqual(enum, 8630) # ERROR_DS_DRA_SECRETS_DENIED + + # send the same request again and we should get the same response + try: + (level, ctr) = self.rodc_drs.DsGetNCChanges(self.rodc_drs_handle, 10, req10) + self.fail("Successfully replicated secrets to an RODC that shouldn't have been replicated.") + except WERRORError as e1: + (enum, estr) = e1.args + self.assertEqual(enum, 8630) # ERROR_DS_DRA_SECRETS_DENIED + + # Retry with Administrator credentials, ignores password replication groups + (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req10) + + # Check that the user has been added to msDSRevealedUsers + self._assert_in_revealed_users(user_dn, expected_user_attributes) + + def test_rodc_repl_secrets_follow_on_req(self): + """ + Checks that an RODC can't subvert an existing (valid) GetNCChanges + request to reveal secrets it shouldn't have access to. + """ + + # send an acceptable request that will match as many GUIDs as possible. + # Here we set the SPECIAL_SECRET_PROCESSING flag so that the request gets accepted. + # (On the server, this builds up the getnc_state->guids array) + req8 = self._exop_req8(dest_dsa=str(self.rodc_ctx.ntds_guid), + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=self.ldb_dc1.domain_dn(), + exop=drsuapi.DRSUAPI_EXOP_NONE, + max_objects=1, + replica_flags=drsuapi.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING) + (level, ctr) = self.rodc_drs.DsGetNCChanges(self.rodc_drs_handle, 8, req8) + + # Get the next replication chunk, but set REPL_SECRET this time. This + # is following on the the previous accepted request, but we've changed + # exop to now request secrets. This request should fail + try: + req8 = self._exop_req8(dest_dsa=str(self.rodc_ctx.ntds_guid), + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=self.ldb_dc1.domain_dn(), + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET) + req8.highwatermark = ctr.new_highwatermark + + (level, ctr) = self.rodc_drs.DsGetNCChanges(self.rodc_drs_handle, 8, req8) + + self.fail("Successfully replicated secrets to an RODC that shouldn't have been replicated.") + except RuntimeError as e2: + (enum, estr) = e2.args + pass + + def test_msDSRevealedUsers_admin(self): + """ + When a secret attribute is to be replicated to an RODC, the contents + of the attribute should be added to the msDSRevealedUsers attribute + of the computer object corresponding to the RODC. + """ + + rand = random.randint(1, 10000000) + expected_user_attributes = [drsuapi.DRSUAPI_ATTID_lmPwdHistory, + drsuapi.DRSUAPI_ATTID_supplementalCredentials, + drsuapi.DRSUAPI_ATTID_ntPwdHistory, + drsuapi.DRSUAPI_ATTID_unicodePwd, + drsuapi.DRSUAPI_ATTID_dBCSPwd] + + # Add a user on DC1, add it to allowed password replication + # group, and replicate to RODC with EXOP_REPL_SECRETS + user_name = "test_rodcC_%s" % rand + password = "password12#" + user_dn = "CN=%s,%s" % (user_name, self.ou) + self.ldb_dc1.add({ + "dn": user_dn, + "objectclass": "user", + "sAMAccountName": user_name + }) + + # Store some secret on this user + self.ldb_dc1.setpassword("(sAMAccountName=%s)" % user_name, password, False, user_name) + + self.ldb_dc1.add_remove_group_members("Allowed RODC Password Replication Group", + [user_name], + add_members_operation=True) + + req10 = self._getnc_req10(dest_dsa=str(self.rodc_ctx.ntds_guid), + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=user_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, + partial_attribute_set=drs_get_rodc_partial_attribute_set(self.ldb_dc1, self.tmp_samdb), + max_objects=133, + replica_flags=0) + (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req10) + + # Check that the user has been added to msDSRevealedUsers + (packed_attrs_1, unpacked_attrs_1) = self._assert_in_revealed_users(user_dn, expected_user_attributes) + + # Change the user's password on DC1 + self.ldb_dc1.setpassword("(sAMAccountName=%s)" % user_name, password + "1", False, user_name) + + (packed_attrs_2, unpacked_attrs_2) = self._assert_in_revealed_users(user_dn, expected_user_attributes) + self._assert_attrlist_equals(unpacked_attrs_1, unpacked_attrs_2) + + # Replicate to RODC again with EXOP_REPL_SECRETS + req10 = self._getnc_req10(dest_dsa=str(self.rodc_ctx.ntds_guid), + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=user_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, + partial_attribute_set=drs_get_rodc_partial_attribute_set(self.ldb_dc1, self.tmp_samdb), + max_objects=133, + replica_flags=0) + (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req10) + + # This is important for Windows, because the entry won't have been + # updated in time if we don't have it. Even with this sleep, it only + # passes some of the time... + time.sleep(5) + + # Check that the entry in msDSRevealedUsers has been updated + (packed_attrs_3, unpacked_attrs_3) = self._assert_in_revealed_users(user_dn, expected_user_attributes) + self._assert_attrlist_changed(unpacked_attrs_2, unpacked_attrs_3, expected_user_attributes) + + # We should be able to delete the user + self.ldb_dc1.deleteuser(user_name) + + res = self.ldb_dc1.search(scope=ldb.SCOPE_BASE, base=self.computer_dn, + attrs=["msDS-RevealedUsers"]) + self.assertFalse("msDS-RevealedUsers" in res[0]) + + def test_msDSRevealedUsers(self): + """ + When a secret attribute is to be replicated to an RODC, the contents + of the attribute should be added to the msDSRevealedUsers attribute + of the computer object corresponding to the RODC. + """ + + rand = random.randint(1, 10000000) + expected_user_attributes = [drsuapi.DRSUAPI_ATTID_lmPwdHistory, + drsuapi.DRSUAPI_ATTID_supplementalCredentials, + drsuapi.DRSUAPI_ATTID_ntPwdHistory, + drsuapi.DRSUAPI_ATTID_unicodePwd, + drsuapi.DRSUAPI_ATTID_dBCSPwd] + + # Add a user on DC1, add it to allowed password replication + # group, and replicate to RODC with EXOP_REPL_SECRETS + user_name = "test_rodcD_%s" % rand + password = "password12#" + user_dn = "CN=%s,%s" % (user_name, self.ou) + self.ldb_dc1.add({ + "dn": user_dn, + "objectclass": "user", + "sAMAccountName": user_name + }) + + # Store some secret on this user + self.ldb_dc1.setpassword("(sAMAccountName=%s)" % user_name, password, False, user_name) + + self.ldb_dc1.add_remove_group_members("Allowed RODC Password Replication Group", + [user_name], + add_members_operation=True) + + req10 = self._getnc_req10(dest_dsa=str(self.rodc_ctx.ntds_guid), + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=user_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, + partial_attribute_set=drs_get_rodc_partial_attribute_set(self.ldb_dc1, self.tmp_samdb), + max_objects=133, + replica_flags=0) + (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req10) + + # Check that the user has been added to msDSRevealedUsers + (packed_attrs_1, unpacked_attrs_1) = self._assert_in_revealed_users(user_dn, expected_user_attributes) + + # Change the user's password on DC1 + self.ldb_dc1.setpassword("(sAMAccountName=%s)" % user_name, password + "1", False, user_name) + + (packed_attrs_2, unpacked_attrs_2) = self._assert_in_revealed_users(user_dn, expected_user_attributes) + self._assert_attrlist_equals(unpacked_attrs_1, unpacked_attrs_2) + + # Replicate to RODC again with EXOP_REPL_SECRETS + req10 = self._getnc_req10(dest_dsa=str(self.rodc_ctx.ntds_guid), + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=user_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, + partial_attribute_set=drs_get_rodc_partial_attribute_set(self.ldb_dc1, self.tmp_samdb), + max_objects=133, + replica_flags=0) + (level, ctr) = self.rodc_drs.DsGetNCChanges(self.rodc_drs_handle, 10, req10) + + # This is important for Windows, because the entry won't have been + # updated in time if we don't have it. Even with this sleep, it only + # passes some of the time... + time.sleep(5) + + # Check that the entry in msDSRevealedUsers has been updated + (packed_attrs_3, unpacked_attrs_3) = self._assert_in_revealed_users(user_dn, expected_user_attributes) + self._assert_attrlist_changed(unpacked_attrs_2, unpacked_attrs_3, expected_user_attributes) + + # We should be able to delete the user + self.ldb_dc1.deleteuser(user_name) + + res = self.ldb_dc1.search(scope=ldb.SCOPE_BASE, base=self.computer_dn, + attrs=["msDS-RevealedUsers"]) + self.assertFalse("msDS-RevealedUsers" in res[0]) + + def test_msDSRevealedUsers_pas(self): + """ + If we provide a Partial Attribute Set when replicating to an RODC, + we should ignore it and replicate all of the secret attributes anyway + msDSRevealedUsers attribute. + """ + rand = random.randint(1, 10000000) + expected_user_attributes = [drsuapi.DRSUAPI_ATTID_lmPwdHistory, + drsuapi.DRSUAPI_ATTID_supplementalCredentials, + drsuapi.DRSUAPI_ATTID_ntPwdHistory, + drsuapi.DRSUAPI_ATTID_unicodePwd, + drsuapi.DRSUAPI_ATTID_dBCSPwd] + pas_exceptions = [drsuapi.DRSUAPI_ATTID_lmPwdHistory, + drsuapi.DRSUAPI_ATTID_supplementalCredentials, + drsuapi.DRSUAPI_ATTID_ntPwdHistory, + drsuapi.DRSUAPI_ATTID_dBCSPwd] + + # Add a user on DC1, add it to allowed password replication + # group, and replicate to RODC with EXOP_REPL_SECRETS + user_name = "test_rodcE_%s" % rand + password = "password12#" + user_dn = "CN=%s,%s" % (user_name, self.ou) + self.ldb_dc1.add({ + "dn": user_dn, + "objectclass": "user", + "sAMAccountName": user_name + }) + + # Store some secret on this user + self.ldb_dc1.setpassword("(sAMAccountName=%s)" % user_name, password, False, user_name) + + self.ldb_dc1.add_remove_group_members("Allowed RODC Password Replication Group", + [user_name], + add_members_operation=True) + + pas = drs_get_rodc_partial_attribute_set(self.ldb_dc1, self.tmp_samdb, exceptions=pas_exceptions) + req10 = self._getnc_req10(dest_dsa=str(self.rodc_ctx.ntds_guid), + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=user_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, + partial_attribute_set=pas, + max_objects=133, + replica_flags=0) + (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req10) + + # Make sure that we still replicate the secrets + for attribute in ctr.first_object.object.attribute_ctr.attributes: + if attribute.attid in pas_exceptions: + pas_exceptions.remove(attribute.attid) + for attribute in pas_exceptions: + self.fail("%d was not replicated even though the partial attribute set should be ignored." + % attribute) + + # Check that the user has been added to msDSRevealedUsers + (packed_attrs_1, unpacked_attrs_1) = self._assert_in_revealed_users(user_dn, expected_user_attributes) + + def test_msDSRevealedUsers_using_other_RODC(self): + """ + Ensure that the machine account is tied to the destination DSA. + """ + # Create a new identical RODC with just the first letter missing + other_rodc_name = self.rodc_name[1:] + other_rodc_ctx = DCJoinContext(server=self.ldb_dc1.host_dns_name(), + creds=self.get_credentials(), + lp=self.get_loadparm(), site=self.site, + netbios_name=other_rodc_name, + targetdir=None, domain=None, + machinepass=self.rodc_pass) + self._create_rodc(other_rodc_ctx) + + other_rodc_creds = Credentials() + other_rodc_creds.guess(other_rodc_ctx.lp) + other_rodc_creds.set_username(other_rodc_name + '$') + other_rodc_creds.set_password(self.rodc_pass) + + (other_rodc_drs, other_rodc_drs_handle) = self._ds_bind(self.dnsname_dc1, other_rodc_creds) + + rand = random.randint(1, 10000000) + + user_name = "test_rodcF_%s" % rand + user_dn = "CN=%s,%s" % (user_name, self.ou) + self.ldb_dc1.add({ + "dn": user_dn, + "objectclass": "user", + "sAMAccountName": user_name + }) + + # Store some secret on this user + self.ldb_dc1.setpassword("(sAMAccountName=%s)" % user_name, 'penguin12#', False, user_name) + self.ldb_dc1.add_remove_group_members("Allowed RODC Password Replication Group", + [user_name], + add_members_operation=True) + + req10 = self._getnc_req10(dest_dsa=str(other_rodc_ctx.ntds_guid), + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=user_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, + partial_attribute_set=drs_get_rodc_partial_attribute_set(self.ldb_dc1, self.tmp_samdb), + max_objects=133, + replica_flags=0) + + try: + (level, ctr) = self.rodc_drs.DsGetNCChanges(self.rodc_drs_handle, 10, req10) + self.fail("Successfully replicated secrets to an RODC that shouldn't have been replicated.") + except WERRORError as e3: + (enum, estr) = e3.args + self.assertEqual(enum, 8630) # ERROR_DS_DRA_SECRETS_DENIED + + req10 = self._getnc_req10(dest_dsa=str(self.rodc_ctx.ntds_guid), + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=user_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, + partial_attribute_set=drs_get_rodc_partial_attribute_set(self.ldb_dc1, self.tmp_samdb), + max_objects=133, + replica_flags=0) + + try: + (level, ctr) = other_rodc_drs.DsGetNCChanges(other_rodc_drs_handle, 10, req10) + self.fail("Successfully replicated secrets to an RODC that shouldn't have been replicated.") + except WERRORError as e4: + (enum, estr) = e4.args + self.assertEqual(enum, 8630) # ERROR_DS_DRA_SECRETS_DENIED + + def test_msDSRevealedUsers_local_deny_allow(self): + """ + Ensure that the deny trumps allow, and we can modify these + attributes directly instead of the global groups. + + This may fail on Windows due to tokenGroup calculation caching. + """ + rand = random.randint(1, 10000000) + expected_user_attributes = [drsuapi.DRSUAPI_ATTID_lmPwdHistory, + drsuapi.DRSUAPI_ATTID_supplementalCredentials, + drsuapi.DRSUAPI_ATTID_ntPwdHistory, + drsuapi.DRSUAPI_ATTID_unicodePwd, + drsuapi.DRSUAPI_ATTID_dBCSPwd] + + # Add a user on DC1, add it to allowed password replication + # group, and replicate to RODC with EXOP_REPL_SECRETS + user_name = "test_rodcF_%s" % rand + password = "password12#" + user_dn = "CN=%s,%s" % (user_name, self.ou) + self.ldb_dc1.add({ + "dn": user_dn, + "objectclass": "user", + "sAMAccountName": user_name + }) + + # Store some secret on this user + self.ldb_dc1.setpassword("(sAMAccountName=%s)" % user_name, password, False, user_name) + + req10 = self._getnc_req10(dest_dsa=str(self.rodc_ctx.ntds_guid), + invocation_id=self.ldb_dc1.get_invocation_id(), + nc_dn_str=user_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_SECRET, + partial_attribute_set=drs_get_rodc_partial_attribute_set(self.ldb_dc1, self.tmp_samdb), + max_objects=133, + replica_flags=0) + + m = ldb.Message() + m.dn = ldb.Dn(self.ldb_dc1, self.computer_dn) + + m["msDS-RevealOnDemandGroup"] = \ + ldb.MessageElement(user_dn, ldb.FLAG_MOD_ADD, + "msDS-RevealOnDemandGroup") + self.ldb_dc1.modify(m) + + # In local allow, should be success + try: + (level, ctr) = self.rodc_drs.DsGetNCChanges(self.rodc_drs_handle, 10, req10) + except: + self.fail("Should have succeeded when in local allow group") + + self._assert_in_revealed_users(user_dn, expected_user_attributes) + + (self.rodc_drs, self.rodc_drs_handle) = self._ds_bind(self.dnsname_dc1, self.rodc_creds) + + m = ldb.Message() + m.dn = ldb.Dn(self.ldb_dc1, self.computer_dn) + + m["msDS-NeverRevealGroup"] = \ + ldb.MessageElement(user_dn, ldb.FLAG_MOD_ADD, + "msDS-NeverRevealGroup") + self.ldb_dc1.modify(m) + + # In local allow and deny, should be failure + try: + (level, ctr) = self.rodc_drs.DsGetNCChanges(self.rodc_drs_handle, 10, req10) + self.fail("Successfully replicated secrets to an RODC that shouldn't have been replicated.") + except WERRORError as e5: + (enum, estr) = e5.args + self.assertEqual(enum, 8630) # ERROR_DS_DRA_SECRETS_DENIED + + m = ldb.Message() + m.dn = ldb.Dn(self.ldb_dc1, self.computer_dn) + + m["msDS-RevealOnDemandGroup"] = \ + ldb.MessageElement(user_dn, ldb.FLAG_MOD_DELETE, + "msDS-RevealOnDemandGroup") + self.ldb_dc1.modify(m) + + # In local deny, should be failure + (self.rodc_drs, self.rodc_drs_handle) = self._ds_bind(self.dnsname_dc1, self.rodc_creds) + try: + (level, ctr) = self.rodc_drs.DsGetNCChanges(self.rodc_drs_handle, 10, req10) + self.fail("Successfully replicated secrets to an RODC that shouldn't have been replicated.") + except WERRORError as e6: + (enum, estr) = e6.args + self.assertEqual(enum, 8630) # ERROR_DS_DRA_SECRETS_DENIED + + def _assert_in_revealed_users(self, user_dn, attrlist): + res = self.ldb_dc1.search(scope=ldb.SCOPE_BASE, base=self.computer_dn, + attrs=["msDS-RevealedUsers"]) + revealed_users = res[0]["msDS-RevealedUsers"] + actual_attrids = [] + packed_attrs = [] + unpacked_attrs = [] + for attribute in revealed_users: + attribute = attribute.decode('utf8') + dsdb_dn = dsdb_Dn(self.ldb_dc1, attribute) + metadata = ndr_unpack(drsblobs.replPropertyMetaData1, dsdb_dn.get_bytes()) + if user_dn in attribute: + unpacked_attrs.append(metadata) + packed_attrs.append(dsdb_dn.get_bytes()) + actual_attrids.append(metadata.attid) + + self.assertEqual(sorted(actual_attrids), sorted(attrlist)) + + return (packed_attrs, unpacked_attrs) + + def _assert_attrlist_equals(self, list_1, list_2): + return self._assert_attrlist_changed(list_1, list_2, [], num_changes=0, expected_new_usn=False) + + def _assert_attrlist_changed(self, list_1, list_2, changed_attributes, num_changes=1, expected_new_usn=True): + for i in range(len(list_2)): + self.assertEqual(list_1[i].attid, list_2[i].attid) + self.assertEqual(list_1[i].originating_invocation_id, list_2[i].originating_invocation_id) + self.assertEqual(list_1[i].version + num_changes, list_2[i].version) + + if expected_new_usn: + self.assertTrue(list_1[i].originating_usn < list_2[i].originating_usn) + self.assertTrue(list_1[i].local_usn < list_2[i].local_usn) + else: + self.assertEqual(list_1[i].originating_usn, list_2[i].originating_usn) + self.assertEqual(list_1[i].local_usn, list_2[i].local_usn) + + if list_1[i].attid in changed_attributes: + # We do the changes too quickly, so unless we put sleeps + # in between calls, these remain the same. Checking the USNs + # is enough. + pass + #self.assertTrue(list_1[i].originating_change_time < list_2[i].originating_change_time) + else: + self.assertEqual(list_1[i].originating_change_time, list_2[i].originating_change_time) + + 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() diff --git a/source4/torture/drs/python/repl_schema.py b/source4/torture/drs/python/repl_schema.py new file mode 100644 index 0000000..9c039a5 --- /dev/null +++ b/source4/torture/drs/python/repl_schema.py @@ -0,0 +1,444 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Tests various schema replication scenarios +# +# 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 DC1=dc1_dns_name +# export DC2=dc2_dns_name +# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun +# PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN repl_schema -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD" +# + +import time +import random +import ldb +import drs_base + +from ldb import ( + ERR_NO_SUCH_OBJECT, + LdbError, + SCOPE_BASE, + Message, + FLAG_MOD_ADD, + FLAG_MOD_REPLACE +) +from samba.dcerpc import drsuapi, misc +from samba.drs_utils import drs_DsBind +from samba import dsdb + + +class DrsReplSchemaTestCase(drs_base.DrsBaseTestCase): + + # prefix for all objects created + obj_prefix = None + # current Class or Attribute object id + obj_id = 0 + + def _exop_req8(self, dest_dsa, invocation_id, nc_dn_str, exop, + replica_flags=0, max_objects=0): + req8 = drsuapi.DsGetNCChangesRequest8() + + req8.destination_dsa_guid = misc.GUID(dest_dsa) if dest_dsa else misc.GUID() + req8.source_dsa_invocation_id = misc.GUID(invocation_id) + req8.naming_context = drsuapi.DsReplicaObjectIdentifier() + req8.naming_context.dn = str(nc_dn_str) + req8.highwatermark = drsuapi.DsReplicaHighWaterMark() + req8.highwatermark.tmp_highest_usn = 0 + req8.highwatermark.reserved_usn = 0 + req8.highwatermark.highest_usn = 0 + req8.uptodateness_vector = None + req8.replica_flags = replica_flags + req8.max_object_count = max_objects + req8.max_ndr_size = 402116 + req8.extended_op = exop + req8.fsmo_info = 0 + req8.partial_attribute_set = None + req8.partial_attribute_set_ex = None + req8.mapping_ctr.num_mappings = 0 + req8.mapping_ctr.mappings = None + + return req8 + + def setUp(self): + super(DrsReplSchemaTestCase, self).setUp() + + # disable automatic replication temporary + self._disable_all_repl(self.dnsname_dc1) + self._disable_all_repl(self.dnsname_dc2) + + # make sure DCs are synchronized before the test + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + # initialize objects prefix if not done yet + if self.obj_prefix is None: + t = time.strftime("%s", time.gmtime()) + DrsReplSchemaTestCase.obj_prefix = "DrsReplSchema-%s" % t + + def tearDown(self): + self._enable_all_repl(self.dnsname_dc1) + self._enable_all_repl(self.dnsname_dc2) + super(DrsReplSchemaTestCase, self).tearDown() + + def _make_obj_names(self, base_name): + '''Try to create a unique name for an object + that is to be added to schema''' + self.obj_id += 1 + obj_name = "%s-%d-%s" % (self.obj_prefix, self.obj_id, base_name) + obj_ldn = obj_name.replace("-", "") + obj_dn = ldb.Dn(self.ldb_dc1, "CN=X") + obj_dn.add_base(ldb.Dn(self.ldb_dc1, self.schema_dn)) + obj_dn.set_component(0, "CN", obj_name) + return (obj_dn, obj_name, obj_ldn) + + def _schema_new_class(self, ldb_ctx, base_name, base_int, oc_cat=1, attrs=None): + (class_dn, class_name, class_ldn) = self._make_obj_names(base_name) + rec = {"dn": class_dn, + "objectClass": ["top", "classSchema"], + "cn": class_name, + "lDAPDisplayName": class_ldn, + "governsId": "1.3.6.1.4.1.7165.4.6.2.5." + + str((100000 * base_int) + random.randint(1, 100000)) + ".1.5.13", + "instanceType": "4", + "objectClassCategory": "%d" % oc_cat, + "subClassOf": "top", + "systemOnly": "FALSE"} + # allow overriding/adding attributes + if attrs is not None: + rec.update(attrs) + # add it to the Schema + try: + ldb_ctx.add(rec) + except LdbError as e: + (enum, estr) = e.args + self.fail("Adding record failed with %d/%s" % (enum, estr)) + + self._ldap_schemaUpdateNow(ldb_ctx) + return (rec["lDAPDisplayName"], rec["dn"]) + + def _schema_new_attr(self, ldb_ctx, base_name, base_int, attrs=None): + (attr_dn, attr_name, attr_ldn) = self._make_obj_names(base_name) + rec = {"dn": attr_dn, + "objectClass": ["top", "attributeSchema"], + "cn": attr_name, + "lDAPDisplayName": attr_ldn, + "attributeId": "1.3.6.1.4.1.7165.4.6.1.5." + + str((100000 * base_int) + random.randint(1, 100000)) + ".1.5.13", + "attributeSyntax": "2.5.5.12", + "omSyntax": "64", + "instanceType": "4", + "isSingleValued": "TRUE", + "systemOnly": "FALSE"} + # allow overriding/adding attributes + if attrs is not None: + rec.update(attrs) + # add it to the Schema + ldb_ctx.add(rec) + self._ldap_schemaUpdateNow(ldb_ctx) + return (rec["lDAPDisplayName"], rec["dn"]) + + def _check_object(self, obj_dn): + '''Check if object obj_dn exists on both DCs''' + res_dc1 = self.ldb_dc1.search(base=obj_dn, + scope=SCOPE_BASE, + attrs=["*"]) + self.assertEqual(len(res_dc1), 1, + "%s doesn't exists on %s" % (obj_dn, self.dnsname_dc1)) + try: + res_dc2 = self.ldb_dc2.search(base=obj_dn, + scope=SCOPE_BASE, + attrs=["*"]) + except LdbError as e1: + (enum, estr) = e1.args + if enum == ERR_NO_SUCH_OBJECT: + self.fail("%s doesn't exists on %s" % (obj_dn, self.dnsname_dc2)) + raise + self.assertEqual(len(res_dc2), 1, + "%s doesn't exists on %s" % (obj_dn, self.dnsname_dc2)) + + def test_class(self): + """Simple test for classSchema replication""" + # add new classSchema object + (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-S", 0) + # force replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.schema_dn, forced=True) + # check object is replicated + self._check_object(c_dn) + + def test_classInheritance(self): + """Test inheritance through subClassOf + I think 5 levels of inheritance is pretty decent for now.""" + # add 5 levels deep hierarchy + c_dn_list = [] + c_ldn_last = None + for i in range(1, 6): + base_name = "cls-I-%02d" % i + (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, base_name, i) + c_dn_list.append(c_dn) + if c_ldn_last: + # inherit from last class added + m = Message.from_dict(self.ldb_dc1, + {"dn": c_dn, + "subClassOf": c_ldn_last}, + FLAG_MOD_REPLACE) + self.ldb_dc1.modify(m) + # store last class ldapDisplayName + c_ldn_last = c_ldn + # force replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.schema_dn, forced=True) + # check objects are replicated + for c_dn in c_dn_list: + self._check_object(c_dn) + + def test_classWithCustomAttribute(self): + """Create new Attribute and a Class, + that has value for newly created attribute. + This should check code path that searches for + AttributeID_id in Schema cache""" + # add new attributeSchema object + (a_ldn, a_dn) = self._schema_new_attr(self.ldb_dc1, "attr-A", 7) + # add a base classSchema class so we can use our new + # attribute in class definition in a sibling class + (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-A", 8, + 1, + {"systemMayContain": a_ldn, + "subClassOf": "classSchema"}) + # add new classSchema object with value for a_ldb attribute + (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-B", 9, + 1, + {"objectClass": ["top", "classSchema", c_ldn], + a_ldn: "test_classWithCustomAttribute"}) + # force replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.schema_dn, forced=True) + # check objects are replicated + self._check_object(c_dn) + self._check_object(a_dn) + + def test_classWithCustomLinkAttribute(self): + """Create new Attribute and a Class, + that has value for newly created attribute. + This should check code path that searches for + AttributeID_id in Schema cache""" + # add new attributeSchema object + (a_ldn, a_dn) = self._schema_new_attr(self.ldb_dc1, "attr-Link-X", 10, + attrs={'linkID': "1.2.840.113556.1.2.50", + "attributeSyntax": "2.5.5.1", + "omSyntax": "127"}) + # add a base classSchema class so we can use our new + # attribute in class definition in a sibling class + (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-Link-Y", 11, + 1, + {"systemMayContain": a_ldn, + "subClassOf": "classSchema"}) + # add new classSchema object with value for a_ldb attribute + (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-Link-Z", 12, + 1, + {"objectClass": ["top", "classSchema", c_ldn], + a_ldn: self.schema_dn}) + # force replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.schema_dn, forced=True) + # check objects are replicated + self._check_object(c_dn) + self._check_object(a_dn) + + res = self.ldb_dc1.search(base="", + scope=SCOPE_BASE, + attrs=["domainFunctionality"]) + + if int(res[0]["domainFunctionality"][0]) > dsdb.DS_DOMAIN_FUNCTION_2000: + res = self.ldb_dc1.search(base=a_dn, + scope=SCOPE_BASE, + attrs=["msDS-IntId"]) + self.assertEqual(1, len(res)) + self.assertTrue("msDS-IntId" in res[0]) + int_id = int(res[0]["msDS-IntId"][0]) + if int_id < 0: + int_id += (1 << 32) + + dc_guid_1 = self.ldb_dc1.get_invocation_id() + + drs, drs_handle = self._ds_bind(self.dnsname_dc1, ip=self.url_dc1) + + req8 = self._exop_req8(dest_dsa=None, + invocation_id=dc_guid_1, + nc_dn_str=c_dn, + exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ, + replica_flags=drsuapi.DRSUAPI_DRS_SYNC_FORCED) + + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + + for link in ctr.linked_attributes: + self.assertTrue(link.attid != int_id, + 'Got %d for both' % link.attid) + + def test_attribute(self): + """Simple test for attributeSchema replication""" + # add new attributeSchema object + (a_ldn, a_dn) = self._schema_new_attr(self.ldb_dc1, "attr-S", 13) + # force replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.schema_dn, forced=True) + # check object is replicated + self._check_object(a_dn) + + def test_attribute_on_ou(self): + """Simple test having an OU with a custom attribute replicated correctly + + This ensures that the server + """ + + # add new attributeSchema object + (a_ldn, a_dn) = self._schema_new_attr(self.ldb_dc1, "attr-OU-S", 14) + (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-OU-A", 15, + 3, + {"mayContain": a_ldn}) + ou_dn = ldb.Dn(self.ldb_dc1, "ou=X") + ou_dn.add_base(self.ldb_dc1.get_default_basedn()) + ou_dn.set_component(0, "OU", a_dn.get_component_value(0)) + rec = {"dn": ou_dn, + "objectClass": ["top", "organizationalUnit", c_ldn], + "ou": ou_dn.get_component_value(0), + a_ldn: "test OU"} + self.ldb_dc1.add(rec) + + # force replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.domain_dn, forced=True) + # check objects are replicated + self._check_object(c_dn) + self._check_object(a_dn) + self._check_object(ou_dn) + self.ldb_dc1.delete(ou_dn) + + def test_all(self): + """Basic plan is to create bunch of classSchema + and attributeSchema objects, replicate Schema NC + and then check all objects are replicated correctly""" + + # add new classSchema object + (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-A", 16) + # add new attributeSchema object + (a_ldn, a_dn) = self._schema_new_attr(self.ldb_dc1, "attr-A", 17) + + # add attribute to the class we have + m = Message.from_dict(self.ldb_dc1, + {"dn": c_dn, + "mayContain": a_ldn}, + FLAG_MOD_ADD) + self.ldb_dc1.modify(m) + + # force replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=self.schema_dn, forced=True) + + # check objects are replicated + self._check_object(c_dn) + self._check_object(a_dn) + + def test_classWithCustomBinaryDNLinkAttribute(self): + # Add a new attribute to the schema, which has binary DN syntax (2.5.5.7) + (bin_ldn, bin_dn) = self._schema_new_attr(self.ldb_dc1, "attr-Link-Bin", 18, + attrs={"linkID": "1.2.840.113556.1.2.50", + "attributeSyntax": "2.5.5.7", + "omSyntax": "127"}) + + (bin_ldn_b, bin_dn_b) = self._schema_new_attr(self.ldb_dc1, "attr-Link-Bin-Back", 19, + attrs={"linkID": bin_ldn, + "attributeSyntax": "2.5.5.1", + "omSyntax": "127"}) + + # Add a new class to the schema which can have the binary DN attribute + (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-Link-Bin", 20, + 3, + {"mayContain": bin_ldn}) + (c_ldn_b, c_dn_b) = self._schema_new_class(self.ldb_dc1, "cls-Link-Bin-Back", 21, + 3, + {"mayContain": bin_ldn_b}) + + link_end_dn = ldb.Dn(self.ldb_dc1, "ou=X") + link_end_dn.add_base(self.ldb_dc1.get_default_basedn()) + link_end_dn.set_component(0, "OU", bin_dn_b.get_component_value(0)) + + ou_dn = ldb.Dn(self.ldb_dc1, "ou=X") + ou_dn.add_base(self.ldb_dc1.get_default_basedn()) + ou_dn.set_component(0, "OU", bin_dn.get_component_value(0)) + + # Add an instance of the class to be pointed at + rec = {"dn": link_end_dn, + "objectClass": ["top", "organizationalUnit", c_ldn_b], + "ou": link_end_dn.get_component_value(0)} + self.ldb_dc1.add(rec) + + # .. and one that does, and points to the first one + rec = {"dn": ou_dn, + "objectClass": ["top", "organizationalUnit", c_ldn], + "ou": ou_dn.get_component_value(0)} + self.ldb_dc1.add(rec) + + m = Message.from_dict(self.ldb_dc1, + {"dn": ou_dn, + bin_ldn: "B:8:1234ABCD:%s" % str(link_end_dn)}, + FLAG_MOD_ADD) + self.ldb_dc1.modify(m) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, + nc_dn=self.schema_dn, forced=True) + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, + nc_dn=self.domain_dn, forced=True) + + self._check_object(c_dn) + self._check_object(bin_dn) + + # Make sure we can delete the backlink + self.ldb_dc1.delete(link_end_dn) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, + nc_dn=self.schema_dn, forced=True) + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, + nc_dn=self.domain_dn, forced=True) + + def test_rename(self): + """Basic plan is to create a classSchema + and attributeSchema objects, replicate Schema NC + and then check all objects are replicated correctly""" + + # add new classSchema object + (c_ldn, c_dn) = self._schema_new_class(self.ldb_dc1, "cls-B", 20) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, + nc_dn=self.schema_dn, forced=True) + + # check objects are replicated + self._check_object(c_dn) + + # rename the Class CN + c_dn_new = ldb.Dn(self.ldb_dc1, str(c_dn)) + c_dn_new.set_component(0, + "CN", + c_dn.get_component_value(0) + "-NEW") + try: + self.ldb_dc1.rename(c_dn, c_dn_new) + except LdbError as e2: + (num, _) = e2.args + self.fail("failed to change CN for %s: %s" % (c_dn, _)) + + # force replication from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, + nc_dn=self.schema_dn, forced=True) + + # check objects are replicated + self._check_object(c_dn_new) diff --git a/source4/torture/drs/python/repl_secdesc.py b/source4/torture/drs/python/repl_secdesc.py new file mode 100644 index 0000000..38ae25a --- /dev/null +++ b/source4/torture/drs/python/repl_secdesc.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Unix SMB/CIFS implementation. +# Copyright (C) Catalyst.Net Ltd. 2017 +# 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/>. +# +import drs_base +import ldb +import samba +from samba import sd_utils +from ldb import LdbError + +class ReplAclTestCase(drs_base.DrsBaseTestCase): + + def setUp(self): + super(ReplAclTestCase, self).setUp() + self.mod = "(A;CIOI;GA;;;SY)" + self.mod_becomes = "(A;OICIIO;GA;;;SY)" + self.mod_inherits_as = "(A;OICIIOID;GA;;;SY)" + + self.sd_utils_dc1 = sd_utils.SDUtils(self.ldb_dc1) + self.sd_utils_dc2 = sd_utils.SDUtils(self.ldb_dc2) + + self.ou = samba.tests.create_test_ou(self.ldb_dc1, + "test_acl_inherit") + + # disable replication for the tests so we can control at what point + # the DCs try to replicate + self._disable_all_repl(self.dnsname_dc1) + self._disable_all_repl(self.dnsname_dc2) + + # make sure DCs are synchronized before the test + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + + def tearDown(self): + self.ldb_dc1.delete(self.ou, ["tree_delete:1"]) + + # re-enable replication + self._enable_all_repl(self.dnsname_dc1) + self._enable_all_repl(self.dnsname_dc2) + + super(ReplAclTestCase, self).tearDown() + + def test_acl_inheirt_new_object_1_pass(self): + # Set the inherited ACL on the parent OU + self.sd_utils_dc1.dacl_add_ace(self.ou, self.mod) + + # Assert ACL set stuck as expected + self.assertIn(self.mod_becomes, + self.sd_utils_dc1.get_sd_as_sddl(self.ou)) + + # Make a new object + dn = ldb.Dn(self.ldb_dc1, "OU=l2,%s" % self.ou) + self.ldb_dc1.add({"dn": dn, "objectclass": "organizationalUnit"}) + + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + forced=True) + + # Assert ACL replicated as expected + self.assertIn(self.mod_becomes, + self.sd_utils_dc2.get_sd_as_sddl(self.ou)) + + # Confirm inherited ACLs are identical and were inherited + + self.assertIn(self.mod_inherits_as, + self.sd_utils_dc1.get_sd_as_sddl(dn)) + self.assertEqual(self.sd_utils_dc1.get_sd_as_sddl(dn), + self.sd_utils_dc2.get_sd_as_sddl(dn)) + + def test_acl_inheirt_new_object(self): + # Set the inherited ACL on the parent OU + self.sd_utils_dc1.dacl_add_ace(self.ou, self.mod) + + # Assert ACL set stuck as expected + self.assertIn(self.mod_becomes, + self.sd_utils_dc1.get_sd_as_sddl(self.ou)) + + # Replicate to DC2 + + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + forced=True) + + # Make a new object + dn = ldb.Dn(self.ldb_dc1, "OU=l2,%s" % self.ou) + self.ldb_dc1.add({"dn": dn, "objectclass": "organizationalUnit"}) + + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + forced=True) + + # Assert ACL replicated as expected + self.assertIn(self.mod_becomes, + self.sd_utils_dc2.get_sd_as_sddl(self.ou)) + + # Confirm inherited ACLs are identical and were inherited + + self.assertIn(self.mod_inherits_as, + self.sd_utils_dc1.get_sd_as_sddl(dn)) + self.assertEqual(self.sd_utils_dc1.get_sd_as_sddl(dn), + self.sd_utils_dc2.get_sd_as_sddl(dn)) + + def test_acl_inherit_existing_object(self): + # Make a new object + dn = ldb.Dn(self.ldb_dc1, "OU=l2,%s" % self.ou) + self.ldb_dc1.add({"dn": dn, "objectclass": "organizationalUnit"}) + + try: + self.ldb_dc2.search(scope=ldb.SCOPE_BASE, + base=dn, + attrs=[]) + self.fail() + except LdbError as err: + enum = err.args[0] + self.assertEqual(enum, ldb.ERR_NO_SUCH_OBJECT) + + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + forced=True) + + # Confirm it is now replicated + self.ldb_dc2.search(scope=ldb.SCOPE_BASE, + base=dn, + attrs=[]) + + # Set the inherited ACL on the parent OU + self.sd_utils_dc1.dacl_add_ace(self.ou, self.mod) + + # Assert ACL set stuck as expected + self.assertIn(self.mod_becomes, + self.sd_utils_dc1.get_sd_as_sddl(self.ou)) + + # Replicate to DC2 + + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + forced=True) + + # Confirm inherited ACLs are identical and were inherited + + # Assert ACL replicated as expected + self.assertIn(self.mod_becomes, + self.sd_utils_dc2.get_sd_as_sddl(self.ou)) + + self.assertIn(self.mod_inherits_as, + self.sd_utils_dc1.get_sd_as_sddl(dn)) + self.assertEqual(self.sd_utils_dc1.get_sd_as_sddl(dn), + self.sd_utils_dc2.get_sd_as_sddl(dn)) + + def test_acl_inheirt_existing_object_1_pass(self): + # Make a new object + dn = ldb.Dn(self.ldb_dc1, "OU=l2,%s" % self.ou) + self.ldb_dc1.add({"dn": dn, "objectclass": "organizationalUnit"}) + + try: + self.ldb_dc2.search(scope=ldb.SCOPE_BASE, + base=dn, + attrs=[]) + self.fail() + except LdbError as err: + enum = err.args[0] + self.assertEqual(enum, ldb.ERR_NO_SUCH_OBJECT) + + # Set the inherited ACL on the parent OU + self.sd_utils_dc1.dacl_add_ace(self.ou, self.mod) + + # Assert ACL set as expected + self.assertIn(self.mod_becomes, + self.sd_utils_dc1.get_sd_as_sddl(self.ou)) + + # Replicate to DC2 + + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + forced=True) + + # Assert ACL replicated as expected + self.assertIn(self.mod_becomes, + self.sd_utils_dc2.get_sd_as_sddl(self.ou)) + + # Confirm inherited ACLs are identical and were inherited + + self.assertIn(self.mod_inherits_as, + self.sd_utils_dc1.get_sd_as_sddl(dn)) + self.assertEqual(self.sd_utils_dc1.get_sd_as_sddl(dn), + self.sd_utils_dc2.get_sd_as_sddl(dn)) + + def test_acl_inheirt_renamed_object(self): + # Make a new object + new_ou = samba.tests.create_test_ou(self.ldb_dc1, + "acl_test_l2") + + sub_ou_dn = ldb.Dn(self.ldb_dc1, "OU=l2,%s" % self.ou) + + try: + self.ldb_dc2.search(scope=ldb.SCOPE_BASE, + base=new_ou, + attrs=[]) + self.fail() + except LdbError as err: + enum = err.args[0] + self.assertEqual(enum, ldb.ERR_NO_SUCH_OBJECT) + + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + forced=True) + + # Confirm it is now replicated + self.ldb_dc2.search(scope=ldb.SCOPE_BASE, + base=new_ou, + attrs=[]) + + # Set the inherited ACL on the parent OU on DC1 + self.sd_utils_dc1.dacl_add_ace(self.ou, self.mod) + + # Assert ACL set as expected + self.assertIn(self.mod_becomes, + self.sd_utils_dc1.get_sd_as_sddl(self.ou)) + + # Replicate to DC2 + + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + forced=True) + + # Assert ACL replicated as expected + self.assertIn(self.mod_becomes, + self.sd_utils_dc2.get_sd_as_sddl(self.ou)) + + # Rename to under self.ou + + self.ldb_dc1.rename(new_ou, sub_ou_dn) + + # Replicate to DC2 + + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + forced=True) + + # Confirm inherited ACLs are identical and were inherited + self.assertIn(self.mod_inherits_as, + self.sd_utils_dc1.get_sd_as_sddl(sub_ou_dn)) + self.assertEqual(self.sd_utils_dc1.get_sd_as_sddl(sub_ou_dn), + self.sd_utils_dc2.get_sd_as_sddl(sub_ou_dn)) + + + def test_acl_inheirt_renamed_child_object(self): + # Make a new OU + new_ou = samba.tests.create_test_ou(self.ldb_dc1, + "acl_test_l2") + + # Here is where the new OU will end up at the end. + sub2_ou_dn_final = ldb.Dn(self.ldb_dc1, "OU=l2,%s" % self.ou) + + sub3_ou_dn = ldb.Dn(self.ldb_dc1, "OU=l3,%s" % new_ou) + sub3_ou_dn_final = ldb.Dn(self.ldb_dc1, "OU=l3,%s" % sub2_ou_dn_final) + + self.ldb_dc1.add({"dn": sub3_ou_dn, + "objectclass": "organizationalUnit"}) + + sub4_ou_dn = ldb.Dn(self.ldb_dc1, "OU=l4,%s" % sub3_ou_dn) + sub4_ou_dn_final = ldb.Dn(self.ldb_dc1, "OU=l4,%s" % sub3_ou_dn_final) + + self.ldb_dc1.add({"dn": sub4_ou_dn, + "objectclass": "organizationalUnit"}) + + try: + self.ldb_dc2.search(scope=ldb.SCOPE_BASE, + base=new_ou, + attrs=[]) + self.fail() + except LdbError as err: + enum = err.args[0] + self.assertEqual(enum, ldb.ERR_NO_SUCH_OBJECT) + + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + forced=True) + + # Confirm it is now replicated + self.ldb_dc2.search(scope=ldb.SCOPE_BASE, + base=new_ou, + attrs=[]) + + # + # Given a tree new_ou -> l3 -> l4 + # + + # Set the inherited ACL on the grandchild OU (l3) on DC1 + self.sd_utils_dc1.dacl_add_ace(sub3_ou_dn, self.mod) + + # Assert ACL set stuck as expected + self.assertIn(self.mod_becomes, + self.sd_utils_dc1.get_sd_as_sddl(sub3_ou_dn)) + + # Rename new_ou (l2) to under self.ou (this must happen second). If the + # inheritance between l3 and l4 is name-based, this could + # break. + + # The tree is now self.ou -> l2 -> l3 -> l4 + + self.ldb_dc1.rename(new_ou, sub2_ou_dn_final) + + # Assert ACL set remained as expected + self.assertIn(self.mod_becomes, + self.sd_utils_dc1.get_sd_as_sddl(sub3_ou_dn_final)) + + # Replicate to DC2 + + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + forced=True) + + # Confirm set ACLs (on l3 ) are identical and were inherited + self.assertIn(self.mod_becomes, + self.sd_utils_dc2.get_sd_as_sddl(sub3_ou_dn_final)) + self.assertEqual(self.sd_utils_dc1.get_sd_as_sddl(sub3_ou_dn_final), + self.sd_utils_dc2.get_sd_as_sddl(sub3_ou_dn_final)) + + # Confirm inherited ACLs (from l3 to l4) are identical + # and were inherited + self.assertIn(self.mod_inherits_as, + self.sd_utils_dc1.get_sd_as_sddl(sub4_ou_dn_final)) + self.assertEqual(self.sd_utils_dc1.get_sd_as_sddl(sub4_ou_dn_final), + self.sd_utils_dc2.get_sd_as_sddl(sub4_ou_dn_final)) + + + def test_acl_inheirt_renamed_object_in_conflict(self): + # Make a new object to be renamed under self.ou + new_ou = samba.tests.create_test_ou(self.ldb_dc1, + "acl_test_l2") + + # Make a new OU under self.ou (on DC2) + sub_ou_dn = ldb.Dn(self.ldb_dc2, "OU=l2,%s" % self.ou) + self.ldb_dc2.add({"dn": sub_ou_dn, + "objectclass": "organizationalUnit"}) + + # Set the inherited ACL on the parent OU + self.sd_utils_dc1.dacl_add_ace(self.ou, self.mod) + + # Assert ACL set stuck as expected + self.assertIn(self.mod_becomes, + self.sd_utils_dc1.get_sd_as_sddl(self.ou)) + + # Replicate to DC2 + + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + forced=True) + + # Rename to under self.ou + self.ldb_dc1.rename(new_ou, sub_ou_dn) + self.assertIn(self.mod_inherits_as, + self.sd_utils_dc1.get_sd_as_sddl(sub_ou_dn)) + + # Replicate to DC2 (will cause a conflict, DC1 to win, version + # is higher since named twice) + + self._net_drs_replicate(DC=self.dnsname_dc2, + fromDC=self.dnsname_dc1, + forced=True) + + children = self.ldb_dc2.search(scope=ldb.SCOPE_ONELEVEL, + base=self.ou, + attrs=[]) + for child in children: + self.assertIn(self.mod_inherits_as, + self.sd_utils_dc2.get_sd_as_sddl(child.dn)) + self.assertEqual(self.sd_utils_dc1.get_sd_as_sddl(sub_ou_dn), + self.sd_utils_dc2.get_sd_as_sddl(child.dn)) + + # Replicate back + self._net_drs_replicate(DC=self.dnsname_dc1, + fromDC=self.dnsname_dc2, + forced=True) + + self.assertIn(self.mod_inherits_as, + self.sd_utils_dc1.get_sd_as_sddl(sub_ou_dn)) + + for child in children: + self.assertIn(self.mod_inherits_as, + self.sd_utils_dc1.get_sd_as_sddl(child.dn)) + self.assertEqual(self.sd_utils_dc1.get_sd_as_sddl(child.dn), + self.sd_utils_dc2.get_sd_as_sddl(child.dn)) diff --git a/source4/torture/drs/python/replica_sync.py b/source4/torture/drs/python/replica_sync.py new file mode 100644 index 0000000..f40b16d --- /dev/null +++ b/source4/torture/drs/python/replica_sync.py @@ -0,0 +1,747 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Tests various schema replication scenarios +# +# Copyright (C) Kamen Mazdrashki <kamenim@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/>. +# + +# +# Usage: +# export DC1=dc1_dns_name +# export DC2=dc2_dns_name +# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun +# PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN replica_sync -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD" +# + +import drs_base +import samba.tests +import time +import ldb + +from ldb import ( + SCOPE_BASE, LdbError, ERR_NO_SUCH_OBJECT) + + +class DrsReplicaSyncTestCase(drs_base.DrsBaseTestCase): + """Intended as a black box test case for DsReplicaSync + implementation. It should test the behavior of this + case in cases when inbound replication is disabled""" + + def setUp(self): + super(DrsReplicaSyncTestCase, self).setUp() + + # This OU avoids this test conflicting with anything + # that may already be in the DB + self.top_ou = samba.tests.create_test_ou(self.ldb_dc1, + "replica_sync") + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + self.ou1 = None + self.ou2 = None + + def tearDown(self): + self._cleanup_object(self.ou1) + self._cleanup_object(self.ou2) + self._cleanup_dn(self.top_ou) + + # re-enable replication + self._enable_inbound_repl(self.dnsname_dc1) + self._enable_inbound_repl(self.dnsname_dc2) + + super(DrsReplicaSyncTestCase, self).tearDown() + + def _cleanup_dn(self, dn): + try: + self.ldb_dc2.delete(dn, ["tree_delete:1"]) + except LdbError as e: + (num, _) = e.args + self.assertEqual(num, ERR_NO_SUCH_OBJECT) + try: + self.ldb_dc1.delete(dn, ["tree_delete:1"]) + except LdbError as e1: + (num, _) = e1.args + self.assertEqual(num, ERR_NO_SUCH_OBJECT) + + def _cleanup_object(self, guid): + """Cleans up a test object, if it still exists""" + if guid is not None: + self._cleanup_dn('<GUID=%s>' % guid) + + def test_ReplEnabled(self): + """Tests we can replicate when replication is enabled""" + self._enable_inbound_repl(self.dnsname_dc1) + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=False) + + def test_ReplDisabled(self): + """Tests we can't replicate when replication is disabled""" + self._disable_inbound_repl(self.dnsname_dc1) + + ccache_name = self.get_creds_ccache_name() + + # Tunnel the command line credentials down to the + # subcommand to avoid a new kinit + cmdline_auth = "--use-krb5-ccache=%s" % ccache_name + + # bin/samba-tool drs <drs_command> <cmdline_auth> + cmd_list = ["drs", "replicate", cmdline_auth] + + nc_dn = self.domain_dn + # bin/samba-tool drs replicate <Dest_DC_NAME> <Src_DC_NAME> <Naming Context> + cmd_list += [self.dnsname_dc1, self.dnsname_dc2, nc_dn] + + (result, out, err) = self.runsubcmd(*cmd_list) + self.assertCmdFail(result) + self.assertTrue('WERR_DS_DRA_SINK_DISABLED' in err) + + def test_ReplDisabledForced(self): + """Tests we can force replicate when replication is disabled""" + self._disable_inbound_repl(self.dnsname_dc1) + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True) + + def test_ReplLocal(self): + """Tests we can replicate direct to the local db""" + self._enable_inbound_repl(self.dnsname_dc1) + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=False, local=True, full_sync=True) + + def _create_ou(self, samdb, name): + ldif = """ +dn: %s,%s +objectClass: organizationalUnit +""" % (name, self.top_ou) + samdb.add_ldif(ldif) + res = samdb.search(base="%s,%s" % (name, self.top_ou), + scope=SCOPE_BASE, attrs=["objectGUID"]) + return self._GUID_string(res[0]["objectGUID"][0]) + + def _check_deleted(self, sam_ldb, guid): + # search the user by guid as it may be deleted + res = sam_ldb.search(base='<GUID=%s>' % guid, + controls=["show_deleted:1"], + attrs=["isDeleted", "objectCategory", "ou"]) + self.assertEqual(len(res), 1) + ou_cur = res[0] + # Deleted Object base DN + dodn = self._deleted_objects_dn(sam_ldb) + # now check properties of the user + name_cur = ou_cur["ou"][0] + self.assertEqual(ou_cur["isDeleted"][0], b"TRUE") + self.assertTrue(not("objectCategory" in ou_cur)) + self.assertTrue(dodn in str(ou_cur["dn"]), + "OU %s is deleted but it is not located under %s!" % (name_cur, dodn)) + + def test_ReplConflictsFullSync(self): + """Tests that objects created in conflict become conflict DNs (honour full sync override)""" + + # First confirm local replication (so when we test against windows, this fails fast without creating objects) + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, local=True, forced=True, full_sync=True) + + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + # Create conflicting objects on DC1 and DC2, with DC1 object created first + self.ou1 = self._create_ou(self.ldb_dc1, "OU=Test Full Sync") + # We have to sleep to ensure that the two objects have different timestamps + time.sleep(1) + self.ou2 = self._create_ou(self.ldb_dc2, "OU=Test Full Sync") + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, local=True, forced=True, full_sync=True) + + # Check that DC2 got the DC1 object, and OU1 was make into conflict + res1 = self.ldb_dc2.search(base="<GUID=%s>" % self.ou1, + scope=SCOPE_BASE, attrs=["name"]) + res2 = self.ldb_dc2.search(base="<GUID=%s>" % self.ou2, + scope=SCOPE_BASE, attrs=["name"]) + print(res1[0]["name"][0]) + print(res2[0]["name"][0]) + self.assertFalse('CNF:%s' % self.ou2 in str(res2[0]["name"][0])) + self.assertTrue('CNF:%s' % self.ou1 in str(res1[0]["name"][0])) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc2, self.domain_dn) not in str(res1[0].dn)) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc2, self.domain_dn) not in str(res2[0].dn)) + self.assertEqual(str(res1[0]["name"][0]), res1[0].dn.get_rdn_value()) + self.assertEqual(str(res2[0]["name"][0]), res2[0].dn.get_rdn_value()) + + # Delete both objects by GUID on DC2 + + self.ldb_dc2.delete('<GUID=%s>' % self.ou1) + self.ldb_dc2.delete('<GUID=%s>' % self.ou2) + + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=True) + + self._check_deleted(self.ldb_dc1, self.ou1) + self._check_deleted(self.ldb_dc1, self.ou2) + # Check deleted on DC2 + self._check_deleted(self.ldb_dc2, self.ou1) + self._check_deleted(self.ldb_dc2, self.ou2) + + def test_ReplConflictsRemoteWin(self): + """Tests that objects created in conflict become conflict DNs""" + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + # Create conflicting objects on DC1 and DC2, with DC1 object created first + self.ou1 = self._create_ou(self.ldb_dc1, "OU=Test Remote Conflict") + # We have to sleep to ensure that the two objects have different timestamps + time.sleep(1) + self.ou2 = self._create_ou(self.ldb_dc2, "OU=Test Remote Conflict") + + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Check that DC2 got the DC1 object, and OU1 was make into conflict + res1 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou1, + scope=SCOPE_BASE, attrs=["name"]) + res2 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou2, + scope=SCOPE_BASE, attrs=["name"]) + print(res1[0]["name"][0]) + print(res2[0]["name"][0]) + self.assertTrue('CNF:%s' % self.ou1 in str(res1[0]["name"][0])) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res1[0].dn)) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res2[0].dn)) + self.assertEqual(str(res1[0]["name"][0]), res1[0].dn.get_rdn_value()) + self.assertEqual(str(res2[0]["name"][0]), res2[0].dn.get_rdn_value()) + + # Delete both objects by GUID on DC1 + + self.ldb_dc1.delete('<GUID=%s>' % self.ou1) + self.ldb_dc1.delete('<GUID=%s>' % self.ou2) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True, full_sync=False) + + self._check_deleted(self.ldb_dc1, self.ou1) + self._check_deleted(self.ldb_dc1, self.ou2) + # Check deleted on DC2 + self._check_deleted(self.ldb_dc2, self.ou1) + self._check_deleted(self.ldb_dc2, self.ou2) + + def test_ReplConflictsLocalWin(self): + """Tests that objects created in conflict become conflict DNs""" + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + # Create conflicting objects on DC1 and DC2, with DC2 object created first + self.ou2 = self._create_ou(self.ldb_dc2, "OU=Test Local Conflict") + # We have to sleep to ensure that the two objects have different timestamps + time.sleep(1) + self.ou1 = self._create_ou(self.ldb_dc1, "OU=Test Local Conflict") + + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Check that DC2 got the DC1 object, and OU2 was make into conflict + res1 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou1, + scope=SCOPE_BASE, attrs=["name"]) + res2 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou2, + scope=SCOPE_BASE, attrs=["name"]) + print(res1[0]["name"][0]) + print(res2[0]["name"][0]) + self.assertTrue('CNF:%s' % self.ou2 in str(res2[0]["name"][0]), "Got %s for %s" % (str(res2[0]["name"][0]), self.ou2)) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res1[0].dn)) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res2[0].dn)) + self.assertEqual(str(res1[0]["name"][0]), res1[0].dn.get_rdn_value()) + self.assertEqual(str(res2[0]["name"][0]), res2[0].dn.get_rdn_value()) + + # Delete both objects by GUID on DC1 + + self.ldb_dc1.delete('<GUID=%s>' % self.ou1) + self.ldb_dc1.delete('<GUID=%s>' % self.ou2) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True, full_sync=False) + + self._check_deleted(self.ldb_dc1, self.ou1) + self._check_deleted(self.ldb_dc1, self.ou2) + # Check deleted on DC2 + self._check_deleted(self.ldb_dc2, self.ou1) + self._check_deleted(self.ldb_dc2, self.ou2) + + def test_ReplConflictsRemoteWin_with_child(self): + """Tests that objects created in conflict become conflict DNs""" + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + # Create conflicting objects on DC1 and DC2, with DC1 object created first + self.ou1 = self._create_ou(self.ldb_dc1, "OU=Test Parent Remote Conflict") + # We have to sleep to ensure that the two objects have different timestamps + time.sleep(1) + self.ou2 = self._create_ou(self.ldb_dc2, "OU=Test Parent Remote Conflict") + # Create children on DC2 + ou1_child = self._create_ou(self.ldb_dc1, "OU=Test Child,OU=Test Parent Remote Conflict") + ou2_child = self._create_ou(self.ldb_dc2, "OU=Test Child,OU=Test Parent Remote Conflict") + + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Check that DC2 got the DC1 object, and SELF.OU1 was make into conflict + res1 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou1, + scope=SCOPE_BASE, attrs=["name"]) + res2 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou2, + scope=SCOPE_BASE, attrs=["name"]) + print(res1[0]["name"][0]) + print(res2[0]["name"][0]) + self.assertTrue('CNF:%s' % self.ou1 in str(res1[0]["name"][0])) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res1[0].dn)) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res2[0].dn)) + self.assertEqual(str(res1[0]["name"][0]), res1[0].dn.get_rdn_value()) + self.assertEqual(str(res2[0]["name"][0]), res2[0].dn.get_rdn_value()) + + # Delete both objects by GUID on DC1 + + self.ldb_dc1.delete('<GUID=%s>' % self.ou1, ["tree_delete:1"]) + self.ldb_dc1.delete('<GUID=%s>' % self.ou2, ["tree_delete:1"]) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True, full_sync=False) + + self._check_deleted(self.ldb_dc1, self.ou1) + self._check_deleted(self.ldb_dc1, self.ou2) + # Check deleted on DC2 + self._check_deleted(self.ldb_dc2, self.ou1) + self._check_deleted(self.ldb_dc2, self.ou2) + + self._check_deleted(self.ldb_dc1, ou1_child) + self._check_deleted(self.ldb_dc1, ou2_child) + # Check deleted on DC2 + self._check_deleted(self.ldb_dc2, ou1_child) + self._check_deleted(self.ldb_dc2, ou2_child) + + def test_ReplConflictsRenamedVsNewRemoteWin(self): + """Tests resolving a DN conflict between a renamed object and a new object""" + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + # Create an OU and rename it on DC1 + self.ou1 = self._create_ou(self.ldb_dc1, "OU=Test Remote Rename Conflict orig") + self.ldb_dc1.rename("<GUID=%s>" % self.ou1, "OU=Test Remote Rename Conflict,%s" % self.top_ou) + + # We have to sleep to ensure that the two objects have different timestamps + time.sleep(1) + + # create a conflicting object with the same DN on DC2 + self.ou2 = self._create_ou(self.ldb_dc2, "OU=Test Remote Rename Conflict") + + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Check that DC2 got the DC1 object, and SELF.OU1 was made into conflict + res1 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou1, + scope=SCOPE_BASE, attrs=["name"]) + res2 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou2, + scope=SCOPE_BASE, attrs=["name"]) + print(res1[0]["name"][0]) + print(res2[0]["name"][0]) + self.assertTrue('CNF:%s' % self.ou1 in str(res1[0]["name"][0])) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res1[0].dn)) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res2[0].dn)) + self.assertEqual(str(res1[0]["name"][0]), res1[0].dn.get_rdn_value()) + self.assertEqual(str(res2[0]["name"][0]), res2[0].dn.get_rdn_value()) + + # Delete both objects by GUID on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self.ou1) + self.ldb_dc1.delete('<GUID=%s>' % self.ou2) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True, full_sync=False) + + self._check_deleted(self.ldb_dc1, self.ou1) + self._check_deleted(self.ldb_dc1, self.ou2) + # Check deleted on DC2 + self._check_deleted(self.ldb_dc2, self.ou1) + self._check_deleted(self.ldb_dc2, self.ou2) + + def test_ReplConflictsRenamedVsNewLocalWin(self): + """Tests resolving a DN conflict between a renamed object and a new object""" + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + # Create conflicting objects on DC1 and DC2, where the DC2 object has been renamed + self.ou2 = self._create_ou(self.ldb_dc2, "OU=Test Rename Local Conflict orig") + self.ldb_dc2.rename("<GUID=%s>" % self.ou2, "OU=Test Rename Local Conflict,%s" % self.top_ou) + # We have to sleep to ensure that the two objects have different timestamps + time.sleep(1) + self.ou1 = self._create_ou(self.ldb_dc1, "OU=Test Rename Local Conflict") + + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Check that DC2 got the DC1 object, and OU2 was made into conflict + res1 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou1, + scope=SCOPE_BASE, attrs=["name"]) + res2 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou2, + scope=SCOPE_BASE, attrs=["name"]) + print(res1[0]["name"][0]) + print(res2[0]["name"][0]) + self.assertTrue('CNF:%s' % self.ou2 in str(res2[0]["name"][0])) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res1[0].dn)) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res2[0].dn)) + self.assertEqual(str(res1[0]["name"][0]), res1[0].dn.get_rdn_value()) + self.assertEqual(str(res2[0]["name"][0]), res2[0].dn.get_rdn_value()) + + # Delete both objects by GUID on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self.ou1) + self.ldb_dc1.delete('<GUID=%s>' % self.ou2) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True, full_sync=False) + + self._check_deleted(self.ldb_dc1, self.ou1) + self._check_deleted(self.ldb_dc1, self.ou2) + # Check deleted on DC2 + self._check_deleted(self.ldb_dc2, self.ou1) + self._check_deleted(self.ldb_dc2, self.ou2) + + def test_ReplConflictsRenameRemoteWin(self): + """Tests that objects created in conflict become conflict DNs""" + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + # Create conflicting objects on DC1 and DC2, with DC1 object created first + self.ou1 = self._create_ou(self.ldb_dc1, "OU=Test Remote Rename Conflict") + self.ou2 = self._create_ou(self.ldb_dc2, "OU=Test Remote Rename Conflict 2") + + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + self.ldb_dc1.rename("<GUID=%s>" % self.ou1, "OU=Test Remote Rename Conflict 3,%s" % self.top_ou) + # We have to sleep to ensure that the two objects have different timestamps + time.sleep(1) + self.ldb_dc2.rename("<GUID=%s>" % self.ou2, "OU=Test Remote Rename Conflict 3,%s" % self.top_ou) + + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Check that DC2 got the DC1 object, and SELF.OU1 was make into conflict + res1 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou1, + scope=SCOPE_BASE, attrs=["name"]) + res2 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou2, + scope=SCOPE_BASE, attrs=["name"]) + print(res1[0]["name"][0]) + print(res2[0]["name"][0]) + self.assertTrue('CNF:%s' % self.ou1 in str(res1[0]["name"][0])) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res1[0].dn)) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res2[0].dn)) + self.assertEqual(str(res1[0]["name"][0]), res1[0].dn.get_rdn_value()) + self.assertEqual(str(res2[0]["name"][0]), res2[0].dn.get_rdn_value()) + + # Delete both objects by GUID on DC1 + + self.ldb_dc1.delete('<GUID=%s>' % self.ou1) + self.ldb_dc1.delete('<GUID=%s>' % self.ou2) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True, full_sync=False) + + self._check_deleted(self.ldb_dc1, self.ou1) + self._check_deleted(self.ldb_dc1, self.ou2) + # Check deleted on DC2 + self._check_deleted(self.ldb_dc2, self.ou1) + self._check_deleted(self.ldb_dc2, self.ou2) + + def test_ReplConflictsRenameRemoteWin_with_child(self): + """Tests that objects created in conflict become conflict DNs""" + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + # Create conflicting objects on DC1 and DC2, with DC1 object created first + self.ou1 = self._create_ou(self.ldb_dc1, "OU=Test Parent Remote Rename Conflict") + self.ou2 = self._create_ou(self.ldb_dc2, "OU=Test Parent Remote Rename Conflict 2") + # Create children on DC2 + ou1_child = self._create_ou(self.ldb_dc1, "OU=Test Child,OU=Test Parent Remote Rename Conflict") + ou2_child = self._create_ou(self.ldb_dc2, "OU=Test Child,OU=Test Parent Remote Rename Conflict 2") + + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + self.ldb_dc1.rename("<GUID=%s>" % self.ou1, "OU=Test Parent Remote Rename Conflict 3,%s" % self.top_ou) + # We have to sleep to ensure that the two objects have different timestamps + time.sleep(1) + self.ldb_dc2.rename("<GUID=%s>" % self.ou2, "OU=Test Parent Remote Rename Conflict 3,%s" % self.top_ou) + + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Check that DC2 got the DC1 object, and SELF.OU1 was make into conflict + res1 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou1, + scope=SCOPE_BASE, attrs=["name"]) + res2 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou2, + scope=SCOPE_BASE, attrs=["name"]) + print(res1[0]["name"][0]) + print(res2[0]["name"][0]) + self.assertTrue('CNF:%s' % self.ou1 in str(res1[0]["name"][0])) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res1[0].dn)) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res2[0].dn)) + self.assertEqual(str(res1[0]["name"][0]), res1[0].dn.get_rdn_value()) + self.assertEqual(str(res2[0]["name"][0]), res2[0].dn.get_rdn_value()) + + # Delete both objects by GUID on DC1 + + self.ldb_dc1.delete('<GUID=%s>' % self.ou1, ["tree_delete:1"]) + self.ldb_dc1.delete('<GUID=%s>' % self.ou2, ["tree_delete:1"]) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True, full_sync=False) + + self._check_deleted(self.ldb_dc1, self.ou1) + self._check_deleted(self.ldb_dc1, self.ou2) + # Check deleted on DC2 + self._check_deleted(self.ldb_dc2, self.ou1) + self._check_deleted(self.ldb_dc2, self.ou2) + + self._check_deleted(self.ldb_dc1, ou1_child) + self._check_deleted(self.ldb_dc1, ou2_child) + # Check deleted on DC2 + self._check_deleted(self.ldb_dc2, ou1_child) + self._check_deleted(self.ldb_dc2, ou2_child) + + def test_ReplConflictsRenameLocalWin(self): + """Tests that objects created in conflict become conflict DNs""" + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + # Create conflicting objects on DC1 and DC2, with DC1 object created first + self.ou1 = self._create_ou(self.ldb_dc1, "OU=Test Rename Local Conflict") + self.ou2 = self._create_ou(self.ldb_dc2, "OU=Test Rename Local Conflict 2") + + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + self.ldb_dc2.rename("<GUID=%s>" % self.ou2, "OU=Test Rename Local Conflict 3,%s" % self.top_ou) + # We have to sleep to ensure that the two objects have different timestamps + time.sleep(1) + self.ldb_dc1.rename("<GUID=%s>" % self.ou1, "OU=Test Rename Local Conflict 3,%s" % self.top_ou) + + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Check that DC2 got the DC1 object, and OU2 was make into conflict + res1 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou1, + scope=SCOPE_BASE, attrs=["name"]) + res2 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou2, + scope=SCOPE_BASE, attrs=["name"]) + print(res1[0]["name"][0]) + print(res2[0]["name"][0]) + self.assertTrue('CNF:%s' % self.ou2 in str(res2[0]["name"][0])) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res1[0].dn)) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) not in str(res2[0].dn)) + self.assertEqual(str(res1[0]["name"][0]), res1[0].dn.get_rdn_value()) + self.assertEqual(str(res2[0]["name"][0]), res2[0].dn.get_rdn_value()) + + # Delete both objects by GUID on DC1 + + self.ldb_dc1.delete('<GUID=%s>' % self.ou1) + self.ldb_dc1.delete('<GUID=%s>' % self.ou2) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True, full_sync=False) + + self._check_deleted(self.ldb_dc1, self.ou1) + self._check_deleted(self.ldb_dc1, self.ou2) + # Check deleted on DC2 + self._check_deleted(self.ldb_dc2, self.ou1) + self._check_deleted(self.ldb_dc2, self.ou2) + + def test_ReplLostAndFound(self): + """Tests that objects created under a OU deleted eleswhere end up in lostAndFound""" + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + # Create two OUs on DC2 + self.ou1 = self._create_ou(self.ldb_dc2, "OU=Deleted parent") + self.ou2 = self._create_ou(self.ldb_dc2, "OU=Deleted parent 2") + + # replicate them from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Delete both objects by GUID on DC1 + + self.ldb_dc1.delete('<GUID=%s>' % self.ou1) + self.ldb_dc1.delete('<GUID=%s>' % self.ou2) + + # Create children on DC2 + ou1_child = self._create_ou(self.ldb_dc2, "OU=Test Child,OU=Deleted parent") + ou2_child = self._create_ou(self.ldb_dc2, "OU=Test Child,OU=Deleted parent 2") + + # Replicate from DC2 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Check the sub-OUs are now in lostAndFound and the first one is a conflict DN + + # Check that DC2 got the DC1 object, and one or other object was make into conflict + res1 = self.ldb_dc1.search(base="<GUID=%s>" % ou1_child, + scope=SCOPE_BASE, attrs=["name"]) + res2 = self.ldb_dc1.search(base="<GUID=%s>" % ou2_child, + scope=SCOPE_BASE, attrs=["name"]) + print(res1[0]["name"][0]) + print(res2[0]["name"][0]) + self.assertTrue('CNF:%s' % ou1_child in str(res1[0]["name"][0]) or 'CNF:%s' % ou2_child in str(res2[0]["name"][0])) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) in str(res1[0].dn)) + self.assertTrue(self._lost_and_found_dn(self.ldb_dc1, self.domain_dn) in str(res2[0].dn)) + self.assertEqual(str(res1[0]["name"][0]), res1[0].dn.get_rdn_value()) + self.assertEqual(str(res2[0]["name"][0]), res2[0].dn.get_rdn_value()) + + # Delete all objects by GUID on DC1 + + self.ldb_dc1.delete('<GUID=%s>' % ou1_child) + self.ldb_dc1.delete('<GUID=%s>' % ou2_child) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True, full_sync=False) + + # Check all deleted on DC1 + self._check_deleted(self.ldb_dc1, self.ou1) + self._check_deleted(self.ldb_dc1, self.ou2) + self._check_deleted(self.ldb_dc1, ou1_child) + self._check_deleted(self.ldb_dc1, ou2_child) + # Check all deleted on DC2 + self._check_deleted(self.ldb_dc2, self.ou1) + self._check_deleted(self.ldb_dc2, self.ou2) + self._check_deleted(self.ldb_dc2, ou1_child) + self._check_deleted(self.ldb_dc2, ou2_child) + + def test_ReplRenames(self): + """Tests that objects created under a OU deleted eleswhere end up in lostAndFound""" + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + # Create two OUs on DC2 + self.ou1 = self._create_ou(self.ldb_dc2, "OU=Original parent") + self.ou2 = self._create_ou(self.ldb_dc2, "OU=Original parent 2") + + # replicate them from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Create children on DC1 + ou1_child = self._create_ou(self.ldb_dc1, "OU=Test Child,OU=Original parent") + ou2_child = self._create_ou(self.ldb_dc1, "OU=Test Child 2,OU=Original parent") + ou3_child = self._create_ou(self.ldb_dc1, "OU=Test Case Child,OU=Original parent") + + # replicate them from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True, full_sync=False) + + self.ldb_dc1.rename("<GUID=%s>" % ou2_child, "OU=Test Child 3,OU=Original parent 2,%s" % self.top_ou) + self.ldb_dc1.rename("<GUID=%s>" % ou1_child, "OU=Test Child 2,OU=Original parent 2,%s" % self.top_ou) + self.ldb_dc1.rename("<GUID=%s>" % ou2_child, "OU=Test Child,OU=Original parent 2,%s" % self.top_ou) + self.ldb_dc1.rename("<GUID=%s>" % ou3_child, "OU=Test CASE Child,OU=Original parent,%s" % self.top_ou) + self.ldb_dc2.rename("<GUID=%s>" % self.ou2, "OU=Original parent 3,%s" % self.top_ou) + self.ldb_dc2.rename("<GUID=%s>" % self.ou1, "OU=Original parent 2,%s" % self.top_ou) + + # replicate them from DC1 to DC2 + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True, full_sync=False) + + # Check the sub-OUs are now under Original Parent 3 (original + # parent 2 for Test CASE Child), and both have the right names + + # Check that DC2 got the DC1 object, and the renames are all correct + res1 = self.ldb_dc2.search(base="<GUID=%s>" % ou1_child, + scope=SCOPE_BASE, attrs=["name"]) + res2 = self.ldb_dc2.search(base="<GUID=%s>" % ou2_child, + scope=SCOPE_BASE, attrs=["name"]) + res3 = self.ldb_dc2.search(base="<GUID=%s>" % ou3_child, + scope=SCOPE_BASE, attrs=["name"]) + print(res1[0].dn) + print(res2[0].dn) + print(res3[0].dn) + self.assertEqual('Test Child 2', str(res1[0]["name"][0])) + self.assertEqual('Test Child', str(res2[0]["name"][0])) + self.assertEqual('Test CASE Child', str(res3[0]["name"][0])) + self.assertEqual(str(res1[0].dn), "OU=Test Child 2,OU=Original parent 3,%s" % self.top_ou) + self.assertEqual(str(res2[0].dn), "OU=Test Child,OU=Original parent 3,%s" % self.top_ou) + self.assertEqual(str(res3[0].dn), "OU=Test CASE Child,OU=Original parent 2,%s" % self.top_ou) + + # replicate them from DC2 to DC1 + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Check that DC1 got the DC2 object, and the renames are all correct + res1 = self.ldb_dc1.search(base="<GUID=%s>" % ou1_child, + scope=SCOPE_BASE, attrs=["name"]) + res2 = self.ldb_dc1.search(base="<GUID=%s>" % ou2_child, + scope=SCOPE_BASE, attrs=["name"]) + res3 = self.ldb_dc1.search(base="<GUID=%s>" % ou3_child, + scope=SCOPE_BASE, attrs=["name"]) + print(res1[0].dn) + print(res2[0].dn) + print(res3[0].dn) + self.assertEqual('Test Child 2', str(res1[0]["name"][0])) + self.assertEqual('Test Child', str(res2[0]["name"][0])) + self.assertEqual('Test CASE Child', str(res3[0]["name"][0])) + self.assertEqual(str(res1[0].dn), "OU=Test Child 2,OU=Original parent 3,%s" % self.top_ou) + self.assertEqual(str(res2[0].dn), "OU=Test Child,OU=Original parent 3,%s" % self.top_ou) + self.assertEqual(str(res3[0].dn), "OU=Test CASE Child,OU=Original parent 2,%s" % self.top_ou) + + # Delete all objects by GUID on DC1 + + self.ldb_dc1.delete('<GUID=%s>' % ou1_child) + self.ldb_dc1.delete('<GUID=%s>' % ou2_child) + self.ldb_dc1.delete('<GUID=%s>' % ou3_child) + self.ldb_dc1.delete('<GUID=%s>' % self.ou1) + self.ldb_dc1.delete('<GUID=%s>' % self.ou2) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True, full_sync=False) + + # Check all deleted on DC1 + self._check_deleted(self.ldb_dc1, self.ou1) + self._check_deleted(self.ldb_dc1, self.ou2) + self._check_deleted(self.ldb_dc1, ou1_child) + self._check_deleted(self.ldb_dc1, ou2_child) + self._check_deleted(self.ldb_dc1, ou3_child) + # Check all deleted on DC2 + self._check_deleted(self.ldb_dc2, self.ou1) + self._check_deleted(self.ldb_dc2, self.ou2) + self._check_deleted(self.ldb_dc2, ou1_child) + self._check_deleted(self.ldb_dc2, ou2_child) + self._check_deleted(self.ldb_dc2, ou3_child) + + def reanimate_object(self, samdb, guid, new_dn): + """Re-animates a deleted object""" + res = samdb.search(base="<GUID=%s>" % guid, attrs=["isDeleted"], + controls=['show_deleted:1'], scope=SCOPE_BASE) + if len(res) != 1: + return + + msg = ldb.Message() + msg.dn = res[0].dn + msg["isDeleted"] = ldb.MessageElement([], ldb.FLAG_MOD_DELETE, "isDeleted") + msg["distinguishedName"] = ldb.MessageElement([new_dn], ldb.FLAG_MOD_REPLACE, "distinguishedName") + samdb.modify(msg, ["show_deleted:1"]) + + def test_ReplReanimationConflict(self): + """ + Checks that if a reanimated object conflicts with a new object, then + the conflict is resolved correctly. + """ + + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + # create an object, "accidentally" delete it, and replicate the changes to both DCs + self.ou1 = self._create_ou(self.ldb_dc2, "OU=Conflict object") + self.ldb_dc2.delete('<GUID=%s>' % self.ou1) + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Now pretend that the admin for one DC resolves the problem by + # re-animating the object... + self.reanimate_object(self.ldb_dc1, self.ou1, "OU=Conflict object,%s" % self.top_ou) + + # ...whereas another admin just creates a user with the same name + # again on a different DC + time.sleep(1) + self.ou2 = self._create_ou(self.ldb_dc2, "OU=Conflict object") + + # Now sync the DCs to resolve the conflict + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, forced=True, full_sync=False) + + # Check the latest change won and SELF.OU1 was made into a conflict + res1 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou1, + scope=SCOPE_BASE, attrs=["name"]) + res2 = self.ldb_dc1.search(base="<GUID=%s>" % self.ou2, + scope=SCOPE_BASE, attrs=["name"]) + print(res1[0]["name"][0]) + print(res2[0]["name"][0]) + self.assertTrue('CNF:%s' % self.ou1 in str(res1[0]["name"][0])) + self.assertFalse('CNF:%s' % self.ou2 in str(res2[0]["name"][0])) + + # Delete both objects by GUID on DC1 + self.ldb_dc1.delete('<GUID=%s>' % self.ou1) + self.ldb_dc1.delete('<GUID=%s>' % self.ou2) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True, full_sync=False) + + self._check_deleted(self.ldb_dc1, self.ou1) + self._check_deleted(self.ldb_dc1, self.ou2) + # Check deleted on DC2 + self._check_deleted(self.ldb_dc2, self.ou1) + self._check_deleted(self.ldb_dc2, self.ou2) diff --git a/source4/torture/drs/python/replica_sync_rodc.py b/source4/torture/drs/python/replica_sync_rodc.py new file mode 100644 index 0000000..cbdcc12 --- /dev/null +++ b/source4/torture/drs/python/replica_sync_rodc.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Test conflict scenarios on the RODC +# +# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2011 +# Copyright (C) Catalyst.NET Ltd 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 DC1=dc1_dns_name +# export DC2=dc2_dns_name (RODC) +# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun +# PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN replica_sync_rodc -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD" +# + +import drs_base +import samba.tests +import time +import ldb +from samba.common import get_string + +from ldb import ( + SCOPE_BASE, LdbError, ERR_NO_SUCH_OBJECT) + + +class DrsReplicaSyncTestCase(drs_base.DrsBaseTestCase): + """Intended as a black box test case for DsReplicaSync + implementation. It should test the behavior of this + case in cases when inbound replication is disabled""" + + def setUp(self): + super(DrsReplicaSyncTestCase, self).setUp() + self._disable_all_repl(self.dnsname_dc1) + self.ou1 = None + self.ou2 = None + + def tearDown(self): + # re-enable replication + self._enable_all_repl(self.dnsname_dc1) + + super(DrsReplicaSyncTestCase, self).tearDown() + + def _create_ou(self, samdb, name): + ldif = """ +dn: %s,%s +objectClass: organizationalUnit +""" % (name, self.domain_dn) + samdb.add_ldif(ldif) + res = samdb.search(base="%s,%s" % (name, self.domain_dn), + scope=SCOPE_BASE, attrs=["objectGUID"]) + return get_string(self._GUID_string(res[0]["objectGUID"][0])) + + def _check_deleted(self, sam_ldb, guid): + # search the user by guid as it may be deleted + res = sam_ldb.search(base='<GUID=%s>' % guid, + controls=["show_deleted:1"], + attrs=["isDeleted", "objectCategory", "ou"]) + self.assertEqual(len(res), 1) + ou_cur = res[0] + # Deleted Object base DN + dodn = self._deleted_objects_dn(sam_ldb) + # now check properties of the user + name_cur = ou_cur["ou"][0] + self.assertEqual(ou_cur["isDeleted"][0], "TRUE") + self.assertTrue(not("objectCategory" in ou_cur)) + self.assertTrue(dodn in str(ou_cur["dn"]), + "OU %s is deleted but it is not located under %s!" % (name_cur, dodn)) + + def test_ReplConflictsRODC(self): + """Tests that objects created in conflict become conflict DNs""" + # Replicate all objects to RODC beforehand + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + # Create conflicting objects on DC1 and DC2, with DC1 object created first + name = "OU=Test RODC Conflict" + self.ou1 = self._create_ou(self.ldb_dc1, name) + + # Replicate single object + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, + nc_dn="%s,%s" % (name, self.domain_dn), + local=True, single=True, forced=True) + + # Delete the object, so another can be added + self.ldb_dc1.delete('<GUID=%s>' % self.ou1) + + # Create a conflicting DN as it would appear to the RODC + self.ou2 = self._create_ou(self.ldb_dc1, name) + + try: + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, + nc_dn="%s,%s" % (name, self.domain_dn), + local=True, single=True, forced=True) + except: + # Cleanup the object + self.ldb_dc1.delete('<GUID=%s>' % self.ou2) + return + + # Replicate cannot succeed, HWM would be updated incorrectly. + self.fail("DRS replicate should have failed.") + + def test_ReplConflictsRODCRename(self): + """Tests that objects created in conflict become conflict DNs""" + # Replicate all objects to RODC beforehand + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, forced=True) + + # Create conflicting objects on DC1 and DC2, with DC1 object created first + name = "OU=Test RODC Rename Conflict" + self.ou1 = self._create_ou(self.ldb_dc1, name) + + # Replicate single object + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, + nc_dn="%s,%s" % (name, self.domain_dn), + local=True, single=True, forced=True) + + # Create a non-conflicting DN to rename as conflicting + free_name = "OU=Test RODC Rename No Conflict" + self.ou2 = self._create_ou(self.ldb_dc1, free_name) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, + nc_dn="%s,%s" % (free_name, self.domain_dn), + local=True, single=True, forced=True) + + # Delete the object, so we can rename freely + # DO NOT REPLICATE TO THE RODC + self.ldb_dc1.delete('<GUID=%s>' % self.ou1) + + # Collide the name from the RODC perspective + self.ldb_dc1.rename("<GUID=%s>" % self.ou2, "%s,%s" % (name, self.domain_dn)) + + try: + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, + nc_dn="%s,%s" % (name, self.domain_dn), + local=True, single=True, forced=True) + except: + # Cleanup the object + self.ldb_dc1.delete('<GUID=%s>' % self.ou2) + return + + # Replicate cannot succeed, HWM would be updated incorrectly. + self.fail("DRS replicate should have failed.") diff --git a/source4/torture/drs/python/ridalloc_exop.py b/source4/torture/drs/python/ridalloc_exop.py new file mode 100644 index 0000000..ecd5cec --- /dev/null +++ b/source4/torture/drs/python/ridalloc_exop.py @@ -0,0 +1,802 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Tests various RID allocation scenarios +# +# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2011 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2016 +# Copyright (C) Catalyst IT Ltd. 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/>. +# + +# +# Usage: +# export DC1=dc1_dns_name +# export DC2=dc2_dns_name +# export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun +# PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN ridalloc_exop -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD" +# + +import drs_base + +import ldb +from ldb import SCOPE_BASE + +from samba.dcerpc import drsuapi, misc +from samba.samdb import SamDB + +import shutil +import os +from samba.auth import system_session, admin_session +from samba.dbchecker import dbcheck +from samba.ndr import ndr_pack +from samba.dcerpc import security +from samba import drs_utils, dsdb + + +class DrsReplicaSyncTestCase(drs_base.DrsBaseTestCase): + """Intended as a semi-black box test case for DsGetNCChanges + implementation for extended operations. It should be testing + how DsGetNCChanges handles different input params (mostly invalid). + Final goal is to make DsGetNCChanges as binary compatible to + Windows implementation as possible""" + + def setUp(self): + super(DrsReplicaSyncTestCase, self).setUp() + + def tearDown(self): + super(DrsReplicaSyncTestCase, self).tearDown() + + def _determine_fSMORoleOwner(self, fsmo_obj_dn): + """Returns (owner, not_owner) pair where: + owner: dns name for FSMO owner + not_owner: dns name for DC not owning the FSMO""" + # collect info to return later + fsmo_info_1 = {"dns_name": self.dnsname_dc1, + "invocation_id": self.ldb_dc1.get_invocation_id(), + "ntds_guid": self.ldb_dc1.get_ntds_GUID(), + "server_dn": self.ldb_dc1.get_serverName()} + fsmo_info_2 = {"dns_name": self.dnsname_dc2, + "invocation_id": self.ldb_dc2.get_invocation_id(), + "ntds_guid": self.ldb_dc2.get_ntds_GUID(), + "server_dn": self.ldb_dc2.get_serverName()} + + msgs = self.ldb_dc1.search(scope=ldb.SCOPE_BASE, base=fsmo_info_1["server_dn"], attrs=["serverReference"]) + fsmo_info_1["server_acct_dn"] = ldb.Dn(self.ldb_dc1, msgs[0]["serverReference"][0].decode('utf8')) + fsmo_info_1["rid_set_dn"] = ldb.Dn(self.ldb_dc1, "CN=RID Set") + fsmo_info_1["server_acct_dn"] + + msgs = self.ldb_dc2.search(scope=ldb.SCOPE_BASE, base=fsmo_info_2["server_dn"], attrs=["serverReference"]) + fsmo_info_2["server_acct_dn"] = ldb.Dn(self.ldb_dc2, msgs[0]["serverReference"][0].decode('utf8')) + fsmo_info_2["rid_set_dn"] = ldb.Dn(self.ldb_dc2, "CN=RID Set") + fsmo_info_2["server_acct_dn"] + + # determine the owner dc + res = self.ldb_dc1.search(fsmo_obj_dn, + scope=SCOPE_BASE, attrs=["fSMORoleOwner"]) + assert len(res) == 1, "Only one fSMORoleOwner value expected for %s!" % fsmo_obj_dn + fsmo_owner = res[0]["fSMORoleOwner"][0] + if fsmo_owner == self.info_dc1["dsServiceName"][0]: + return (fsmo_info_1, fsmo_info_2) + return (fsmo_info_2, fsmo_info_1) + + def _check_exop_failed(self, ctr6, expected_failure): + self.assertEqual(ctr6.extended_ret, expected_failure) + #self.assertEqual(ctr6.object_count, 0) + #self.assertEqual(ctr6.first_object, None) + self.assertEqual(ctr6.more_data, False) + self.assertEqual(ctr6.nc_object_count, 0) + self.assertEqual(ctr6.nc_linked_attributes_count, 0) + self.assertEqual(ctr6.linked_attributes_count, 0) + self.assertEqual(ctr6.linked_attributes, []) + self.assertEqual(ctr6.drs_error[0], 0) + + def test_InvalidDestDSA_ridalloc(self): + """Test RID allocation with invalid destination DSA guid""" + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + req8 = self._exop_req8(dest_dsa="9c637462-5b8c-4467-aef2-bdb1f57bc4ef", + invocation_id=fsmo_owner["invocation_id"], + nc_dn_str=fsmo_dn, + exop=drsuapi.DRSUAPI_EXOP_FSMO_RID_ALLOC) + + (drs, drs_handle) = self._ds_bind(fsmo_owner["dns_name"]) + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + self.assertEqual(level, 6, "Expected level 6 response!") + self._check_exop_failed(ctr, drsuapi.DRSUAPI_EXOP_ERR_UNKNOWN_CALLER) + self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_owner["ntds_guid"])) + self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_owner["invocation_id"])) + + def test_do_ridalloc(self): + """Test doing a RID allocation with a valid destination DSA guid""" + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + req8 = self._exop_req8(dest_dsa=fsmo_not_owner["ntds_guid"], + invocation_id=fsmo_owner["invocation_id"], + nc_dn_str=fsmo_dn, + exop=drsuapi.DRSUAPI_EXOP_FSMO_RID_ALLOC) + + (drs, drs_handle) = self._ds_bind(fsmo_owner["dns_name"]) + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + self.assertEqual(level, 6, "Expected level 6 response!") + self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_owner["ntds_guid"])) + self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_owner["invocation_id"])) + ctr6 = ctr + self.assertEqual(ctr6.extended_ret, drsuapi.DRSUAPI_EXOP_ERR_SUCCESS) + self.assertEqual(ctr6.object_count, 3) + self.assertNotEqual(ctr6.first_object, None) + self.assertEqual(ldb.Dn(self.ldb_dc1, ctr6.first_object.object.identifier.dn), fsmo_dn) + self.assertNotEqual(ctr6.first_object.next_object, None) + self.assertNotEqual(ctr6.first_object.next_object.next_object, None) + second_object = ctr6.first_object.next_object.object + self.assertEqual(ldb.Dn(self.ldb_dc1, second_object.identifier.dn), fsmo_not_owner["rid_set_dn"]) + third_object = ctr6.first_object.next_object.next_object.object + self.assertEqual(ldb.Dn(self.ldb_dc1, third_object.identifier.dn), fsmo_not_owner["server_acct_dn"]) + + self.assertEqual(ctr6.more_data, False) + self.assertEqual(ctr6.nc_object_count, 0) + self.assertEqual(ctr6.nc_linked_attributes_count, 0) + self.assertEqual(ctr6.drs_error[0], 0) + # We don't check the linked_attributes_count as if the domain + # has an RODC, it can gain links on the server account object + + def test_do_ridalloc_get_anc(self): + """Test doing a RID allocation with a valid destination DSA guid and GET_ANC flag""" + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + req8 = self._exop_req8(dest_dsa=fsmo_not_owner["ntds_guid"], + invocation_id=fsmo_owner["invocation_id"], + nc_dn_str=fsmo_dn, + exop=drsuapi.DRSUAPI_EXOP_FSMO_RID_ALLOC, + replica_flags=drsuapi.DRSUAPI_DRS_GET_ANC) + + (drs, drs_handle) = self._ds_bind(fsmo_owner["dns_name"]) + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + self.assertEqual(level, 6, "Expected level 6 response!") + self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_owner["ntds_guid"])) + self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_owner["invocation_id"])) + ctr6 = ctr + self.assertEqual(ctr6.extended_ret, drsuapi.DRSUAPI_EXOP_ERR_SUCCESS) + self.assertEqual(ctr6.object_count, 3) + self.assertNotEqual(ctr6.first_object, None) + self.assertEqual(ldb.Dn(self.ldb_dc1, ctr6.first_object.object.identifier.dn), fsmo_dn) + self.assertNotEqual(ctr6.first_object.next_object, None) + self.assertNotEqual(ctr6.first_object.next_object.next_object, None) + second_object = ctr6.first_object.next_object.object + self.assertEqual(ldb.Dn(self.ldb_dc1, second_object.identifier.dn), fsmo_not_owner["rid_set_dn"]) + third_object = ctr6.first_object.next_object.next_object.object + self.assertEqual(ldb.Dn(self.ldb_dc1, third_object.identifier.dn), fsmo_not_owner["server_acct_dn"]) + self.assertEqual(ctr6.more_data, False) + self.assertEqual(ctr6.nc_object_count, 0) + self.assertEqual(ctr6.nc_linked_attributes_count, 0) + self.assertEqual(ctr6.drs_error[0], 0) + # We don't check the linked_attributes_count as if the domain + # has an RODC, it can gain links on the server account object + + def test_edit_rid_master(self): + """Test doing a RID allocation after changing the RID master from the original one. + This should set rIDNextRID to 0 on the new RID master.""" + # 1. a. Transfer role to non-RID master + # b. Check that it succeeds correctly + # + # 2. a. Call the RID alloc against the former master. + # b. Check that it succeeds. + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + # 1. Swap RID master role + m = ldb.Message() + m.dn = ldb.Dn(self.ldb_dc1, "") + m["becomeRidMaster"] = ldb.MessageElement("1", ldb.FLAG_MOD_REPLACE, + "becomeRidMaster") + + # Make sure that ldb_dc1 == RID Master + + server_dn = str(ldb.Dn(self.ldb_dc1, self.ldb_dc1.get_dsServiceName()).parent()) + + # self.ldb_dc1 == LOCALDC + if server_dn == fsmo_owner['server_dn']: + # ldb_dc1 == VAMPIREDC + ldb_dc1, ldb_dc2 = self.ldb_dc2, self.ldb_dc1 + else: + # Otherwise switch the two + ldb_dc1, ldb_dc2 = self.ldb_dc1, self.ldb_dc2 + + try: + # ldb_dc1 is now RID MASTER (as VAMPIREDC) + ldb_dc1.modify(m) + except ldb.LdbError as e1: + (num, msg) = e1.args + self.fail("Failed to reassign RID Master " + msg) + + try: + # 2. Perform a RID alloc + req8 = self._exop_req8(dest_dsa=fsmo_owner["ntds_guid"], + invocation_id=fsmo_not_owner["invocation_id"], + nc_dn_str=fsmo_dn, + exop=drsuapi.DRSUAPI_EXOP_FSMO_RID_ALLOC) + + (drs, drs_handle) = self._ds_bind(fsmo_not_owner["dns_name"]) + # 3. Make sure the allocation succeeds + try: + (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8) + except RuntimeError as e: + self.fail("RID allocation failed: " + str(e)) + + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + + self.assertEqual(level, 6, "Expected level 6 response!") + self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_not_owner["ntds_guid"])) + self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_not_owner["invocation_id"])) + ctr6 = ctr + self.assertEqual(ctr6.extended_ret, drsuapi.DRSUAPI_EXOP_ERR_SUCCESS) + self.assertEqual(ctr6.object_count, 3) + self.assertNotEqual(ctr6.first_object, None) + self.assertEqual(ldb.Dn(ldb_dc2, ctr6.first_object.object.identifier.dn), fsmo_dn) + self.assertNotEqual(ctr6.first_object.next_object, None) + self.assertNotEqual(ctr6.first_object.next_object.next_object, None) + second_object = ctr6.first_object.next_object.object + self.assertEqual(ldb.Dn(self.ldb_dc1, second_object.identifier.dn), fsmo_owner["rid_set_dn"]) + third_object = ctr6.first_object.next_object.next_object.object + self.assertEqual(ldb.Dn(self.ldb_dc1, third_object.identifier.dn), fsmo_owner["server_acct_dn"]) + finally: + # Swap the RID master back for other tests + m = ldb.Message() + m.dn = ldb.Dn(ldb_dc2, "") + m["becomeRidMaster"] = ldb.MessageElement("1", ldb.FLAG_MOD_REPLACE, "becomeRidMaster") + try: + ldb_dc2.modify(m) + except ldb.LdbError as e: + (num, msg) = e.args + self.fail("Failed to restore RID Master " + msg) + + def test_offline_samba_tool_seized_ridalloc(self): + """Perform a join against the non-RID manager and then seize the RID Manager role""" + + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + targetdir = self._test_join(fsmo_not_owner['dns_name'], "RIDALLOCTEST1") + try: + # Connect to the database + ldb_url = "tdb://%s" % os.path.join(targetdir, "private/sam.ldb") + smbconf = os.path.join(targetdir, "etc/smb.conf") + + lp = self.get_loadparm() + new_ldb = SamDB(ldb_url, credentials=self.get_credentials(), + session_info=system_session(lp), lp=lp) + + # 1. Get server name + res = new_ldb.search(base=ldb.Dn(new_ldb, new_ldb.get_serverName()), + scope=ldb.SCOPE_BASE, attrs=["serverReference"]) + # 2. Get server reference + server_ref_dn = ldb.Dn(new_ldb, res[0]['serverReference'][0].decode('utf8')) + + # Assert that no RID Set has been set + res = new_ldb.search(base=server_ref_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDSetReferences']) + + self.assertFalse("rIDSetReferences" in res[0]) + + (result, out, err) = self.runsubcmd("fsmo", "seize", "--role", "rid", "-H", ldb_url, "--configfile=%s" % (smbconf), "--force") + self.assertCmdSuccess(result, out, err) + self.assertEqual(err, "", "Shouldn't be any error messages") + + # 3. Assert we get the RID Set + res = new_ldb.search(base=server_ref_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDSetReferences']) + + self.assertTrue("rIDSetReferences" in res[0]) + finally: + shutil.rmtree(targetdir, ignore_errors=True) + self._test_force_demote(fsmo_not_owner['dns_name'], "RIDALLOCTEST1") + + def _test_join(self, server, netbios_name): + tmpdir = os.path.join(self.tempdir, "targetdir") + creds = self.get_credentials() + (result, out, err) = self.runsubcmd("domain", "join", + creds.get_realm(), + "dc", "-U%s%%%s" % (creds.get_username(), + creds.get_password()), + '--targetdir=%s' % tmpdir, + '--server=%s' % server, + "--option=netbios name = %s" % netbios_name) + self.assertCmdSuccess(result, out, err) + return tmpdir + + def _test_force_demote(self, server, netbios_name): + creds = self.get_credentials() + (result, out, err) = self.runsubcmd("domain", "demote", + "-U%s%%%s" % (creds.get_username(), + creds.get_password()), + '--server=%s' % server, + "--remove-other-dead-server=%s" % netbios_name) + self.assertCmdSuccess(result, out, err) + + def test_offline_manual_seized_ridalloc_with_dbcheck(self): + """Perform the same actions as test_offline_samba_tool_seized_ridalloc, + but do not create the RID set. Confirm that dbcheck correctly creates + the RID Set. + + Also check + """ + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + targetdir = self._test_join(fsmo_not_owner['dns_name'], "RIDALLOCTEST2") + try: + # Connect to the database + ldb_url = "tdb://%s" % os.path.join(targetdir, "private/sam.ldb") + lp = self.get_loadparm() + + new_ldb = SamDB(ldb_url, credentials=self.get_credentials(), + session_info=system_session(lp), lp=lp) + + serviceName = new_ldb.get_dsServiceName() + m = ldb.Message() + m.dn = fsmo_dn + m["fSMORoleOwner"] = ldb.MessageElement(serviceName, + ldb.FLAG_MOD_REPLACE, + "fSMORoleOwner") + new_ldb.modify(m) + + # 1. Get server name + res = new_ldb.search(base=ldb.Dn(new_ldb, new_ldb.get_serverName()), + scope=ldb.SCOPE_BASE, attrs=["serverReference"]) + # 2. Get server reference + server_ref_dn = ldb.Dn(new_ldb, res[0]['serverReference'][0].decode('utf8')) + + # Assert that no RID Set has been set + res = new_ldb.search(base=server_ref_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDSetReferences']) + + self.assertFalse("rIDSetReferences" in res[0]) + + chk = dbcheck(new_ldb, verbose=False, fix=True, yes=True, quiet=True) + + self.assertEqual(chk.check_database(DN=server_ref_dn, scope=ldb.SCOPE_BASE), 1, "Should have fixed one error (missing RID Set)") + + # 3. Assert we get the RID Set + res = new_ldb.search(base=server_ref_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDSetReferences']) + + self.assertTrue("rIDSetReferences" in res[0]) + finally: + self._test_force_demote(fsmo_not_owner['dns_name'], "RIDALLOCTEST2") + shutil.rmtree(targetdir, ignore_errors=True) + + def test_offline_manual_seized_ridalloc_add_user(self): + """Perform the same actions as test_offline_samba_tool_seized_ridalloc, + but do not create the RID set. Confirm that user-add correctly creates + the RID Set.""" + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + targetdir = self._test_join(fsmo_not_owner['dns_name'], "RIDALLOCTEST3") + try: + # Connect to the database + ldb_url = "tdb://%s" % os.path.join(targetdir, "private/sam.ldb") + lp = self.get_loadparm() + + new_ldb = SamDB(ldb_url, credentials=self.get_credentials(), + session_info=system_session(lp), lp=lp) + + serviceName = new_ldb.get_dsServiceName() + m = ldb.Message() + m.dn = fsmo_dn + m["fSMORoleOwner"] = ldb.MessageElement(serviceName, + ldb.FLAG_MOD_REPLACE, + "fSMORoleOwner") + new_ldb.modify(m) + + # 1. Get server name + res = new_ldb.search(base=ldb.Dn(new_ldb, new_ldb.get_serverName()), + scope=ldb.SCOPE_BASE, attrs=["serverReference"]) + # 2. Get server reference + server_ref_dn = ldb.Dn(new_ldb, res[0]['serverReference'][0].decode('utf8')) + + # Assert that no RID Set has been set + res = new_ldb.search(base=server_ref_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDSetReferences']) + + self.assertFalse("rIDSetReferences" in res[0]) + + new_ldb.newuser("ridalloctestuser", "P@ssword!") + + # 3. Assert we get the RID Set + res = new_ldb.search(base=server_ref_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDSetReferences']) + + self.assertTrue("rIDSetReferences" in res[0]) + + finally: + self._test_force_demote(fsmo_not_owner['dns_name'], "RIDALLOCTEST3") + shutil.rmtree(targetdir, ignore_errors=True) + + def test_offline_manual_seized_ridalloc_add_user_as_admin(self): + """Perform the same actions as test_offline_samba_tool_seized_ridalloc, + but do not create the RID set. Confirm that user-add correctly creates + the RID Set.""" + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + targetdir = self._test_join(fsmo_not_owner['dns_name'], "RIDALLOCTEST4") + try: + # Connect to the database + ldb_url = "tdb://%s" % os.path.join(targetdir, "private/sam.ldb") + lp = self.get_loadparm() + + new_ldb = SamDB(ldb_url, credentials=self.get_credentials(), + session_info=admin_session(lp, self.ldb_dc1.get_domain_sid()), lp=lp) + + serviceName = new_ldb.get_dsServiceName() + m = ldb.Message() + m.dn = fsmo_dn + m["fSMORoleOwner"] = ldb.MessageElement(serviceName, + ldb.FLAG_MOD_REPLACE, + "fSMORoleOwner") + new_ldb.modify(m) + + # 1. Get server name + res = new_ldb.search(base=ldb.Dn(new_ldb, new_ldb.get_serverName()), + scope=ldb.SCOPE_BASE, attrs=["serverReference"]) + # 2. Get server reference + server_ref_dn = ldb.Dn(new_ldb, res[0]['serverReference'][0].decode('utf8')) + + # Assert that no RID Set has been set + res = new_ldb.search(base=server_ref_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDSetReferences']) + + self.assertFalse("rIDSetReferences" in res[0]) + + # Create a user to allocate a RID Set for itself (the RID master) + new_ldb.newuser("ridalloctestuser", "P@ssword!") + + # 3. Assert we get the RID Set + res = new_ldb.search(base=server_ref_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDSetReferences']) + + self.assertTrue("rIDSetReferences" in res[0]) + + finally: + self._test_force_demote(fsmo_not_owner['dns_name'], "RIDALLOCTEST4") + shutil.rmtree(targetdir, ignore_errors=True) + + def test_join_time_ridalloc(self): + """Perform a join against the RID manager and assert we have a RID Set""" + + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + targetdir = self._test_join(fsmo_owner['dns_name'], "RIDALLOCTEST5") + try: + # Connect to the database + ldb_url = "tdb://%s" % os.path.join(targetdir, "private/sam.ldb") + + lp = self.get_loadparm() + new_ldb = SamDB(ldb_url, credentials=self.get_credentials(), + session_info=system_session(lp), lp=lp) + + # 1. Get server name + res = new_ldb.search(base=ldb.Dn(new_ldb, new_ldb.get_serverName()), + scope=ldb.SCOPE_BASE, attrs=["serverReference"]) + # 2. Get server reference + server_ref_dn = ldb.Dn(new_ldb, res[0]['serverReference'][0].decode('utf8')) + + # 3. Assert we get the RID Set + res = new_ldb.search(base=server_ref_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDSetReferences']) + + self.assertTrue("rIDSetReferences" in res[0]) + finally: + self._test_force_demote(fsmo_owner['dns_name'], "RIDALLOCTEST5") + shutil.rmtree(targetdir, ignore_errors=True) + + def test_rid_set_dbcheck(self): + """Perform a join against the RID manager and assert we have a RID Set. + Using dbcheck, we assert that we can detect out of range users.""" + + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + targetdir = self._test_join(fsmo_owner['dns_name'], "RIDALLOCTEST6") + try: + # Connect to the database + ldb_url = "tdb://%s" % os.path.join(targetdir, "private/sam.ldb") + + lp = self.get_loadparm() + new_ldb = SamDB(ldb_url, credentials=self.get_credentials(), + session_info=system_session(lp), lp=lp) + + # 1. Get server name + res = new_ldb.search(base=ldb.Dn(new_ldb, new_ldb.get_serverName()), + scope=ldb.SCOPE_BASE, attrs=["serverReference"]) + # 2. Get server reference + server_ref_dn = ldb.Dn(new_ldb, res[0]['serverReference'][0].decode('utf8')) + + # 3. Assert we get the RID Set + res = new_ldb.search(base=server_ref_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDSetReferences']) + + self.assertTrue("rIDSetReferences" in res[0]) + rid_set_dn = ldb.Dn(new_ldb, res[0]["rIDSetReferences"][0].decode('utf8')) + + # 4. Add a new user (triggers RID set work) + new_ldb.newuser("ridalloctestuser", "P@ssword!") + + # 5. Now fetch the RID SET + rid_set_res = new_ldb.search(base=rid_set_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDNextRid', + 'rIDAllocationPool']) + next_pool = int(rid_set_res[0]["rIDAllocationPool"][0]) + last_rid = (0xFFFFFFFF00000000 & next_pool) >> 32 + + # 6. Add user above the ridNextRid and at mid-range. + # + # We can do this with safety because this is an offline DB that will be + # destroyed. + m = ldb.Message() + m.dn = ldb.Dn(new_ldb, "CN=ridsettestuser1,CN=Users") + m.dn.add_base(new_ldb.get_default_basedn()) + m['objectClass'] = ldb.MessageElement('user', ldb.FLAG_MOD_ADD, 'objectClass') + m['objectSid'] = ldb.MessageElement(ndr_pack(security.dom_sid(str(new_ldb.get_domain_sid()) + "-%d" % (last_rid - 10))), + ldb.FLAG_MOD_ADD, + 'objectSid') + new_ldb.add(m, controls=["relax:0"]) + + # 7. Check the RID Set + chk = dbcheck(new_ldb, verbose=False, fix=True, yes=True, quiet=True) + + # Should have one error (wrong rIDNextRID) + self.assertEqual(chk.check_database(DN=rid_set_dn, scope=ldb.SCOPE_BASE), 1) + + # 8. Assert we get didn't show any other errors + chk = dbcheck(new_ldb, verbose=False, fix=False, quiet=True) + + rid_set_res = new_ldb.search(base=rid_set_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDNextRid', + 'rIDAllocationPool']) + last_allocated_rid = int(rid_set_res[0]["rIDNextRid"][0]) + self.assertEqual(last_allocated_rid, last_rid - 10) + + # 9. Assert that the range wasn't thrown away + + next_pool = int(rid_set_res[0]["rIDAllocationPool"][0]) + self.assertEqual(last_rid, (0xFFFFFFFF00000000 & next_pool) >> 32, "rid pool should have changed") + finally: + self._test_force_demote(fsmo_owner['dns_name'], "RIDALLOCTEST6") + shutil.rmtree(targetdir, ignore_errors=True) + + def test_rid_set_dbcheck_after_seize(self): + """Perform a join against the RID manager and assert we have a RID Set. + We seize the RID master role, then using dbcheck, we assert that we can + detect out of range users (and then bump the RID set as required).""" + + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + targetdir = self._test_join(fsmo_owner['dns_name'], "RIDALLOCTEST7") + try: + # Connect to the database + ldb_url = "tdb://%s" % os.path.join(targetdir, "private/sam.ldb") + smbconf = os.path.join(targetdir, "etc/smb.conf") + + lp = self.get_loadparm() + new_ldb = SamDB(ldb_url, credentials=self.get_credentials(), + session_info=system_session(lp), lp=lp) + + # 1. Get server name + res = new_ldb.search(base=ldb.Dn(new_ldb, new_ldb.get_serverName()), + scope=ldb.SCOPE_BASE, attrs=["serverReference"]) + # 2. Get server reference + server_ref_dn = ldb.Dn(new_ldb, res[0]['serverReference'][0].decode('utf8')) + + # 3. Assert we get the RID Set + res = new_ldb.search(base=server_ref_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDSetReferences']) + + self.assertTrue("rIDSetReferences" in res[0]) + rid_set_dn = ldb.Dn(new_ldb, res[0]["rIDSetReferences"][0].decode('utf8')) + # 4. Seize the RID Manager role + (result, out, err) = self.runsubcmd("fsmo", "seize", "--role", "rid", "-H", ldb_url, "--configfile=%s" % (smbconf), "--force") + self.assertCmdSuccess(result, out, err) + self.assertEqual(err, "", "Shouldn't be any error messages") + + # 5. Add a new user (triggers RID set work) + new_ldb.newuser("ridalloctestuser", "P@ssword!") + + # 6. Now fetch the RID SET + rid_set_res = new_ldb.search(base=rid_set_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDNextRid', + 'rIDAllocationPool']) + next_pool = int(rid_set_res[0]["rIDAllocationPool"][0]) + last_rid = (0xFFFFFFFF00000000 & next_pool) >> 32 + + # 7. Add user above the ridNextRid and at almost the end of the range. + # + m = ldb.Message() + m.dn = ldb.Dn(new_ldb, "CN=ridsettestuser2,CN=Users") + m.dn.add_base(new_ldb.get_default_basedn()) + m['objectClass'] = ldb.MessageElement('user', ldb.FLAG_MOD_ADD, 'objectClass') + m['objectSid'] = ldb.MessageElement(ndr_pack(security.dom_sid(str(new_ldb.get_domain_sid()) + "-%d" % (last_rid - 3))), + ldb.FLAG_MOD_ADD, + 'objectSid') + new_ldb.add(m, controls=["relax:0"]) + + # 8. Add user above the ridNextRid and at the end of the range + m = ldb.Message() + m.dn = ldb.Dn(new_ldb, "CN=ridsettestuser3,CN=Users") + m.dn.add_base(new_ldb.get_default_basedn()) + m['objectClass'] = ldb.MessageElement('user', ldb.FLAG_MOD_ADD, 'objectClass') + m['objectSid'] = ldb.MessageElement(ndr_pack(security.dom_sid(str(new_ldb.get_domain_sid()) + "-%d" % last_rid)), + ldb.FLAG_MOD_ADD, + 'objectSid') + new_ldb.add(m, controls=["relax:0"]) + + chk = dbcheck(new_ldb, verbose=False, fix=True, yes=True, quiet=True) + + # Should have fixed two errors (wrong ridNextRid) + self.assertEqual(chk.check_database(DN=rid_set_dn, scope=ldb.SCOPE_BASE), 2) + + # 9. Assert we get didn't show any other errors + chk = dbcheck(new_ldb, verbose=False, fix=False, quiet=True) + + # 10. Add another user (checks RID rollover) + # We have seized the role, so we can do that. + new_ldb.newuser("ridalloctestuser3", "P@ssword!") + + rid_set_res = new_ldb.search(base=rid_set_dn, + scope=ldb.SCOPE_BASE, attrs=['rIDNextRid', + 'rIDAllocationPool']) + next_pool = int(rid_set_res[0]["rIDAllocationPool"][0]) + self.assertNotEqual(last_rid, (0xFFFFFFFF00000000 & next_pool) >> 32, "rid pool should have changed") + finally: + self._test_force_demote(fsmo_owner['dns_name'], "RIDALLOCTEST7") + shutil.rmtree(targetdir, ignore_errors=True) + + def test_replicate_against_deleted_objects_transaction(self): + """Not related to RID allocation, but uses the infrastructure here. + Do a join, create a link between two objects remotely, but + remove the target locally. Show that we need to set a magic + opaque if there is an outer transaction. + + """ + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + test_user4 = "ridalloctestuser4" + test_group = "ridalloctestgroup1" + + self.ldb_dc1.newuser(test_user4, "P@ssword!") + + self.addCleanup(self.ldb_dc1.deleteuser, test_user4) + + self.ldb_dc1.newgroup(test_group) + self.addCleanup(self.ldb_dc1.deletegroup, test_group) + + targetdir = self._test_join(self.dnsname_dc1, "RIDALLOCTEST8") + try: + # Connect to the database + ldb_url = "tdb://%s" % os.path.join(targetdir, "private/sam.ldb") + lp = self.get_loadparm() + + new_ldb = SamDB(ldb_url, + session_info=system_session(lp), lp=lp) + + destination_dsa_guid = misc.GUID(new_ldb.get_ntds_GUID()) + + repl = drs_utils.drs_Replicate(f'ncacn_ip_tcp:{self.dnsname_dc1}[seal]', + lp, + self.get_credentials(), + new_ldb, + destination_dsa_guid) + + source_dsa_invocation_id = misc.GUID(self.ldb_dc1.invocation_id) + + # Add the link on the remote DC + self.ldb_dc1.add_remove_group_members(test_group, [test_user4]) + + # Starting a transaction overrides, currently the logic + # inside repl.replicatate to retry with GET_TGT which in + # turn tells the repl_meta_data module that the most up to + # date info is already available + new_ldb.transaction_start() + repl.replicate(self.ldb_dc1.domain_dn(), + source_dsa_invocation_id, + destination_dsa_guid) + + # Delete the user locally, before applying the links. + # This simulates getting the delete in the replciation + # stream. + new_ldb.deleteuser(test_user4) + + # This fails as the user has been deleted locally but a remote link is sent + self.assertRaises(ldb.LdbError, new_ldb.transaction_commit) + + new_ldb.transaction_start() + repl.replicate(self.ldb_dc1.domain_dn(), + source_dsa_invocation_id, + destination_dsa_guid) + + # Delete the user locally (the previous transaction + # doesn't apply), before applying the links. This + # simulates getting the delete in the replciation stream. + new_ldb.deleteuser(test_user4) + + new_ldb.set_opaque_integer(dsdb.DSDB_FULL_JOIN_REPLICATION_COMPLETED_OPAQUE_NAME, + 1) + + # This should now work + try: + new_ldb.transaction_commit() + except ldb.LdbError as e: + self.fail(f"Failed to replicate despite setting opaque with {e.args[1]}") + + finally: + self._test_force_demote(self.dnsname_dc1, "RIDALLOCTEST8") + shutil.rmtree(targetdir, ignore_errors=True) + + def test_replicate_against_deleted_objects_normal(self): + """Not related to RID allocation, but uses the infrastructure here. + Do a join, create a link between two objects remotely, but + remove the target locally. . + + """ + fsmo_dn = ldb.Dn(self.ldb_dc1, "CN=RID Manager$,CN=System," + self.ldb_dc1.domain_dn()) + (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn) + + test_user5 = "ridalloctestuser5" + test_group2 = "ridalloctestgroup2" + + self.ldb_dc1.newuser(test_user5, "P@ssword!") + self.addCleanup(self.ldb_dc1.deleteuser, test_user5) + + self.ldb_dc1.newgroup(test_group2) + self.addCleanup(self.ldb_dc1.deletegroup, test_group2) + + targetdir = self._test_join(self.dnsname_dc1, "RIDALLOCTEST9") + try: + # Connect to the database + ldb_url = "tdb://%s" % os.path.join(targetdir, "private/sam.ldb") + lp = self.get_loadparm() + + new_ldb = SamDB(ldb_url, + session_info=system_session(lp), lp=lp) + + destination_dsa_guid = misc.GUID(new_ldb.get_ntds_GUID()) + + repl = drs_utils.drs_Replicate(f'ncacn_ip_tcp:{self.dnsname_dc1}[seal]', + lp, + self.get_credentials(), + new_ldb, + destination_dsa_guid) + + source_dsa_invocation_id = misc.GUID(self.ldb_dc1.invocation_id) + + # Add the link on the remote DC + self.ldb_dc1.add_remove_group_members(test_group2, [test_user5]) + + # Delete the user locally + new_ldb.deleteuser(test_user5) + + # Confirm replication copes with a link to a locally deleted user + repl.replicate(self.ldb_dc1.domain_dn(), + source_dsa_invocation_id, + destination_dsa_guid) + + finally: + self._test_force_demote(self.dnsname_dc1, "RIDALLOCTEST9") + shutil.rmtree(targetdir, ignore_errors=True) diff --git a/source4/torture/drs/python/samba_tool_drs.py b/source4/torture/drs/python/samba_tool_drs.py new file mode 100644 index 0000000..e622fe4 --- /dev/null +++ b/source4/torture/drs/python/samba_tool_drs.py @@ -0,0 +1,410 @@ +# Blackbox tests for "samba-tool drs" command +# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2011 +# 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/>. +# + +"""Blackbox tests for samba-tool drs.""" + +import samba.tests +import os +import ldb +import drs_base + + +class SambaToolDrsTests(drs_base.DrsBaseTestCase): + """Blackbox test case for samba-tool drs.""" + + def setUp(self): + super(SambaToolDrsTests, self).setUp() + + self.dc1 = samba.tests.env_get_var_value("DC1") + self.dc2 = samba.tests.env_get_var_value("DC2") + + creds = self.get_credentials() + self.cmdline_creds = "-U%s/%s%%%s" % (creds.get_domain(), + creds.get_username(), creds.get_password()) + + def tearDown(self): + self._enable_inbound_repl(self.dnsname_dc1) + self._enable_inbound_repl(self.dnsname_dc2) + + self.rm_files('names.tdb', allow_missing=True) + self.rm_dirs('etc', 'msg.lock', 'private', 'state', 'bind-dns', + allow_missing=True) + + super(SambaToolDrsTests, self).tearDown() + + def _get_rootDSE(self, dc, ldap_only=True): + samdb = samba.tests.connect_samdb(dc, lp=self.get_loadparm(), + credentials=self.get_credentials(), + ldap_only=ldap_only) + return samdb.search(base="", scope=samba.tests.ldb.SCOPE_BASE)[0] + + def test_samba_tool_bind(self): + """Tests 'samba-tool drs bind' command.""" + + # Output should be like: + # Extensions supported: + # <list-of-supported-extensions> + # Site GUID: <GUID> + # Repl epoch: 0 + out = self.check_output("samba-tool drs bind %s %s" % (self.dc1, + self.cmdline_creds)) + self.assertTrue("Site GUID:" in out.decode('utf8')) + self.assertTrue("Repl epoch:" in out.decode('utf8')) + + def test_samba_tool_kcc(self): + """Tests 'samba-tool drs kcc' command.""" + + # Output should be like 'Consistency check on <DC> successful.' + out = self.check_output("samba-tool drs kcc %s %s" % (self.dc1, + self.cmdline_creds)) + self.assertTrue(b"Consistency check on" in out) + self.assertTrue(b"successful" in out) + + def test_samba_tool_options(self): + """Tests 'samba-tool drs options' command + """ + # Output should be like 'Current DSA options: IS_GC <OTHER_FLAGS>' + out = self.check_output("samba-tool drs options %s %s" % (self.dc1, + self.cmdline_creds)) + self.assertTrue(b"Current DSA options:" in out) + + def test_samba_tool_replicate(self): + """Tests 'samba-tool drs replicate' command.""" + + # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.' + nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"] + out = self.check_output("samba-tool drs replicate %s %s %s %s" % (self.dc1, + self.dc2, + nc_name, + self.cmdline_creds)) + self.assertTrue(b"Replicate from" in out) + self.assertTrue(b"was successful" in out) + + def test_samba_tool_replicate_async(self): + """Tests 'samba-tool drs replicate --async-op' command.""" + + # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was started.' + nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"] + out = self.check_output("samba-tool drs replicate --async-op %s %s %s %s" % (self.dc1, + self.dc2, + nc_name, + self.cmdline_creds)) + self.assertTrue(b"Replicate from" in out) + self.assertTrue(b"was started" in out) + + def test_samba_tool_replicate_local_online(self): + """Tests 'samba-tool drs replicate --local-online' command.""" + + # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.' + nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"] + out = self.check_output("samba-tool drs replicate --local-online %s %s %s" % (self.dc1, + self.dc2, + nc_name)) + self.assertTrue(b"Replicate from" in out) + self.assertTrue(b"was successful" in out) + + def test_samba_tool_replicate_local_online_async(self): + """Tests 'samba-tool drs replicate --local-online --async-op' command.""" + + # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was started.' + nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"] + out = self.check_output("samba-tool drs replicate --local-online --async-op %s %s %s" % (self.dc1, + self.dc2, + nc_name)) + self.assertTrue(b"Replicate from" in out) + self.assertTrue(b"was started" in out) + + def test_samba_tool_replicate_local_machine_creds(self): + """Tests 'samba-tool drs replicate --local -P' command (uses machine creds).""" + + # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.' + nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"] + out = self.check_output("samba-tool drs replicate -P --local %s %s %s" % (self.dc1, + self.dc2, + nc_name)) + self.assertTrue(b"Incremental" in out) + self.assertTrue(b"was successful" in out) + + def test_samba_tool_replicate_local(self): + """Tests 'samba-tool drs replicate --local' command (uses machine creds).""" + + # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.' + nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"] + + def get_num_obj_links(output): + num_objs = None + num_links = None + for word in output.decode('utf8').split(" "): + try: + int(word) + if num_objs is None: + num_objs = int(word) + elif num_links is None: + num_links = int(word) + except ValueError: + pass + + return (num_objs, num_links) + + out = self.check_output("samba-tool drs replicate --local --full-sync %s %s %s %s" + % (self.dc1, self.dc2, nc_name, self.cmdline_creds)) + self.assertTrue(b"was successful" in out) + self.assertTrue(b"Full" in out) + + (first_obj, _) = get_num_obj_links(out) + + out = self.check_output("samba-tool drs replicate --local %s %s %s %s" + % (self.dc1, self.dc2, nc_name, self.cmdline_creds)) + self.assertTrue(b"was successful" in out) + self.assertTrue(b"Incremental" in out) + + (second_obj, _) = get_num_obj_links(out) + + self.assertTrue(first_obj > second_obj) + + server_rootdse = self._get_rootDSE(self.dc1) + server_ldap_service_name = str(server_rootdse["ldapServiceName"][0]) + server_realm = server_ldap_service_name.split(":")[0] + + # We have to give it a different netbiosname every time + # it runs, otherwise the collision causes strange issues + # to happen. This should be different on different environments. + netbiosname = "test" + self.dc2 + if len(netbiosname) > 15: + netbiosname = netbiosname[:15] + + out = self.check_output("samba-tool domain join %s dc --server=%s %s --targetdir=%s --option=netbiosname=%s" + % (server_realm, self.dc1, self.cmdline_creds, self.tempdir, netbiosname)) + + new_dc_config_file = "%s/etc/smb.conf" % self.tempdir + + self.check_output("samba-tool drs replicate --local %s %s %s %s --configfile=%s" + % ("invalid", self.dc1, nc_name, + self.cmdline_creds, new_dc_config_file)) + + self._disable_inbound_repl(self.dnsname_dc1) + self._disable_inbound_repl(self.dnsname_dc2) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1) + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2) + + # add an object with link on dc1 + group_name = "group-repl-local-%s" % self.dc2 + user_name = "user-repl-local-%s" % self.dc2 + + self.check_output("samba-tool group add %s %s -H ldap://%s" + % (group_name, self.cmdline_creds, self.dc1)) + self.check_output("samba-tool user add %s %s --random-password -H ldap://%s" + % (user_name, self.cmdline_creds, self.dc1)) + self.check_output("samba-tool group addmembers %s %s %s -H ldap://%s" + % (group_name, user_name, self.cmdline_creds, self.dc1)) + + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1) + + # pull that change with --local into local db from dc1: should send link and some objects + out = self.check_output("samba-tool drs replicate --local %s %s %s %s --configfile=%s" + % ("invalid", self.dc1, nc_name, + self.cmdline_creds, new_dc_config_file)) + + (obj_1, link_1) = get_num_obj_links(out) + + self.assertGreaterEqual(obj_1, 2) + self.assertEqual(link_1, 1) + + # pull that change with --local into local db from dc2: shouldn't send link or object + # as we sent an up-to-dateness vector showing that we had already synced with DC1 + out = self.check_output("samba-tool drs replicate --local %s %s %s %s --configfile=%s" + % ("invalid", self.dc2, nc_name, + self.cmdline_creds, new_dc_config_file)) + + (obj_2, link_2) = get_num_obj_links(out) + + self.assertEqual(obj_2, 0) + self.assertEqual(link_2, 0) + + self.check_output("samba-tool domain demote --remove-other-dead-server=%s -H ldap://%s %s --configfile=%s" + % (netbiosname, self.dc1, self.cmdline_creds, new_dc_config_file)) + + def test_samba_tool_replicate_machine_creds_P(self): + """Tests 'samba-tool drs replicate -P' command with machine creds.""" + + # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.' + nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"] + out = self.check_output("samba-tool drs replicate -P %s %s %s" % (self.dc1, + self.dc2, + nc_name)) + self.assertTrue(b"Replicate from" in out) + self.assertTrue(b"was successful" in out) + + def test_samba_tool_replicate_machine_creds(self): + """Tests 'samba-tool drs replicate' command with implicit machine creds.""" + + # Output should be like 'Replicate from <DC-SRC> to <DC-DEST> was successful.' + nc_name = self._get_rootDSE(self.dc1)["defaultNamingContext"] + out = self.check_output("samba-tool drs replicate %s %s %s" % (self.dc1, + self.dc2, + nc_name)) + self.assertTrue(b"Replicate from" in out) + self.assertTrue(b"was successful" in out) + + def test_samba_tool_drs_clone_dc(self): + """Tests 'samba-tool drs clone-dc-database' command.""" + server_rootdse = self._get_rootDSE(self.dc1) + server_nc_name = server_rootdse["defaultNamingContext"] + server_ds_name = server_rootdse["dsServiceName"] + server_ldap_service_name = str(server_rootdse["ldapServiceName"][0]) + server_realm = server_ldap_service_name.split(":")[0] + self.check_output("samba-tool drs clone-dc-database %s --server=%s %s --targetdir=%s" + % (server_realm, + self.dc1, + self.cmdline_creds, + self.tempdir)) + ldb_rootdse = self._get_rootDSE("ldb://" + os.path.join(self.tempdir, "private", "sam.ldb"), ldap_only=False) + nc_name = ldb_rootdse["defaultNamingContext"] + ds_name = ldb_rootdse["dsServiceName"] + ldap_service_name = str(server_rootdse["ldapServiceName"][0]) + self.assertEqual(nc_name, server_nc_name) + # The clone should pretend to be the source server + self.assertEqual(ds_name, server_ds_name) + self.assertEqual(ldap_service_name, server_ldap_service_name) + + samdb = samba.tests.connect_samdb("ldb://" + os.path.join(self.tempdir, "private", "sam.ldb"), + ldap_only=False, lp=self.get_loadparm()) + + def get_krbtgt_pw(): + samdb.searchone("unicodePwd", "cn=krbtgt,CN=users,%s" % nc_name) + self.assertRaises(KeyError, get_krbtgt_pw) + + server_dn = samdb.searchone("serverReferenceBL", "cn=%s,ou=domain controllers,%s" % (self.dc2, server_nc_name)).decode('utf8') + ntds_guid = samdb.searchone("objectGUID", "cn=ntds settings,%s" % server_dn).decode('utf8') + + res = samdb.search(base=str(server_nc_name), + expression="(&(objectclass=user)(cn=dns-%s))" % (self.dc2), + attrs=[], scope=ldb.SCOPE_SUBTREE) + if len(res) == 1: + dns_obj = res[0] + else: + dns_obj = None + + # While we have this cloned, try demoting the other server on the clone, by GUID + self.check_output("samba-tool domain demote --remove-other-dead-server=%s -H %s/private/sam.ldb" + % (ntds_guid, + self.tempdir)) + + # Check some of the objects that should have been removed + def check_machine_obj(): + samdb.searchone("CN", "cn=%s,ou=domain controllers,%s" % (self.dc2, server_nc_name)) + self.assertRaises(ldb.LdbError, check_machine_obj) + + def check_server_obj(): + samdb.searchone("CN", server_dn) + self.assertRaises(ldb.LdbError, check_server_obj) + + def check_ntds_guid(): + samdb.searchone("CN", "<GUID=%s>" % ntds_guid) + self.assertRaises(ldb.LdbError, check_ntds_guid) + + if dns_obj is not None: + # Check some of the objects that should have been removed + def check_dns_account_obj(): + samdb.search(base=dns_obj.dn, scope=ldb.SCOPE_BASE, + attrs=[]) + self.assertRaises(ldb.LdbError, check_dns_account_obj) + + def test_samba_tool_drs_clone_dc_secrets(self): + """Tests 'samba-tool drs clone-dc-database --include-secrets' command .""" + server_rootdse = self._get_rootDSE(self.dc1) + server_nc_name = server_rootdse["defaultNamingContext"] + server_ds_name = server_rootdse["dsServiceName"] + server_ldap_service_name = str(server_rootdse["ldapServiceName"][0]) + server_realm = server_ldap_service_name.split(":")[0] + self.check_output("samba-tool drs clone-dc-database %s --server=%s %s --targetdir=%s --include-secrets" + % (server_realm, + self.dc1, + self.cmdline_creds, + self.tempdir)) + ldb_rootdse = self._get_rootDSE("ldb://" + os.path.join(self.tempdir, "private", "sam.ldb"), ldap_only=False) + nc_name = ldb_rootdse["defaultNamingContext"] + ds_name = ldb_rootdse["dsServiceName"] + ldap_service_name = str(server_rootdse["ldapServiceName"][0]) + + samdb = samba.tests.connect_samdb("ldb://" + os.path.join(self.tempdir, "private", "sam.ldb"), + ldap_only=False, lp=self.get_loadparm()) + krbtgt_pw = samdb.searchone("unicodePwd", "cn=krbtgt,CN=users,%s" % nc_name) + self.assertIsNotNone(krbtgt_pw) + + self.assertEqual(nc_name, server_nc_name) + # The clone should pretend to be the source server + self.assertEqual(ds_name, server_ds_name) + self.assertEqual(ldap_service_name, server_ldap_service_name) + + server_dn = samdb.searchone("serverReferenceBL", "cn=%s,ou=domain controllers,%s" % (self.dc2, server_nc_name)).decode('utf8') + ntds_guid = samdb.searchone("objectGUID", "cn=ntds settings,%s" % server_dn).decode('utf8') + + res = samdb.search(base=str(server_nc_name), + expression="(&(objectclass=user)(cn=dns-%s))" % (self.dc2), + attrs=[], scope=ldb.SCOPE_SUBTREE) + if len(res) == 1: + dns_obj = res[0] + else: + dns_obj = None + + def demote_self(): + # While we have this cloned, try demoting the other server on the clone + self.check_output("samba-tool domain demote --remove-other-dead-server=%s -H %s/private/sam.ldb" + % (self.dc1, + self.tempdir)) + self.assertRaises(samba.tests.BlackboxProcessError, demote_self) + + # While we have this cloned, try demoting the other server on the clone + self.check_output("samba-tool domain demote --remove-other-dead-server=%s -H ldb://%s/private/sam.ldb" + % (self.dc2, + self.tempdir)) + + # Check some of the objects that should have been removed + def check_machine_obj(): + samdb.searchone("CN", "cn=%s,ou=domain controllers,%s" % (self.dc2, server_nc_name)) + self.assertRaises(ldb.LdbError, check_machine_obj) + + def check_server_obj(): + samdb.searchone("CN", server_dn) + self.assertRaises(ldb.LdbError, check_server_obj) + + def check_ntds_guid(): + samdb.searchone("CN", "<GUID=%s>" % ntds_guid) + self.assertRaises(ldb.LdbError, check_ntds_guid) + + if dns_obj is not None: + # Check some of the objects that should have been removed + def check_dns_account_obj(): + samdb.search(base=dns_obj.dn, scope=ldb.SCOPE_BASE, + attrs=[]) + self.assertRaises(ldb.LdbError, check_dns_account_obj) + + def test_samba_tool_drs_clone_dc_secrets_without_targetdir(self): + """Tests 'samba-tool drs clone-dc-database' command without --targetdir.""" + server_rootdse = self._get_rootDSE(self.dc1) + server_ldap_service_name = str(server_rootdse["ldapServiceName"][0]) + server_realm = server_ldap_service_name.split(":")[0] + + def attempt_clone(): + self.check_output("samba-tool drs clone-dc-database %s --server=%s %s" + % (server_realm, + self.dc1, + self.cmdline_creds)) + self.assertRaises(samba.tests.BlackboxProcessError, attempt_clone) diff --git a/source4/torture/drs/python/samba_tool_drs_critical.py b/source4/torture/drs/python/samba_tool_drs_critical.py new file mode 100644 index 0000000..5260e15 --- /dev/null +++ b/source4/torture/drs/python/samba_tool_drs_critical.py @@ -0,0 +1,98 @@ +# Blackbox tests for "samba-tool drs" command +# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2011 +# 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/>. +# + +"""Blackbox tests for samba-tool drs.""" + +import samba.tests +import os +import ldb +import drs_base +import random + +class SambaToolDrsTests(drs_base.DrsBaseTestCase): + """Blackbox test case for samba-tool drs.""" + + def setUp(self): + super(SambaToolDrsTests, self).setUp() + + self.dc1 = samba.tests.env_get_var_value("DC1") + self.dc2 = samba.tests.env_get_var_value("DC2") + + creds = self.get_credentials() + self.cmdline_creds = "-U%s/%s%%%s" % (creds.get_domain(), + creds.get_username(), creds.get_password()) + + def tearDown(self): + self._enable_inbound_repl(self.dnsname_dc1) + self._enable_inbound_repl(self.dnsname_dc2) + + self.rm_files('names.tdb', allow_missing=True) + self.rm_dirs('etc', 'msg.lock', 'private', 'state', 'bind-dns', + allow_missing=True) + + super(SambaToolDrsTests, self).tearDown() + + # This test is for the Samba 4.5 emulation servers (but runs + # against a normal server as well) that fail to correctly + # implement DRSUAPI_DRS_GET_ANC when DRSUAPI_DRS_CRITICAL_ONLY is + # set. + def test_samba_tool_drs_clone_dc_critical_object_chain(self): + """Tests 'samba-tool drs clone-dc-database' command with a Critical/non-critical/critical object chain.""" + + samdb = samba.tests.connect_samdb(self.dc1, lp=self.get_loadparm(), + credentials=self.get_credentials(), + ldap_only=True) + server_rootdse = samdb.search(base="", + scope=samba.tests.ldb.SCOPE_BASE)[0] + nc_name = server_rootdse["defaultNamingContext"][0] + server_ldap_service_name = str(server_rootdse["ldapServiceName"][0]) + server_realm = server_ldap_service_name.split(":")[0] + + not_critical_dn = f"OU=not-critical{random.randint(1, 10000000)},{nc_name}" + samdb.create_ou(not_critical_dn) + self.addCleanup(samdb.delete, + not_critical_dn) + domain_sid = samdb.get_domain_sid() + admin_sid = f"{domain_sid}-500" + samdb.rename(f"<SID={admin_sid}>", + f"cn=administrator,{not_critical_dn}") + self.addCleanup(samdb.rename, + f"<SID={admin_sid}>", + f"cn=administrator,cn=users,{nc_name}") + + try: + self.check_output("samba-tool drs clone-dc-database %s --server=%s %s --targetdir=%s" + % (server_realm, + self.dc1, + self.cmdline_creds, + self.tempdir)) + except samba.tests.BlackboxProcessError as e: + self.fail("Error calling samba-tool: %s" % e) + + local_samdb = samba.tests.connect_samdb("ldb://" + os.path.join(self.tempdir, "private", "sam.ldb"), + ldap_only=False, lp=self.get_loadparm()) + + # Check administrator was replicated and is in the right place + res = local_samdb.search(base=str(nc_name), + expression="(&(objectclass=user)(cn=administrator))", + attrs=[], scope=ldb.SCOPE_SUBTREE) + self.assertEqual(len(res), 1) + + admin_obj = res[0] + + self.assertEqual(admin_obj.dn, ldb.Dn(samdb, f"cn=administrator,{not_critical_dn}")) diff --git a/source4/torture/drs/python/samba_tool_drs_no_dns.py b/source4/torture/drs/python/samba_tool_drs_no_dns.py new file mode 100644 index 0000000..aad5966 --- /dev/null +++ b/source4/torture/drs/python/samba_tool_drs_no_dns.py @@ -0,0 +1,174 @@ +# Blackbox tests for "samba-tool drs" command +# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2011 +# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017 +# Copyright (C) Catalyst.Net Ltd 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/>. +# + +""" +Blackbox tests for samba-tool drs with no DNS partitions + +Adapted from samba_tool_drs.py +""" + +import samba.tests +import os +import ldb +import drs_base + +from samba.tests import BlackboxProcessError +from samba.common import get_string + + +class SambaToolDrsNoDnsTests(drs_base.DrsBaseTestCase): + """Blackbox test case for samba-tool drs.""" + + def setUp(self): + super(SambaToolDrsNoDnsTests, self).setUp() + + self.dc1 = samba.tests.env_get_var_value("DC1") + + creds = self.get_credentials() + self.cmdline_creds = "-U%s/%s%%%s" % (creds.get_domain(), + creds.get_username(), creds.get_password()) + + def tearDown(self): + self._enable_inbound_repl(self.dnsname_dc1) + self.rm_files('names.tdb', allow_missing=True) + self.rm_dirs('etc', 'msg.lock', 'private', 'state', 'bind-dns', + allow_missing=True) + + super(SambaToolDrsNoDnsTests, self).tearDown() + + def _get_rootDSE(self, dc, ldap_only=True): + samdb = samba.tests.connect_samdb(dc, lp=self.get_loadparm(), + credentials=self.get_credentials(), + ldap_only=ldap_only) + return samdb.search(base="", scope=samba.tests.ldb.SCOPE_BASE)[0], samdb + + def test_samba_tool_replicate_local_no_dns_tdb(self): + self.backend = 'tdb' + self._test_samba_tool_replicate_local_no_dns() + + def test_samba_tool_replicate_local_no_dns_mdb(self): + self.backend = 'mdb' + self._test_samba_tool_replicate_local_no_dns() + + def _test_samba_tool_replicate_local_no_dns(self): + """Check we can provision a database without DNS partitions + (and then add them afterwards).""" + + server_rootdse, _ = self._get_rootDSE(self.dc1) + nc_name = server_rootdse["defaultNamingContext"] + server_ldap_service_name = str(server_rootdse["ldapServiceName"][0]) + server_realm = server_ldap_service_name.split(":")[0] + + # We have to give it a different netbiosname every time + # it runs, otherwise the collision causes strange issues + # to happen. This should be different on different environments. + netbiosname = "dns" + self.backend + self.dc1 + if len(netbiosname) > 15: + netbiosname = netbiosname[:15] + + self.check_output("samba-tool domain join %s dc --server=%s %s --targetdir=%s --option=netbiosname=%s %s --backend-store=%s" + % (server_realm, self.dc1, self.cmdline_creds, + self.tempdir, netbiosname, + "--dns-backend=NONE", + self.backend)) + + new_dc_config_file = os.path.join(self.tempdir, "etc", "smb.conf") + new_dc_sam = os.path.join(self.tempdir, "private", "sam.ldb") + + forestdns_dn = ldb.binary_encode('DC=ForestDNSZones,' + str(nc_name)) + domaindns_dn = ldb.binary_encode('DC=DomainDNSZones,' + str(nc_name)) + + self.check_output("samba-tool drs replicate --local %s %s %s %s --configfile=%s --full-sync" + % ("invalid", self.dc1, forestdns_dn, + self.cmdline_creds, new_dc_config_file)) + + self.check_output("samba-tool drs replicate --local %s %s %s %s --configfile=%s --full-sync" + % ("invalid", self.dc1, domaindns_dn, + self.cmdline_creds, new_dc_config_file)) + + server_rootdse, samdb = self._get_rootDSE("ldb://" + new_dc_sam, ldap_only=False) + server_ds_name = ldb.binary_encode(server_rootdse["dsServiceName"][0].decode('utf-8')) + + # Show that Has-Master-NCs is fixed by samba_upgradedns + res = samdb.search(base=server_ds_name, + expression="(msds-hasmasterncs=%s)" % forestdns_dn) + self.assertEqual(len(res), 0) + res = samdb.search(base=server_ds_name, + expression="(msds-hasmasterncs=%s)" % domaindns_dn) + self.assertEqual(len(res), 0) + + self.check_output("samba_upgradedns --configfile=%s" % (new_dc_config_file)) + + res = samdb.search(base=server_ds_name, + expression="(msds-hasmasterncs=%s)" % forestdns_dn) + self.assertEqual(len(res), 1) + res = samdb.search(base=server_ds_name, + expression="(msds-hasmasterncs=%s)" % domaindns_dn) + self.assertEqual(len(res), 1) + + # Show that replica locations is fixed by dbcheck + res = samdb.search(controls=["search_options:1:2"], + expression="(&(msds-nc-replica-locations=%s)(ncname=%s))" + % (server_ds_name, forestdns_dn)) + self.assertEqual(len(res), 0) + res = samdb.search(controls=["search_options:1:2"], + expression="(&(msds-nc-replica-locations=%s)(ncname=%s))" + % (server_ds_name, domaindns_dn)) + self.assertEqual(len(res), 0) + + try: + # This fixes any forward-link-backward-link issues with the tools + self.check_output("samba-tool dbcheck --configfile=%s --cross-ncs --fix --yes" % (new_dc_config_file)) + except BlackboxProcessError as e: + self.assertTrue("Checked " in get_string(e.stdout)) + + self.check_output("samba-tool dbcheck --configfile=%s --cross-ncs" % (new_dc_config_file)) + + # Compare the two directories + self.check_output("samba-tool ldapcmp ldap://%s ldb://%s %s --filter=%s" % + (self.dc1, new_dc_sam, self.cmdline_creds, + "msDs-masteredBy,msDS-NC-Replica-Locations,msDS-hasMasterNCs")) + + # Check all ForestDNS connections and backlinks + res = samdb.search(base=server_ds_name, + expression="(msds-hasmasterncs=%s)" % forestdns_dn) + self.assertEqual(len(res), 1) + res = samdb.search(base=forestdns_dn, + expression="(msds-masteredby=%s)" % server_ds_name) + self.assertEqual(len(res), 1) + res = samdb.search(controls=["search_options:1:2"], + expression="(&(msds-nc-replica-locations=%s)(ncname=%s))" + % (server_ds_name, forestdns_dn)) + self.assertEqual(len(res), 1) + + # Check all DomainDNS connections and backlinks + res = samdb.search(base=server_ds_name, + expression="(msds-hasmasterncs=%s)" % domaindns_dn) + self.assertEqual(len(res), 1) + res = samdb.search(base=domaindns_dn, + expression="(msds-masteredby=%s)" % server_ds_name) + self.assertEqual(len(res), 1) + res = samdb.search(controls=["search_options:1:2"], + expression="(&(msds-nc-replica-locations=%s)(ncname=%s))" + % (server_ds_name, domaindns_dn)) + self.assertEqual(len(res), 1) + + # Demote the DC we created in the test + self.check_output("samba-tool domain demote --remove-other-dead-server=%s -H ldap://%s %s --configfile=%s" + % (netbiosname, self.dc1, self.cmdline_creds, new_dc_config_file)) diff --git a/source4/torture/drs/python/samba_tool_drs_showrepl.py b/source4/torture/drs/python/samba_tool_drs_showrepl.py new file mode 100644 index 0000000..0f0ed86 --- /dev/null +++ b/source4/torture/drs/python/samba_tool_drs_showrepl.py @@ -0,0 +1,377 @@ +# Blackbox tests for "samba-tool drs" command +# Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2011 +# 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/>. +# + +"""Blackbox tests for samba-tool drs showrepl.""" +import samba.tests +import drs_base +from samba.dcerpc import drsuapi +from samba import drs_utils +import os +import json +import ldb +import random +from samba.common import get_string + +GUID_RE = r'[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}' +HEX8_RE = r'0x[\da-f]{8}' +DN_RE = r'(?:(?:CN|DC)=[\\:\w -]+,)+DC=com' + + +class SambaToolDrsShowReplTests(drs_base.DrsBaseTestCase): + """Blackbox test case for samba-tool drs.""" + + def setUp(self): + super(SambaToolDrsShowReplTests, self).setUp() + + self.dc1 = samba.tests.env_get_var_value("DC1") + self.dc2 = samba.tests.env_get_var_value("DC2") + + creds = self.get_credentials() + self.cmdline_creds = "-U%s/%s%%%s" % (creds.get_domain(), + creds.get_username(), + creds.get_password()) + + def test_samba_tool_showrepl(self): + """Tests 'samba-tool drs showrepl' command. + """ + nc_list = [self.config_dn, self.domain_dn, self.schema_dn] + dns_name = self.ldb_dc1.domain_dns_name() + + # Manually run kcc to create a "Connection" object, so we can find + # this for the expected output below. + kcc_out = self.check_output("samba-tool drs kcc %s %s" % (self.dc1, + self.cmdline_creds)) + self.assertIn(b"successful", kcc_out) + + # Run replicate to ensure there are incoming and outgoing partners + # exist, so we can expect these in the output below. + for nc in nc_list: + self._net_drs_replicate(DC=self.dnsname_dc2, fromDC=self.dnsname_dc1, nc_dn=nc, forced=True) + self._net_drs_replicate(DC=self.dnsname_dc1, fromDC=self.dnsname_dc2, nc_dn=nc, forced=True) + + # Output should be like: + # <site-name>/<domain-name> + # DSA Options: <hex-options> + # DSA object GUID: <DSA-object-GUID> + # DSA invocationId: <DSA-invocationId> + # <Inbound-connections-list> + # <Outbound-connections-list> + # <KCC-objects> + # ... + # TODO: Perhaps we should check at least for + # DSA's objectGUDI and invocationId + out = self.check_output("samba-tool drs showrepl " + "%s %s" % (self.dc1, self.cmdline_creds)) + + out = get_string(out) + # We want to assert that we are getting the same results, but + # dates and GUIDs change randomly. + # + # There are sections with headers like ==== THIS ====" + (header, + _inbound, inbound, + _outbound, outbound, + _conn, conn) = out.split("====") + + self.assertEqual(_inbound, ' INBOUND NEIGHBORS ') + self.assertEqual(_outbound, ' OUTBOUND NEIGHBORS ') + self.assertEqual(_conn, ' KCC CONNECTION OBJECTS ') + + self.assertRegex(header, + r'^Default-First-Site-Name\\%s\s+' + r"DSA Options: %s\s+" + r"DSA object GUID: %s\s+" + r"DSA invocationId: %s" % + (self.dc1.upper(), HEX8_RE, GUID_RE, GUID_RE)) + + # We don't assert the DomainDnsZones and ForestDnsZones are + # there because we don't know that they have been set up yet. + + for p in nc_list: + self.assertRegex( + inbound, + r'%s\n' + r'\tDefault-First-Site-Name\\[A-Z0-9]+ via RPC\n' + r'\t\tDSA object GUID: %s\n' + r'\t\tLast attempt @ [^\n]+\n' + r'\t\t\d+ consecutive failure\(s\).\n' + r'\t\tLast success @ [^\n]+\n' + r'\n' % (p, GUID_RE), + msg="%s inbound missing" % p) + + self.assertRegex( + outbound, + r'%s\n' + r'\tDefault-First-Site-Name\\[A-Z0-9]+ via RPC\n' + r'\t\tDSA object GUID: %s\n' + r'\t\tLast attempt @ [^\n]+\n' + r'\t\t\d+ consecutive failure\(s\).\n' + r'\t\tLast success @ [^\n]+\n' + r'\n' % (p, GUID_RE), + msg="%s outbound missing" % p) + + self.assertRegex(conn, + r'Connection --\n' + r'\tConnection name: %s\n' + r'\tEnabled : TRUE\n' + r'\tServer DNS name : \w+.%s\n' + r'\tServer DN name : %s' + r'\n' % (GUID_RE, dns_name, DN_RE)) + + def test_samba_tool_showrepl_json(self): + """Tests 'samba-tool drs showrepl --json' command. + """ + dns_name = self.ldb_dc1.domain_dns_name() + out = self.check_output("samba-tool drs showrepl %s %s --json" % + (self.dc1, self.cmdline_creds)) + d = json.loads(get_string(out)) + self.assertEqual(set(d), set(['repsFrom', + 'repsTo', + "NTDSConnections", + "dsa"])) + + # dsa + for k in ["objectGUID", "invocationId"]: + self.assertRegex(d['dsa'][k], '^%s$' % GUID_RE) + self.assertTrue(isinstance(d['dsa']["options"], int)) + + # repsfrom and repsto + for reps in (d['repsFrom'], d['repsTo']): + for r in reps: + for k in ('NC dn', "NTDS DN"): + self.assertRegex(r[k], '^%s$' % DN_RE) + for k in ("last attempt time", + "last attempt message", + "last success"): + self.assertTrue(isinstance(r[k], str)) + self.assertRegex(r["DSA objectGUID"], '^%s$' % GUID_RE) + self.assertTrue(isinstance(r["consecutive failures"], int)) + + # ntdsconnection + for n in d["NTDSConnections"]: + self.assertTrue(n["dns name"].endswith(dns_name)) + self.assertRegex(n["name"], "^%s$" % GUID_RE) + self.assertTrue(isinstance(n['enabled'], bool)) + self.assertTrue(isinstance(n['options'], int)) + self.assertTrue(isinstance(n['replicates NC'], list)) + self.assertRegex(n["remote DN"], "^%s$" % DN_RE) + + def _force_all_reps(self, samdb, dc, direction): + if direction == 'inbound': + info_type = drsuapi.DRSUAPI_DS_REPLICA_INFO_NEIGHBORS + elif direction == 'outbound': + info_type = drsuapi.DRSUAPI_DS_REPLICA_INFO_REPSTO + else: + raise ValueError("expected 'inbound' or 'outbound'") + + self._enable_all_repl(dc) + lp = self.get_loadparm() + creds = self.get_credentials() + drsuapi_conn, drsuapi_handle, _ = drs_utils.drsuapi_connect(dc, lp, creds) + req1 = drsuapi.DsReplicaGetInfoRequest1() + req1.info_type = info_type + _, info = drsuapi_conn.DsReplicaGetInfo(drsuapi_handle, 1, req1) + for x in info.array: + # you might think x.source_dsa_address was the thing, but no. + # and we need to filter out RODCs and deleted DCs + + res = [] + try: + res = samdb.search(base=x.source_dsa_obj_dn, + scope=ldb.SCOPE_BASE, + attrs=['msDS-isRODC', 'isDeleted'], + controls=['show_deleted:0']) + except ldb.LdbError as e: + if e.args[0] != ldb.ERR_NO_SUCH_OBJECT: + raise + + if (len(res) == 0 or + len(res[0].get('msDS-isRODC', '')) > 0 or + res[0]['isDeleted'] == 'TRUE'): + continue + + dsa_dn = str(ldb.Dn(samdb, x.source_dsa_obj_dn).parent()) + try: + res = samdb.search(base=dsa_dn, + scope=ldb.SCOPE_BASE, + attrs=['dNSHostName']) + except ldb.LdbError as e: + if e.args[0] != ldb.ERR_NO_SUCH_OBJECT: + raise + continue + + if len(res) == 0: + print("server %s has no dNSHostName" % dsa_dn) + continue + + remote = res[0].get('dNSHostName', [''])[0] + if remote: + self._enable_all_repl(remote) + + if direction == 'inbound': + src, dest = remote, dc + else: + src, dest = dc, remote + self._net_drs_replicate(dest, src, forced=True) + + def test_samba_tool_showrepl_pull_summary_all_good(self): + """Tests 'samba-tool drs showrepl --pull-summary' command.""" + # To be sure that all is good we need to force replication + # with everyone (because others might have it turned off), and + # turn replication on for them in case they suddenly decide to + # try again. + # + # We don't restore them to the non-auto-replication state. + samdb1 = self.getSamDB("-H", "ldap://%s" % self.dc1, + self.cmdline_creds) + self._enable_all_repl(self.dc1) + self._force_all_reps(samdb1, self.dc1, 'inbound') + self._force_all_reps(samdb1, self.dc1, 'outbound') + old_no_color = os.environ.get('NO_COLOR') + all_good_green = "\033[1;32m[ALL GOOD]\033[0m\n" + all_good = "[ALL GOOD]\n" + + try: + out = self.check_output( + "samba-tool drs showrepl --pull-summary %s %s" % + (self.dc1, self.cmdline_creds)) + out = get_string(out) + self.assertStringsEqual(out, all_good) + out = get_string(out) + + out = self.check_output("samba-tool drs showrepl --pull-summary " + "--color=yes %s %s" % + (self.dc1, self.cmdline_creds)) + out = get_string(out) + self.assertStringsEqual(out, all_good_green) + + # --verbose output is still quiet when all is good. + out = self.check_output( + "samba-tool drs showrepl --pull-summary -v %s %s" % + (self.dc1, self.cmdline_creds)) + out = get_string(out) + self.assertStringsEqual(out, all_good) + + out = self.check_output("samba-tool drs showrepl --pull-summary -v " + "--color=always %s %s" % + (self.dc1, self.cmdline_creds)) + out = get_string(out) + self.assertStringsEqual(out, all_good_green) + + out = self.check_output("samba-tool drs showrepl --pull-summary -v " + "--color=never %s %s" % + (self.dc1, self.cmdline_creds)) + out = get_string(out) + self.assertStringsEqual(out, all_good) + + os.environ['NO_COLOR'] = 'bean' + + out = self.check_output("samba-tool drs showrepl --pull-summary -v " + "--color=auto %s %s" % + (self.dc1, self.cmdline_creds)) + out = get_string(out) + self.assertStringsEqual(out, all_good) + + os.environ['NO_COLOR'] = '' + + out = self.check_output("samba-tool drs showrepl --pull-summary -v " + "--color=auto %s %s" % + (self.dc1, self.cmdline_creds)) + out = get_string(out) + self.assertStringsEqual(out, all_good_green) + + except samba.tests.BlackboxProcessError as e: + self.fail(str(e)) + finally: + if old_no_color is None: + os.environ.pop('NO_COLOR', None) + else: + os.environ['NO_COLOR'] = old_no_color + + def test_samba_tool_showrepl_summary_forced_failure(self): + """Tests 'samba-tool drs showrepl --summary' command when we break the + network on purpose. + """ + self.addCleanup(self._enable_all_repl, self.dc1) + self._disable_all_repl(self.dc1) + + samdb1 = self.getSamDB("-H", "ldap://%s" % self.dc1, + self.cmdline_creds) + samdb2 = self.getSamDB("-H", "ldap://%s" % self.dc2, + self.cmdline_creds) + domain_dn = samdb1.domain_dn() + + # Add some things to NOT replicate + ou1 = "OU=dc1.%x,%s" % (random.randrange(1 << 64), domain_dn) + ou2 = "OU=dc2.%x,%s" % (random.randrange(1 << 64), domain_dn) + samdb1.add({ + "dn": ou1, + "objectclass": "organizationalUnit" + }) + self.addCleanup(samdb1.delete, ou1, ['tree_delete:1']) + samdb2.add({ + "dn": ou2, + "objectclass": "organizationalUnit" + }) + self.addCleanup(samdb2.delete, ou2, ['tree_delete:1']) + + dn1 = 'cn=u1.%%d,%s' % (ou1) + dn2 = 'cn=u2.%%d,%s' % (ou2) + + try: + for i in range(100): + samdb1.add({ + "dn": dn1 % i, + "objectclass": "user" + }) + samdb2.add({ + "dn": dn2 % i, + "objectclass": "user" + }) + out = self.check_output("samba-tool drs showrepl --summary -v " + "%s %s" % + (self.dc1, self.cmdline_creds)) + out = get_string(out) + self.assertStringsEqual('[ALL GOOD]', out, strip=True) + out = self.check_output("samba-tool drs showrepl --summary -v " + "--color=yes %s %s" % + (self.dc2, self.cmdline_creds)) + out = get_string(out) + self.assertIn('[ALL GOOD]', out) + + except samba.tests.BlackboxProcessError as e: + e_stdout = get_string(e.stdout) + e_stderr = get_string(e.stderr) + print("Good, failed as expected after %d rounds: %r" % (i, e.cmd)) + self.assertIn('There are failing connections', e_stdout, + msg=('stdout: %r\nstderr: %r\nretcode: %s' + '\nmessage: %r\ncmd: %r') % (e_stdout, + e_stderr, + e.returncode, + e.msg, + e.cmd)) + self.assertRegex( + e_stdout, + r'result 845[67] ' + r'\(WERR_DS_DRA_(SINK|SOURCE)_DISABLED\)', + msg=("The process should have failed " + "because replication was forced off, " + "but it failed for some other reason.")) + self.assertIn('consecutive failure(s).', e_stdout) + else: + self.fail("No DRS failure noticed after 100 rounds of trying") diff --git a/source4/torture/drs/rpc/dssync.c b/source4/torture/drs/rpc/dssync.c new file mode 100644 index 0000000..64d0498 --- /dev/null +++ b/source4/torture/drs/rpc/dssync.c @@ -0,0 +1,1072 @@ +/* + Unix SMB/CIFS implementation. + + DsGetNCChanges replication test + + Copyright (C) Stefan (metze) Metzmacher 2005 + Copyright (C) Brad Henry 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/>. +*/ + +#include "includes.h" +#include "lib/cmdline/cmdline.h" +#include "librpc/gen_ndr/ndr_drsuapi_c.h" +#include "librpc/gen_ndr/ndr_drsblobs.h" +#include "libcli/cldap/cldap.h" +#include "torture/torture.h" +#include "../libcli/drsuapi/drsuapi.h" +#include "auth/gensec/gensec.h" +#include "param/param.h" +#include "dsdb/samdb/samdb.h" +#include "torture/rpc/torture_rpc.h" +#include "torture/drs/proto.h" +#include "lib/tsocket/tsocket.h" +#include "libcli/resolve/resolve.h" +#include "lib/util/util_paths.h" + +#undef strcasecmp + +struct DsSyncBindInfo { + struct dcerpc_pipe *drs_pipe; + struct dcerpc_binding_handle *drs_handle; + struct drsuapi_DsBind req; + struct GUID bind_guid; + struct drsuapi_DsBindInfoCtr our_bind_info_ctr; + struct drsuapi_DsBindInfo28 our_bind_info28; + struct drsuapi_DsBindInfo28 peer_bind_info28; + struct policy_handle bind_handle; +}; + +struct DsSyncLDAPInfo { + struct ldb_context *ldb; +}; + +struct DsSyncTest { + struct dcerpc_binding *drsuapi_binding; + + const char *ldap_url; + const char *dest_address; + const char *domain_dn; + const char *config_dn; + const char *schema_dn; + + /* what we need to do as 'Administrator' */ + struct { + struct cli_credentials *credentials; + struct DsSyncBindInfo drsuapi; + struct DsSyncLDAPInfo ldap; + } admin; + + /* what we need to do as the new dc machine account */ + struct { + struct cli_credentials *credentials; + struct DsSyncBindInfo drsuapi; + struct drsuapi_DsGetDCInfo2 dc_info2; + struct GUID invocation_id; + struct GUID object_guid; + } new_dc; + + /* info about the old dc */ + struct { + struct drsuapi_DsGetDomainControllerInfo dc_info; + } old_dc; +}; + +static struct DsSyncTest *test_create_context(struct torture_context *tctx) +{ + NTSTATUS status; + struct DsSyncTest *ctx; + struct drsuapi_DsBindInfo28 *our_bind_info28; + struct drsuapi_DsBindInfoCtr *our_bind_info_ctr; + const char *binding = torture_setting_string(tctx, "binding", NULL); + const char *host; + struct nbt_name name; + + ctx = talloc_zero(tctx, struct DsSyncTest); + if (!ctx) return NULL; + + status = dcerpc_parse_binding(ctx, binding, &ctx->drsuapi_binding); + if (!NT_STATUS_IS_OK(status)) { + printf("Bad binding string %s\n", binding); + return NULL; + } + status = dcerpc_binding_set_flags(ctx->drsuapi_binding, + DCERPC_SIGN | DCERPC_SEAL, 0); + if (!NT_STATUS_IS_OK(status)) { + printf("dcerpc_binding_set_flags - %s\n", nt_errstr(status)); + return NULL; + } + + host = dcerpc_binding_get_string_option(ctx->drsuapi_binding, "host"); + + ctx->ldap_url = talloc_asprintf(ctx, "ldap://%s", host); + + make_nbt_name_server(&name, host); + + /* do an initial name resolution to find its IP */ + status = resolve_name_ex(lpcfg_resolve_context(tctx->lp_ctx), + 0, 0, &name, tctx, + &ctx->dest_address, tctx->ev); + if (!NT_STATUS_IS_OK(status)) { + printf("Failed to resolve %s - %s\n", + name.name, nt_errstr(status)); + return NULL; + } + + /* ctx->admin ...*/ + ctx->admin.credentials = samba_cmdline_get_creds(); + + our_bind_info28 = &ctx->admin.drsuapi.our_bind_info28; + our_bind_info28->supported_extensions = 0xFFFFFFFF; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_ADDENTRYREPLY_V3; + our_bind_info28->site_guid = GUID_zero(); + our_bind_info28->pid = 0; + our_bind_info28->repl_epoch = 1; + + our_bind_info_ctr = &ctx->admin.drsuapi.our_bind_info_ctr; + our_bind_info_ctr->length = 28; + our_bind_info_ctr->info.info28 = *our_bind_info28; + + GUID_from_string(DRSUAPI_DS_BIND_GUID, &ctx->admin.drsuapi.bind_guid); + + ctx->admin.drsuapi.req.in.bind_guid = &ctx->admin.drsuapi.bind_guid; + ctx->admin.drsuapi.req.in.bind_info = our_bind_info_ctr; + ctx->admin.drsuapi.req.out.bind_handle = &ctx->admin.drsuapi.bind_handle; + + /* ctx->new_dc ...*/ + ctx->new_dc.credentials = samba_cmdline_get_creds(); + + our_bind_info28 = &ctx->new_dc.drsuapi.our_bind_info28; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_BASE; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_ASYNC_REPLICATION; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_REMOVEAPI; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_MOVEREQ_V2; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GETCHG_COMPRESS; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V1; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_RESTORE_USN_OPTIMIZATION; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_KCC_EXECUTE; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_ADDENTRY_V2; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_LINKED_VALUE_REPLICATION; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V2; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_INSTANCE_TYPE_NOT_REQ_ON_MOD; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_CRYPTO_BIND; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GET_REPL_INFO; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_STRONG_ENCRYPTION; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V01; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_TRANSITIVE_MEMBERSHIP; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_ADD_SID_HISTORY; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_POST_BETA3; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GET_MEMBERSHIPS2; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V6; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_NONDOMAIN_NCS; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V5; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V6; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_ADDENTRYREPLY_V3; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V7; + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_VERIFY_OBJECT; + if (lpcfg_parm_bool(tctx->lp_ctx, NULL, "dssync", "xpress", false)) { + our_bind_info28->supported_extensions |= DRSUAPI_SUPPORTED_EXTENSION_XPRESS_COMPRESS; + } + our_bind_info28->site_guid = GUID_zero(); + our_bind_info28->pid = 0; + our_bind_info28->repl_epoch = 0; + + our_bind_info_ctr = &ctx->new_dc.drsuapi.our_bind_info_ctr; + our_bind_info_ctr->length = 28; + our_bind_info_ctr->info.info28 = *our_bind_info28; + + GUID_from_string(DRSUAPI_DS_BIND_GUID_W2K3, &ctx->new_dc.drsuapi.bind_guid); + + ctx->new_dc.drsuapi.req.in.bind_guid = &ctx->new_dc.drsuapi.bind_guid; + ctx->new_dc.drsuapi.req.in.bind_info = our_bind_info_ctr; + ctx->new_dc.drsuapi.req.out.bind_handle = &ctx->new_dc.drsuapi.bind_handle; + + ctx->new_dc.invocation_id = ctx->new_dc.drsuapi.bind_guid; + + /* ctx->old_dc ...*/ + + return ctx; +} + +static bool _test_DsBind(struct torture_context *tctx, + struct DsSyncTest *ctx, struct cli_credentials *credentials, struct DsSyncBindInfo *b) +{ + NTSTATUS status; + bool ret = true; + + status = dcerpc_pipe_connect_b(ctx, + &b->drs_pipe, ctx->drsuapi_binding, + &ndr_table_drsuapi, + credentials, tctx->ev, tctx->lp_ctx); + + if (!NT_STATUS_IS_OK(status)) { + printf("Failed to connect to server as a BDC: %s\n", nt_errstr(status)); + return false; + } + b->drs_handle = b->drs_pipe->binding_handle; + + status = dcerpc_drsuapi_DsBind_r(b->drs_handle, ctx, &b->req); + if (!NT_STATUS_IS_OK(status)) { + const char *errstr = nt_errstr(status); + printf("dcerpc_drsuapi_DsBind failed - %s\n", errstr); + ret = false; + } else if (!W_ERROR_IS_OK(b->req.out.result)) { + printf("DsBind failed - %s\n", win_errstr(b->req.out.result)); + ret = false; + } + + ZERO_STRUCT(b->peer_bind_info28); + if (b->req.out.bind_info) { + switch (b->req.out.bind_info->length) { + case 24: { + struct drsuapi_DsBindInfo24 *info24; + info24 = &b->req.out.bind_info->info.info24; + b->peer_bind_info28.supported_extensions= info24->supported_extensions; + b->peer_bind_info28.site_guid = info24->site_guid; + b->peer_bind_info28.pid = info24->pid; + b->peer_bind_info28.repl_epoch = 0; + break; + } + case 28: { + b->peer_bind_info28 = b->req.out.bind_info->info.info28; + break; + } + case 32: { + struct drsuapi_DsBindInfo32 *info32; + info32 = &b->req.out.bind_info->info.info32; + b->peer_bind_info28.supported_extensions= info32->supported_extensions; + b->peer_bind_info28.site_guid = info32->site_guid; + b->peer_bind_info28.pid = info32->pid; + b->peer_bind_info28.repl_epoch = info32->repl_epoch; + break; + } + case 48: { + struct drsuapi_DsBindInfo48 *info48; + info48 = &b->req.out.bind_info->info.info48; + b->peer_bind_info28.supported_extensions= info48->supported_extensions; + b->peer_bind_info28.site_guid = info48->site_guid; + b->peer_bind_info28.pid = info48->pid; + b->peer_bind_info28.repl_epoch = info48->repl_epoch; + break; + } + case 52: { + struct drsuapi_DsBindInfo52 *info52; + info52 = &b->req.out.bind_info->info.info52; + b->peer_bind_info28.supported_extensions= info52->supported_extensions; + b->peer_bind_info28.site_guid = info52->site_guid; + b->peer_bind_info28.pid = info52->pid; + b->peer_bind_info28.repl_epoch = info52->repl_epoch; + break; + } + default: + printf("DsBind - warning: unknown BindInfo length: %u\n", + b->req.out.bind_info->length); + } + } + + return ret; +} + +static bool test_LDAPBind(struct torture_context *tctx, struct DsSyncTest *ctx, + struct cli_credentials *credentials, struct DsSyncLDAPInfo *l) +{ + bool ret = true; + + struct ldb_context *ldb; + + const char *modules_option[] = { "modules:paged_searches", NULL }; + ctx->admin.ldap.ldb = ldb = ldb_init(ctx, tctx->ev); + if (ldb == NULL) { + return false; + } + + /* Despite us loading the schema from the AD server, we need + * the samba handlers to get the extended DN syntax stuff */ + ret = ldb_register_samba_handlers(ldb); + if (ret != LDB_SUCCESS) { + talloc_free(ldb); + return NULL; + } + + ldb_set_modules_dir(ldb, modules_path(ldb, "ldb")); + + if (ldb_set_opaque(ldb, "credentials", credentials)) { + talloc_free(ldb); + return NULL; + } + + if (ldb_set_opaque(ldb, "loadparm", tctx->lp_ctx)) { + talloc_free(ldb); + return NULL; + } + + ret = ldb_connect(ldb, ctx->ldap_url, 0, modules_option); + if (ret != LDB_SUCCESS) { + talloc_free(ldb); + torture_assert_int_equal(tctx, ret, LDB_SUCCESS, "Failed to make LDB connection to target"); + } + + printf("connected to LDAP: %s\n", ctx->ldap_url); + + return true; +} + +static bool test_GetInfo(struct torture_context *tctx, struct DsSyncTest *ctx) +{ + struct ldb_context *ldb = ctx->admin.ldap.ldb; + + /* We must have LDB connection ready by this time */ + SMB_ASSERT(ldb != NULL); + + ctx->domain_dn = ldb_dn_get_linearized(ldb_get_default_basedn(ldb)); + torture_assert(tctx, ctx->domain_dn != NULL, "Failed to get Domain DN"); + + ctx->config_dn = ldb_dn_get_linearized(ldb_get_config_basedn(ldb)); + torture_assert(tctx, ctx->config_dn != NULL, "Failed to get Domain DN"); + + ctx->schema_dn = ldb_dn_get_linearized(ldb_get_schema_basedn(ldb)); + torture_assert(tctx, ctx->schema_dn != NULL, "Failed to get Domain DN"); + + return true; +} + +static bool test_analyse_objects(struct torture_context *tctx, + struct DsSyncTest *ctx, + const char *partition, + const struct drsuapi_DsReplicaOIDMapping_Ctr *mapping_ctr, + uint32_t object_count, + const struct drsuapi_DsReplicaObjectListItemEx *first_object, + const DATA_BLOB *gensec_skey) +{ + static uint32_t object_id; + const char *save_values_dir; + const struct drsuapi_DsReplicaObjectListItemEx *cur; + struct ldb_context *ldb = ctx->admin.ldap.ldb; + struct ldb_dn *deleted_dn; + WERROR status; + int i, j, ret; + struct dsdb_extended_replicated_objects *objs; + struct ldb_extended_dn_control *extended_dn_ctrl; + struct dsdb_schema *ldap_schema; + struct ldb_dn *partition_dn = ldb_dn_new(tctx, ldb, partition); + + torture_assert_not_null(tctx, partition_dn, "Failed to parse partition DN as as DN"); + + /* load dsdb_schema using remote prefixMap */ + torture_assert(tctx, + drs_util_dsdb_schema_load_ldb(tctx, ldb, mapping_ctr, false), + "drs_util_dsdb_schema_load_ldb() failed"); + ldap_schema = dsdb_get_schema(ldb, NULL); + + status = dsdb_replicated_objects_convert(ldb, + ldap_schema, + partition_dn, + mapping_ctr, + object_count, + first_object, + 0, NULL, + NULL, NULL, + gensec_skey, + 0, + ctx, &objs); + torture_assert_werr_ok(tctx, status, "dsdb_extended_replicated_objects_convert() failed!"); + + extended_dn_ctrl = talloc(objs, struct ldb_extended_dn_control); + extended_dn_ctrl->type = 1; + + deleted_dn = ldb_dn_new(objs, ldb, partition); + ldb_dn_add_child_fmt(deleted_dn, "CN=Deleted Objects"); + + for (i=0; i < objs->num_objects; i++) { + struct ldb_request *search_req; + struct ldb_result *res; + struct ldb_message *new_msg, *drs_msg, *ldap_msg; + size_t num_attrs = objs->objects[i].msg->num_elements+1; + const char **attrs = talloc_array(objs, const char *, num_attrs); + for (j=0; j < objs->objects[i].msg->num_elements; j++) { + attrs[j] = objs->objects[i].msg->elements[j].name; + } + attrs[j] = NULL; + res = talloc_zero(objs, struct ldb_result); + if (!res) { + return LDB_ERR_OPERATIONS_ERROR; + } + ret = ldb_build_search_req(&search_req, ldb, objs, + objs->objects[i].msg->dn, + LDB_SCOPE_BASE, + NULL, + attrs, + NULL, + res, + ldb_search_default_callback, + NULL); + if (ret != LDB_SUCCESS) { + return false; + } + talloc_steal(search_req, res); + ret = ldb_request_add_control(search_req, LDB_CONTROL_SHOW_DELETED_OID, true, NULL); + if (ret != LDB_SUCCESS) { + return false; + } + + ret = ldb_request_add_control(search_req, LDB_CONTROL_EXTENDED_DN_OID, true, extended_dn_ctrl); + if (ret != LDB_SUCCESS) { + return false; + } + + ret = ldb_request(ldb, search_req); + if (ret == LDB_SUCCESS) { + ret = ldb_wait(search_req->handle, LDB_WAIT_ALL); + } + + torture_assert_int_equal(tctx, ret, LDB_SUCCESS, + talloc_asprintf(tctx, + "Could not re-fetch object just delivered over DRS: %s", + ldb_errstring(ldb))); + torture_assert_int_equal(tctx, res->count, 1, "Could not re-fetch object just delivered over DRS"); + ldap_msg = res->msgs[0]; + for (j=0; j < ldap_msg->num_elements; j++) { + ldap_msg->elements[j].flags = LDB_FLAG_MOD_ADD; + /* For unknown reasons, there is no nTSecurityDescriptor on cn=deleted objects over LDAP, but there is over DRS! Skip it on both transports for now here so */ + if ((ldb_attr_cmp(ldap_msg->elements[j].name, "nTSecurityDescriptor") == 0) && + (ldb_dn_compare(ldap_msg->dn, deleted_dn) == 0)) { + ldb_msg_remove_element(ldap_msg, &ldap_msg->elements[j]); + /* Don't skip one */ + j--; + } + } + + ret = ldb_msg_normalize(ldb, search_req, + objs->objects[i].msg, &drs_msg); + torture_assert(tctx, ret == LDB_SUCCESS, + "ldb_msg_normalize() has failed"); + + for (j=0; j < drs_msg->num_elements; j++) { + if (drs_msg->elements[j].num_values == 0) { + ldb_msg_remove_element(drs_msg, &drs_msg->elements[j]); + /* Don't skip one */ + j--; + + /* For unknown reasons, there is no nTSecurityDescriptor on cn=deleted objects over LDAP, but there is over DRS! */ + } else if ((ldb_attr_cmp(drs_msg->elements[j].name, "nTSecurityDescriptor") == 0) && + (ldb_dn_compare(drs_msg->dn, deleted_dn) == 0)) { + ldb_msg_remove_element(drs_msg, &drs_msg->elements[j]); + /* Don't skip one */ + j--; + } else if (ldb_attr_cmp(drs_msg->elements[j].name, "unicodePwd") == 0 || + ldb_attr_cmp(drs_msg->elements[j].name, "dBCSPwd") == 0 || + ldb_attr_cmp(drs_msg->elements[j].name, "userPassword") == 0 || + ldb_attr_cmp(drs_msg->elements[j].name, "ntPwdHistory") == 0 || + ldb_attr_cmp(drs_msg->elements[j].name, "lmPwdHistory") == 0 || + ldb_attr_cmp(drs_msg->elements[j].name, "supplementalCredentials") == 0 || + ldb_attr_cmp(drs_msg->elements[j].name, "priorValue") == 0 || + ldb_attr_cmp(drs_msg->elements[j].name, "currentValue") == 0 || + ldb_attr_cmp(drs_msg->elements[j].name, "trustAuthOutgoing") == 0 || + ldb_attr_cmp(drs_msg->elements[j].name, "trustAuthIncoming") == 0 || + ldb_attr_cmp(drs_msg->elements[j].name, "initialAuthOutgoing") == 0 || + ldb_attr_cmp(drs_msg->elements[j].name, "initialAuthIncoming") == 0) { + + /* These are not shown over LDAP, so we need to skip them for the comparison */ + ldb_msg_remove_element(drs_msg, &drs_msg->elements[j]); + /* Don't skip one */ + j--; + } else { + drs_msg->elements[j].flags = LDB_FLAG_MOD_ADD; + } + } + + + ret = ldb_msg_difference(ldb, search_req, + drs_msg, ldap_msg, &new_msg); + torture_assert(tctx, ret == LDB_SUCCESS, "ldb_msg_difference() has failed"); + if (new_msg->num_elements != 0) { + char *s; + bool is_warning = true; + unsigned int idx; + struct ldb_message_element *el; + const struct dsdb_attribute * a; + struct ldb_ldif ldif; + ldif.changetype = LDB_CHANGETYPE_MODIFY; + ldif.msg = new_msg; + s = ldb_ldif_write_string(ldb, new_msg, &ldif); + s = talloc_asprintf(tctx, "\n# Difference in between DRS and LDAP objects: \n%s", s); + + ret = ldb_msg_difference(ldb, search_req, + ldap_msg, drs_msg, &ldif.msg); + torture_assert(tctx, ret == LDB_SUCCESS, "ldb_msg_difference() has failed"); + s = talloc_asprintf_append(s, + "\n# Difference in between LDAP and DRS objects: \n%s", + ldb_ldif_write_string(ldb, new_msg, &ldif)); + + s = talloc_asprintf_append(s, + "# Should have no objects in 'difference' message. Diff elements: %d", + new_msg->num_elements); + + /* + * In case differences in messages are: + * 1. Attributes with different values, i.e. 'replace' + * 2. Those attributes are forward-link attributes + * then we just warn about those differences. + * It turns out windows doesn't send all of those values + * in replicated_object but in linked_attributes. + */ + for (idx = 0; idx < new_msg->num_elements && is_warning; idx++) { + el = &new_msg->elements[idx]; + a = dsdb_attribute_by_lDAPDisplayName(ldap_schema, + el->name); + if (LDB_FLAG_MOD_TYPE(el->flags) != LDB_FLAG_MOD_ADD && + LDB_FLAG_MOD_TYPE(el->flags) != LDB_FLAG_MOD_REPLACE) + { + /* DRS only value */ + is_warning = false; + } else if (a->linkID & 1) { + is_warning = false; + } + } + if (is_warning) { + torture_warning(tctx, "%s", s); + } else { + torture_fail(tctx, s); + } + } + + /* search_req is used as a tmp talloc context in the above */ + talloc_free(search_req); + } + + if (!lpcfg_parm_bool(tctx->lp_ctx, NULL, "dssync", "print_pwd_blobs", false)) { + talloc_free(objs); + return true; + } + + save_values_dir = lpcfg_parm_string(tctx->lp_ctx, NULL, "dssync", "save_pwd_blobs_dir"); + + for (cur = first_object; cur; cur = cur->next_object) { + const char *dn; + bool dn_printed = false; + + if (!cur->object.identifier) continue; + + dn = cur->object.identifier->dn; + + for (i=0; i < cur->object.attribute_ctr.num_attributes; i++) { + const char *name = NULL; + DATA_BLOB plain_data; + struct drsuapi_DsReplicaAttribute *attr; + ndr_pull_flags_fn_t pull_fn = NULL; + ndr_print_fn_t print_fn = NULL; + void *ptr = NULL; + attr = &cur->object.attribute_ctr.attributes[i]; + + switch (attr->attid) { + case DRSUAPI_ATTID_dBCSPwd: + name = "dBCSPwd"; + break; + case DRSUAPI_ATTID_unicodePwd: + name = "unicodePwd"; + break; + case DRSUAPI_ATTID_ntPwdHistory: + name = "ntPwdHistory"; + break; + case DRSUAPI_ATTID_lmPwdHistory: + name = "lmPwdHistory"; + break; + case DRSUAPI_ATTID_supplementalCredentials: + name = "supplementalCredentials"; + pull_fn = (ndr_pull_flags_fn_t)ndr_pull_supplementalCredentialsBlob; + print_fn = (ndr_print_fn_t)ndr_print_supplementalCredentialsBlob; + ptr = talloc(ctx, struct supplementalCredentialsBlob); + break; + case DRSUAPI_ATTID_priorValue: + name = "priorValue"; + break; + case DRSUAPI_ATTID_currentValue: + name = "currentValue"; + break; + case DRSUAPI_ATTID_trustAuthOutgoing: + name = "trustAuthOutgoing"; + pull_fn = (ndr_pull_flags_fn_t)ndr_pull_trustAuthInOutBlob; + print_fn = (ndr_print_fn_t)ndr_print_trustAuthInOutBlob; + ptr = talloc(ctx, struct trustAuthInOutBlob); + break; + case DRSUAPI_ATTID_trustAuthIncoming: + name = "trustAuthIncoming"; + pull_fn = (ndr_pull_flags_fn_t)ndr_pull_trustAuthInOutBlob; + print_fn = (ndr_print_fn_t)ndr_print_trustAuthInOutBlob; + ptr = talloc(ctx, struct trustAuthInOutBlob); + break; + case DRSUAPI_ATTID_initialAuthOutgoing: + name = "initialAuthOutgoing"; + break; + case DRSUAPI_ATTID_initialAuthIncoming: + name = "initialAuthIncoming"; + break; + default: + continue; + } + + if (attr->value_ctr.num_values != 1) continue; + + if (!attr->value_ctr.values[0].blob) continue; + + plain_data = *attr->value_ctr.values[0].blob; + + if (!dn_printed) { + object_id++; + DEBUG(0,("DN[%u] %s\n", object_id, dn)); + dn_printed = true; + } + DEBUGADD(0,("ATTR: %s plain.length=%lu\n", + name, (long)plain_data.length)); + if (plain_data.length) { + enum ndr_err_code ndr_err; + dump_data(0, plain_data.data, plain_data.length); + if (save_values_dir) { + char *fname; + fname = talloc_asprintf(ctx, "%s/%s%02d", + save_values_dir, + name, object_id); + if (fname) { + bool ok; + ok = file_save(fname, plain_data.data, plain_data.length); + if (!ok) { + DEBUGADD(0,("Failed to save '%s'\n", fname)); + } + } + talloc_free(fname); + } + + if (pull_fn) { + /* Can't use '_all' because of PIDL bugs with relative pointers */ + ndr_err = ndr_pull_struct_blob(&plain_data, ptr, + ptr, pull_fn); + if (NDR_ERR_CODE_IS_SUCCESS(ndr_err)) { + (void)ndr_print_debug(1, print_fn, name, ptr, __location__, __func__); + } else { + DEBUG(0, ("Failed to decode %s\n", name)); + } + } + } + talloc_free(ptr); + } + } + talloc_free(objs); + return true; +} + +static bool test_GetNCChanges(struct torture_context *tctx, + struct DsSyncTest *ctx, + const char *nc_dn_str) +{ + NTSTATUS status; + bool ret = true; + int i, y = 0; + uint64_t highest_usn = 0; + struct drsuapi_DsGetNCChanges r; + union drsuapi_DsGetNCChangesRequest req; + struct drsuapi_DsReplicaObjectIdentifier nc; + struct drsuapi_DsGetNCChangesCtr1 *ctr1 = NULL; + struct drsuapi_DsGetNCChangesCtr6 *ctr6 = NULL; + uint32_t out_level = 0; + struct dom_sid null_sid; + DATA_BLOB gensec_skey; + struct { + uint32_t level; + } array[] = { +/* { + 5 + }, +*/ { + 8 + } + }; + + ZERO_STRUCT(null_sid); + + highest_usn = lpcfg_parm_int(tctx->lp_ctx, NULL, "dssync", "highest_usn", 0); + + array[0].level = lpcfg_parm_int(tctx->lp_ctx, NULL, "dssync", "get_nc_changes_level", array[0].level); + + if (lpcfg_parm_bool(tctx->lp_ctx, NULL, "dssync", "print_pwd_blobs", false)) { + const struct samr_Password *nthash; + nthash = cli_credentials_get_nt_hash(ctx->new_dc.credentials, ctx); + if (nthash) { + dump_data_pw("CREDENTIALS nthash:", nthash->hash, sizeof(nthash->hash)); + } + } + status = gensec_session_key(ctx->new_dc.drsuapi.drs_pipe->conn->security_state.generic_state, + ctx, &gensec_skey); + if (!NT_STATUS_IS_OK(status)) { + printf("failed to get gensec session key: %s\n", nt_errstr(status)); + return false; + } + + for (i=0; i < ARRAY_SIZE(array); i++) { + printf("Testing DsGetNCChanges level %d\n", + array[i].level); + + r.in.bind_handle = &ctx->new_dc.drsuapi.bind_handle; + r.in.level = array[i].level; + + switch (r.in.level) { + case 5: + nc.guid = GUID_zero(); + nc.sid = null_sid; + nc.dn = nc_dn_str; + + r.in.req = &req; + r.in.req->req5.destination_dsa_guid = ctx->new_dc.invocation_id; + r.in.req->req5.source_dsa_invocation_id = GUID_zero(); + r.in.req->req5.naming_context = &nc; + r.in.req->req5.highwatermark.tmp_highest_usn = highest_usn; + r.in.req->req5.highwatermark.reserved_usn = 0; + r.in.req->req5.highwatermark.highest_usn = highest_usn; + r.in.req->req5.uptodateness_vector = NULL; + r.in.req->req5.replica_flags = 0; + if (lpcfg_parm_bool(tctx->lp_ctx, NULL, "dssync", "compression", false)) { + r.in.req->req5.replica_flags |= DRSUAPI_DRS_USE_COMPRESSION; + } + if (lpcfg_parm_bool(tctx->lp_ctx, NULL, "dssync", "neighbour_writeable", true)) { + r.in.req->req5.replica_flags |= DRSUAPI_DRS_WRIT_REP; + } + r.in.req->req5.replica_flags |= DRSUAPI_DRS_INIT_SYNC + | DRSUAPI_DRS_PER_SYNC + | DRSUAPI_DRS_GET_ANC + | DRSUAPI_DRS_NEVER_SYNCED + ; + r.in.req->req5.max_object_count = 133; + r.in.req->req5.max_ndr_size = 1336770; + r.in.req->req5.extended_op = DRSUAPI_EXOP_NONE; + r.in.req->req5.fsmo_info = 0; + + break; + case 8: + nc.guid = GUID_zero(); + nc.sid = null_sid; + nc.dn = nc_dn_str; + /* nc.dn can be set to any other ad partition */ + + r.in.req = &req; + r.in.req->req8.destination_dsa_guid = ctx->new_dc.invocation_id; + r.in.req->req8.source_dsa_invocation_id = GUID_zero(); + r.in.req->req8.naming_context = &nc; + r.in.req->req8.highwatermark.tmp_highest_usn = highest_usn; + r.in.req->req8.highwatermark.reserved_usn = 0; + r.in.req->req8.highwatermark.highest_usn = highest_usn; + r.in.req->req8.uptodateness_vector = NULL; + r.in.req->req8.replica_flags = 0; + if (lpcfg_parm_bool(tctx->lp_ctx, NULL, "dssync", "compression", false)) { + r.in.req->req8.replica_flags |= DRSUAPI_DRS_USE_COMPRESSION; + } + if (lpcfg_parm_bool(tctx->lp_ctx, NULL, "dssync", "neighbour_writeable", true)) { + r.in.req->req8.replica_flags |= DRSUAPI_DRS_WRIT_REP; + } + r.in.req->req8.replica_flags |= DRSUAPI_DRS_INIT_SYNC + | DRSUAPI_DRS_PER_SYNC + | DRSUAPI_DRS_GET_ANC + | DRSUAPI_DRS_NEVER_SYNCED + ; + r.in.req->req8.max_object_count = 402; + r.in.req->req8.max_ndr_size = 402116; + + r.in.req->req8.extended_op = DRSUAPI_EXOP_NONE; + r.in.req->req8.fsmo_info = 0; + r.in.req->req8.partial_attribute_set = NULL; + r.in.req->req8.partial_attribute_set_ex = NULL; + r.in.req->req8.mapping_ctr.num_mappings = 0; + r.in.req->req8.mapping_ctr.mappings = NULL; + + break; + } + + for (y=0; ;y++) { + uint32_t _level = 0; + union drsuapi_DsGetNCChangesCtr ctr; + + ZERO_STRUCT(r.out); + + r.out.level_out = &_level; + r.out.ctr = &ctr; + + if (r.in.level == 5) { + torture_comment(tctx, + "start[%d] tmp_higest_usn: %llu , highest_usn: %llu\n", + y, + (unsigned long long) r.in.req->req5.highwatermark.tmp_highest_usn, + (unsigned long long) r.in.req->req5.highwatermark.highest_usn); + } + + if (r.in.level == 8) { + torture_comment(tctx, + "start[%d] tmp_higest_usn: %llu , highest_usn: %llu\n", + y, + (unsigned long long) r.in.req->req8.highwatermark.tmp_highest_usn, + (unsigned long long) r.in.req->req8.highwatermark.highest_usn); + } + + status = dcerpc_drsuapi_DsGetNCChanges_r(ctx->new_dc.drsuapi.drs_handle, ctx, &r); + torture_drsuapi_assert_call(tctx, ctx->new_dc.drsuapi.drs_pipe, status, + &r, "dcerpc_drsuapi_DsGetNCChanges"); + + if (ret == true && *r.out.level_out == 1) { + out_level = 1; + ctr1 = &r.out.ctr->ctr1; + } else if (ret == true && *r.out.level_out == 2 && + r.out.ctr->ctr2.mszip1.ts) { + out_level = 1; + ctr1 = &r.out.ctr->ctr2.mszip1.ts->ctr1; + } + + if (out_level == 1) { + torture_comment(tctx, + "end[%d] tmp_highest_usn: %llu , highest_usn: %llu\n", + y, + (unsigned long long) ctr1->new_highwatermark.tmp_highest_usn, + (unsigned long long) ctr1->new_highwatermark.highest_usn); + + if (!test_analyse_objects(tctx, ctx, nc_dn_str, &ctr1->mapping_ctr, ctr1->object_count, + ctr1->first_object, &gensec_skey)) { + return false; + } + + if (ctr1->more_data) { + r.in.req->req5.highwatermark = ctr1->new_highwatermark; + continue; + } + } + + if (ret == true && *r.out.level_out == 6) { + out_level = 6; + ctr6 = &r.out.ctr->ctr6; + } else if (ret == true && *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) { + out_level = 6; + ctr6 = &r.out.ctr->ctr7.ctr.mszip6.ts->ctr6; + } else if (ret == true && *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) { + out_level = 6; + ctr6 = &r.out.ctr->ctr7.ctr.xpress6.ts->ctr6; + } + + if (out_level == 6) { + torture_comment(tctx, + "end[%d] tmp_highest_usn: %llu , highest_usn: %llu\n", + y, + (unsigned long long) ctr6->new_highwatermark.tmp_highest_usn, + (unsigned long long) ctr6->new_highwatermark.highest_usn); + + if (!test_analyse_objects(tctx, ctx, nc_dn_str, &ctr6->mapping_ctr, ctr6->object_count, + ctr6->first_object, &gensec_skey)) { + return false; + } + + if (ctr6->more_data) { + r.in.req->req8.highwatermark = ctr6->new_highwatermark; + continue; + } + } + + break; + } + } + + return ret; +} + +/** + * Test DsGetNCChanges() DRSUAPI call against one + * or more Naming Contexts. + * Specific NC to test with may be supplied + * in lp_ctx configuration. If no NC is specified, + * it will test DsGetNCChanges() on all NCs on remote DC + */ +static bool test_FetchData(struct torture_context *tctx, struct DsSyncTest *ctx) +{ + bool ret = true; + size_t i, count; + const char *nc_dn_str; + const char **nc_list; + + nc_list = const_str_list(str_list_make_empty(ctx)); + torture_assert(tctx, nc_list, "Not enough memory!"); + + /* make a list of partitions to test with */ + nc_dn_str = lpcfg_parm_string(tctx->lp_ctx, NULL, "dssync", "partition"); + if (nc_dn_str == NULL) { + nc_list = str_list_add_const(nc_list, ctx->domain_dn); + nc_list = str_list_add_const(nc_list, ctx->config_dn); + nc_list = str_list_add_const(nc_list, ctx->schema_dn); + } else { + nc_list = str_list_add_const(nc_list, nc_dn_str); + } + + count = str_list_length(nc_list); + for (i = 0; i < count && ret; i++) { + torture_comment(tctx, "\nNaming Context: %s\n", nc_list[i]); + ret = test_GetNCChanges(tctx, ctx, nc_list[i]); + } + + talloc_free(nc_list); + return ret; +} + + +static bool test_FetchNT4Data(struct torture_context *tctx, + struct DsSyncTest *ctx) +{ + NTSTATUS status; + struct drsuapi_DsGetNT4ChangeLog r; + union drsuapi_DsGetNT4ChangeLogRequest req; + union drsuapi_DsGetNT4ChangeLogInfo info; + uint32_t level_out = 0; + DATA_BLOB cookie; + + ZERO_STRUCT(cookie); + + ZERO_STRUCT(r); + r.in.bind_handle = &ctx->new_dc.drsuapi.bind_handle; + r.in.level = 1; + r.out.info = &info; + r.out.level_out = &level_out; + + req.req1.flags = lpcfg_parm_int(tctx->lp_ctx, NULL, + "dssync", "nt4changelog_flags", + DRSUAPI_NT4_CHANGELOG_GET_CHANGELOG | + DRSUAPI_NT4_CHANGELOG_GET_SERIAL_NUMBERS); + req.req1.preferred_maximum_length = lpcfg_parm_int(tctx->lp_ctx, NULL, + "dssync", "nt4changelog_preferred_len", + 0x00004000); + + while (1) { + req.req1.restart_length = cookie.length; + req.req1.restart_data = cookie.data; + + r.in.req = &req; + + status = dcerpc_drsuapi_DsGetNT4ChangeLog_r(ctx->new_dc.drsuapi.drs_handle, ctx, &r); + if (NT_STATUS_EQUAL(status, NT_STATUS_NOT_IMPLEMENTED)) { + torture_skip(tctx, + "DsGetNT4ChangeLog not supported: NT_STATUS_NOT_IMPLEMENTED"); + } else if (!NT_STATUS_IS_OK(status)) { + const char *errstr = nt_errstr(status); + if (NT_STATUS_EQUAL(status, NT_STATUS_RPC_PROCNUM_OUT_OF_RANGE)) { + torture_skip(tctx, + "DsGetNT4ChangeLog not supported: NT_STATUS_RPC_PROCNUM_OUT_OF_RANGE"); + } + torture_fail(tctx, + talloc_asprintf(tctx, "dcerpc_drsuapi_DsGetNT4ChangeLog failed - %s\n", + errstr)); + } else if (W_ERROR_EQUAL(r.out.result, WERR_INVALID_DOMAIN_ROLE)) { + torture_skip(tctx, + "DsGetNT4ChangeLog not supported: WERR_INVALID_DOMAIN_ROLE"); + } else if (!W_ERROR_IS_OK(r.out.result)) { + torture_fail(tctx, + talloc_asprintf(tctx, "DsGetNT4ChangeLog failed - %s\n", + win_errstr(r.out.result))); + } else if (*r.out.level_out != 1) { + torture_fail(tctx, + talloc_asprintf(tctx, "DsGetNT4ChangeLog unknown level - %u\n", + *r.out.level_out)); + } else if (NT_STATUS_IS_OK(r.out.info->info1.status)) { + } else if (NT_STATUS_EQUAL(r.out.info->info1.status, STATUS_MORE_ENTRIES)) { + cookie.length = r.out.info->info1.restart_length; + cookie.data = r.out.info->info1.restart_data; + continue; + } else { + torture_fail(tctx, + talloc_asprintf(tctx, "DsGetNT4ChangeLog failed - %s\n", + nt_errstr(r.out.info->info1.status))); + } + + break; + } + + return true; +} + +/** + * DSSYNC test case setup + */ +static bool torture_dssync_tcase_setup(struct torture_context *tctx, void **data) +{ + bool bret; + struct DsSyncTest *ctx; + + *data = ctx = test_create_context(tctx); + torture_assert(tctx, ctx, "test_create_context() failed"); + + bret = _test_DsBind(tctx, ctx, ctx->admin.credentials, &ctx->admin.drsuapi); + torture_assert(tctx, bret, "_test_DsBind() failed"); + + bret = test_LDAPBind(tctx, ctx, ctx->admin.credentials, &ctx->admin.ldap); + torture_assert(tctx, bret, "test_LDAPBind() failed"); + + bret = test_GetInfo(tctx, ctx); + torture_assert(tctx, bret, "test_GetInfo() failed"); + + bret = _test_DsBind(tctx, ctx, ctx->new_dc.credentials, &ctx->new_dc.drsuapi); + torture_assert(tctx, bret, "_test_DsBind() failed"); + + return true; +} + +/** + * DSSYNC test case cleanup + */ +static bool torture_dssync_tcase_teardown(struct torture_context *tctx, void *data) +{ + struct DsSyncTest *ctx; + struct drsuapi_DsUnbind r; + struct policy_handle bind_handle; + + ctx = talloc_get_type(data, struct DsSyncTest); + + ZERO_STRUCT(r); + r.out.bind_handle = &bind_handle; + + /* Unbing admin handle */ + r.in.bind_handle = &ctx->admin.drsuapi.bind_handle; + dcerpc_drsuapi_DsUnbind_r(ctx->admin.drsuapi.drs_handle, ctx, &r); + + /* Unbing new_dc handle */ + r.in.bind_handle = &ctx->new_dc.drsuapi.bind_handle; + dcerpc_drsuapi_DsUnbind_r(ctx->new_dc.drsuapi.drs_handle, ctx, &r); + + talloc_free(ctx); + + return true; +} + +/** + * DSSYNC test case implementation + */ +void torture_drs_rpc_dssync_tcase(struct torture_suite *suite) +{ + typedef bool (*run_func) (struct torture_context *test, void *tcase_data); + struct torture_tcase *tcase = torture_suite_add_tcase(suite, "dssync"); + + torture_tcase_set_fixture(tcase, + torture_dssync_tcase_setup, + torture_dssync_tcase_teardown); + + torture_tcase_add_simple_test(tcase, "DC_FetchData", (run_func)test_FetchData); + torture_tcase_add_simple_test(tcase, "FetchNT4Data", (run_func)test_FetchNT4Data); +} + diff --git a/source4/torture/drs/rpc/msds_intid.c b/source4/torture/drs/rpc/msds_intid.c new file mode 100644 index 0000000..1bc5c32 --- /dev/null +++ b/source4/torture/drs/rpc/msds_intid.c @@ -0,0 +1,792 @@ +/* + Unix SMB/CIFS implementation. + + msDS-IntId attribute replication test. + + 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 "lib/cmdline/cmdline.h" +#include "librpc/gen_ndr/ndr_drsuapi_c.h" +#include "librpc/gen_ndr/ndr_drsblobs.h" +#include "libcli/cldap/cldap.h" +#include "torture/torture.h" +#include "../libcli/drsuapi/drsuapi.h" +#include "auth/gensec/gensec.h" +#include "param/param.h" +#include "dsdb/samdb/samdb.h" +#include "torture/rpc/torture_rpc.h" +#include "torture/drs/proto.h" +#include "lib/tsocket/tsocket.h" +#include "libcli/resolve/resolve.h" +#include "lib/util/util_paths.h" + +struct DsSyncBindInfo { + struct dcerpc_pipe *drs_pipe; + struct dcerpc_binding_handle *drs_handle; + struct drsuapi_DsBind req; + struct GUID bind_guid; + struct drsuapi_DsBindInfoCtr our_bind_info_ctr; + struct drsuapi_DsBindInfo28 our_bind_info28; + struct drsuapi_DsBindInfo28 peer_bind_info28; + struct policy_handle bind_handle; +}; + +struct DsaBindInfo { + struct dcerpc_binding *server_binding; + + struct dcerpc_pipe *drs_pipe; + struct dcerpc_binding_handle *drs_handle; + + DATA_BLOB gensec_skey; + struct drsuapi_DsBindInfo48 srv_info48; + struct policy_handle rpc_handle; +}; + +struct DsIntIdTestCtx { + const char *ldap_url; + const char *domain_dn; + const char *config_dn; + const char *schema_dn; + + /* what we need to do as 'Administrator' */ + struct cli_credentials *creds; + struct DsaBindInfo dsa_bind; + struct ldb_context *ldb; + +}; + +/* Format string to create provision LDIF with */ +#define PROVISION_LDIF_FMT \ + "###########################################################\n" \ + "# Format string with positional params:\n" \ + "# 1 - (int) Unique ID between 1 and 2^16\n" \ + "# 2 - (string) Domain DN\n" \ + "###########################################################\n" \ + "\n" \ + "###########################################################\n" \ + "# Update schema\n" \ + "###########################################################\n" \ + "dn: CN=msds-intid-%1$d,CN=Schema,CN=Configuration,%2$s\n" \ + "changetype: add\n" \ + "objectClass: top\n" \ + "objectClass: attributeSchema\n" \ + "cn: msds-intid-%1$d\n" \ + "attributeID: 1.3.6.1.4.1.7165.4.6.1.%1$d.1.5.9940\n" \ + "attributeSyntax: 2.5.5.10\n" \ + "omSyntax: 4\n" \ + "instanceType: 4\n" \ + "isSingleValued: TRUE\n" \ + "systemOnly: FALSE\n" \ + "\n" \ + "# schemaUpdateNow\n" \ + "DN:\n" \ + "changeType: modify\n" \ + "add: schemaUpdateNow\n" \ + "schemaUpdateNow: 1\n" \ + "-\n" \ + "\n" \ + "###########################################################\n" \ + "# Update schema (with linked attribute)\n" \ + "###########################################################\n" \ + "dn: CN=msds-intid-link-%1$d,CN=Schema,CN=Configuration,%2$s\n" \ + "changetype: add\n" \ + "objectClass: top\n" \ + "objectClass: attributeSchema\n" \ + "cn: msds-intid-link-%1$d\n" \ + "attributeID: 1.3.6.1.4.1.7165.4.6.1.%1$d.1.5.9941\n" \ + "attributeSyntax: 2.5.5.1\n" \ + "omSyntax: 127\n" \ + "instanceType: 4\n" \ + "isSingleValued: TRUE\n" \ + "systemOnly: FALSE\n" \ + "linkID: 1.2.840.113556.1.2.50\n" \ + "\n" \ + "# schemaUpdateNow\n" \ + "DN:\n" \ + "changeType: modify\n" \ + "add: schemaUpdateNow\n" \ + "schemaUpdateNow: 1\n" \ + "-\n" \ + "\n" \ + "###########################################################\n" \ + "# Update User class\n" \ + "###########################################################\n" \ + "dn: CN=User,CN=Schema,CN=Configuration,%2$s\n" \ + "changetype: modify\n" \ + "add: mayContain\n" \ + "mayContain: msdsIntid%1$d\n" \ + "mayContain: msdsIntidLink%1$d\n" \ + "-\n" \ + "\n" \ + "# schemaUpdateNow\n" \ + "DN:\n" \ + "changeType: modify\n" \ + "add: schemaUpdateNow\n" \ + "schemaUpdateNow: 1\n" \ + "-\n" \ + "\n" \ + "###########################################################\n" \ + "# create user to test with\n" \ + "###########################################################\n" \ + "dn: CN=dsIntId_usr_%1$d,CN=Users,%2$s\n" \ + "changetype: add\n" \ + "objectClass: user\n" \ + "cn: dsIntId_usr_%1$d\n" \ + "name: dsIntId_usr_%1$d\n" \ + "displayName: dsIntId_usr_%1$d\n" \ + "sAMAccountName: dsIntId_usr_%1$d\n" \ + "msdsIntid%1$d: msDS-IntId-%1$d attribute value\n" \ + "msdsIntidLink%1$d: %2$s\n" \ + "\n" + + +static struct DsIntIdTestCtx *_dsintid_create_context(struct torture_context *tctx) +{ + NTSTATUS status; + struct DsIntIdTestCtx *ctx; + struct dcerpc_binding *server_binding; + const char *binding = torture_setting_string(tctx, "binding", NULL); + + /* Create test suite context */ + ctx = talloc_zero(tctx, struct DsIntIdTestCtx); + if (!ctx) { + torture_result(tctx, TORTURE_FAIL, "Not enough memory!"); + return NULL; + } + + /* parse binding object */ + status = dcerpc_parse_binding(ctx, binding, &server_binding); + if (!NT_STATUS_IS_OK(status)) { + torture_result(tctx, TORTURE_FAIL, + "Bad binding string '%s': %s", binding, nt_errstr(status)); + return NULL; + } + + status = dcerpc_binding_set_flags(server_binding, + DCERPC_SIGN | DCERPC_SEAL, 0); + if (!NT_STATUS_IS_OK(status)) { + torture_result(tctx, TORTURE_FAIL, + "dcerpc_binding_set_flags: %s", nt_errstr(status)); + return NULL; + } + + /* populate test suite context */ + ctx->creds = samba_cmdline_get_creds(); + ctx->dsa_bind.server_binding = server_binding; + + ctx->ldap_url = talloc_asprintf(ctx, "ldap://%s", + dcerpc_binding_get_string_option(server_binding, "host")); + + return ctx; +} + +static bool _test_DsaBind(struct torture_context *tctx, + TALLOC_CTX *mem_ctx, + struct cli_credentials *credentials, + uint32_t req_extensions, + struct DsaBindInfo *bi) +{ + NTSTATUS status; + struct GUID bind_guid; + struct drsuapi_DsBind r; + struct drsuapi_DsBindInfoCtr bind_info_ctr; + uint32_t supported_extensions; + + /* make DCE RPC connection */ + status = dcerpc_pipe_connect_b(mem_ctx, + &bi->drs_pipe, + bi->server_binding, + &ndr_table_drsuapi, + credentials, tctx->ev, tctx->lp_ctx); + torture_assert_ntstatus_ok(tctx, status, "Failed to connect to server"); + + bi->drs_handle = bi->drs_pipe->binding_handle; + + status = gensec_session_key(bi->drs_pipe->conn->security_state.generic_state, + mem_ctx, &bi->gensec_skey); + torture_assert_ntstatus_ok(tctx, status, "failed to get gensec session key"); + + /* Bind to DRSUAPI interface */ + GUID_from_string(DRSUAPI_DS_BIND_GUID_W2K3, &bind_guid); + + /* + * Add flags that should be 1, according to MS docs. + * It turns out DRSUAPI_SUPPORTED_EXTENSION_POST_BETA3 + * is actually required in order for GetNCChanges() to + * return schemaInfo entry in the prefixMap returned. + * Use DRSUAPI_SUPPORTED_EXTENSION_STRONG_ENCRYPTION so + * we are able to fetch sensitive data. + */ + supported_extensions = req_extensions + | DRSUAPI_SUPPORTED_EXTENSION_BASE + | DRSUAPI_SUPPORTED_EXTENSION_RESTORE_USN_OPTIMIZATION + | DRSUAPI_SUPPORTED_EXTENSION_INSTANCE_TYPE_NOT_REQ_ON_MOD + | DRSUAPI_SUPPORTED_EXTENSION_POST_BETA3 + | DRSUAPI_SUPPORTED_EXTENSION_STRONG_ENCRYPTION; + + ZERO_STRUCT(bind_info_ctr); + bind_info_ctr.length = 28; + bind_info_ctr.info.info28.supported_extensions = supported_extensions; + + r.in.bind_guid = &bind_guid; + r.in.bind_info = &bind_info_ctr; + r.out.bind_handle = &bi->rpc_handle; + + status = dcerpc_drsuapi_DsBind_r(bi->drs_handle, mem_ctx, &r); + torture_drsuapi_assert_call(tctx, bi->drs_pipe, status, + &r, "dcerpc_drsuapi_DsBind_r"); + + + switch (r.out.bind_info->length) { + case 24: { + struct drsuapi_DsBindInfo24 *info24; + info24 = &r.out.bind_info->info.info24; + bi->srv_info48.supported_extensions = info24->supported_extensions; + bi->srv_info48.site_guid = info24->site_guid; + bi->srv_info48.pid = info24->pid; + break; + } + case 28: { + struct drsuapi_DsBindInfo28 *info28; + info28 = &r.out.bind_info->info.info28; + bi->srv_info48.supported_extensions = info28->supported_extensions; + bi->srv_info48.site_guid = info28->site_guid; + bi->srv_info48.pid = info28->pid; + bi->srv_info48.repl_epoch = info28->repl_epoch; + break; + } + case 32: { + struct drsuapi_DsBindInfo32 *info32; + info32 = &r.out.bind_info->info.info32; + bi->srv_info48.supported_extensions = info32->supported_extensions; + bi->srv_info48.site_guid = info32->site_guid; + bi->srv_info48.pid = info32->pid; + bi->srv_info48.repl_epoch = info32->repl_epoch; + break; + } + case 48: { + bi->srv_info48 = r.out.bind_info->info.info48; + break; + } + case 52: { + struct drsuapi_DsBindInfo52 *info52; + info52 = &r.out.bind_info->info.info52; + bi->srv_info48.supported_extensions = info52->supported_extensions; + bi->srv_info48.site_guid = info52->site_guid; + bi->srv_info48.pid = info52->pid; + bi->srv_info48.repl_epoch = info52->repl_epoch; + break; + } + default: + torture_result(tctx, TORTURE_FAIL, + "DsBind: unknown BindInfo length: %u", + r.out.bind_info->length); + return false; + } + + /* check if server supports extensions we've requested */ + if ((bi->srv_info48.supported_extensions & req_extensions) != req_extensions) { + torture_result(tctx, TORTURE_FAIL, + "Server does not support requested extensions. " + "Requested: 0x%08X, Supported: 0x%08X", + req_extensions, bi->srv_info48.supported_extensions); + return false; + } + + return true; +} + +static bool _test_LDAPBind(struct torture_context *tctx, + TALLOC_CTX *mem_ctx, + struct cli_credentials *credentials, + const char *ldap_url, + struct ldb_context **_ldb) +{ + bool ret = true; + + struct ldb_context *ldb; + + const char *modules_option[] = { "modules:paged_searches", NULL }; + ldb = ldb_init(mem_ctx, tctx->ev); + if (ldb == NULL) { + return false; + } + + /* Despite us loading the schema from the AD server, we need + * the samba handlers to get the extended DN syntax stuff */ + ret = ldb_register_samba_handlers(ldb); + if (ret != LDB_SUCCESS) { + talloc_free(ldb); + return NULL; + } + + ldb_set_modules_dir(ldb, modules_path(ldb, "ldb")); + + if (ldb_set_opaque(ldb, "credentials", credentials) != LDB_SUCCESS) { + talloc_free(ldb); + return NULL; + } + + if (ldb_set_opaque(ldb, "loadparm", tctx->lp_ctx) != LDB_SUCCESS) { + talloc_free(ldb); + return NULL; + } + + ret = ldb_connect(ldb, ldap_url, 0, modules_option); + if (ret != LDB_SUCCESS) { + talloc_free(ldb); + torture_assert_int_equal(tctx, ret, LDB_SUCCESS, "Failed to make LDB connection to target"); + } + + *_ldb = ldb; + + return true; +} + +static bool _test_provision(struct torture_context *tctx, struct DsIntIdTestCtx *ctx) +{ + int ret; + char *ldif_str; + const char *pstr; + struct ldb_ldif *ldif; + uint32_t attr_id; + struct ldb_context *ldb = ctx->ldb; + + /* We must have LDB connection ready by this time */ + SMB_ASSERT(ldb != NULL); + + ctx->domain_dn = ldb_dn_get_linearized(ldb_get_default_basedn(ldb)); + torture_assert(tctx, ctx->domain_dn != NULL, "Failed to get Domain DN"); + + ctx->config_dn = ldb_dn_get_linearized(ldb_get_config_basedn(ldb)); + torture_assert(tctx, ctx->config_dn != NULL, "Failed to get Domain DN"); + + ctx->schema_dn = ldb_dn_get_linearized(ldb_get_schema_basedn(ldb)); + torture_assert(tctx, ctx->schema_dn != NULL, "Failed to get Domain DN"); + + /* prepare LDIF to provision with */ + attr_id = generate_random() % 0xFFFF; + pstr = ldif_str = talloc_asprintf(ctx, PROVISION_LDIF_FMT, + attr_id, ctx->domain_dn); + + /* Provision test data */ + while ((ldif = ldb_ldif_read_string(ldb, &pstr)) != NULL) { + switch (ldif->changetype) { + case LDB_CHANGETYPE_DELETE: + ret = ldb_delete(ldb, ldif->msg->dn); + break; + case LDB_CHANGETYPE_MODIFY: + ret = ldb_modify(ldb, ldif->msg); + break; + case LDB_CHANGETYPE_ADD: + default: + ret = ldb_add(ldb, ldif->msg); + break; + } + if (ret != LDB_SUCCESS) { + char *msg = talloc_asprintf(ctx, + "Failed to apply ldif - %s (%s): \n%s", + ldb_errstring(ldb), + ldb_strerror(ret), + ldb_ldif_write_string(ldb, ctx, ldif)); + torture_fail(tctx, msg); + + } + ldb_ldif_read_free(ldb, ldif); + } + + return true; +} + + +static bool _test_GetNCChanges(struct torture_context *tctx, + struct DsaBindInfo *bi, + const char *nc_dn_str, + TALLOC_CTX *mem_ctx, + struct drsuapi_DsGetNCChangesCtr6 **_ctr6) +{ + NTSTATUS status; + struct drsuapi_DsGetNCChanges r; + union drsuapi_DsGetNCChangesRequest req; + struct drsuapi_DsReplicaObjectIdentifier nc; + struct drsuapi_DsGetNCChangesCtr6 *ctr6_chunk = NULL; + struct drsuapi_DsGetNCChangesCtr6 ctr6; + uint32_t _level = 0; + union drsuapi_DsGetNCChangesCtr ctr; + + struct dom_sid null_sid; + + ZERO_STRUCT(null_sid); + + /* fill-in Naming Context */ + nc.guid = GUID_zero(); + nc.sid = null_sid; + nc.dn = nc_dn_str; + + /* fill-in request fields */ + req.req8.destination_dsa_guid = GUID_random(); + req.req8.source_dsa_invocation_id = GUID_zero(); + req.req8.naming_context = &nc; + req.req8.highwatermark.tmp_highest_usn = 0; + req.req8.highwatermark.reserved_usn = 0; + req.req8.highwatermark.highest_usn = 0; + req.req8.uptodateness_vector = NULL; + req.req8.replica_flags = DRSUAPI_DRS_WRIT_REP + | DRSUAPI_DRS_INIT_SYNC + | DRSUAPI_DRS_PER_SYNC + | DRSUAPI_DRS_GET_ANC + | DRSUAPI_DRS_NEVER_SYNCED + ; + req.req8.max_object_count = 402; + req.req8.max_ndr_size = 402116; + + req.req8.extended_op = DRSUAPI_EXOP_NONE; + req.req8.fsmo_info = 0; + req.req8.partial_attribute_set = NULL; + req.req8.partial_attribute_set_ex = NULL; + req.req8.mapping_ctr.num_mappings = 0; + req.req8.mapping_ctr.mappings = NULL; + + r.in.bind_handle = &bi->rpc_handle; + r.in.level = 8; + r.in.req = &req; + + ZERO_STRUCT(r.out); + r.out.level_out = &_level; + r.out.ctr = &ctr; + + ZERO_STRUCT(ctr6); + do { + ZERO_STRUCT(ctr); + + status = dcerpc_drsuapi_DsGetNCChanges_r(bi->drs_handle, mem_ctx, &r); + torture_drsuapi_assert_call(tctx, bi->drs_pipe, status, + &r, "dcerpc_drsuapi_DsGetNCChanges_r"); + + /* we expect to get level 6 reply */ + torture_assert_int_equal(tctx, _level, 6, "Expected level 6 reply"); + + /* store this chunk for later use */ + ctr6_chunk = &r.out.ctr->ctr6; + + if (!ctr6.first_object) { + ctr6 = *ctr6_chunk; + } else { + struct drsuapi_DsReplicaObjectListItemEx *cur; + + ctr6.object_count += ctr6_chunk->object_count; + for (cur = ctr6.first_object; cur->next_object; cur = cur->next_object) {} + cur->next_object = ctr6_chunk->first_object; + + if (ctr6_chunk->linked_attributes_count != 0) { + uint32_t i; + ctr6.linked_attributes = talloc_realloc(mem_ctx, ctr6.linked_attributes, + struct drsuapi_DsReplicaLinkedAttribute, + ctr6.linked_attributes_count + ctr6_chunk->linked_attributes_count); + for (i = 0; i < ctr6_chunk->linked_attributes_count; i++) { + ctr6.linked_attributes[ctr6.linked_attributes_count++] = ctr6_chunk->linked_attributes[i]; + } + } + } + + /* prepare for next request */ + r.in.req->req8.highwatermark = ctr6_chunk->new_highwatermark; + + } while (ctr6_chunk->more_data); + + *_ctr6 = talloc(mem_ctx, struct drsuapi_DsGetNCChangesCtr6); + torture_assert(mem_ctx, *_ctr6, "Not enough memory"); + **_ctr6 = ctr6; + + return true; +} + +static char * _make_error_message(TALLOC_CTX *mem_ctx, + enum drsuapi_DsAttributeId drs_attid, + const struct dsdb_attribute *dsdb_attr, + const struct drsuapi_DsReplicaObjectIdentifier *identifier) +{ + return talloc_asprintf(mem_ctx, "\nInvalid ATTID for %1$s (%2$s)\n" + " drs_attid: %3$11d (0x%3$08X)\n" + " msDS_IntId: %4$11d (0x%4$08X)\n" + " attributeId_id: %5$11d (0x%5$08X)", + dsdb_attr->lDAPDisplayName, + identifier->dn, + drs_attid, + dsdb_attr->msDS_IntId, + dsdb_attr->attributeID_id); +} + +/** + * Fetch Schema NC and check ATTID values returned. + * When Schema partition is replicated, ATTID + * should always be made using prefixMap + */ +static bool test_dsintid_schema(struct torture_context *tctx, struct DsIntIdTestCtx *ctx) +{ + uint32_t i; + const struct dsdb_schema *ldap_schema; + struct drsuapi_DsGetNCChangesCtr6 *ctr6 = NULL; + const struct dsdb_attribute *dsdb_attr; + const struct drsuapi_DsReplicaAttribute *drs_attr; + const struct drsuapi_DsReplicaAttributeCtr *attr_ctr; + const struct drsuapi_DsReplicaObjectListItemEx *cur; + const struct drsuapi_DsReplicaLinkedAttribute *la; + TALLOC_CTX *mem_ctx; + + mem_ctx = talloc_new(ctx); + torture_assert(tctx, mem_ctx, "Not enough memory"); + + /* fetch whole Schema partition */ + torture_comment(tctx, "Fetch partition: %s\n", ctx->schema_dn); + if (!_test_GetNCChanges(tctx, &ctx->dsa_bind, ctx->schema_dn, mem_ctx, &ctr6)) { + torture_fail(tctx, "_test_GetNCChanges() failed"); + } + + /* load schema if not loaded yet */ + torture_comment(tctx, "Loading schema...\n"); + if (!drs_util_dsdb_schema_load_ldb(tctx, ctx->ldb, &ctr6->mapping_ctr, false)) { + torture_fail(tctx, "drs_util_dsdb_schema_load_ldb() failed"); + } + ldap_schema = dsdb_get_schema(ctx->ldb, NULL); + + /* verify ATTIDs fetched */ + torture_comment(tctx, "Verify ATTIDs fetched\n"); + for (cur = ctr6->first_object; cur; cur = cur->next_object) { + attr_ctr = &cur->object.attribute_ctr; + for (i = 0; i < attr_ctr->num_attributes; i++) { + drs_attr = &attr_ctr->attributes[i]; + dsdb_attr = dsdb_attribute_by_attributeID_id(ldap_schema, + drs_attr->attid); + + torture_assert(tctx, + drs_attr->attid == dsdb_attr->attributeID_id, + _make_error_message(ctx, drs_attr->attid, + dsdb_attr, + cur->object.identifier)); + if (dsdb_attr->msDS_IntId) { + torture_assert(tctx, + drs_attr->attid != dsdb_attr->msDS_IntId, + _make_error_message(ctx, drs_attr->attid, + dsdb_attr, + cur->object.identifier)); + } + } + } + + /* verify ATTIDs for Linked Attributes */ + torture_comment(tctx, "Verify ATTIDs for Linked Attributes (%u)\n", + ctr6->linked_attributes_count); + for (i = 0; i < ctr6->linked_attributes_count; i++) { + la = &ctr6->linked_attributes[i]; + dsdb_attr = dsdb_attribute_by_attributeID_id(ldap_schema, la->attid); + + torture_assert(tctx, + la->attid == dsdb_attr->attributeID_id, + _make_error_message(ctx, la->attid, + dsdb_attr, + la->identifier)); + if (dsdb_attr->msDS_IntId) { + torture_assert(tctx, + la->attid != dsdb_attr->msDS_IntId, + _make_error_message(ctx, la->attid, + dsdb_attr, + la->identifier)); + } + } + + talloc_free(mem_ctx); + + return true; +} + +/** + * Fetch non-Schema NC and check ATTID values returned. + * When non-Schema partition is replicated, ATTID + * should be msDS-IntId value for the attribute + * if this value exists + */ +static bool _test_dsintid(struct torture_context *tctx, + struct DsIntIdTestCtx *ctx, + const char *nc_dn_str) +{ + uint32_t i; + const struct dsdb_schema *ldap_schema; + struct drsuapi_DsGetNCChangesCtr6 *ctr6 = NULL; + const struct dsdb_attribute *dsdb_attr; + const struct drsuapi_DsReplicaAttribute *drs_attr; + const struct drsuapi_DsReplicaAttributeCtr *attr_ctr; + const struct drsuapi_DsReplicaObjectListItemEx *cur; + const struct drsuapi_DsReplicaLinkedAttribute *la; + TALLOC_CTX *mem_ctx; + + mem_ctx = talloc_new(ctx); + torture_assert(tctx, mem_ctx, "Not enough memory"); + + /* fetch whole Schema partition */ + torture_comment(tctx, "Fetch partition: %s\n", nc_dn_str); + if (!_test_GetNCChanges(tctx, &ctx->dsa_bind, nc_dn_str, mem_ctx, &ctr6)) { + torture_fail(tctx, "_test_GetNCChanges() failed"); + } + + /* load schema if not loaded yet */ + torture_comment(tctx, "Loading schema...\n"); + if (!drs_util_dsdb_schema_load_ldb(tctx, ctx->ldb, &ctr6->mapping_ctr, false)) { + torture_fail(tctx, "drs_util_dsdb_schema_load_ldb() failed"); + } + ldap_schema = dsdb_get_schema(ctx->ldb, NULL); + + /* verify ATTIDs fetched */ + torture_comment(tctx, "Verify ATTIDs fetched\n"); + for (cur = ctr6->first_object; cur; cur = cur->next_object) { + attr_ctr = &cur->object.attribute_ctr; + for (i = 0; i < attr_ctr->num_attributes; i++) { + drs_attr = &attr_ctr->attributes[i]; + dsdb_attr = dsdb_attribute_by_attributeID_id(ldap_schema, + drs_attr->attid); + if (dsdb_attr->msDS_IntId) { + torture_assert(tctx, + drs_attr->attid == dsdb_attr->msDS_IntId, + _make_error_message(ctx, drs_attr->attid, + dsdb_attr, + cur->object.identifier)); + } else { + torture_assert(tctx, + drs_attr->attid == dsdb_attr->attributeID_id, + _make_error_message(ctx, drs_attr->attid, + dsdb_attr, + cur->object.identifier)); + } + } + } + + /* verify ATTIDs for Linked Attributes */ + torture_comment(tctx, "Verify ATTIDs for Linked Attributes (%u)\n", + ctr6->linked_attributes_count); + for (i = 0; i < ctr6->linked_attributes_count; i++) { + la = &ctr6->linked_attributes[i]; + dsdb_attr = dsdb_attribute_by_attributeID_id(ldap_schema, la->attid); + + if (dsdb_attr->msDS_IntId) { + torture_assert(tctx, + la->attid == dsdb_attr->msDS_IntId, + _make_error_message(ctx, la->attid, + dsdb_attr, + la->identifier)); + } else { + torture_assert(tctx, + la->attid == dsdb_attr->attributeID_id, + _make_error_message(ctx, la->attid, + dsdb_attr, + la->identifier)); + } + } + + talloc_free(mem_ctx); + + return true; +} + +/** + * Fetch Domain NC and check ATTID values returned. + * When Domain partition is replicated, ATTID + * should be msDS-IntId value for the attribute + * if this value exists + */ +static bool test_dsintid_configuration(struct torture_context *tctx, struct DsIntIdTestCtx *ctx) +{ + return _test_dsintid(tctx, ctx, ctx->config_dn); +} + +/** + * Fetch Configuration NC and check ATTID values returned. + * When Configuration partition is replicated, ATTID + * should be msDS-IntId value for the attribute + * if this value exists + */ +static bool test_dsintid_domain(struct torture_context *tctx, struct DsIntIdTestCtx *ctx) +{ + return _test_dsintid(tctx, ctx, ctx->domain_dn); +} + + +/** + * DSSYNC test case setup + */ +static bool torture_dsintid_tcase_setup(struct torture_context *tctx, void **data) +{ + bool bret; + struct DsIntIdTestCtx *ctx; + + *data = ctx = _dsintid_create_context(tctx); + torture_assert(tctx, ctx, "test_create_context() failed"); + + bret = _test_DsaBind(tctx, ctx, ctx->creds, + DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8 | + DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V6, + &ctx->dsa_bind); + torture_assert(tctx, bret, "_test_DsaBind() failed"); + + bret = _test_LDAPBind(tctx, ctx, ctx->creds, ctx->ldap_url, &ctx->ldb); + torture_assert(tctx, bret, "_test_LDAPBind() failed"); + + bret = _test_provision(tctx, ctx); + torture_assert(tctx, bret, "_test_provision() failed"); + + return true; +} + +/** + * DSSYNC test case cleanup + */ +static bool torture_dsintid_tcase_teardown(struct torture_context *tctx, void *data) +{ + struct DsIntIdTestCtx *ctx; + struct drsuapi_DsUnbind r; + struct policy_handle bind_handle; + + ctx = talloc_get_type(data, struct DsIntIdTestCtx); + + ZERO_STRUCT(r); + r.out.bind_handle = &bind_handle; + + /* Release DRSUAPI handle */ + r.in.bind_handle = &ctx->dsa_bind.rpc_handle; + dcerpc_drsuapi_DsUnbind_r(ctx->dsa_bind.drs_handle, ctx, &r); + + talloc_free(ctx); + + return true; +} + +/** + * DSSYNC test case implementation + */ +void torture_drs_rpc_dsintid_tcase(struct torture_suite *suite) +{ + typedef bool (*run_func) (struct torture_context *test, void *tcase_data); + struct torture_tcase *tcase = torture_suite_add_tcase(suite, "msDSIntId"); + + torture_tcase_set_fixture(tcase, + torture_dsintid_tcase_setup, + torture_dsintid_tcase_teardown); + + torture_tcase_add_simple_test(tcase, "Schema", (run_func)test_dsintid_schema); + torture_tcase_add_simple_test(tcase, "Configuration", (run_func)test_dsintid_configuration); + torture_tcase_add_simple_test(tcase, "Domain", (run_func)test_dsintid_domain); +} diff --git a/source4/torture/drs/unit/prefixmap_tests.c b/source4/torture/drs/unit/prefixmap_tests.c new file mode 100644 index 0000000..35764cd --- /dev/null +++ b/source4/torture/drs/unit/prefixmap_tests.c @@ -0,0 +1,900 @@ +/* + Unix SMB/CIFS implementation. + + DRSUAPI prefixMap unit tests + + Copyright (C) Kamen Mazdrashki <kamenim@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 "system/filesys.h" +#include "torture/smbtorture.h" +#include "dsdb/samdb/samdb.h" +#include "torture/rpc/drsuapi.h" +#include "torture/drs/proto.h" +#include "param/param.h" +#include "librpc/ndr/libndr.h" + +/** + * Private data to be shared among all test in Test case + */ +struct drsut_prefixmap_data { + struct dsdb_schema_prefixmap *pfm_new; + struct dsdb_schema_prefixmap *pfm_full; + + /* default schemaInfo value to test with */ + struct dsdb_schema_info *schi_default; + + struct ldb_context *ldb_ctx; +}; + +/** + * Test-oid data structure + */ +struct drsut_pfm_oid_data { + uint32_t id; + const char *bin_oid; + const char *oid_prefix; +}; + +/** + * Default prefixMap initialization data. + * This prefixMap is what dsdb_schema_pfm_new() should return. + * Based on: MS-DRSR, 5.16.4 ATTRTYP-to-OID Conversion + * procedure NewPrefixTable( ) + */ +static const struct drsut_pfm_oid_data _prefixmap_test_new_data[] = { + {.id=0x00000000, .bin_oid="5504", .oid_prefix="2.5.4"}, + {.id=0x00000001, .bin_oid="5506", .oid_prefix="2.5.6"}, + {.id=0x00000002, .bin_oid="2A864886F7140102", .oid_prefix="1.2.840.113556.1.2"}, + {.id=0x00000003, .bin_oid="2A864886F7140103", .oid_prefix="1.2.840.113556.1.3"}, + {.id=0x00000004, .bin_oid="6086480165020201", .oid_prefix="2.16.840.1.101.2.2.1"}, + {.id=0x00000005, .bin_oid="6086480165020203", .oid_prefix="2.16.840.1.101.2.2.3"}, + {.id=0x00000006, .bin_oid="6086480165020105", .oid_prefix="2.16.840.1.101.2.1.5"}, + {.id=0x00000007, .bin_oid="6086480165020104", .oid_prefix="2.16.840.1.101.2.1.4"}, + {.id=0x00000008, .bin_oid="5505", .oid_prefix="2.5.5"}, + {.id=0x00000009, .bin_oid="2A864886F7140104", .oid_prefix="1.2.840.113556.1.4"}, + {.id=0x0000000A, .bin_oid="2A864886F7140105", .oid_prefix="1.2.840.113556.1.5"}, + {.id=0x00000013, .bin_oid="0992268993F22C64", .oid_prefix="0.9.2342.19200300.100"}, + {.id=0x00000014, .bin_oid="6086480186F84203", .oid_prefix="2.16.840.1.113730.3"}, + {.id=0x00000015, .bin_oid="0992268993F22C6401", .oid_prefix="0.9.2342.19200300.100.1"}, + {.id=0x00000016, .bin_oid="6086480186F8420301", .oid_prefix="2.16.840.1.113730.3.1"}, + {.id=0x00000017, .bin_oid="2A864886F7140105B658", .oid_prefix="1.2.840.113556.1.5.7000"}, + {.id=0x00000018, .bin_oid="5515", .oid_prefix="2.5.21"}, + {.id=0x00000019, .bin_oid="5512", .oid_prefix="2.5.18"}, + {.id=0x0000001A, .bin_oid="5514", .oid_prefix="2.5.20"}, +}; + +/** + * Data to be used for creating full prefix map for testing. + * 'full-prefixMap' is based on what w2k8 returns as a prefixMap + * on clean installation - i.e. prefixMap for clean Schema + */ +static const struct drsut_pfm_oid_data _prefixmap_full_map_data[] = { + {.id=0x00000000, .bin_oid="0x5504", .oid_prefix="2.5.4"}, + {.id=0x00000001, .bin_oid="0x5506", .oid_prefix="2.5.6"}, + {.id=0x00000002, .bin_oid="0x2A864886F7140102", .oid_prefix="1.2.840.113556.1.2"}, + {.id=0x00000003, .bin_oid="0x2A864886F7140103", .oid_prefix="1.2.840.113556.1.3"}, + {.id=0x00000004, .bin_oid="0x6086480165020201", .oid_prefix="2.16.840.1.101.2.2.1"}, + {.id=0x00000005, .bin_oid="0x6086480165020203", .oid_prefix="2.16.840.1.101.2.2.3"}, + {.id=0x00000006, .bin_oid="0x6086480165020105", .oid_prefix="2.16.840.1.101.2.1.5"}, + {.id=0x00000007, .bin_oid="0x6086480165020104", .oid_prefix="2.16.840.1.101.2.1.4"}, + {.id=0x00000008, .bin_oid="0x5505", .oid_prefix="2.5.5"}, + {.id=0x00000009, .bin_oid="0x2A864886F7140104", .oid_prefix="1.2.840.113556.1.4"}, + {.id=0x0000000a, .bin_oid="0x2A864886F7140105", .oid_prefix="1.2.840.113556.1.5"}, + {.id=0x00000013, .bin_oid="0x0992268993F22C64", .oid_prefix="0.9.2342.19200300.100"}, + {.id=0x00000014, .bin_oid="0x6086480186F84203", .oid_prefix="2.16.840.1.113730.3"}, + {.id=0x00000015, .bin_oid="0x0992268993F22C6401", .oid_prefix="0.9.2342.19200300.100.1"}, + {.id=0x00000016, .bin_oid="0x6086480186F8420301", .oid_prefix="2.16.840.1.113730.3.1"}, + {.id=0x00000017, .bin_oid="0x2A864886F7140105B658", .oid_prefix="1.2.840.113556.1.5.7000"}, + {.id=0x00000018, .bin_oid="0x5515", .oid_prefix="2.5.21"}, + {.id=0x00000019, .bin_oid="0x5512", .oid_prefix="2.5.18"}, + {.id=0x0000001a, .bin_oid="0x5514", .oid_prefix="2.5.20"}, + {.id=0x0000000b, .bin_oid="0x2A864886F71401048204", .oid_prefix="1.2.840.113556.1.4.260"}, + {.id=0x0000000c, .bin_oid="0x2A864886F714010538", .oid_prefix="1.2.840.113556.1.5.56"}, + {.id=0x0000000d, .bin_oid="0x2A864886F71401048206", .oid_prefix="1.2.840.113556.1.4.262"}, + {.id=0x0000000e, .bin_oid="0x2A864886F714010539", .oid_prefix="1.2.840.113556.1.5.57"}, + {.id=0x0000000f, .bin_oid="0x2A864886F71401048207", .oid_prefix="1.2.840.113556.1.4.263"}, + {.id=0x00000010, .bin_oid="0x2A864886F71401053A", .oid_prefix="1.2.840.113556.1.5.58"}, + {.id=0x00000011, .bin_oid="0x2A864886F714010549", .oid_prefix="1.2.840.113556.1.5.73"}, + {.id=0x00000012, .bin_oid="0x2A864886F71401048231", .oid_prefix="1.2.840.113556.1.4.305"}, + {.id=0x0000001b, .bin_oid="0x2B060104018B3A6577", .oid_prefix="1.3.6.1.4.1.1466.101.119"}, + {.id=0x0000001c, .bin_oid="0x6086480186F8420302", .oid_prefix="2.16.840.1.113730.3.2"}, + {.id=0x0000001d, .bin_oid="0x2B06010401817A01", .oid_prefix="1.3.6.1.4.1.250.1"}, + {.id=0x0000001e, .bin_oid="0x2A864886F70D0109", .oid_prefix="1.2.840.113549.1.9"}, + {.id=0x0000001f, .bin_oid="0x0992268993F22C6404", .oid_prefix="0.9.2342.19200300.100.4"}, + {.id=0x00000020, .bin_oid="0x2A864886F714010617", .oid_prefix="1.2.840.113556.1.6.23"}, + {.id=0x00000021, .bin_oid="0x2A864886F71401061201", .oid_prefix="1.2.840.113556.1.6.18.1"}, + {.id=0x00000022, .bin_oid="0x2A864886F71401061202", .oid_prefix="1.2.840.113556.1.6.18.2"}, + {.id=0x00000023, .bin_oid="0x2A864886F71401060D03", .oid_prefix="1.2.840.113556.1.6.13.3"}, + {.id=0x00000024, .bin_oid="0x2A864886F71401060D04", .oid_prefix="1.2.840.113556.1.6.13.4"}, + {.id=0x00000025, .bin_oid="0x2B0601010101", .oid_prefix="1.3.6.1.1.1.1"}, + {.id=0x00000026, .bin_oid="0x2B0601010102", .oid_prefix="1.3.6.1.1.1.2"}, + {.id=0x000003ed, .bin_oid="0x2A864886F7140104B65866", .oid_prefix="1.2.840.113556.1.4.7000.102"}, + {.id=0x00000428, .bin_oid="0x2A864886F7140105B6583E", .oid_prefix="1.2.840.113556.1.5.7000.62"}, + {.id=0x0000044c, .bin_oid="0x2A864886F7140104B6586683", .oid_prefix="1.2.840.113556.1.4.7000.102:0x83"}, + {.id=0x0000044f, .bin_oid="0x2A864886F7140104B6586681", .oid_prefix="1.2.840.113556.1.4.7000.102:0x81"}, + {.id=0x0000047d, .bin_oid="0x2A864886F7140105B6583E81", .oid_prefix="1.2.840.113556.1.5.7000.62:0x81"}, + {.id=0x00000561, .bin_oid="0x2A864886F7140105B6583E83", .oid_prefix="1.2.840.113556.1.5.7000.62:0x83"}, + {.id=0x000007d1, .bin_oid="0x2A864886F71401061401", .oid_prefix="1.2.840.113556.1.6.20.1"}, + {.id=0x000007e1, .bin_oid="0x2A864886F71401061402", .oid_prefix="1.2.840.113556.1.6.20.2"}, + {.id=0x00001b86, .bin_oid="0x2A817A", .oid_prefix="1.2.250"}, + {.id=0x00001c78, .bin_oid="0x2A817A81", .oid_prefix="1.2.250:0x81"}, + {.id=0x00001c7b, .bin_oid="0x2A817A8180", .oid_prefix="1.2.250:0x8180"}, +}; + + +/** + * OID-to-ATTID mappings to be used for testing. + * An entry is marked as 'exists=true' if it exists in + * base prefixMap (_prefixmap_test_new_data) + */ +static const struct { + const char *oid; + uint32_t id; + uint32_t attid; + bool exists; +} _prefixmap_test_data[] = { + {.oid="2.5.4.0", .id=0x00000000, .attid=0x000000, .exists=true}, + {.oid="2.5.4.42", .id=0x00000000, .attid=0x00002a, .exists=true}, + {.oid="1.2.840.113556.1.2.1", .id=0x00000002, .attid=0x020001, .exists=true}, + {.oid="1.2.840.113556.1.2.13", .id=0x00000002, .attid=0x02000d, .exists=true}, + {.oid="1.2.840.113556.1.2.281", .id=0x00000002, .attid=0x020119, .exists=true}, + {.oid="1.2.840.113556.1.4.125", .id=0x00000009, .attid=0x09007d, .exists=true}, + {.oid="1.2.840.113556.1.4.146", .id=0x00000009, .attid=0x090092, .exists=true}, + {.oid="1.2.250.1", .id=0x00001b86, .attid=0x1b860001, .exists=false}, + {.oid="1.2.250.16386", .id=0x00001c78, .attid=0x1c788002, .exists=false}, + {.oid="1.2.250.2097154", .id=0x00001c7b, .attid=0x1c7b8002, .exists=false}, +}; + + +/** + * Creates dsdb_schema_prefixmap based on predefined data + */ +static WERROR _drsut_prefixmap_new(const struct drsut_pfm_oid_data *_pfm_init_data, uint32_t count, + TALLOC_CTX *mem_ctx, struct dsdb_schema_prefixmap **_pfm) +{ + uint32_t i; + struct dsdb_schema_prefixmap *pfm; + + pfm = talloc(mem_ctx, struct dsdb_schema_prefixmap); + W_ERROR_HAVE_NO_MEMORY(pfm); + + pfm->length = count; + pfm->prefixes = talloc_array(pfm, struct dsdb_schema_prefixmap_oid, pfm->length); + if (!pfm->prefixes) { + talloc_free(pfm); + return WERR_NOT_ENOUGH_MEMORY; + } + + for (i = 0; i < pfm->length; i++) { + pfm->prefixes[i].id = _pfm_init_data[i].id; + pfm->prefixes[i].bin_oid = strhex_to_data_blob(pfm, _pfm_init_data[i].bin_oid); + if (!pfm->prefixes[i].bin_oid.data) { + talloc_free(pfm); + return WERR_NOT_ENOUGH_MEMORY; + } + } + + *_pfm = pfm; + + return WERR_OK; +} + +/** + * Compares two prefixMaps for being equal - same items on same indexes + */ +static bool _torture_drs_pfm_compare_same(struct torture_context *tctx, + const struct dsdb_schema_prefixmap *pfm_left, + const struct dsdb_schema_prefixmap *pfm_right, + bool quiet) +{ + uint32_t i; + char *err_msg = NULL; + + if (pfm_left->length != pfm_right->length) { + err_msg = talloc_asprintf(tctx, "prefixMaps differ in size; left = %d, right = %d", + pfm_left->length, pfm_right->length); + goto failed; + } + + for (i = 0; i < pfm_left->length; i++) { + struct dsdb_schema_prefixmap_oid *entry_left = &pfm_left->prefixes[i]; + struct dsdb_schema_prefixmap_oid *entry_right = &pfm_right->prefixes[i]; + + if (entry_left->id != entry_right->id) { + err_msg = talloc_asprintf(tctx, "Different IDs for index=%d", i); + goto failed; + } + if (data_blob_cmp(&entry_left->bin_oid, &entry_right->bin_oid)) { + err_msg = talloc_asprintf(tctx, "Different bin_oid for index=%d", i); + goto failed; + } + } + + return true; + +failed: + if (!quiet) { + torture_comment(tctx, "_torture_drs_pfm_compare_same: %s", err_msg); + } + talloc_free(err_msg); + + return false; +} + +/* + * Tests dsdb_schema_pfm_new() + */ +static bool torture_drs_unit_pfm_new(struct torture_context *tctx, struct drsut_prefixmap_data *priv) +{ + WERROR werr; + bool bret; + TALLOC_CTX *mem_ctx; + struct dsdb_schema_prefixmap *pfm = NULL; + + mem_ctx = talloc_new(priv); + + /* create new prefix map */ + werr = dsdb_schema_pfm_new(mem_ctx, &pfm); + torture_assert_werr_ok(tctx, werr, "dsdb_schema_pfm_new() failed!"); + torture_assert(tctx, pfm != NULL, "NULL prefixMap created!"); + torture_assert(tctx, pfm->length > 0, "Empty prefixMap created!"); + torture_assert(tctx, pfm->prefixes != NULL, "No prefixes for newly created prefixMap!"); + + /* compare newly created prefixMap with template one */ + bret = _torture_drs_pfm_compare_same(tctx, priv->pfm_new, pfm, false); + + talloc_free(mem_ctx); + + return bret; +} + +/** + * Tests dsdb_schema_pfm_make_attid() using full prefixMap. + * In this test we know exactly which ATTID and prefixMap->ID + * should be returned, i.e. no prefixMap entries should be added. + */ +static bool torture_drs_unit_pfm_make_attid_full_map(struct torture_context *tctx, struct drsut_prefixmap_data *priv) +{ + WERROR werr; + uint32_t i, count; + uint32_t attid; + char *err_msg; + + count = ARRAY_SIZE(_prefixmap_test_data); + for (i = 0; i < count; i++) { + werr = dsdb_schema_pfm_make_attid(priv->pfm_full, _prefixmap_test_data[i].oid, &attid); + /* prepare error message */ + err_msg = talloc_asprintf(priv, "dsdb_schema_pfm_make_attid() failed with %s", + _prefixmap_test_data[i].oid); + torture_assert(tctx, err_msg, "Unexpected: Have no memory!"); + /* verify result and returned ATTID */ + torture_assert_werr_ok(tctx, werr, err_msg); + torture_assert_int_equal(tctx, attid, _prefixmap_test_data[i].attid, err_msg); + /* reclaim memory for prepared error message */ + talloc_free(err_msg); + } + + return true; +} + +/** + * Tests dsdb_schema_pfm_make_attid() using initially small prefixMap. + * In this test we don't know exactly which ATTID and prefixMap->ID + * should be returned, but we can verify lo-word of ATTID. + * This test verifies implementation branch when a new + * prefix should be added into prefixMap. + */ +static bool torture_drs_unit_pfm_make_attid_small_map(struct torture_context *tctx, struct drsut_prefixmap_data *priv) +{ + WERROR werr; + uint32_t i, j; + uint32_t idx; + uint32_t attid, attid_2; + char *err_msg; + struct dsdb_schema_prefixmap *pfm = NULL; + TALLOC_CTX *mem_ctx; + + mem_ctx = talloc_new(priv); + + /* create new prefix map */ + werr = dsdb_schema_pfm_new(mem_ctx, &pfm); + torture_assert_werr_ok(tctx, werr, "dsdb_schema_pfm_new() failed!"); + + /* make some ATTIDs and check result */ + for (i = 0; i < ARRAY_SIZE(_prefixmap_test_data); i++) { + werr = dsdb_schema_pfm_make_attid(pfm, _prefixmap_test_data[i].oid, &attid); + + /* prepare error message */ + err_msg = talloc_asprintf(mem_ctx, "dsdb_schema_pfm_make_attid() failed with %s", + _prefixmap_test_data[i].oid); + torture_assert(tctx, err_msg, "Unexpected: Have no memory!"); + + /* verify result and returned ATTID */ + torture_assert_werr_ok(tctx, werr, err_msg); + /* verify ATTID lo-word */ + torture_assert_int_equal(tctx, attid & 0xFFFF, _prefixmap_test_data[i].attid & 0xFFFF, err_msg); + + /* try again, this time verify for whole ATTID */ + werr = dsdb_schema_pfm_make_attid(pfm, _prefixmap_test_data[i].oid, &attid_2); + torture_assert_werr_ok(tctx, werr, err_msg); + torture_assert_int_equal(tctx, attid_2, attid, err_msg); + + /* reclaim memory for prepared error message */ + talloc_free(err_msg); + + /* check there is such an index in modified prefixMap */ + idx = (attid >> 16); + for (j = 0; j < pfm->length; j++) { + if (pfm->prefixes[j].id == idx) + break; + } + if (j >= pfm->length) { + torture_result(tctx, TORTURE_FAIL, __location__": No prefix for ATTID=0x%08X", attid); + return false; + } + + } + + talloc_free(mem_ctx); + + return true; +} + +/** + * Tests dsdb_schema_pfm_attid_from_oid() using full prefixMap. + * In this test we know exactly which ATTID and prefixMap->ID + * should be returned- dsdb_schema_pfm_attid_from_oid() should succeed. + */ +static bool torture_drs_unit_pfm_attid_from_oid_full_map(struct torture_context *tctx, + struct drsut_prefixmap_data *priv) +{ + WERROR werr; + uint32_t i, count; + uint32_t attid; + char *err_msg; + + count = ARRAY_SIZE(_prefixmap_test_data); + for (i = 0; i < count; i++) { + werr = dsdb_schema_pfm_attid_from_oid(priv->pfm_full, + _prefixmap_test_data[i].oid, + &attid); + /* prepare error message */ + err_msg = talloc_asprintf(priv, "dsdb_schema_pfm_attid_from_oid() failed with %s", + _prefixmap_test_data[i].oid); + torture_assert(tctx, err_msg, "Unexpected: Have no memory!"); + /* verify result and returned ATTID */ + torture_assert_werr_ok(tctx, werr, err_msg); + torture_assert_int_equal(tctx, attid, _prefixmap_test_data[i].attid, err_msg); + /* reclaim memory for prepared error message */ + talloc_free(err_msg); + } + + return true; +} + +/** + * Tests dsdb_schema_pfm_attid_from_oid() using base (initial) prefixMap. + * dsdb_schema_pfm_attid_from_oid() should fail when testing with OID + * that are not already in the prefixMap. + */ +static bool torture_drs_unit_pfm_attid_from_oid_base_map(struct torture_context *tctx, + struct drsut_prefixmap_data *priv) +{ + WERROR werr; + uint32_t i; + uint32_t attid; + char *err_msg; + struct dsdb_schema_prefixmap *pfm = NULL; + struct dsdb_schema_prefixmap pfm_prev; + TALLOC_CTX *mem_ctx; + + mem_ctx = talloc_new(priv); + torture_assert(tctx, mem_ctx, "Unexpected: Have no memory!"); + + /* create new prefix map */ + werr = dsdb_schema_pfm_new(mem_ctx, &pfm); + torture_assert_werr_ok(tctx, werr, "dsdb_schema_pfm_new() failed!"); + + /* keep initial pfm around for testing */ + pfm_prev = *pfm; + pfm_prev.prefixes = talloc_reference(mem_ctx, pfm->prefixes); + + /* get some ATTIDs and check result */ + for (i = 0; i < ARRAY_SIZE(_prefixmap_test_data); i++) { + werr = dsdb_schema_pfm_attid_from_oid(pfm, _prefixmap_test_data[i].oid, &attid); + + /* prepare error message */ + err_msg = talloc_asprintf(mem_ctx, + "dsdb_schema_pfm_attid_from_oid() failed for %s", + _prefixmap_test_data[i].oid); + torture_assert(tctx, err_msg, "Unexpected: Have no memory!"); + + + /* verify pfm hasn't been altered */ + if (_prefixmap_test_data[i].exists) { + /* should succeed and return valid ATTID */ + torture_assert_werr_ok(tctx, werr, err_msg); + /* verify ATTID */ + torture_assert_int_equal(tctx, + attid, _prefixmap_test_data[i].attid, + err_msg); + } else { + /* should fail */ + torture_assert_werr_equal(tctx, werr, WERR_NOT_FOUND, err_msg); + } + + /* prefixMap should never be changed */ + if (!_torture_drs_pfm_compare_same(tctx, &pfm_prev, pfm, true)) { + torture_fail(tctx, "schema->prefixmap has changed"); + } + + /* reclaim memory for prepared error message */ + talloc_free(err_msg); + } + + talloc_free(mem_ctx); + + return true; +} + +/** + * Tests dsdb_schema_pfm_oid_from_attid() using full prefixMap. + */ +static bool torture_drs_unit_pfm_oid_from_attid(struct torture_context *tctx, struct drsut_prefixmap_data *priv) +{ + WERROR werr; + uint32_t i, count; + char *err_msg; + const char *oid; + + count = ARRAY_SIZE(_prefixmap_test_data); + for (i = 0; i < count; i++) { + oid = NULL; + werr = dsdb_schema_pfm_oid_from_attid(priv->pfm_full, _prefixmap_test_data[i].attid, + priv, &oid); + /* prepare error message */ + err_msg = talloc_asprintf(priv, "dsdb_schema_pfm_oid_from_attid() failed with 0x%08X", + _prefixmap_test_data[i].attid); + torture_assert(tctx, err_msg, "Unexpected: Have no memory!"); + /* verify result and returned ATTID */ + torture_assert_werr_ok(tctx, werr, err_msg); + torture_assert(tctx, oid, "dsdb_schema_pfm_oid_from_attid() returned NULL OID!!!"); + torture_assert_str_equal(tctx, oid, _prefixmap_test_data[i].oid, err_msg); + /* reclaim memory for prepared error message */ + talloc_free(err_msg); + /* free memory for OID */ + talloc_free(discard_const(oid)); + } + + return true; +} + +/** + * Tests dsdb_schema_pfm_oid_from_attid() for handling + * correctly different type of attid values. + * See: MS-ADTS, 3.1.1.2.6 ATTRTYP + */ +static bool torture_drs_unit_pfm_oid_from_attid_check_attid(struct torture_context *tctx, + struct drsut_prefixmap_data *priv) +{ + WERROR werr; + const char *oid; + + /* Test with valid prefixMap attid */ + werr = dsdb_schema_pfm_oid_from_attid(priv->pfm_full, 0x00010001, tctx, &oid); + torture_assert_werr_ok(tctx, werr, "Testing prefixMap type attid = 0x00010001"); + + /* Test with valid attid but invalid index */ + werr = dsdb_schema_pfm_oid_from_attid(priv->pfm_full, 0x01110001, tctx, &oid); + torture_assert_werr_equal(tctx, werr, WERR_DS_NO_ATTRIBUTE_OR_VALUE, + "Testing invalid-index attid = 0x01110001"); + + /* Test with attid in msDS-IntId range */ + werr = dsdb_schema_pfm_oid_from_attid(priv->pfm_full, 0x80000000, tctx, &oid); + torture_assert_werr_equal(tctx, werr, WERR_INVALID_PARAMETER, + "Testing msDS-IntId type attid = 0x80000000"); + werr = dsdb_schema_pfm_oid_from_attid(priv->pfm_full, 0xBFFFFFFF, tctx, &oid); + torture_assert_werr_equal(tctx, werr, WERR_INVALID_PARAMETER, + "Testing msDS-IntId type attid = 0xBFFFFFFF"); + + /* Test with attid in RESERVED range */ + werr = dsdb_schema_pfm_oid_from_attid(priv->pfm_full, 0xC0000000, tctx, &oid); + torture_assert_werr_equal(tctx, werr, WERR_INVALID_PARAMETER, + "Testing RESERVED type attid = 0xC0000000"); + werr = dsdb_schema_pfm_oid_from_attid(priv->pfm_full, 0xFFFEFFFF, tctx, &oid); + torture_assert_werr_equal(tctx, werr, WERR_INVALID_PARAMETER, + "Testing RESERVED type attid = 0xFFFEFFFF"); + + /* Test with attid in INTERNAL range */ + werr = dsdb_schema_pfm_oid_from_attid(priv->pfm_full, 0xFFFF0000, tctx, &oid); + torture_assert_werr_equal(tctx, werr, WERR_INVALID_PARAMETER, + "Testing INTERNAL type attid = 0xFFFF0000"); + werr = dsdb_schema_pfm_oid_from_attid(priv->pfm_full, 0xFFFFFFFF, tctx, &oid); + torture_assert_werr_equal(tctx, werr, WERR_INVALID_PARAMETER, + "Testing INTERNAL type attid = 0xFFFFFFFF"); + + return true; +} + +/** + * Test Schema prefixMap conversions to/from drsuapi prefixMap + * representation. + */ +static bool torture_drs_unit_pfm_to_from_drsuapi(struct torture_context *tctx, struct drsut_prefixmap_data *priv) +{ + WERROR werr; + struct dsdb_schema_info *schema_info; + DATA_BLOB schema_info_blob; + struct dsdb_schema_prefixmap *pfm; + struct drsuapi_DsReplicaOIDMapping_Ctr *ctr; + TALLOC_CTX *mem_ctx; + + mem_ctx = talloc_new(tctx); + torture_assert(tctx, mem_ctx, "Unexpected: Have no memory!"); + + /* convert Schema_prefixMap to drsuapi_prefixMap */ + werr = dsdb_drsuapi_pfm_from_schema_pfm(priv->pfm_full, priv->schi_default, mem_ctx, &ctr); + torture_assert_werr_ok(tctx, werr, "dsdb_drsuapi_pfm_from_schema_pfm() failed"); + torture_assert(tctx, ctr && ctr->mappings, "drsuapi_prefixMap not constructed correctly"); + torture_assert_int_equal(tctx, ctr->num_mappings, priv->pfm_full->length + 1, + "drs_mappings count does not match"); + /* look for schema_info entry - it should be the last one */ + schema_info_blob = data_blob_const(ctr->mappings[ctr->num_mappings - 1].oid.binary_oid, + ctr->mappings[ctr->num_mappings - 1].oid.length); + werr = dsdb_schema_info_from_blob(&schema_info_blob, tctx, &schema_info); + torture_assert_werr_ok(tctx, werr, "dsdb_schema_info_from_blob failed"); + torture_assert_int_equal(tctx, schema_info->revision, priv->schi_default->revision, + "schema_info (revision) not stored correctly or not last entry"); + torture_assert(tctx, GUID_equal(&schema_info->invocation_id, &priv->schi_default->invocation_id), + "schema_info (invocation_id) not stored correctly or not last entry"); + + /* compare schema_prefixMap and drsuapi_prefixMap */ + werr = dsdb_schema_pfm_contains_drsuapi_pfm(priv->pfm_full, ctr); + torture_assert_werr_ok(tctx, werr, "dsdb_schema_pfm_contains_drsuapi_pfm() failed"); + + /* convert back drsuapi_prefixMap to schema_prefixMap */ + werr = dsdb_schema_pfm_from_drsuapi_pfm(ctr, true, mem_ctx, &pfm, &schema_info); + torture_assert_werr_ok(tctx, werr, "dsdb_schema_pfm_from_drsuapi_pfm() failed"); + torture_assert_int_equal(tctx, schema_info->revision, priv->schi_default->revision, + "Fetched schema_info is different (revision)"); + torture_assert(tctx, GUID_equal(&schema_info->invocation_id, &priv->schi_default->invocation_id), + "Fetched schema_info is different (invocation_id)"); + + /* compare against the original */ + if (!_torture_drs_pfm_compare_same(tctx, priv->pfm_full, pfm, true)) { + talloc_free(mem_ctx); + return false; + } + + /* test conversion with partial drsuapi_prefixMap */ + ctr->num_mappings--; + werr = dsdb_schema_pfm_from_drsuapi_pfm(ctr, false, mem_ctx, &pfm, NULL); + torture_assert_werr_ok(tctx, werr, "dsdb_schema_pfm_from_drsuapi_pfm() failed"); + /* compare against the original */ + if (!_torture_drs_pfm_compare_same(tctx, priv->pfm_full, pfm, false)) { + talloc_free(mem_ctx); + return false; + } + + talloc_free(mem_ctx); + return true; +} + + +/** + * Test Schema prefixMap conversions to/from ldb_val + * blob representation. + */ +static bool torture_drs_unit_pfm_to_from_ldb_val(struct torture_context *tctx, struct drsut_prefixmap_data *priv) +{ + WERROR werr; + struct dsdb_schema *schema; + struct ldb_val pfm_ldb_val; + struct ldb_val schema_info_ldb_val; + TALLOC_CTX *mem_ctx; + + mem_ctx = talloc_new(tctx); + torture_assert(tctx, mem_ctx, "Unexpected: Have no memory!"); + + schema = dsdb_new_schema(mem_ctx); + torture_assert(tctx, schema, "Unexpected: failed to allocate schema object"); + + /* set priv->pfm_full as prefixMap for new schema object */ + schema->prefixmap = priv->pfm_full; + schema->schema_info = priv->schi_default; + + /* convert schema_prefixMap to ldb_val blob */ + werr = dsdb_get_oid_mappings_ldb(schema, mem_ctx, &pfm_ldb_val, &schema_info_ldb_val); + torture_assert_werr_ok(tctx, werr, "dsdb_get_oid_mappings_ldb() failed"); + torture_assert(tctx, pfm_ldb_val.data && pfm_ldb_val.length, + "pfm_ldb_val not constructed correctly"); + torture_assert(tctx, schema_info_ldb_val.data && schema_info_ldb_val.length, + "schema_info_ldb_val not constructed correctly"); + + /* convert pfm_ldb_val back to schema_prefixMap */ + schema->prefixmap = NULL; + schema->schema_info = NULL; + werr = dsdb_load_oid_mappings_ldb(schema, &pfm_ldb_val, &schema_info_ldb_val); + torture_assert_werr_ok(tctx, werr, "dsdb_load_oid_mappings_ldb() failed"); + /* compare against the original */ + if (!_torture_drs_pfm_compare_same(tctx, schema->prefixmap, priv->pfm_full, false)) { + talloc_free(mem_ctx); + return false; + } + torture_assert_int_equal(tctx, schema->schema_info->revision, priv->schi_default->revision, + "Fetched schema_info is different (revision)"); + torture_assert(tctx, GUID_equal(&schema->schema_info->invocation_id, &priv->schi_default->invocation_id), + "Fetched schema_info is different (invocation_id)"); + + talloc_free(mem_ctx); + return true; +} + +/** + * Test read/write in ldb implementation + */ +static bool torture_drs_unit_pfm_read_write_ldb(struct torture_context *tctx, struct drsut_prefixmap_data *priv) +{ + WERROR werr; + struct dsdb_schema *schema; + struct dsdb_schema_prefixmap *pfm; + TALLOC_CTX *mem_ctx; + + mem_ctx = talloc_new(tctx); + torture_assert(tctx, mem_ctx, "Unexpected: Have no memory!"); + + /* makeup a dsdb_schema to test with */ + schema = dsdb_new_schema(mem_ctx); + torture_assert(tctx, schema, "Unexpected: failed to allocate schema object"); + /* set priv->pfm_full as prefixMap for new schema object */ + schema->prefixmap = priv->pfm_full; + schema->schema_info = priv->schi_default; + + /* write prfixMap to ldb */ + werr = dsdb_write_prefixes_from_schema_to_ldb(mem_ctx, priv->ldb_ctx, schema); + torture_assert_werr_ok(tctx, werr, "dsdb_write_prefixes_from_schema_to_ldb() failed"); + + /* read from ldb what we have written */ + werr = dsdb_read_prefixes_from_ldb(priv->ldb_ctx, mem_ctx, &pfm); + torture_assert_werr_ok(tctx, werr, "dsdb_read_prefixes_from_ldb() failed"); + + /* compare data written/read */ + if (!_torture_drs_pfm_compare_same(tctx, schema->prefixmap, priv->pfm_full, false)) { + torture_fail(tctx, "prefixMap read/write in LDB is not consistent"); + } + + talloc_free(mem_ctx); + + return true; +} + +/** + * Test dsdb_create_prefix_mapping + */ +static bool torture_drs_unit_dsdb_create_prefix_mapping(struct torture_context *tctx, struct drsut_prefixmap_data *priv) +{ + WERROR werr; + uint32_t i; + struct dsdb_schema *schema; + TALLOC_CTX *mem_ctx; + struct dsdb_schema_prefixmap *pfm_ldb = NULL; + + mem_ctx = talloc_new(tctx); + torture_assert(tctx, mem_ctx, "Unexpected: Have no memory!"); + + /* makeup a dsdb_schema to test with */ + schema = dsdb_new_schema(mem_ctx); + torture_assert(tctx, schema, "Unexpected: failed to allocate schema object"); + /* set priv->pfm_full as prefixMap for new schema object */ + schema->schema_info = priv->schi_default; + werr = _drsut_prefixmap_new(_prefixmap_test_new_data, ARRAY_SIZE(_prefixmap_test_new_data), + schema, &schema->prefixmap); + torture_assert_werr_ok(tctx, werr, "_drsut_prefixmap_new() failed"); + /* write prfixMap to ldb */ + werr = dsdb_write_prefixes_from_schema_to_ldb(mem_ctx, priv->ldb_ctx, schema); + torture_assert_werr_ok(tctx, werr, "dsdb_write_prefixes_from_schema_to_ldb() failed"); + + /* read from ldb what we have written */ + werr = dsdb_read_prefixes_from_ldb(priv->ldb_ctx, mem_ctx, &pfm_ldb); + torture_assert_werr_ok(tctx, werr, "dsdb_read_prefixes_from_ldb() failed"); + /* compare data written/read */ + if (!_torture_drs_pfm_compare_same(tctx, schema->prefixmap, pfm_ldb, true)) { + torture_fail(tctx, "pfm in LDB is different"); + } + TALLOC_FREE(pfm_ldb); + + for (i = 0; i < ARRAY_SIZE(_prefixmap_test_data); i++) { + struct dsdb_schema_prefixmap *pfm_prev; + struct dsdb_schema_prefixmap *pfm_new; + + pfm_prev = schema->prefixmap; + + pfm_new = dsdb_schema_pfm_copy_shallow(schema, pfm_prev); + torture_assert(tctx, pfm_new != NULL, "dsdb_schema_pfm_copy_shallow() failed"); + + if (!_prefixmap_test_data[i].exists) { + uint32_t attid; + + werr = dsdb_schema_pfm_make_attid(pfm_new, + _prefixmap_test_data[i].oid, + &attid); + torture_assert_werr_ok(tctx, werr, "dsdb_schema_pfm_make_attid() failed"); + } + + /* call dsdb_create_prefix_mapping() and check result accordingly */ + werr = dsdb_create_prefix_mapping(priv->ldb_ctx, schema, _prefixmap_test_data[i].oid); + torture_assert_werr_ok(tctx, werr, "dsdb_create_prefix_mapping() failed"); + + /* + * The prefix should not change, only on reload + */ + torture_assert(tctx, pfm_prev == schema->prefixmap, + "schema->prefixmap has been reallocated!"); + if (!_torture_drs_pfm_compare_same(tctx, pfm_prev, schema->prefixmap, true)) { + torture_fail(tctx, "schema->prefixmap has changed"); + } + + /* read from ldb what we have written */ + werr = dsdb_read_prefixes_from_ldb(priv->ldb_ctx, mem_ctx, &pfm_ldb); + torture_assert_werr_ok(tctx, werr, "dsdb_read_prefixes_from_ldb() failed"); + /* compare data written/read */ + if (!_torture_drs_pfm_compare_same(tctx, pfm_new, pfm_ldb, true)) { + torture_fail(tctx, talloc_asprintf(tctx, "%u: pfm in LDB is different", i)); + } + /* free mem for pfm read from LDB */ + TALLOC_FREE(pfm_ldb); + + /* prepare for the next round */ + schema->prefixmap = pfm_new; + } + + talloc_free(mem_ctx); + + return true; +} + +/** + * Prepare temporary LDB and opens it + */ +static bool torture_drs_unit_ldb_setup(struct torture_context *tctx, struct drsut_prefixmap_data *priv) +{ + int ldb_err; + char *ldb_url; + bool bret = true; + TALLOC_CTX* mem_ctx; + char *tempdir; + NTSTATUS status; + + mem_ctx = talloc_new(priv); + + status = torture_temp_dir(tctx, "drs_", &tempdir); + torture_assert_ntstatus_ok(tctx, status, "creating temp dir"); + + ldb_url = talloc_asprintf(priv, "%s/drs_test.ldb", tempdir); + + /* create LDB */ + priv->ldb_ctx = ldb_init(priv, tctx->ev); + ldb_err = ldb_connect(priv->ldb_ctx, ldb_url, 0, NULL); + torture_assert_int_equal_goto(tctx, ldb_err, LDB_SUCCESS, bret, DONE, "ldb_connect() failed"); + + /* set some schemaNamingContext */ + ldb_err = ldb_set_opaque(priv->ldb_ctx, + "schemaNamingContext", + ldb_dn_new(priv->ldb_ctx, priv->ldb_ctx, "CN=Schema,CN=Config")); + torture_assert_int_equal_goto(tctx, ldb_err, LDB_SUCCESS, bret, DONE, "ldb_set_opaque() failed"); + + /* add prefixMap attribute so tested layer could work properly */ + { + struct ldb_message *msg = ldb_msg_new(mem_ctx); + msg->dn = ldb_get_schema_basedn(priv->ldb_ctx); + ldb_err = ldb_msg_add_string(msg, "prefixMap", "prefixMap"); + torture_assert_int_equal_goto(tctx, ldb_err, LDB_SUCCESS, bret, DONE, + "ldb_msg_add_string() failed"); + + ldb_err = ldb_add(priv->ldb_ctx, msg); + torture_assert_int_equal_goto(tctx, ldb_err, LDB_SUCCESS, bret, DONE, "ldb_add() failed"); + } + +DONE: + talloc_free(mem_ctx); + return bret; +} + +/* + * Setup/Teardown for test case + */ +static bool torture_drs_unit_prefixmap_setup(struct torture_context *tctx, struct drsut_prefixmap_data **_priv) +{ + WERROR werr; + DATA_BLOB blob; + struct drsut_prefixmap_data *priv; + + priv = *_priv = talloc_zero(tctx, struct drsut_prefixmap_data); + torture_assert(tctx, priv != NULL, "Not enough memory"); + + werr = _drsut_prefixmap_new(_prefixmap_test_new_data, ARRAY_SIZE(_prefixmap_test_new_data), + tctx, &priv->pfm_new); + torture_assert_werr_ok(tctx, werr, "failed to create pfm_new"); + + werr = _drsut_prefixmap_new(_prefixmap_full_map_data, ARRAY_SIZE(_prefixmap_full_map_data), + tctx, &priv->pfm_full); + torture_assert_werr_ok(tctx, werr, "failed to create pfm_test"); + + torture_assert(tctx, drsut_schemainfo_new(tctx, &priv->schi_default), + "drsut_schemainfo_new() failed"); + + werr = dsdb_blob_from_schema_info(priv->schi_default, priv, &blob); + torture_assert_werr_ok(tctx, werr, "dsdb_blob_from_schema_info() failed"); + + /* create temporary LDB and populate with data */ + if (!torture_drs_unit_ldb_setup(tctx, priv)) { + return false; + } + + return true; +} + +static bool torture_drs_unit_prefixmap_teardown(struct torture_context *tctx, struct drsut_prefixmap_data *priv) +{ + talloc_free(priv); + + return true; +} + +/** + * Test case initialization for + * drs.unit.prefixMap + */ +struct torture_tcase * torture_drs_unit_prefixmap(struct torture_suite *suite) +{ + typedef bool (*pfn_setup)(struct torture_context *, void **); + typedef bool (*pfn_teardown)(struct torture_context *, void *); + typedef bool (*pfn_run)(struct torture_context *, void *); + + struct torture_tcase * tc = torture_suite_add_tcase(suite, "prefixMap"); + + torture_tcase_set_fixture(tc, + (pfn_setup)torture_drs_unit_prefixmap_setup, + (pfn_teardown)torture_drs_unit_prefixmap_teardown); + + tc->description = talloc_strdup(tc, "Unit tests for DRSUAPI::prefixMap implementation"); + + torture_tcase_add_simple_test(tc, "new", (pfn_run)torture_drs_unit_pfm_new); + + torture_tcase_add_simple_test(tc, "make_attid_full_map", (pfn_run)torture_drs_unit_pfm_make_attid_full_map); + torture_tcase_add_simple_test(tc, "make_attid_small_map", (pfn_run)torture_drs_unit_pfm_make_attid_small_map); + + torture_tcase_add_simple_test(tc, "attid_from_oid_full_map", + (pfn_run)torture_drs_unit_pfm_attid_from_oid_full_map); + torture_tcase_add_simple_test(tc, "attid_from_oid_empty_map", + (pfn_run)torture_drs_unit_pfm_attid_from_oid_base_map); + + torture_tcase_add_simple_test(tc, "oid_from_attid_full_map", (pfn_run)torture_drs_unit_pfm_oid_from_attid); + torture_tcase_add_simple_test(tc, "oid_from_attid_check_attid", + (pfn_run)torture_drs_unit_pfm_oid_from_attid_check_attid); + + torture_tcase_add_simple_test(tc, "pfm_to_from_drsuapi", (pfn_run)torture_drs_unit_pfm_to_from_drsuapi); + + torture_tcase_add_simple_test(tc, "pfm_to_from_ldb_val", (pfn_run)torture_drs_unit_pfm_to_from_ldb_val); + + torture_tcase_add_simple_test(tc, "pfm_read_write_ldb", (pfn_run)torture_drs_unit_pfm_read_write_ldb); + + torture_tcase_add_simple_test(tc, "dsdb_create_prefix_mapping", (pfn_run)torture_drs_unit_dsdb_create_prefix_mapping); + + return tc; +} diff --git a/source4/torture/drs/unit/schemainfo_tests.c b/source4/torture/drs/unit/schemainfo_tests.c new file mode 100644 index 0000000..4b4cca6 --- /dev/null +++ b/source4/torture/drs/unit/schemainfo_tests.c @@ -0,0 +1,740 @@ +/* + Unix SMB/CIFS implementation. + + DRSUAPI schemaInfo unit tests + + 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 "system/filesys.h" +#include "torture/smbtorture.h" +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "ldb_wrap.h" +#include <ldb_module.h> +#include "torture/rpc/drsuapi.h" +#include "librpc/ndr/libndr.h" +#include "param/param.h" +#include "torture/drs/proto.h" +#include "torture/drs/proto.h" + + +/** + * schemaInfo to init ldb context with + * Rev: 0 + * GUID: 00000000-0000-0000-0000-000000000000 + */ +#define SCHEMA_INFO_INIT_STR "FF0000000000000000000000000000000000000000" + +/** + * Default schema_info string to be used for testing + * Rev: 01 + * GUID: 071c82fd-45c7-4351-a3db-51f75a630a7f + */ +#define SCHEMA_INFO_DEFAULT_STR "FF00000001FD821C07C7455143A3DB51F75A630A7F" + +/** + * Schema info data to test with + */ +struct schemainfo_data { + DATA_BLOB ndr_blob; + struct dsdb_schema_info schi; + WERROR werr_expected; + bool test_both_ways; +}; + +/** + * Schema info test data in human-readable format (... kind of) + */ +static const struct { + const char *schema_info_str; + uint32_t revision; + const char *guid_str; + WERROR werr_expected; + bool test_both_ways; +} _schemainfo_test_data[] = { + { + .schema_info_str = "FF0000000000000000000000000000000000000000", + .revision = 0, + .guid_str = "00000000-0000-0000-0000-000000000000", + .werr_expected = WERR_OK, + .test_both_ways = true + }, + { + .schema_info_str = "FF00000001FD821C07C7455143A3DB51F75A630A7F", + .revision = 1, + .guid_str = "071c82fd-45c7-4351-a3db-51f75a630a7f", + .werr_expected = WERR_OK, + .test_both_ways = true + }, + { + .schema_info_str = "FFFFFFFFFFFD821C07C7455143A3DB51F75A630A7F", + .revision = 0xFFFFFFFF, + .guid_str = "071c82fd-45c7-4351-a3db-51f75a630a7f", + .werr_expected = WERR_OK, + .test_both_ways = true + }, + { /* len == 21 */ + .schema_info_str = "FF00000001FD821C07C7455143A3DB51F75A630A7F00", + .revision = 1, + .guid_str = "071c82fd-45c7-4351-a3db-51f75a630a7f", + .werr_expected = WERR_INVALID_PARAMETER, + .test_both_ways = false + }, + { /* marker == FF */ + .schema_info_str = "AA00000001FD821C07C7455143A3DB51F75A630A7F", + .revision = 1, + .guid_str = "071c82fd-45c7-4351-a3db-51f75a630a7f", + .werr_expected = WERR_INVALID_PARAMETER, + .test_both_ways = false + } +}; + +/** + * Private data to be shared among all test in Test case + */ +struct drsut_schemainfo_data { + struct ldb_context *ldb; + struct ldb_module *ldb_module; + struct dsdb_schema *schema; + + /* Initial schemaInfo set in ldb to test with */ + struct dsdb_schema_info *schema_info; + + uint32_t test_data_count; + struct schemainfo_data *test_data; +}; + +/** + * torture macro to assert for equal dsdb_schema_info's + */ +#define torture_assert_schema_info_equal(torture_ctx,got,expected,cmt)\ + do { const struct dsdb_schema_info *__got = (got), *__expected = (expected); \ + if (__got->revision != __expected->revision) { \ + torture_result(torture_ctx, TORTURE_FAIL, \ + __location__": "#got".revision %d did not match "#expected".revision %d: %s", \ + (int)__got->revision, (int)__expected->revision, cmt); \ + return false; \ + } \ + if (!GUID_equal(&__got->invocation_id, &__expected->invocation_id)) { \ + torture_result(torture_ctx, TORTURE_FAIL, \ + __location__": "#got".invocation_id did not match "#expected".invocation_id: %s", cmt); \ + return false; \ + } \ + } while(0) + +/* + * forward declaration for internal functions + */ +static bool _drsut_ldb_schema_info_reset(struct torture_context *tctx, + struct ldb_context *ldb, + const char *schema_info_str, + bool in_setup); + + +/** + * Creates dsdb_schema_info object based on NDR data + * passed as hex string + */ +static bool _drsut_schemainfo_new(struct torture_context *tctx, + const char *schema_info_str, struct dsdb_schema_info **_si) +{ + WERROR werr; + DATA_BLOB blob; + + blob = strhex_to_data_blob(tctx, schema_info_str); + if (!blob.data) { + torture_comment(tctx, "Not enough memory!\n"); + return false; + } + + werr = dsdb_schema_info_from_blob(&blob, tctx, _si); + if (!W_ERROR_IS_OK(werr)) { + torture_comment(tctx, + "Failed to create dsdb_schema_info object for %s: %s", + schema_info_str, + win_errstr(werr)); + return false; + } + + data_blob_free(&blob); + + return true; +} + +/** + * Creates dsdb_schema_info object based on predefined data + * Function is public as it is intended to be used by other + * tests (e.g. prefixMap tests) + */ +bool drsut_schemainfo_new(struct torture_context *tctx, struct dsdb_schema_info **_si) +{ + return _drsut_schemainfo_new(tctx, SCHEMA_INFO_DEFAULT_STR, _si); +} + + +/* + * Tests dsdb_schema_info_new() and dsdb_schema_info_blob_new() + */ +static bool test_dsdb_schema_info_new(struct torture_context *tctx, + struct drsut_schemainfo_data *priv) +{ + WERROR werr; + DATA_BLOB ndr_blob; + DATA_BLOB ndr_blob_expected; + struct dsdb_schema_info *schi; + TALLOC_CTX *mem_ctx; + + mem_ctx = talloc_new(priv); + torture_assert(tctx, mem_ctx, "Not enough memory!"); + ndr_blob_expected = strhex_to_data_blob(mem_ctx, SCHEMA_INFO_INIT_STR); + torture_assert(tctx, ndr_blob_expected.data, "Not enough memory!"); + + werr = dsdb_schema_info_new(mem_ctx, &schi); + torture_assert_werr_ok(tctx, werr, "dsdb_schema_info_new() failed"); + torture_assert_int_equal(tctx, schi->revision, 0, + "dsdb_schema_info_new() creates schemaInfo with invalid revision"); + torture_assert(tctx, GUID_all_zero(&schi->invocation_id), + "dsdb_schema_info_new() creates schemaInfo with not ZERO GUID"); + + werr = dsdb_schema_info_blob_new(mem_ctx, &ndr_blob); + torture_assert_werr_ok(tctx, werr, "dsdb_schema_info_blob_new() failed"); + torture_assert_data_blob_equal(tctx, ndr_blob, ndr_blob_expected, + "dsdb_schema_info_blob_new() returned invalid blob"); + + talloc_free(mem_ctx); + return true; +} + +/* + * Tests dsdb_schema_info_from_blob() + */ +static bool test_dsdb_schema_info_from_blob(struct torture_context *tctx, + struct drsut_schemainfo_data *priv) +{ + uint32_t i; + WERROR werr; + char *msg; + struct dsdb_schema_info *schema_info; + TALLOC_CTX *mem_ctx; + + mem_ctx = talloc_new(priv); + torture_assert(tctx, mem_ctx, "Not enough memory!"); + + for (i = 0; i < priv->test_data_count; i++) { + struct schemainfo_data *data = &priv->test_data[i]; + + msg = talloc_asprintf(tctx, "dsdb_schema_info_from_blob() [%d]-[%s]", + i, _schemainfo_test_data[i].schema_info_str); + + werr = dsdb_schema_info_from_blob(&data->ndr_blob, mem_ctx, &schema_info); + torture_assert_werr_equal(tctx, werr, data->werr_expected, msg); + + /* test returned data */ + if (W_ERROR_IS_OK(werr)) { + torture_assert_schema_info_equal(tctx, + schema_info, &data->schi, + "after dsdb_schema_info_from_blob() call"); + } + } + + talloc_free(mem_ctx); + + return true; +} + +/* + * Tests dsdb_blob_from_schema_info() + */ +static bool test_dsdb_blob_from_schema_info(struct torture_context *tctx, + struct drsut_schemainfo_data *priv) +{ + uint32_t i; + WERROR werr; + char *msg; + DATA_BLOB ndr_blob; + TALLOC_CTX *mem_ctx; + + mem_ctx = talloc_new(priv); + torture_assert(tctx, mem_ctx, "Not enough memory!"); + + for (i = 0; i < priv->test_data_count; i++) { + struct schemainfo_data *data = &priv->test_data[i]; + + /* not all test are valid reverse type of conversion */ + if (!data->test_both_ways) { + continue; + } + + msg = talloc_asprintf(tctx, "dsdb_blob_from_schema_info() [%d]-[%s]", + i, _schemainfo_test_data[i].schema_info_str); + + werr = dsdb_blob_from_schema_info(&data->schi, mem_ctx, &ndr_blob); + torture_assert_werr_equal(tctx, werr, data->werr_expected, msg); + + /* test returned data */ + if (W_ERROR_IS_OK(werr)) { + torture_assert_data_blob_equal(tctx, + ndr_blob, data->ndr_blob, + "dsdb_blob_from_schema_info()"); + } + } + + talloc_free(mem_ctx); + + return true; +} + +static bool test_dsdb_schema_info_cmp(struct torture_context *tctx, + struct drsut_schemainfo_data *priv) +{ + DATA_BLOB blob; + struct drsuapi_DsReplicaOIDMapping_Ctr *ctr; + struct dsdb_schema_info schema_info; + + ctr = talloc_zero(priv, struct drsuapi_DsReplicaOIDMapping_Ctr); + torture_assert(tctx, ctr, "Not enough memory!"); + + /* not enough elements */ + torture_assert_werr_equal(tctx, + dsdb_schema_info_cmp(priv->schema, ctr), + WERR_INVALID_PARAMETER, + "dsdb_schema_info_cmp(): unexpected result"); + + /* an empty element for schemaInfo */ + ctr->num_mappings = 1; + ctr->mappings = talloc_zero_array(ctr, struct drsuapi_DsReplicaOIDMapping, 1); + torture_assert(tctx, ctr->mappings, "Not enough memory!"); + torture_assert_werr_equal(tctx, + dsdb_schema_info_cmp(priv->schema, ctr), + WERR_INVALID_PARAMETER, + "dsdb_schema_info_cmp(): unexpected result"); + + /* test with invalid schemaInfo - length != 21 */ + blob = strhex_to_data_blob(ctr, "FF00000001FD821C07C7455143A3DB51F75A630A7F00"); + torture_assert(tctx, blob.data, "Not enough memory!"); + ctr->mappings[0].oid.length = blob.length; + ctr->mappings[0].oid.binary_oid = blob.data; + torture_assert_werr_equal(tctx, + dsdb_schema_info_cmp(priv->schema, ctr), + WERR_INVALID_PARAMETER, + "dsdb_schema_info_cmp(): unexpected result"); + + /* test with invalid schemaInfo - marker != 0xFF */ + blob = strhex_to_data_blob(ctr, "AA00000001FD821C07C7455143A3DB51F75A630A7F"); + torture_assert(tctx, blob.data, "Not enough memory!"); + ctr->mappings[0].oid.length = blob.length; + ctr->mappings[0].oid.binary_oid = blob.data; + torture_assert_werr_equal(tctx, + dsdb_schema_info_cmp(priv->schema, ctr), + WERR_INVALID_PARAMETER, + "dsdb_schema_info_cmp(): unexpected result"); + + /* test with valid schemaInfo, but older one should be ok */ + blob = strhex_to_data_blob(ctr, "FF0000000000000000000000000000000000000000"); + torture_assert(tctx, blob.data, "Not enough memory!"); + ctr->mappings[0].oid.length = blob.length; + ctr->mappings[0].oid.binary_oid = blob.data; + torture_assert_werr_equal(tctx, + dsdb_schema_info_cmp(priv->schema, ctr), + WERR_OK, + "dsdb_schema_info_cmp(): unexpected result"); + + /* test with correct schemaInfo, but invalid ATTID */ + schema_info = *priv->schema->schema_info; + torture_assert_werr_ok(tctx, + dsdb_blob_from_schema_info(&schema_info, tctx, &blob), + "dsdb_blob_from_schema_info() failed"); + ctr->mappings[0].id_prefix = 1; + ctr->mappings[0].oid.length = blob.length; + ctr->mappings[0].oid.binary_oid = blob.data; + torture_assert_werr_equal(tctx, + dsdb_schema_info_cmp(priv->schema, ctr), + WERR_INVALID_PARAMETER, + "dsdb_schema_info_cmp(): unexpected result"); + + /* test with valid schemaInfo */ + ctr->mappings[0].id_prefix = 0; + torture_assert_werr_ok(tctx, + dsdb_schema_info_cmp(priv->schema, ctr), + "dsdb_schema_info_cmp(): unexpected result"); + + /* test with valid schemaInfo, but older revision */ + schema_info = *priv->schema->schema_info; + schema_info.revision -= 1; + torture_assert_werr_ok(tctx, + dsdb_blob_from_schema_info(&schema_info, tctx, &blob), + "dsdb_blob_from_schema_info() failed"); + ctr->mappings[0].oid.length = blob.length; + ctr->mappings[0].oid.binary_oid = blob.data; + torture_assert_werr_equal(tctx, + dsdb_schema_info_cmp(priv->schema, ctr), + WERR_OK, + "dsdb_schema_info_cmp(): unexpected result"); + + /* test with valid schemaInfo, but newer revision */ + schema_info = *priv->schema->schema_info; + schema_info.revision += 1; + torture_assert_werr_ok(tctx, + dsdb_blob_from_schema_info(&schema_info, tctx, &blob), + "dsdb_blob_from_schema_info() failed"); + ctr->mappings[0].oid.length = blob.length; + ctr->mappings[0].oid.binary_oid = blob.data; + torture_assert_werr_equal(tctx, + dsdb_schema_info_cmp(priv->schema, ctr), + WERR_DS_DRA_SCHEMA_MISMATCH, + "dsdb_schema_info_cmp(): unexpected result"); + + /* test with valid schemaInfo, but newer revision and other invocationId */ + schema_info = *priv->schema->schema_info; + schema_info.revision += 1; + schema_info.invocation_id.time_mid += 1; + torture_assert_werr_ok(tctx, + dsdb_blob_from_schema_info(&schema_info, tctx, &blob), + "dsdb_blob_from_schema_info() failed"); + ctr->mappings[0].oid.length = blob.length; + ctr->mappings[0].oid.binary_oid = blob.data; + torture_assert_werr_equal(tctx, + dsdb_schema_info_cmp(priv->schema, ctr), + WERR_DS_DRA_SCHEMA_MISMATCH, + "dsdb_schema_info_cmp(): unexpected result"); + + /* test with valid schemaInfo, but older revision and other invocationId */ + schema_info = *priv->schema->schema_info; + schema_info.revision -= 1; + schema_info.invocation_id.time_mid += 1; + torture_assert_werr_ok(tctx, + dsdb_blob_from_schema_info(&schema_info, tctx, &blob), + "dsdb_blob_from_schema_info() failed"); + ctr->mappings[0].oid.length = blob.length; + ctr->mappings[0].oid.binary_oid = blob.data; + torture_assert_werr_equal(tctx, + dsdb_schema_info_cmp(priv->schema, ctr), + WERR_OK, + "dsdb_schema_info_cmp(): unexpected result"); + + /* test with valid schemaInfo, but same revision and other invocationId */ + schema_info = *priv->schema->schema_info; + schema_info.invocation_id.time_mid += 1; + torture_assert_werr_ok(tctx, + dsdb_blob_from_schema_info(&schema_info, tctx, &blob), + "dsdb_blob_from_schema_info() failed"); + ctr->mappings[0].oid.length = blob.length; + ctr->mappings[0].oid.binary_oid = blob.data; + torture_assert_werr_equal(tctx, + dsdb_schema_info_cmp(priv->schema, ctr), + WERR_DS_DRA_SCHEMA_CONFLICT, + "dsdb_schema_info_cmp(): unexpected result"); + + talloc_free(ctr); + return true; +} + +/* + * Tests dsdb_module_schema_info_blob_read() + * and dsdb_module_schema_info_blob_write() + */ +static bool test_dsdb_module_schema_info_blob_rw(struct torture_context *tctx, + struct drsut_schemainfo_data *priv) +{ + int ldb_err; + DATA_BLOB blob_write; + DATA_BLOB blob_read; + + /* reset schmeInfo to know value */ + torture_assert(tctx, + _drsut_ldb_schema_info_reset(tctx, priv->ldb, SCHEMA_INFO_INIT_STR, false), + "_drsut_ldb_schema_info_reset() failed"); + + /* write tests' default schemaInfo */ + blob_write = strhex_to_data_blob(priv, SCHEMA_INFO_DEFAULT_STR); + torture_assert(tctx, blob_write.data, "Not enough memory!"); + + ldb_err = dsdb_module_schema_info_blob_write(priv->ldb_module, + DSDB_FLAG_TOP_MODULE, + &blob_write, NULL); + torture_assert_int_equal(tctx, ldb_err, LDB_SUCCESS, "dsdb_module_schema_info_blob_write() failed"); + + ldb_err = dsdb_module_schema_info_blob_read(priv->ldb_module, DSDB_FLAG_TOP_MODULE, + priv, &blob_read, NULL); + torture_assert_int_equal(tctx, ldb_err, LDB_SUCCESS, "dsdb_module_schema_info_blob_read() failed"); + + /* check if we get what we wrote */ + torture_assert_data_blob_equal(tctx, blob_read, blob_write, + "Write/Read of schemeInfo blob failed"); + + return true; +} + +/* + * Tests dsdb_schema_update_schema_info() + */ +static bool test_dsdb_module_schema_info_update(struct torture_context *tctx, + struct drsut_schemainfo_data *priv) +{ + int ldb_err; + WERROR werr; + DATA_BLOB blob; + struct dsdb_schema_info *schema_info; + + /* reset schmeInfo to know value */ + torture_assert(tctx, + _drsut_ldb_schema_info_reset(tctx, priv->ldb, SCHEMA_INFO_INIT_STR, false), + "_drsut_ldb_schema_info_reset() failed"); + + ldb_err = dsdb_module_schema_info_update(priv->ldb_module, + priv->schema, + DSDB_FLAG_TOP_MODULE | DSDB_FLAG_AS_SYSTEM, NULL); + torture_assert_int_equal(tctx, ldb_err, LDB_SUCCESS, "dsdb_module_schema_info_update() failed"); + + /* get updated schemaInfo */ + ldb_err = dsdb_module_schema_info_blob_read(priv->ldb_module, DSDB_FLAG_TOP_MODULE, + priv, &blob, NULL); + torture_assert_int_equal(tctx, ldb_err, LDB_SUCCESS, "dsdb_module_schema_info_blob_read() failed"); + + werr = dsdb_schema_info_from_blob(&blob, priv, &schema_info); + torture_assert_werr_ok(tctx, werr, "dsdb_schema_info_from_blob() failed"); + + /* check against default schema_info */ + torture_assert_schema_info_equal(tctx, schema_info, priv->schema_info, + "schemaInfo attribute no updated correctly"); + + return true; +} + + +/** + * Reset schemaInfo record to know value + */ +static bool _drsut_ldb_schema_info_reset(struct torture_context *tctx, + struct ldb_context *ldb, + const char *schema_info_str, + bool in_setup) +{ + bool bret = true; + int ldb_err; + DATA_BLOB blob; + struct ldb_message *msg; + TALLOC_CTX *mem_ctx = talloc_new(tctx); + + blob = strhex_to_data_blob(mem_ctx, schema_info_str); + torture_assert_goto(tctx, blob.data, bret, DONE, "Not enough memory!"); + + msg = ldb_msg_new(mem_ctx); + torture_assert_goto(tctx, msg, bret, DONE, "Not enough memory!"); + + msg->dn = ldb_get_schema_basedn(ldb); + ldb_err = ldb_msg_add_value(msg, "schemaInfo", &blob, NULL); + torture_assert_int_equal_goto(tctx, ldb_err, LDB_SUCCESS, bret, DONE, + "ldb_msg_add_value() failed"); + + if (in_setup) { + ldb_err = ldb_add(ldb, msg); + } else { + ldb_err = dsdb_replace(ldb, msg, DSDB_MODIFY_PERMISSIVE); + } + torture_assert_int_equal_goto(tctx, ldb_err, LDB_SUCCESS, bret, DONE, + "dsdb_replace() failed"); + +DONE: + talloc_free(mem_ctx); + return bret; +} + +/** + * Prepare temporary LDB and opens it + */ +static bool _drsut_ldb_setup(struct torture_context *tctx, struct drsut_schemainfo_data *priv) +{ + int ldb_err; + char *ldb_url; + bool bret = true; + char *tempdir = NULL; + NTSTATUS status; + TALLOC_CTX* mem_ctx; + + mem_ctx = talloc_new(priv); + torture_assert(tctx, mem_ctx, "Not enough memory!"); + + status = torture_temp_dir(tctx, "drs_", &tempdir); + torture_assert_ntstatus_ok_goto(tctx, status, bret, DONE, "creating temp dir"); + + ldb_url = talloc_asprintf(priv, "%s/drs_schemainfo.ldb", tempdir); + torture_assert_goto(tctx, ldb_url, bret, DONE, "Not enough memory!"); + + /* create LDB */ + priv->ldb = ldb_wrap_connect(priv, tctx->ev, tctx->lp_ctx, + ldb_url, NULL, NULL, 0); + torture_assert_goto(tctx, priv->ldb, bret, DONE, "ldb_wrap_connect() failed"); + + /* set some schemaNamingContext */ + ldb_err = ldb_set_opaque(priv->ldb, + "schemaNamingContext", + ldb_dn_new(priv->ldb, priv->ldb, "CN=Schema,CN=Config")); + torture_assert_int_equal_goto(tctx, ldb_err, LDB_SUCCESS, bret, DONE, + "ldb_set_opaque() failed"); + + /* add schemaInfo attribute so tested layer could work properly */ + torture_assert_goto(tctx, + _drsut_ldb_schema_info_reset(tctx, priv->ldb, SCHEMA_INFO_INIT_STR, true), + bret, DONE, + "_drsut_ldb_schema_info_reset() failed"); + +DONE: + talloc_free(tempdir); + talloc_free(mem_ctx); + return bret; +} + +/* + * Setup/Teardown for test case + */ +static bool torture_drs_unit_schemainfo_setup(struct torture_context *tctx, + struct drsut_schemainfo_data **_priv) +{ + size_t i; + int ldb_err; + NTSTATUS status; + DATA_BLOB ndr_blob; + struct GUID guid; + struct drsut_schemainfo_data *priv; + + priv = talloc_zero(tctx, struct drsut_schemainfo_data); + torture_assert(tctx, priv, "Not enough memory!"); + + /* returned allocated pointer here + * teardown() will be called even in case of failure, + * so we'll get a changes to clean up */ + *_priv = priv; + + /* create initial schemaInfo */ + torture_assert(tctx, + _drsut_schemainfo_new(tctx, SCHEMA_INFO_DEFAULT_STR, &priv->schema_info), + "Failed to create schema_info test object"); + + /* create data to test with */ + priv->test_data_count = ARRAY_SIZE(_schemainfo_test_data); + priv->test_data = talloc_array(tctx, struct schemainfo_data, priv->test_data_count); + + for (i = 0; i < ARRAY_SIZE(_schemainfo_test_data); i++) { + struct schemainfo_data *data = &priv->test_data[i]; + + ndr_blob = strhex_to_data_blob(priv, + _schemainfo_test_data[i].schema_info_str); + torture_assert(tctx, ndr_blob.data, "Not enough memory!"); + + status = GUID_from_string(_schemainfo_test_data[i].guid_str, &guid); + torture_assert_ntstatus_ok(tctx, status, + talloc_asprintf(tctx, + "GUID_from_string() failed for %s", + _schemainfo_test_data[i].guid_str)); + + data->ndr_blob = ndr_blob; + data->schi.invocation_id = guid; + data->schi.revision = _schemainfo_test_data[i].revision; + data->werr_expected = _schemainfo_test_data[i].werr_expected; + data->test_both_ways = _schemainfo_test_data[i].test_both_ways; + + } + + /* create temporary LDB and populate with data */ + if (!_drsut_ldb_setup(tctx, priv)) { + return false; + } + + /* create ldb_module mockup object */ + priv->ldb_module = ldb_module_new(priv, priv->ldb, "schemaInfo_test_module", NULL); + torture_assert(tctx, priv->ldb_module, "Not enough memory!"); + + /* create schema mockup object */ + priv->schema = dsdb_new_schema(priv); + + /* set schema_info in dsdb_schema for testing */ + torture_assert(tctx, + _drsut_schemainfo_new(tctx, SCHEMA_INFO_DEFAULT_STR, &priv->schema->schema_info), + "Failed to create schema_info test object"); + + /* pre-cache invocationId for samdb_ntds_invocation_id() + * to work with our mock ldb */ + ldb_err = ldb_set_opaque(priv->ldb, "cache.invocation_id", + &priv->schema_info->invocation_id); + torture_assert_int_equal(tctx, ldb_err, LDB_SUCCESS, "ldb_set_opaque() failed"); + + /* Perform all tests in transactions so that + * underlying modify calls not to fail */ + ldb_err = ldb_transaction_start(priv->ldb); + torture_assert_int_equal(tctx, + ldb_err, + LDB_SUCCESS, + "ldb_transaction_start() failed"); + + return true; +} + +static bool torture_drs_unit_schemainfo_teardown(struct torture_context *tctx, + struct drsut_schemainfo_data *priv) +{ + int ldb_err; + + /* commit pending transaction so we will + * be able to check what LDB state is */ + ldb_err = ldb_transaction_commit(priv->ldb); + if (ldb_err != LDB_SUCCESS) { + torture_comment(tctx, "ldb_transaction_commit() - %s (%s)", + ldb_strerror(ldb_err), + ldb_errstring(priv->ldb)); + } + + talloc_free(priv); + + return true; +} + +/** + * Test case initialization for + * drs.unit.schemaInfo + */ +struct torture_tcase * torture_drs_unit_schemainfo(struct torture_suite *suite) +{ + typedef bool (*pfn_setup)(struct torture_context *, void **); + typedef bool (*pfn_teardown)(struct torture_context *, void *); + typedef bool (*pfn_run)(struct torture_context *, void *); + + struct torture_tcase * tc = torture_suite_add_tcase(suite, "schemaInfo"); + + torture_tcase_set_fixture(tc, + (pfn_setup)torture_drs_unit_schemainfo_setup, + (pfn_teardown)torture_drs_unit_schemainfo_teardown); + + tc->description = talloc_strdup(tc, "Unit tests for DRSUAPI::schemaInfo implementation"); + + torture_tcase_add_simple_test(tc, "dsdb_schema_info_new", + (pfn_run)test_dsdb_schema_info_new); + torture_tcase_add_simple_test(tc, "dsdb_schema_info_from_blob", + (pfn_run)test_dsdb_schema_info_from_blob); + torture_tcase_add_simple_test(tc, "dsdb_blob_from_schema_info", + (pfn_run)test_dsdb_blob_from_schema_info); + torture_tcase_add_simple_test(tc, "dsdb_schema_info_cmp", + (pfn_run)test_dsdb_schema_info_cmp); + torture_tcase_add_simple_test(tc, "dsdb_module_schema_info_blob read|write", + (pfn_run)test_dsdb_module_schema_info_blob_rw); + torture_tcase_add_simple_test(tc, "dsdb_module_schema_info_update", + (pfn_run)test_dsdb_module_schema_info_update); + + + return tc; +} diff --git a/source4/torture/drs/wscript_build b/source4/torture/drs/wscript_build new file mode 100644 index 0000000..0dc26d6 --- /dev/null +++ b/source4/torture/drs/wscript_build @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +bld.SAMBA_MODULE('TORTURE_DRS', + source='drs_init.c drs_util.c unit/prefixmap_tests.c unit/schemainfo_tests.c rpc/dssync.c rpc/msds_intid.c', + autoproto='proto.h', + subsystem='smbtorture', + init_function='torture_drs_init', + deps='samba-util ldb samba-errors torture ldbsamba talloc dcerpc ndr NDR_DRSUAPI gensec samba-hostconfig RPC_NDR_DRSUAPI DSDB_MODULE_HELPERS asn1util samdb NDR_DRSBLOBS samba-credentials samdb-common LIBCLI_RESOLVE LP_RESOLVE torturemain', + internal_module=True, + enabled=bld.PYTHON_BUILD_IS_ENABLED() + ) + |