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
49 def _linked_attribute_compare(la1, la2):
50 """See CompareLinks() in MS-DRSR section 4.1.10.5.17"""
54 # Ascending host object GUID
55 c = cmp_fn(ndr_pack(la1.identifier.guid), ndr_pack(la2.identifier.guid))
59 # Ascending attribute ID
60 if la1.attid != la2.attid:
61 return -1 if la1.attid < la2.attid else 1
63 la1_active = la1.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
64 la2_active = la2.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
66 # Ascending 'is present'
67 if la1_active != la2_active:
68 return 1 if la1_active else -1
70 # Ascending target object GUID
71 return cmp_fn(ndr_pack(la1_target), ndr_pack(la2_target))
74 class DrsReplicaSyncTestCase(drs_base.DrsBaseTestCase):
75 """Intended as a semi-black box test case for DsGetNCChanges
76 implementation for extended operations. It should be testing
77 how DsGetNCChanges handles different input params (mostly invalid).
78 Final goal is to make DsGetNCChanges as binary compatible to
79 Windows implementation as possible"""
82 super(DrsReplicaSyncTestCase, self).setUp()
83 self.base_dn = self.ldb_dc1.get_default_basedn()
84 self.ou = "OU=test_getncchanges,%s" % self.base_dn
87 "objectclass": "organizationalUnit"})
88 (self.drs, self.drs_handle) = self._ds_bind(self.dnsname_dc1)
89 (self.default_hwm, self.default_utdv) = self._get_highest_hwm_utdv(self.ldb_dc1)
93 self.ldb_dc1.delete(self.ou, ["tree_delete:1"])
94 except ldb.LdbError as e:
95 (enum, string) = e.args
96 if enum == ldb.ERR_NO_SUCH_OBJECT:
98 super(DrsReplicaSyncTestCase, self).tearDown()
100 def _determine_fSMORoleOwner(self, fsmo_obj_dn):
101 """Returns (owner, not_owner) pair where:
102 owner: dns name for FSMO owner
103 not_owner: dns name for DC not owning the FSMO"""
104 # collect info to return later
105 fsmo_info_1 = {"dns_name": self.dnsname_dc1,
106 "invocation_id": self.ldb_dc1.get_invocation_id(),
107 "ntds_guid": self.ldb_dc1.get_ntds_GUID(),
108 "server_dn": self.ldb_dc1.get_serverName()}
109 fsmo_info_2 = {"dns_name": self.dnsname_dc2,
110 "invocation_id": self.ldb_dc2.get_invocation_id(),
111 "ntds_guid": self.ldb_dc2.get_ntds_GUID(),
112 "server_dn": self.ldb_dc2.get_serverName()}
114 msgs = self.ldb_dc1.search(scope=ldb.SCOPE_BASE, base=fsmo_info_1["server_dn"], attrs=["serverReference"])
115 fsmo_info_1["server_acct_dn"] = ldb.Dn(self.ldb_dc1, msgs[0]["serverReference"][0].decode('utf8'))
116 fsmo_info_1["rid_set_dn"] = ldb.Dn(self.ldb_dc1, "CN=RID Set") + fsmo_info_1["server_acct_dn"]
118 msgs = self.ldb_dc2.search(scope=ldb.SCOPE_BASE, base=fsmo_info_2["server_dn"], attrs=["serverReference"])
119 fsmo_info_2["server_acct_dn"] = ldb.Dn(self.ldb_dc2, msgs[0]["serverReference"][0].decode('utf8'))
120 fsmo_info_2["rid_set_dn"] = ldb.Dn(self.ldb_dc2, "CN=RID Set") + fsmo_info_2["server_acct_dn"]
122 # determine the owner dc
123 res = self.ldb_dc1.search(fsmo_obj_dn,
124 scope=SCOPE_BASE, attrs=["fSMORoleOwner"])
125 assert len(res) == 1, "Only one fSMORoleOwner value expected for %s!"%fsmo_obj_dn
126 fsmo_owner = res[0]["fSMORoleOwner"][0]
127 if fsmo_owner == self.info_dc1["dsServiceName"][0]:
128 return (fsmo_info_1, fsmo_info_2)
129 return (fsmo_info_2, fsmo_info_1)
131 def _check_exop_failed(self, ctr6, expected_failure):
132 self.assertEqual(ctr6.extended_ret, expected_failure)
133 #self.assertEqual(ctr6.object_count, 0)
134 #self.assertEqual(ctr6.first_object, None)
135 self.assertEqual(ctr6.more_data, False)
136 self.assertEqual(ctr6.nc_object_count, 0)
137 self.assertEqual(ctr6.nc_linked_attributes_count, 0)
138 self.assertEqual(ctr6.linked_attributes_count, 0)
139 self.assertEqual(ctr6.linked_attributes, [])
140 self.assertEqual(ctr6.drs_error[0], 0)
142 def test_do_single_repl(self):
144 Make sure that DRSUAPI_EXOP_REPL_OBJ never replicates more than
145 one object, even when we use DRS_GET_ANC/GET_TGT.
148 ou1 = "OU=get_anc1,%s" % self.ou
151 "objectclass": "organizationalUnit"
153 ou1_id = self._get_identifier(self.ldb_dc1, ou1)
154 ou2 = "OU=get_anc2,%s" % ou1
157 "objectclass": "organizationalUnit"
159 ou2_id = self._get_identifier(self.ldb_dc1, ou2)
160 dc3 = "CN=test_anc_dc_%u,%s" % (random.randint(0, 4294967295), ou2)
163 "objectclass": "computer",
164 "userAccountControl": "%d" % (samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_SERVER_TRUST_ACCOUNT)
166 dc3_id = self._get_identifier(self.ldb_dc1, dc3)
168 # Add some linked attributes (for checking GET_TGT behaviour)
170 m.dn = ldb.Dn(self.ldb_dc2, ou1)
171 m["managedBy"] = ldb.MessageElement(ou2, ldb.FLAG_MOD_ADD, "managedBy")
172 self.ldb_dc1.modify(m)
173 ou1_link = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
174 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
175 ou1_id.guid, ou2_id.guid)
177 m.dn = ldb.Dn(self.ldb_dc2, dc3)
178 m["managedBy"] = ldb.MessageElement(ou2, ldb.FLAG_MOD_ADD, "managedBy")
179 self.ldb_dc1.modify(m)
180 dc3_link = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
181 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
182 dc3_id.guid, ou2_id.guid)
184 req = self._getnc_req10(dest_dsa=None,
185 invocation_id=self.ldb_dc1.get_invocation_id(),
187 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
188 replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP,
189 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT)
190 (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req)
191 self._check_ctr6(ctr, [ou1], expected_links=[ou1_link])
193 # DRSUAPI_DRS_WRIT_REP means that we should only replicate the dn we give (dc3).
194 # DRSUAPI_DRS_GET_ANC means that we should also replicate its ancestors, but
195 # Windows doesn't do this if we use both.
196 req = self._getnc_req10(dest_dsa=None,
197 invocation_id=self.ldb_dc1.get_invocation_id(),
199 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
200 replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP |
201 drsuapi.DRSUAPI_DRS_GET_ANC,
202 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT)
203 (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req)
204 self._check_ctr6(ctr, [dc3], expected_links=[dc3_link])
206 # Even though the ancestor of ou2 (ou1) has changed since last hwm, and we're
207 # sending DRSUAPI_DRS_GET_ANC, the expected response is that it will only try
208 # and replicate the single object still.
209 req = self._getnc_req10(dest_dsa=None,
210 invocation_id=self.ldb_dc1.get_invocation_id(),
212 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
213 replica_flags=drsuapi.DRSUAPI_DRS_CRITICAL_ONLY |
214 drsuapi.DRSUAPI_DRS_GET_ANC,
215 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT)
216 (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req)
217 self._check_ctr6(ctr, [ou2])
219 def test_do_full_repl_on_ou(self):
221 Make sure that a full replication on a not-an-nc fails with
225 non_nc_ou = "OU=not-an-NC,%s" % self.ou
228 "objectclass": "organizationalUnit"
230 req8 = self._exop_req8(dest_dsa=None,
231 invocation_id=self.ldb_dc1.get_invocation_id(),
233 exop=drsuapi.DRSUAPI_EXOP_NONE,
234 replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP)
237 (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 8, req8)
238 self.fail("Expected DsGetNCChanges to fail with WERR_DS_CANT_FIND_EXPECTED_NC")
239 except WERRORError as e1:
240 (enum, estr) = e1.args
241 self.assertEquals(enum, werror.WERR_DS_CANT_FIND_EXPECTED_NC)
243 def test_link_utdv_hwm(self):
244 """Test verify the DRS_GET_ANC behavior."""
246 ou1 = "OU=get_anc1,%s" % self.ou
249 "objectclass": "organizationalUnit"
251 ou1_id = self._get_identifier(self.ldb_dc1, ou1)
252 ou2 = "OU=get_anc2,%s" % ou1
255 "objectclass": "organizationalUnit"
257 ou2_id = self._get_identifier(self.ldb_dc1, ou2)
258 dc3 = "CN=test_anc_dc_%u,%s" % (random.randint(0, 4294967295), ou2)
261 "objectclass": "computer",
262 "userAccountControl": "%d" % (samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_SERVER_TRUST_ACCOUNT)
264 dc3_id = self._get_identifier(self.ldb_dc1, dc3)
266 (hwm1, utdv1) = self._check_replication([ou1,ou2,dc3],
267 drsuapi.DRSUAPI_DRS_WRIT_REP)
269 self._check_replication([ou1,ou2,dc3],
270 drsuapi.DRSUAPI_DRS_WRIT_REP|
271 drsuapi.DRSUAPI_DRS_GET_ANC)
273 self._check_replication([dc3],
274 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
276 self._check_replication([ou1,ou2,dc3],
277 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
278 drsuapi.DRSUAPI_DRS_GET_ANC)
281 m.dn = ldb.Dn(self.ldb_dc1, ou1)
282 m["displayName"] = ldb.MessageElement("OU1", ldb.FLAG_MOD_ADD, "displayName")
283 self.ldb_dc1.modify(m)
285 (hwm2, utdv2) = self._check_replication([ou2,dc3,ou1],
286 drsuapi.DRSUAPI_DRS_WRIT_REP)
288 self._check_replication([ou1,ou2,dc3],
289 drsuapi.DRSUAPI_DRS_WRIT_REP|
290 drsuapi.DRSUAPI_DRS_GET_ANC)
292 self._check_replication([dc3],
293 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
295 self._check_replication([ou1,ou2,dc3],
296 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
297 drsuapi.DRSUAPI_DRS_GET_ANC)
299 self._check_replication([ou1],
300 drsuapi.DRSUAPI_DRS_WRIT_REP,
303 self._check_replication([ou1],
304 drsuapi.DRSUAPI_DRS_WRIT_REP|
305 drsuapi.DRSUAPI_DRS_GET_ANC,
308 self._check_replication([ou1],
309 drsuapi.DRSUAPI_DRS_WRIT_REP|
310 drsuapi.DRSUAPI_DRS_GET_ANC,
311 uptodateness_vector=utdv1)
314 m.dn = ldb.Dn(self.ldb_dc1, ou2)
315 m["displayName"] = ldb.MessageElement("OU2", ldb.FLAG_MOD_ADD, "displayName")
316 self.ldb_dc1.modify(m)
318 (hwm3, utdv3) = self._check_replication([dc3,ou1,ou2],
319 drsuapi.DRSUAPI_DRS_WRIT_REP)
321 self._check_replication([ou1,ou2,dc3],
322 drsuapi.DRSUAPI_DRS_WRIT_REP|
323 drsuapi.DRSUAPI_DRS_GET_ANC)
325 self._check_replication([dc3],
326 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
328 self._check_replication([ou1,ou2,dc3],
329 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
330 drsuapi.DRSUAPI_DRS_GET_ANC)
332 self._check_replication([ou1,ou2],
333 drsuapi.DRSUAPI_DRS_WRIT_REP,
336 self._check_replication([ou1,ou2],
337 drsuapi.DRSUAPI_DRS_WRIT_REP|
338 drsuapi.DRSUAPI_DRS_GET_ANC,
341 self._check_replication([ou1,ou2],
342 drsuapi.DRSUAPI_DRS_WRIT_REP|
343 drsuapi.DRSUAPI_DRS_GET_ANC,
344 uptodateness_vector=utdv1)
347 m.dn = ldb.Dn(self.ldb_dc1, self.ou)
348 m["displayName"] = ldb.MessageElement("OU", ldb.FLAG_MOD_ADD, "displayName")
349 self.ldb_dc1.modify(m)
351 (hwm4, utdv4) = self._check_replication([dc3,ou1,ou2,self.ou],
352 drsuapi.DRSUAPI_DRS_WRIT_REP)
354 self._check_replication([self.ou,ou1,ou2,dc3],
355 drsuapi.DRSUAPI_DRS_WRIT_REP|
356 drsuapi.DRSUAPI_DRS_GET_ANC)
358 self._check_replication([dc3],
359 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
361 self._check_replication([self.ou,ou1,ou2,dc3],
362 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
363 drsuapi.DRSUAPI_DRS_GET_ANC)
365 self._check_replication([self.ou,ou2],
366 drsuapi.DRSUAPI_DRS_WRIT_REP|
367 drsuapi.DRSUAPI_DRS_GET_ANC,
368 uptodateness_vector=utdv2)
370 cn3 = "CN=get_anc3,%s" % ou2
373 "objectclass": "container",
375 cn3_id = self._get_identifier(self.ldb_dc1, cn3)
377 (hwm5, utdv5) = self._check_replication([dc3,ou1,ou2,self.ou,cn3],
378 drsuapi.DRSUAPI_DRS_WRIT_REP)
380 self._check_replication([self.ou,ou1,ou2,dc3,cn3],
381 drsuapi.DRSUAPI_DRS_WRIT_REP|
382 drsuapi.DRSUAPI_DRS_GET_ANC)
384 self._check_replication([dc3],
385 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
387 self._check_replication([self.ou,ou1,ou2,dc3],
388 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
389 drsuapi.DRSUAPI_DRS_GET_ANC)
392 m.dn = ldb.Dn(self.ldb_dc1, ou2)
393 m["managedBy"] = ldb.MessageElement(dc3, ldb.FLAG_MOD_ADD, "managedBy")
394 self.ldb_dc1.modify(m)
395 ou2_managedBy_dc3 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
396 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
397 ou2_id.guid, dc3_id.guid)
399 (hwm6, utdv6) = self._check_replication([dc3,ou1,self.ou,cn3,ou2],
400 drsuapi.DRSUAPI_DRS_WRIT_REP,
401 expected_links=[ou2_managedBy_dc3])
403 # Can fail against Windows due to equal precedence of dc3, cn3
404 self._check_replication([self.ou,ou1,ou2,dc3,cn3],
405 drsuapi.DRSUAPI_DRS_WRIT_REP|
406 drsuapi.DRSUAPI_DRS_GET_ANC,
407 expected_links=[ou2_managedBy_dc3])
409 self._check_replication([dc3],
410 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
412 self._check_replication([self.ou,ou1,ou2,dc3],
413 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
414 drsuapi.DRSUAPI_DRS_GET_ANC)
416 self._check_replication([],
417 drsuapi.DRSUAPI_DRS_WRIT_REP,
418 uptodateness_vector=utdv5,
419 expected_links=[ou2_managedBy_dc3])
421 self._check_replication([],
422 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
423 uptodateness_vector=utdv5)
425 self._check_replication([],
426 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
427 uptodateness_vector=utdv5)
430 m.dn = ldb.Dn(self.ldb_dc1, dc3)
431 m["managedBy"] = ldb.MessageElement(ou1, ldb.FLAG_MOD_ADD, "managedBy")
432 self.ldb_dc1.modify(m)
433 dc3_managedBy_ou1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
434 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
435 dc3_id.guid, ou1_id.guid)
437 (hwm7, utdv7) = self._check_replication([ou1,self.ou,cn3,ou2,dc3],
438 drsuapi.DRSUAPI_DRS_WRIT_REP,
439 expected_links=[ou2_managedBy_dc3,dc3_managedBy_ou1])
441 # Can fail against Windows due to equal precedence of dc3, cn3
442 #self._check_replication([self.ou,ou1,ou2,dc3,cn3],
443 # drsuapi.DRSUAPI_DRS_WRIT_REP|
444 # drsuapi.DRSUAPI_DRS_GET_ANC,
445 # expected_links=[ou2_managedBy_dc3,dc3_managedBy_ou1])
447 self._check_replication([dc3],
448 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
449 expected_links=[dc3_managedBy_ou1])
451 self._check_replication([dc3],
452 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
453 expected_links=[dc3_managedBy_ou1])
455 self._check_replication([self.ou,ou1,ou2,dc3],
456 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
457 drsuapi.DRSUAPI_DRS_GET_ANC,
458 expected_links=[dc3_managedBy_ou1])
460 # GET_TGT seems to override DRS_CRITICAL_ONLY and also returns any
461 # object(s) that relate to the linked attributes (similar to GET_ANC)
462 self._check_replication([ou1, dc3],
463 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
464 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT,
465 expected_links=[dc3_managedBy_ou1], dn_ordered=False)
467 # Change DC3's managedBy to OU2 instead of OU1
468 # Note that the OU1 managedBy linked attribute will still exist as
469 # a tombstone object (and so will be returned in the replication still)
471 m.dn = ldb.Dn(self.ldb_dc1, dc3)
472 m["managedBy"] = ldb.MessageElement(ou2, ldb.FLAG_MOD_REPLACE, "managedBy")
473 self.ldb_dc1.modify(m)
474 dc3_managedBy_ou1.flags &= ~drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
475 dc3_managedBy_ou2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
476 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
477 dc3_id.guid, ou2_id.guid)
479 (hwm8, utdv8) = self._check_replication([ou1,self.ou,cn3,ou2,dc3],
480 drsuapi.DRSUAPI_DRS_WRIT_REP,
481 expected_links=[ou2_managedBy_dc3,dc3_managedBy_ou1,dc3_managedBy_ou2])
483 # Can fail against Windows due to equal precedence of dc3, cn3
484 #self._check_replication([self.ou,ou1,ou2,dc3,cn3],
485 # drsuapi.DRSUAPI_DRS_WRIT_REP|
486 # drsuapi.DRSUAPI_DRS_GET_ANC,
487 # expected_links=[ou2_managedBy_dc3,dc3_managedBy_ou1,dc3_managedBy_ou2])
489 self._check_replication([dc3],
490 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
491 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2])
493 self._check_replication([self.ou,ou1,ou2,dc3],
494 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
495 drsuapi.DRSUAPI_DRS_GET_ANC,
496 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2])
498 # GET_TGT will also return any DNs referenced by the linked attributes
499 # (including the Tombstone attribute)
500 self._check_replication([ou1, ou2, dc3],
501 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
502 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT,
503 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2], dn_ordered=False)
505 # Use the highwater-mark prior to changing ManagedBy - this should
506 # only return the old/Tombstone and new linked attributes (we already
508 self._check_replication([],
509 drsuapi.DRSUAPI_DRS_WRIT_REP,
510 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
513 self._check_replication([],
514 drsuapi.DRSUAPI_DRS_WRIT_REP|
515 drsuapi.DRSUAPI_DRS_GET_ANC,
516 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
519 self._check_replication([],
520 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
521 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
524 self._check_replication([],
525 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
526 drsuapi.DRSUAPI_DRS_GET_ANC,
527 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
530 self._check_replication([],
531 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
532 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT,
533 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
536 # Repeat the above set of tests using the uptodateness_vector
537 # instead of the highwater-mark
538 self._check_replication([],
539 drsuapi.DRSUAPI_DRS_WRIT_REP,
540 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
541 uptodateness_vector=utdv7)
543 self._check_replication([],
544 drsuapi.DRSUAPI_DRS_WRIT_REP|
545 drsuapi.DRSUAPI_DRS_GET_ANC,
546 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
547 uptodateness_vector=utdv7)
549 self._check_replication([],
550 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
551 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
552 uptodateness_vector=utdv7)
554 self._check_replication([],
555 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
556 drsuapi.DRSUAPI_DRS_GET_ANC,
557 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
558 uptodateness_vector=utdv7)
560 self._check_replication([],
561 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
562 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT,
563 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
564 uptodateness_vector=utdv7)
566 def test_FSMONotOwner(self):
567 """Test role transfer with against DC not owner of the role"""
568 fsmo_dn = self.ldb_dc1.get_schema_basedn()
569 (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn)
571 req8 = self._exop_req8(dest_dsa=fsmo_owner["ntds_guid"],
572 invocation_id=fsmo_not_owner["invocation_id"],
574 exop=drsuapi.DRSUAPI_EXOP_FSMO_REQ_ROLE)
576 (drs, drs_handle) = self._ds_bind(fsmo_not_owner["dns_name"])
577 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
578 self.assertEqual(level, 6, "Expected level 6 response!")
579 self._check_exop_failed(ctr, drsuapi.DRSUAPI_EXOP_ERR_FSMO_NOT_OWNER)
580 self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_not_owner["ntds_guid"]))
581 self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_not_owner["invocation_id"]))
583 def test_InvalidDestDSA(self):
584 """Test role transfer with invalid destination DSA guid"""
585 fsmo_dn = self.ldb_dc1.get_schema_basedn()
586 (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn)
588 req8 = self._exop_req8(dest_dsa="9c637462-5b8c-4467-aef2-bdb1f57bc4ef",
589 invocation_id=fsmo_owner["invocation_id"],
591 exop=drsuapi.DRSUAPI_EXOP_FSMO_REQ_ROLE)
593 (drs, drs_handle) = self._ds_bind(fsmo_owner["dns_name"])
594 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
595 self.assertEqual(level, 6, "Expected level 6 response!")
596 self._check_exop_failed(ctr, drsuapi.DRSUAPI_EXOP_ERR_UNKNOWN_CALLER)
597 self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_owner["ntds_guid"]))
598 self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_owner["invocation_id"]))
600 class DrsReplicaPrefixMapTestCase(drs_base.DrsBaseTestCase):
602 super(DrsReplicaPrefixMapTestCase, self).setUp()
603 self.base_dn = self.ldb_dc1.get_default_basedn()
604 self.ou = "ou=pfm_exop,%s" % self.base_dn
607 "objectclass": "organizationalUnit"})
608 self.user = "cn=testuser,%s" % self.ou
611 "objectclass": "user"})
614 super(DrsReplicaPrefixMapTestCase, self).tearDown()
616 self.ldb_dc1.delete(self.ou, ["tree_delete:1"])
617 except ldb.LdbError as e2:
618 (enum, string) = e2.args
619 if enum == ldb.ERR_NO_SUCH_OBJECT:
622 def test_missing_prefix_map_dsa(self):
623 partial_attribute_set = self.get_partial_attribute_set()
625 dc_guid_1 = self.ldb_dc1.get_invocation_id()
627 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
629 req8 = self._exop_req8(dest_dsa=None,
630 invocation_id=dc_guid_1,
632 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
633 partial_attribute_set=partial_attribute_set)
636 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
637 self.assertEqual(ctr.extended_ret, drsuapi.DRSUAPI_EXOP_ERR_SUCCESS)
639 self.fail("Missing prefixmap shouldn't have triggered an error")
641 def test_invalid_prefix_map_attid(self):
642 # Request for invalid attid
643 partial_attribute_set = self.get_partial_attribute_set([99999])
645 dc_guid_1 = self.ldb_dc1.get_invocation_id()
646 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
649 pfm = self._samdb_fetch_pfm_and_schi()
651 # On Windows, prefixMap isn't available over LDAP
652 req8 = self._exop_req8(dest_dsa=None,
653 invocation_id=dc_guid_1,
655 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ)
656 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
657 pfm = ctr.mapping_ctr
659 req8 = self._exop_req8(dest_dsa=None,
660 invocation_id=dc_guid_1,
662 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
663 partial_attribute_set=partial_attribute_set,
667 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
668 self.fail("Invalid attid (99999) should have triggered an error")
669 except RuntimeError as e3:
670 (ecode, emsg) = e3.args
671 self.assertEqual(ecode, 0x000020E2, "Error code should have been "
672 "WERR_DS_DRA_SCHEMA_MISMATCH")
674 def test_secret_prefix_map_attid(self):
675 # Request for a secret attid
676 partial_attribute_set = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_unicodePwd])
678 dc_guid_1 = self.ldb_dc1.get_invocation_id()
679 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
682 pfm = self._samdb_fetch_pfm_and_schi()
684 # On Windows, prefixMap isn't available over LDAP
685 req8 = self._exop_req8(dest_dsa=None,
686 invocation_id=dc_guid_1,
688 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ)
689 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
690 pfm = ctr.mapping_ctr
693 req8 = self._exop_req8(dest_dsa=None,
694 invocation_id=dc_guid_1,
696 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
697 partial_attribute_set=partial_attribute_set,
700 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
703 for attr in ctr.first_object.object.attribute_ctr.attributes:
704 if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd:
708 self.assertTrue(found, "Ensure we get the unicodePwd attribute back")
710 for i, mapping in enumerate(pfm.mappings):
712 # objectClass: 2.5.4.0
713 if mapping.oid.binary_oid == [85, 4]:
715 # OID: 1.2.840.113556.1.4.*
716 # unicodePwd: 1.2.840.113556.1.4.90
717 elif mapping.oid.binary_oid == [42, 134, 72, 134, 247, 20, 1, 4]:
720 (pfm.mappings[idx1].id_prefix,
721 pfm.mappings[idx2].id_prefix) = (pfm.mappings[idx2].id_prefix,
722 pfm.mappings[idx1].id_prefix)
725 tmp[idx1], tmp[idx2] = tmp[idx2], tmp[idx1]
728 # 90 for unicodePwd (with new prefix = 0)
729 # 589824, 589827 for objectClass and CN
730 # Use of three ensures sorting is correct
731 partial_attribute_set = self.get_partial_attribute_set([90, 589824, 589827])
732 req8 = self._exop_req8(dest_dsa=None,
733 invocation_id=dc_guid_1,
735 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
736 partial_attribute_set=partial_attribute_set,
739 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
742 for attr in ctr.first_object.object.attribute_ctr.attributes:
743 if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd:
747 self.assertTrue(found, "Ensure we get the unicodePwd attribute back")
749 def test_regular_prefix_map_attid(self):
750 # Request for a regular (non-secret) attid
751 partial_attribute_set = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_name])
753 dc_guid_1 = self.ldb_dc1.get_invocation_id()
754 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
757 pfm = self._samdb_fetch_pfm_and_schi()
759 # On Windows, prefixMap isn't available over LDAP
760 req8 = self._exop_req8(dest_dsa=None,
761 invocation_id=dc_guid_1,
763 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ)
764 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
765 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
842 req8 = self._exop_req8(dest_dsa=None,
843 invocation_id=dc_guid_1,
845 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
846 partial_attribute_set=partial_attribute_set,
847 partial_attribute_set_ex=partial_attribute_set_ex,
850 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
853 for attr in ctr.first_object.object.attribute_ctr.attributes:
854 if attr.attid == drsuapi.DRSUAPI_ATTID_name:
858 self.assertTrue(found, "Ensure we get the name attribute back")
861 for attr in ctr.first_object.object.attribute_ctr.attributes:
862 if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd:
866 self.assertTrue(found, "Ensure we get the unicodePwd attribute back")
868 for i, mapping in enumerate(pfm.mappings):
870 # objectClass: 2.5.4.0
871 if mapping.oid.binary_oid == [85, 4]:
873 # OID: 1.2.840.113556.1.4.*
874 # name: 1.2.840.113556.1.4.1
875 # unicodePwd: 1.2.840.113556.1.4.90
876 elif mapping.oid.binary_oid == [42, 134, 72, 134, 247, 20, 1, 4]:
879 (pfm.mappings[idx1].id_prefix,
880 pfm.mappings[idx2].id_prefix) = (pfm.mappings[idx2].id_prefix,
881 pfm.mappings[idx1].id_prefix)
884 tmp[idx1], tmp[idx2] = tmp[idx2], tmp[idx1]
887 # 1 for name (with new prefix = 0)
888 partial_attribute_set = self.get_partial_attribute_set([1])
889 # 90 for unicodePwd (with new prefix = 0)
890 # HOWEVER: Windows doesn't seem to respect incoming maps for PartialAttrSetEx
891 partial_attribute_set_ex = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_unicodePwd])
892 req8 = self._exop_req8(dest_dsa=None,
893 invocation_id=dc_guid_1,
895 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
896 partial_attribute_set=partial_attribute_set,
897 partial_attribute_set_ex=partial_attribute_set_ex,
900 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
903 for attr in ctr.first_object.object.attribute_ctr.attributes:
904 if attr.attid == drsuapi.DRSUAPI_ATTID_name:
908 self.assertTrue(found, "Ensure we get the name attribute back")
911 for attr in ctr.first_object.object.attribute_ctr.attributes:
912 if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd:
916 self.assertTrue(found, "Ensure we get the unicodePwd attribute back")
918 def _samdb_fetch_pfm_and_schi(self):
919 """Fetch prefixMap and schemaInfo stored in SamDB using LDB connection"""
921 res = samdb.search(base=samdb.get_schema_basedn(), scope=SCOPE_BASE,
922 attrs=["prefixMap", "schemaInfo"])
924 pfm = ndr_unpack(drsblobs.prefixMapBlob,
925 str(res[0]['prefixMap']))
927 schi = drsuapi.DsReplicaOIDMapping()
929 if 'schemaInfo' in res[0]:
930 binary_oid = [x if isinstance(x, int) else ord(x) for x in res[0]['schemaInfo'][0]]
931 schi.oid.length = len(binary_oid)
932 schi.oid.binary_oid = binary_oid
934 schema_info = drsblobs.schemaInfoBlob()
935 schema_info.revision = 0
936 schema_info.marker = 0xFF
937 schema_info.invocation_id = misc.GUID(samdb.get_invocation_id())
939 binary_oid = [x if isinstance(x, int) else ord(x) for x in ndr_pack(schema_info)]
940 # you have to set the length before setting binary_oid
941 schi.oid.length = len(binary_oid)
942 schi.oid.binary_oid = binary_oid
944 pfm.ctr.mappings = pfm.ctr.mappings + [schi]
945 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)