diff options
Diffstat (limited to 'source4/torture/drs/python/link_conflicts.py')
-rw-r--r-- | source4/torture/drs/python/link_conflicts.py | 763 |
1 files changed, 763 insertions, 0 deletions
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) |