selftest: Windows resolves object conflicts differently to Samba
authorTim Beale <timbeale@catalyst.net.nz>
Tue, 26 Sep 2017 00:11:47 +0000 (13:11 +1300)
committerAndrew Bartlett <abartlet@samba.org>
Tue, 26 Sep 2017 03:33:17 +0000 (05:33 +0200)
While testing link conflicts I noticed that Windows resolves conflicts
differently to Samba. Samba considers the version number first when
resolving the conflict, whereas Windows always takes the latest change.

The existing object conflict test cases didn't detect this problem
because they were both modifying the object the same number of times (so
they had the same version number).

I've added new tests that highlight the problem. They are basically the
same as the existing rename tests, except that only one DC does the
rename. Samba will always pick the renamed object as the winner, whereas
Windows picks the most recent change.

I've marked this test as a known fail for now.

BUG: https://bugzilla.samba.org/show_bug.cgi?id=13039

Signed-off-by: Tim Beale <timbeale@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
selftest/knownfail.d/replica_sync [new file with mode: 0644]
source4/torture/drs/python/replica_sync.py

diff --git a/selftest/knownfail.d/replica_sync b/selftest/knownfail.d/replica_sync
new file mode 100644 (file)
index 0000000..142e1e7
--- /dev/null
@@ -0,0 +1,7 @@
+# Samba currently picks a different winner of object conflicts compared to Windows.
+# Samba uses the version number whereas Windows always takes the most recent change
+samba4.drs.replica_sync.python\(vampire_dc\).replica_sync.DrsReplicaSyncTestCase.test_ReplConflictsRenamedVsNewRemoteWin\(vampire_dc:local\)
+samba4.drs.replica_sync.python\(promoted_dc\).replica_sync.DrsReplicaSyncTestCase.test_ReplConflictsRenamedVsNewRemoteWin\(promoted_dc:local\)
+samba4.drs.replica_sync.python\(vampire_dc\).replica_sync.DrsReplicaSyncTestCase.test_ReplConflictsRenamedVsNewLocalWin\(vampire_dc:local\)
+samba4.drs.replica_sync.python\(promoted_dc\).replica_sync.DrsReplicaSyncTestCase.test_ReplConflictsRenamedVsNewLocalWin\(promoted_dc:local\)
+
index 0886637c71d30dc8a4473c377039b13b8b4d4569..c8944b0f3ca975d860ea353952fd43205986a18f 100644 (file)
@@ -298,6 +298,86 @@ objectClass: organizationalUnit
         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.domain_dn)
+
+        # 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.domain_dn)
+        # 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"""