summaryrefslogtreecommitdiffstats
path: root/source4/torture/drs/python/replica_sync.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 17:20:00 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 17:20:00 +0000
commit8daa83a594a2e98f39d764422bfbdbc62c9efd44 (patch)
tree4099e8021376c7d8c05bdf8503093d80e9c7bad0 /source4/torture/drs/python/replica_sync.py
parentInitial commit. (diff)
downloadsamba-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/python/replica_sync.py')
-rw-r--r--source4/torture/drs/python/replica_sync.py747
1 files changed, 747 insertions, 0 deletions
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)