2 # -*- coding: utf-8 -*-
4 # Tests various schema replication scenarios
6 # Copyright (C) Kamen Mazdrashki <kamenim@samba.org> 2011
7 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2016
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 # export DC1=dc1_dns_name
26 # export DC2=dc2_dns_name
27 # export SUBUNITRUN=$samba4srcdir/scripting/bin/subunitrun
28 # PYTHONPATH="$PYTHONPATH:$samba4srcdir/torture/drs/python" $SUBUNITRUN getnc_exop -U"$DOMAIN/$DC_USERNAME"%"$DC_PASSWORD"
34 from drs_base import AbstractLink
38 from samba import werror, WERRORError
41 from ldb import SCOPE_BASE
43 from samba.dcerpc import drsuapi, misc, drsblobs
44 from samba.drs_utils import drs_DsBind
45 from samba.ndr import ndr_unpack, ndr_pack
46 from samba.compat import cmp_to_key_fn
47 from samba.compat import cmp_fn
50 def _linked_attribute_compare(la1, la2):
51 """See CompareLinks() in MS-DRSR section 4.1.10.5.17"""
55 # Ascending host object GUID
56 c = cmp_fn(ndr_pack(la1.identifier.guid), ndr_pack(la2.identifier.guid))
60 # Ascending attribute ID
61 if la1.attid != la2.attid:
62 return -1 if la1.attid < la2.attid else 1
64 la1_active = la1.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
65 la2_active = la2.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
67 # Ascending 'is present'
68 if la1_active != la2_active:
69 return 1 if la1_active else -1
71 # Ascending target object GUID
72 return cmp_fn(ndr_pack(la1_target), ndr_pack(la2_target))
75 class DrsReplicaSyncTestCase(drs_base.DrsBaseTestCase):
76 """Intended as a semi-black box test case for DsGetNCChanges
77 implementation for extended operations. It should be testing
78 how DsGetNCChanges handles different input params (mostly invalid).
79 Final goal is to make DsGetNCChanges as binary compatible to
80 Windows implementation as possible"""
83 super(DrsReplicaSyncTestCase, self).setUp()
84 self.base_dn = self.ldb_dc1.get_default_basedn()
85 self.ou = "OU=test_getncchanges,%s" % self.base_dn
88 "objectclass": "organizationalUnit"})
89 (self.drs, self.drs_handle) = self._ds_bind(self.dnsname_dc1)
90 (self.default_hwm, self.default_utdv) = self._get_highest_hwm_utdv(self.ldb_dc1)
94 self.ldb_dc1.delete(self.ou, ["tree_delete:1"])
95 except ldb.LdbError as e:
96 (enum, string) = e.args
97 if enum == ldb.ERR_NO_SUCH_OBJECT:
99 super(DrsReplicaSyncTestCase, self).tearDown()
101 def _determine_fSMORoleOwner(self, fsmo_obj_dn):
102 """Returns (owner, not_owner) pair where:
103 owner: dns name for FSMO owner
104 not_owner: dns name for DC not owning the FSMO"""
105 # collect info to return later
106 fsmo_info_1 = {"dns_name": self.dnsname_dc1,
107 "invocation_id": self.ldb_dc1.get_invocation_id(),
108 "ntds_guid": self.ldb_dc1.get_ntds_GUID(),
109 "server_dn": self.ldb_dc1.get_serverName()}
110 fsmo_info_2 = {"dns_name": self.dnsname_dc2,
111 "invocation_id": self.ldb_dc2.get_invocation_id(),
112 "ntds_guid": self.ldb_dc2.get_ntds_GUID(),
113 "server_dn": self.ldb_dc2.get_serverName()}
115 msgs = self.ldb_dc1.search(scope=ldb.SCOPE_BASE, base=fsmo_info_1["server_dn"], attrs=["serverReference"])
116 fsmo_info_1["server_acct_dn"] = ldb.Dn(self.ldb_dc1, msgs[0]["serverReference"][0].decode('utf8'))
117 fsmo_info_1["rid_set_dn"] = ldb.Dn(self.ldb_dc1, "CN=RID Set") + fsmo_info_1["server_acct_dn"]
119 msgs = self.ldb_dc2.search(scope=ldb.SCOPE_BASE, base=fsmo_info_2["server_dn"], attrs=["serverReference"])
120 fsmo_info_2["server_acct_dn"] = ldb.Dn(self.ldb_dc2, msgs[0]["serverReference"][0].decode('utf8'))
121 fsmo_info_2["rid_set_dn"] = ldb.Dn(self.ldb_dc2, "CN=RID Set") + fsmo_info_2["server_acct_dn"]
123 # determine the owner dc
124 res = self.ldb_dc1.search(fsmo_obj_dn,
125 scope=SCOPE_BASE, attrs=["fSMORoleOwner"])
126 assert len(res) == 1, "Only one fSMORoleOwner value expected for %s!" %fsmo_obj_dn
127 fsmo_owner = res[0]["fSMORoleOwner"][0]
128 if fsmo_owner == self.info_dc1["dsServiceName"][0]:
129 return (fsmo_info_1, fsmo_info_2)
130 return (fsmo_info_2, fsmo_info_1)
132 def _check_exop_failed(self, ctr6, expected_failure):
133 self.assertEqual(ctr6.extended_ret, expected_failure)
134 #self.assertEqual(ctr6.object_count, 0)
135 #self.assertEqual(ctr6.first_object, None)
136 self.assertEqual(ctr6.more_data, False)
137 self.assertEqual(ctr6.nc_object_count, 0)
138 self.assertEqual(ctr6.nc_linked_attributes_count, 0)
139 self.assertEqual(ctr6.linked_attributes_count, 0)
140 self.assertEqual(ctr6.linked_attributes, [])
141 self.assertEqual(ctr6.drs_error[0], 0)
143 def test_do_single_repl(self):
145 Make sure that DRSUAPI_EXOP_REPL_OBJ never replicates more than
146 one object, even when we use DRS_GET_ANC/GET_TGT.
149 ou1 = "OU=get_anc1,%s" % self.ou
152 "objectclass": "organizationalUnit"
154 ou1_id = self._get_identifier(self.ldb_dc1, ou1)
155 ou2 = "OU=get_anc2,%s" % ou1
158 "objectclass": "organizationalUnit"
160 ou2_id = self._get_identifier(self.ldb_dc1, ou2)
161 dc3 = "CN=test_anc_dc_%u,%s" % (random.randint(0, 4294967295), ou2)
164 "objectclass": "computer",
165 "userAccountControl": "%d" % (samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_SERVER_TRUST_ACCOUNT)
167 dc3_id = self._get_identifier(self.ldb_dc1, dc3)
169 # Add some linked attributes (for checking GET_TGT behaviour)
171 m.dn = ldb.Dn(self.ldb_dc2, ou1)
172 m["managedBy"] = ldb.MessageElement(ou2, ldb.FLAG_MOD_ADD, "managedBy")
173 self.ldb_dc1.modify(m)
174 ou1_link = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
175 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
176 ou1_id.guid, ou2_id.guid)
178 m.dn = ldb.Dn(self.ldb_dc2, dc3)
179 m["managedBy"] = ldb.MessageElement(ou2, ldb.FLAG_MOD_ADD, "managedBy")
180 self.ldb_dc1.modify(m)
181 dc3_link = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
182 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
183 dc3_id.guid, ou2_id.guid)
185 req = self._getnc_req10(dest_dsa=None,
186 invocation_id=self.ldb_dc1.get_invocation_id(),
188 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
189 replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP,
190 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT)
191 (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req)
192 self._check_ctr6(ctr, [ou1], expected_links=[ou1_link])
194 # DRSUAPI_DRS_WRIT_REP means that we should only replicate the dn we give (dc3).
195 # DRSUAPI_DRS_GET_ANC means that we should also replicate its ancestors, but
196 # Windows doesn't do this if we use both.
197 req = self._getnc_req10(dest_dsa=None,
198 invocation_id=self.ldb_dc1.get_invocation_id(),
200 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
201 replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP |
202 drsuapi.DRSUAPI_DRS_GET_ANC,
203 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT)
204 (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req)
205 self._check_ctr6(ctr, [dc3], expected_links=[dc3_link])
207 # Even though the ancestor of ou2 (ou1) has changed since last hwm, and we're
208 # sending DRSUAPI_DRS_GET_ANC, the expected response is that it will only try
209 # and replicate the single object still.
210 req = self._getnc_req10(dest_dsa=None,
211 invocation_id=self.ldb_dc1.get_invocation_id(),
213 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
214 replica_flags=drsuapi.DRSUAPI_DRS_CRITICAL_ONLY |
215 drsuapi.DRSUAPI_DRS_GET_ANC,
216 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT)
217 (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req)
218 self._check_ctr6(ctr, [ou2])
220 def test_do_full_repl_on_ou(self):
222 Make sure that a full replication on a not-an-nc fails with
226 non_nc_ou = "OU=not-an-NC,%s" % self.ou
229 "objectclass": "organizationalUnit"
231 req8 = self._exop_req8(dest_dsa=None,
232 invocation_id=self.ldb_dc1.get_invocation_id(),
234 exop=drsuapi.DRSUAPI_EXOP_NONE,
235 replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP)
238 (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 8, req8)
239 self.fail("Expected DsGetNCChanges to fail with WERR_DS_CANT_FIND_EXPECTED_NC")
240 except WERRORError as e1:
241 (enum, estr) = e1.args
242 self.assertEquals(enum, werror.WERR_DS_CANT_FIND_EXPECTED_NC)
244 def test_link_utdv_hwm(self):
245 """Test verify the DRS_GET_ANC behavior."""
247 ou1 = "OU=get_anc1,%s" % self.ou
250 "objectclass": "organizationalUnit"
252 ou1_id = self._get_identifier(self.ldb_dc1, ou1)
253 ou2 = "OU=get_anc2,%s" % ou1
256 "objectclass": "organizationalUnit"
258 ou2_id = self._get_identifier(self.ldb_dc1, ou2)
259 dc3 = "CN=test_anc_dc_%u,%s" % (random.randint(0, 4294967295), ou2)
262 "objectclass": "computer",
263 "userAccountControl": "%d" % (samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_SERVER_TRUST_ACCOUNT)
265 dc3_id = self._get_identifier(self.ldb_dc1, dc3)
267 (hwm1, utdv1) = self._check_replication([ou1, ou2, dc3],
268 drsuapi.DRSUAPI_DRS_WRIT_REP)
270 self._check_replication([ou1, ou2, dc3],
271 drsuapi.DRSUAPI_DRS_WRIT_REP |
272 drsuapi.DRSUAPI_DRS_GET_ANC)
274 self._check_replication([dc3],
275 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
277 self._check_replication([ou1, ou2, dc3],
278 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY |
279 drsuapi.DRSUAPI_DRS_GET_ANC)
282 m.dn = ldb.Dn(self.ldb_dc1, ou1)
283 m["displayName"] = ldb.MessageElement("OU1", ldb.FLAG_MOD_ADD, "displayName")
284 self.ldb_dc1.modify(m)
286 (hwm2, utdv2) = self._check_replication([ou2, dc3, ou1],
287 drsuapi.DRSUAPI_DRS_WRIT_REP)
289 self._check_replication([ou1, ou2, dc3],
290 drsuapi.DRSUAPI_DRS_WRIT_REP |
291 drsuapi.DRSUAPI_DRS_GET_ANC)
293 self._check_replication([dc3],
294 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
296 self._check_replication([ou1, ou2, dc3],
297 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY |
298 drsuapi.DRSUAPI_DRS_GET_ANC)
300 self._check_replication([ou1],
301 drsuapi.DRSUAPI_DRS_WRIT_REP,
304 self._check_replication([ou1],
305 drsuapi.DRSUAPI_DRS_WRIT_REP |
306 drsuapi.DRSUAPI_DRS_GET_ANC,
309 self._check_replication([ou1],
310 drsuapi.DRSUAPI_DRS_WRIT_REP |
311 drsuapi.DRSUAPI_DRS_GET_ANC,
312 uptodateness_vector=utdv1)
315 m.dn = ldb.Dn(self.ldb_dc1, ou2)
316 m["displayName"] = ldb.MessageElement("OU2", ldb.FLAG_MOD_ADD, "displayName")
317 self.ldb_dc1.modify(m)
319 (hwm3, utdv3) = self._check_replication([dc3, ou1, ou2],
320 drsuapi.DRSUAPI_DRS_WRIT_REP)
322 self._check_replication([ou1, ou2, dc3],
323 drsuapi.DRSUAPI_DRS_WRIT_REP |
324 drsuapi.DRSUAPI_DRS_GET_ANC)
326 self._check_replication([dc3],
327 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
329 self._check_replication([ou1, ou2, dc3],
330 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY |
331 drsuapi.DRSUAPI_DRS_GET_ANC)
333 self._check_replication([ou1, ou2],
334 drsuapi.DRSUAPI_DRS_WRIT_REP,
337 self._check_replication([ou1, ou2],
338 drsuapi.DRSUAPI_DRS_WRIT_REP |
339 drsuapi.DRSUAPI_DRS_GET_ANC,
342 self._check_replication([ou1, ou2],
343 drsuapi.DRSUAPI_DRS_WRIT_REP |
344 drsuapi.DRSUAPI_DRS_GET_ANC,
345 uptodateness_vector=utdv1)
348 m.dn = ldb.Dn(self.ldb_dc1, self.ou)
349 m["displayName"] = ldb.MessageElement("OU", ldb.FLAG_MOD_ADD, "displayName")
350 self.ldb_dc1.modify(m)
352 (hwm4, utdv4) = self._check_replication([dc3, ou1, ou2, self.ou],
353 drsuapi.DRSUAPI_DRS_WRIT_REP)
355 self._check_replication([self.ou, ou1, ou2, dc3],
356 drsuapi.DRSUAPI_DRS_WRIT_REP |
357 drsuapi.DRSUAPI_DRS_GET_ANC)
359 self._check_replication([dc3],
360 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
362 self._check_replication([self.ou, ou1, ou2, dc3],
363 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY |
364 drsuapi.DRSUAPI_DRS_GET_ANC)
366 self._check_replication([self.ou, ou2],
367 drsuapi.DRSUAPI_DRS_WRIT_REP |
368 drsuapi.DRSUAPI_DRS_GET_ANC,
369 uptodateness_vector=utdv2)
371 cn3 = "CN=get_anc3,%s" % ou2
374 "objectclass": "container",
376 cn3_id = self._get_identifier(self.ldb_dc1, cn3)
378 (hwm5, utdv5) = self._check_replication([dc3, ou1, ou2, self.ou, cn3],
379 drsuapi.DRSUAPI_DRS_WRIT_REP)
381 self._check_replication([self.ou, ou1, ou2, dc3, cn3],
382 drsuapi.DRSUAPI_DRS_WRIT_REP |
383 drsuapi.DRSUAPI_DRS_GET_ANC)
385 self._check_replication([dc3],
386 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
388 self._check_replication([self.ou, ou1, ou2, dc3],
389 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY |
390 drsuapi.DRSUAPI_DRS_GET_ANC)
393 m.dn = ldb.Dn(self.ldb_dc1, ou2)
394 m["managedBy"] = ldb.MessageElement(dc3, ldb.FLAG_MOD_ADD, "managedBy")
395 self.ldb_dc1.modify(m)
396 ou2_managedBy_dc3 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
397 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
398 ou2_id.guid, dc3_id.guid)
400 (hwm6, utdv6) = self._check_replication([dc3, ou1, self.ou, cn3, ou2],
401 drsuapi.DRSUAPI_DRS_WRIT_REP,
402 expected_links=[ou2_managedBy_dc3])
404 # Can fail against Windows due to equal precedence of dc3, cn3
405 self._check_replication([self.ou, ou1, ou2, dc3, cn3],
406 drsuapi.DRSUAPI_DRS_WRIT_REP |
407 drsuapi.DRSUAPI_DRS_GET_ANC,
408 expected_links=[ou2_managedBy_dc3])
410 self._check_replication([dc3],
411 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
413 self._check_replication([self.ou, ou1, ou2, dc3],
414 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY |
415 drsuapi.DRSUAPI_DRS_GET_ANC)
417 self._check_replication([],
418 drsuapi.DRSUAPI_DRS_WRIT_REP,
419 uptodateness_vector=utdv5,
420 expected_links=[ou2_managedBy_dc3])
422 self._check_replication([],
423 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
424 uptodateness_vector=utdv5)
426 self._check_replication([],
427 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
428 uptodateness_vector=utdv5)
431 m.dn = ldb.Dn(self.ldb_dc1, dc3)
432 m["managedBy"] = ldb.MessageElement(ou1, ldb.FLAG_MOD_ADD, "managedBy")
433 self.ldb_dc1.modify(m)
434 dc3_managedBy_ou1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
435 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
436 dc3_id.guid, ou1_id.guid)
438 (hwm7, utdv7) = self._check_replication([ou1, self.ou, cn3, ou2, dc3],
439 drsuapi.DRSUAPI_DRS_WRIT_REP,
440 expected_links=[ou2_managedBy_dc3, dc3_managedBy_ou1])
442 # Can fail against Windows due to equal precedence of dc3, cn3
443 # self._check_replication([self.ou,ou1,ou2,dc3,cn3],
444 # drsuapi.DRSUAPI_DRS_WRIT_REP|
445 # drsuapi.DRSUAPI_DRS_GET_ANC,
446 # expected_links=[ou2_managedBy_dc3,dc3_managedBy_ou1])
448 self._check_replication([dc3],
449 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
450 expected_links=[dc3_managedBy_ou1])
452 self._check_replication([dc3],
453 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
454 expected_links=[dc3_managedBy_ou1])
456 self._check_replication([self.ou, ou1, ou2, dc3],
457 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY |
458 drsuapi.DRSUAPI_DRS_GET_ANC,
459 expected_links=[dc3_managedBy_ou1])
461 # GET_TGT seems to override DRS_CRITICAL_ONLY and also returns any
462 # object(s) that relate to the linked attributes (similar to GET_ANC)
463 self._check_replication([ou1, dc3],
464 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
465 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT,
466 expected_links=[dc3_managedBy_ou1], dn_ordered=False)
468 # Change DC3's managedBy to OU2 instead of OU1
469 # Note that the OU1 managedBy linked attribute will still exist as
470 # a tombstone object (and so will be returned in the replication still)
472 m.dn = ldb.Dn(self.ldb_dc1, dc3)
473 m["managedBy"] = ldb.MessageElement(ou2, ldb.FLAG_MOD_REPLACE, "managedBy")
474 self.ldb_dc1.modify(m)
475 dc3_managedBy_ou1.flags &= ~drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
476 dc3_managedBy_ou2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
477 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
478 dc3_id.guid, ou2_id.guid)
480 (hwm8, utdv8) = self._check_replication([ou1, self.ou, cn3, ou2, dc3],
481 drsuapi.DRSUAPI_DRS_WRIT_REP,
482 expected_links=[ou2_managedBy_dc3, dc3_managedBy_ou1, dc3_managedBy_ou2])
484 # Can fail against Windows due to equal precedence of dc3, cn3
485 # self._check_replication([self.ou,ou1,ou2,dc3,cn3],
486 # drsuapi.DRSUAPI_DRS_WRIT_REP|
487 # drsuapi.DRSUAPI_DRS_GET_ANC,
488 # expected_links=[ou2_managedBy_dc3,dc3_managedBy_ou1,dc3_managedBy_ou2])
490 self._check_replication([dc3],
491 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
492 expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2])
494 self._check_replication([self.ou, ou1, ou2, dc3],
495 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY |
496 drsuapi.DRSUAPI_DRS_GET_ANC,
497 expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2])
499 # GET_TGT will also return any DNs referenced by the linked attributes
500 # (including the Tombstone attribute)
501 self._check_replication([ou1, ou2, dc3],
502 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
503 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT,
504 expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2], dn_ordered=False)
506 # Use the highwater-mark prior to changing ManagedBy - this should
507 # only return the old/Tombstone and new linked attributes (we already
509 self._check_replication([],
510 drsuapi.DRSUAPI_DRS_WRIT_REP,
511 expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2],
514 self._check_replication([],
515 drsuapi.DRSUAPI_DRS_WRIT_REP |
516 drsuapi.DRSUAPI_DRS_GET_ANC,
517 expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2],
520 self._check_replication([],
521 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
522 expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2],
525 self._check_replication([],
526 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY |
527 drsuapi.DRSUAPI_DRS_GET_ANC,
528 expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2],
531 self._check_replication([],
532 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
533 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT,
534 expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2],
537 # Repeat the above set of tests using the uptodateness_vector
538 # instead of the highwater-mark
539 self._check_replication([],
540 drsuapi.DRSUAPI_DRS_WRIT_REP,
541 expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2],
542 uptodateness_vector=utdv7)
544 self._check_replication([],
545 drsuapi.DRSUAPI_DRS_WRIT_REP |
546 drsuapi.DRSUAPI_DRS_GET_ANC,
547 expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2],
548 uptodateness_vector=utdv7)
550 self._check_replication([],
551 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
552 expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2],
553 uptodateness_vector=utdv7)
555 self._check_replication([],
556 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY |
557 drsuapi.DRSUAPI_DRS_GET_ANC,
558 expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2],
559 uptodateness_vector=utdv7)
561 self._check_replication([],
562 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
563 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT,
564 expected_links=[dc3_managedBy_ou1, dc3_managedBy_ou2],
565 uptodateness_vector=utdv7)
567 def test_FSMONotOwner(self):
568 """Test role transfer with against DC not owner of the role"""
569 fsmo_dn = self.ldb_dc1.get_schema_basedn()
570 (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn)
572 req8 = self._exop_req8(dest_dsa=fsmo_owner["ntds_guid"],
573 invocation_id=fsmo_not_owner["invocation_id"],
575 exop=drsuapi.DRSUAPI_EXOP_FSMO_REQ_ROLE)
577 (drs, drs_handle) = self._ds_bind(fsmo_not_owner["dns_name"])
578 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
579 self.assertEqual(level, 6, "Expected level 6 response!")
580 self._check_exop_failed(ctr, drsuapi.DRSUAPI_EXOP_ERR_FSMO_NOT_OWNER)
581 self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_not_owner["ntds_guid"]))
582 self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_not_owner["invocation_id"]))
584 def test_InvalidDestDSA(self):
585 """Test role transfer with invalid destination DSA guid"""
586 fsmo_dn = self.ldb_dc1.get_schema_basedn()
587 (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn)
589 req8 = self._exop_req8(dest_dsa="9c637462-5b8c-4467-aef2-bdb1f57bc4ef",
590 invocation_id=fsmo_owner["invocation_id"],
592 exop=drsuapi.DRSUAPI_EXOP_FSMO_REQ_ROLE)
594 (drs, drs_handle) = self._ds_bind(fsmo_owner["dns_name"])
595 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
596 self.assertEqual(level, 6, "Expected level 6 response!")
597 self._check_exop_failed(ctr, drsuapi.DRSUAPI_EXOP_ERR_UNKNOWN_CALLER)
598 self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_owner["ntds_guid"]))
599 self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_owner["invocation_id"]))
602 class DrsReplicaPrefixMapTestCase(drs_base.DrsBaseTestCase):
604 super(DrsReplicaPrefixMapTestCase, self).setUp()
605 self.base_dn = self.ldb_dc1.get_default_basedn()
606 self.ou = "ou=pfm_exop,%s" % self.base_dn
609 "objectclass": "organizationalUnit"})
610 self.user = "cn=testuser,%s" % self.ou
613 "objectclass": "user"})
616 super(DrsReplicaPrefixMapTestCase, self).tearDown()
618 self.ldb_dc1.delete(self.ou, ["tree_delete:1"])
619 except ldb.LdbError as e2:
620 (enum, string) = e2.args
621 if enum == ldb.ERR_NO_SUCH_OBJECT:
624 def test_missing_prefix_map_dsa(self):
625 partial_attribute_set = self.get_partial_attribute_set()
627 dc_guid_1 = self.ldb_dc1.get_invocation_id()
629 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
631 req8 = self._exop_req8(dest_dsa=None,
632 invocation_id=dc_guid_1,
634 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
635 partial_attribute_set=partial_attribute_set)
638 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
639 self.assertEqual(ctr.extended_ret, drsuapi.DRSUAPI_EXOP_ERR_SUCCESS)
641 self.fail("Missing prefixmap shouldn't have triggered an error")
643 def test_invalid_prefix_map_attid(self):
644 # Request for invalid attid
645 partial_attribute_set = self.get_partial_attribute_set([99999])
647 dc_guid_1 = self.ldb_dc1.get_invocation_id()
648 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
651 pfm = self._samdb_fetch_pfm_and_schi()
653 # On Windows, prefixMap isn't available over LDAP
654 req8 = self._exop_req8(dest_dsa=None,
655 invocation_id=dc_guid_1,
657 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ)
658 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
659 pfm = ctr.mapping_ctr
661 req8 = self._exop_req8(dest_dsa=None,
662 invocation_id=dc_guid_1,
664 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
665 partial_attribute_set=partial_attribute_set,
669 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
670 self.fail("Invalid attid (99999) should have triggered an error")
671 except RuntimeError as e3:
672 (ecode, emsg) = e3.args
673 self.assertEqual(ecode, 0x000020E2, "Error code should have been "
674 "WERR_DS_DRA_SCHEMA_MISMATCH")
676 def test_secret_prefix_map_attid(self):
677 # Request for a secret attid
678 partial_attribute_set = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_unicodePwd])
680 dc_guid_1 = self.ldb_dc1.get_invocation_id()
681 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
684 pfm = self._samdb_fetch_pfm_and_schi()
686 # On Windows, prefixMap isn't available over LDAP
687 req8 = self._exop_req8(dest_dsa=None,
688 invocation_id=dc_guid_1,
690 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ)
691 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
692 pfm = ctr.mapping_ctr
694 req8 = self._exop_req8(dest_dsa=None,
695 invocation_id=dc_guid_1,
697 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
698 partial_attribute_set=partial_attribute_set,
701 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
704 for attr in ctr.first_object.object.attribute_ctr.attributes:
705 if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd:
709 self.assertTrue(found, "Ensure we get the unicodePwd attribute back")
711 for i, mapping in enumerate(pfm.mappings):
713 # objectClass: 2.5.4.0
714 if mapping.oid.binary_oid == [85, 4]:
716 # OID: 1.2.840.113556.1.4.*
717 # unicodePwd: 1.2.840.113556.1.4.90
718 elif mapping.oid.binary_oid == [42, 134, 72, 134, 247, 20, 1, 4]:
721 (pfm.mappings[idx1].id_prefix,
722 pfm.mappings[idx2].id_prefix) = (pfm.mappings[idx2].id_prefix,
723 pfm.mappings[idx1].id_prefix)
726 tmp[idx1], tmp[idx2] = tmp[idx2], tmp[idx1]
729 # 90 for unicodePwd (with new prefix = 0)
730 # 589824, 589827 for objectClass and CN
731 # Use of three ensures sorting is correct
732 partial_attribute_set = self.get_partial_attribute_set([90, 589824, 589827])
733 req8 = self._exop_req8(dest_dsa=None,
734 invocation_id=dc_guid_1,
736 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
737 partial_attribute_set=partial_attribute_set,
740 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
743 for attr in ctr.first_object.object.attribute_ctr.attributes:
744 if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd:
748 self.assertTrue(found, "Ensure we get the unicodePwd attribute back")
750 def test_regular_prefix_map_attid(self):
751 # Request for a regular (non-secret) attid
752 partial_attribute_set = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_name])
754 dc_guid_1 = self.ldb_dc1.get_invocation_id()
755 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
758 pfm = self._samdb_fetch_pfm_and_schi()
760 # On Windows, prefixMap isn't available over LDAP
761 req8 = self._exop_req8(dest_dsa=None,
762 invocation_id=dc_guid_1,
764 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ)
765 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
766 pfm = ctr.mapping_ctr
768 req8 = self._exop_req8(dest_dsa=None,
769 invocation_id=dc_guid_1,
771 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
772 partial_attribute_set=partial_attribute_set,
775 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
778 for attr in ctr.first_object.object.attribute_ctr.attributes:
779 if attr.attid == drsuapi.DRSUAPI_ATTID_name:
783 self.assertTrue(found, "Ensure we get the name attribute back")
785 for i, mapping in enumerate(pfm.mappings):
787 # objectClass: 2.5.4.0
788 if mapping.oid.binary_oid == [85, 4]:
790 # OID: 1.2.840.113556.1.4.*
791 # name: 1.2.840.113556.1.4.1
792 elif mapping.oid.binary_oid == [42, 134, 72, 134, 247, 20, 1, 4]:
795 (pfm.mappings[idx1].id_prefix,
796 pfm.mappings[idx2].id_prefix) = (pfm.mappings[idx2].id_prefix,
797 pfm.mappings[idx1].id_prefix)
800 tmp[idx1], tmp[idx2] = tmp[idx2], tmp[idx1]
803 # 1 for name (with new prefix = 0)
804 partial_attribute_set = self.get_partial_attribute_set([1])
805 req8 = self._exop_req8(dest_dsa=None,
806 invocation_id=dc_guid_1,
808 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
809 partial_attribute_set=partial_attribute_set,
812 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
815 for attr in ctr.first_object.object.attribute_ctr.attributes:
816 if attr.attid == drsuapi.DRSUAPI_ATTID_name:
820 self.assertTrue(found, "Ensure we get the name attribute back")
822 def test_regular_prefix_map_ex_attid(self):
823 # Request for a regular (non-secret) attid
824 partial_attribute_set = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_name])
825 partial_attribute_set_ex = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_unicodePwd])
827 dc_guid_1 = self.ldb_dc1.get_invocation_id()
828 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
831 pfm = self._samdb_fetch_pfm_and_schi()
833 # On Windows, prefixMap isn't available over LDAP
834 req8 = self._exop_req8(dest_dsa=None,
835 invocation_id=dc_guid_1,
837 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ)
838 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
839 pfm = ctr.mapping_ctr
841 req8 = self._exop_req8(dest_dsa=None,
842 invocation_id=dc_guid_1,
844 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
845 partial_attribute_set=partial_attribute_set,
846 partial_attribute_set_ex=partial_attribute_set_ex,
849 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
852 for attr in ctr.first_object.object.attribute_ctr.attributes:
853 if attr.attid == drsuapi.DRSUAPI_ATTID_name:
857 self.assertTrue(found, "Ensure we get the name attribute back")
860 for attr in ctr.first_object.object.attribute_ctr.attributes:
861 if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd:
865 self.assertTrue(found, "Ensure we get the unicodePwd attribute back")
867 for i, mapping in enumerate(pfm.mappings):
869 # objectClass: 2.5.4.0
870 if mapping.oid.binary_oid == [85, 4]:
872 # OID: 1.2.840.113556.1.4.*
873 # name: 1.2.840.113556.1.4.1
874 # unicodePwd: 1.2.840.113556.1.4.90
875 elif mapping.oid.binary_oid == [42, 134, 72, 134, 247, 20, 1, 4]:
878 (pfm.mappings[idx1].id_prefix,
879 pfm.mappings[idx2].id_prefix) = (pfm.mappings[idx2].id_prefix,
880 pfm.mappings[idx1].id_prefix)
883 tmp[idx1], tmp[idx2] = tmp[idx2], tmp[idx1]
886 # 1 for name (with new prefix = 0)
887 partial_attribute_set = self.get_partial_attribute_set([1])
888 # 90 for unicodePwd (with new prefix = 0)
889 # HOWEVER: Windows doesn't seem to respect incoming maps for PartialAttrSetEx
890 partial_attribute_set_ex = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_unicodePwd])
891 req8 = self._exop_req8(dest_dsa=None,
892 invocation_id=dc_guid_1,
894 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
895 partial_attribute_set=partial_attribute_set,
896 partial_attribute_set_ex=partial_attribute_set_ex,
899 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
902 for attr in ctr.first_object.object.attribute_ctr.attributes:
903 if attr.attid == drsuapi.DRSUAPI_ATTID_name:
907 self.assertTrue(found, "Ensure we get the name attribute back")
910 for attr in ctr.first_object.object.attribute_ctr.attributes:
911 if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd:
915 self.assertTrue(found, "Ensure we get the unicodePwd attribute back")
917 def _samdb_fetch_pfm_and_schi(self):
918 """Fetch prefixMap and schemaInfo stored in SamDB using LDB connection"""
920 res = samdb.search(base=samdb.get_schema_basedn(), scope=SCOPE_BASE,
921 attrs=["prefixMap", "schemaInfo"])
923 pfm = ndr_unpack(drsblobs.prefixMapBlob,
924 str(res[0]['prefixMap']))
926 schi = drsuapi.DsReplicaOIDMapping()
928 if 'schemaInfo' in res[0]:
929 binary_oid = [x if isinstance(x, int) else ord(x) for x in res[0]['schemaInfo'][0]]
930 schi.oid.length = len(binary_oid)
931 schi.oid.binary_oid = binary_oid
933 schema_info = drsblobs.schemaInfoBlob()
934 schema_info.revision = 0
935 schema_info.marker = 0xFF
936 schema_info.invocation_id = misc.GUID(samdb.get_invocation_id())
938 binary_oid = [x if isinstance(x, int) else ord(x) for x in ndr_pack(schema_info)]
939 # you have to set the length before setting binary_oid
940 schi.oid.length = len(binary_oid)
941 schi.oid.binary_oid = binary_oid
943 pfm.ctr.mappings = pfm.ctr.mappings + [schi]
944 pfm.ctr.num_mappings += 1
948 class DrsReplicaSyncSortTestCase(drs_base.DrsBaseTestCase):
950 super(DrsReplicaSyncSortTestCase, self).setUp()
951 self.base_dn = self.ldb_dc1.get_default_basedn()
952 self.ou = "ou=sort_exop,%s" % self.base_dn
955 "objectclass": "organizationalUnit"})
958 super(DrsReplicaSyncSortTestCase, self).tearDown()
959 # tidyup groups and users
961 self.ldb_dc1.delete(self.ou, ["tree_delete:1"])
962 except ldb.LdbError as e4:
963 (enum, string) = e4.args
964 if enum == ldb.ERR_NO_SUCH_OBJECT:
967 def add_linked_attribute(self, src, dest, attr='member'):
969 m.dn = ldb.Dn(self.ldb_dc1, src)
970 m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_ADD, attr)
971 self.ldb_dc1.modify(m)
973 def remove_linked_attribute(self, src, dest, attr='member'):
975 m.dn = ldb.Dn(self.ldb_dc1, src)
976 m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_DELETE, attr)
977 self.ldb_dc1.modify(m)
979 def test_sort_behaviour_single_object(self):
980 """Testing sorting behaviour on single objects"""
982 user1_dn = "cn=test_user1,%s" % self.ou
983 user2_dn = "cn=test_user2,%s" % self.ou
984 user3_dn = "cn=test_user3,%s" % self.ou
985 group_dn = "cn=test_group,%s" % self.ou
987 self.ldb_dc1.add({"dn": user1_dn, "objectclass": "user"})
988 self.ldb_dc1.add({"dn": user2_dn, "objectclass": "user"})
989 self.ldb_dc1.add({"dn": user3_dn, "objectclass": "user"})
990 self.ldb_dc1.add({"dn": group_dn, "objectclass": "group"})
992 u1_guid = misc.GUID(self.ldb_dc1.search(base=user1_dn,
993 attrs=["objectGUID"])[0]['objectGUID'][0])
994 u2_guid = misc.GUID(self.ldb_dc1.search(base=user2_dn,
995 attrs=["objectGUID"])[0]['objectGUID'][0])
996 u3_guid = misc.GUID(self.ldb_dc1.search(base=user3_dn,
997 attrs=["objectGUID"])[0]['objectGUID'][0])
998 g_guid = misc.GUID(self.ldb_dc1.search(base=group_dn,
999 attrs=["objectGUID"])[0]['objectGUID'][0])
1001 self.add_linked_attribute(group_dn, user1_dn,
1003 self.add_linked_attribute(group_dn, user2_dn,
1005 self.add_linked_attribute(group_dn, user3_dn,
1007 self.add_linked_attribute(group_dn, user1_dn,
1009 self.add_linked_attribute(group_dn, user2_dn,
1010 attr='nonSecurityMember')
1011 self.add_linked_attribute(group_dn, user3_dn,
1012 attr='nonSecurityMember')
1014 set_inactive = AbstractLink(drsuapi.DRSUAPI_ATTID_nonSecurityMember,
1015 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
1018 expected_links = set([set_inactive,
1019 AbstractLink(drsuapi.DRSUAPI_ATTID_member,
1020 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
1023 AbstractLink(drsuapi.DRSUAPI_ATTID_member,
1024 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
1027 AbstractLink(drsuapi.DRSUAPI_ATTID_member,
1028 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
1031 AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
1032 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
1035 AbstractLink(drsuapi.DRSUAPI_ATTID_nonSecurityMember,
1036 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
1041 dc_guid_1 = self.ldb_dc1.get_invocation_id()
1043 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
1045 req8 = self._exop_req8(dest_dsa=None,
1046 invocation_id=dc_guid_1,
1048 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ)
1050 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
1053 for link in ctr.linked_attributes:
1054 target_guid = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3,
1055 link.value.blob).guid
1056 no_inactive.append((link, target_guid))
1057 self.assertTrue(AbstractLink(link.attid, link.flags,
1058 link.identifier.guid,
1059 target_guid) in expected_links)
1061 no_inactive.sort(key=cmp_to_key_fn(_linked_attribute_compare))
1063 # assert the two arrays are the same
1064 self.assertEqual(len(expected_links), ctr.linked_attributes_count)
1065 self.assertEqual([x[0] for x in no_inactive], ctr.linked_attributes)
1067 self.remove_linked_attribute(group_dn, user3_dn,
1068 attr='nonSecurityMember')
1070 # Set the link inactive
1071 expected_links.remove(set_inactive)
1072 set_inactive.flags = 0
1073 expected_links.add(set_inactive)
1076 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
1077 for link in ctr.linked_attributes:
1078 target_guid = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3,
1079 link.value.blob).guid
1080 has_inactive.append((link, target_guid))
1081 self.assertTrue(AbstractLink(link.attid, link.flags,
1082 link.identifier.guid,
1083 target_guid) in expected_links)
1085 has_inactive.sort(key=cmp_to_key_fn(_linked_attribute_compare))
1087 # assert the two arrays are the same
1088 self.assertEqual(len(expected_links), ctr.linked_attributes_count)
1089 self.assertEqual([x[0] for x in has_inactive], ctr.linked_attributes)
1091 def test_sort_behaviour_ncchanges(self):
1092 """Testing sorting behaviour on a group of objects."""
1093 user1_dn = "cn=test_user1,%s" % self.ou
1094 group_dn = "cn=test_group,%s" % self.ou
1095 self.ldb_dc1.add({"dn": user1_dn, "objectclass": "user"})
1096 self.ldb_dc1.add({"dn": group_dn, "objectclass": "group"})
1098 self.add_linked_attribute(group_dn, user1_dn,
1101 dc_guid_1 = self.ldb_dc1.get_invocation_id()
1103 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
1105 # Make sure the max objects count is high enough
1106 req8 = self._exop_req8(dest_dsa=None,
1107 invocation_id=dc_guid_1,
1108 nc_dn_str=self.base_dn,
1111 exop=drsuapi.DRSUAPI_EXOP_NONE)
1113 # Loop until we get linked attributes, or we get to the end.
1114 # Samba sends linked attributes at the end, unlike Windows.
1116 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
1117 if ctr.more_data == 0 or ctr.linked_attributes_count != 0:
1119 req8.highwatermark = ctr.new_highwatermark
1121 self.assertTrue(ctr.linked_attributes_count != 0)
1124 for link in ctr.linked_attributes:
1126 target_guid = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3,
1127 link.value.blob).guid
1129 target_guid = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3Binary,
1130 link.value.blob).guid
1131 no_inactive.append((link, target_guid))
1133 no_inactive.sort(key=cmp_to_key_fn(_linked_attribute_compare))
1135 # assert the two arrays are the same
1136 self.assertEqual([x[0] for x in no_inactive], ctr.linked_attributes)