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
47 def _linked_attribute_compare(la1, la2):
48 """See CompareLinks() in MS-DRSR section 4.1.10.5.17"""
52 # Ascending host object GUID
53 c = cmp(ndr_pack(la1.identifier.guid), ndr_pack(la2.identifier.guid))
57 # Ascending attribute ID
58 if la1.attid != la2.attid:
59 return -1 if la1.attid < la2.attid else 1
61 la1_active = la1.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
62 la2_active = la2.flags & drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
64 # Ascending 'is present'
65 if la1_active != la2_active:
66 return 1 if la1_active else -1
68 # Ascending target object GUID
69 return cmp(ndr_pack(la1_target), ndr_pack(la2_target))
72 class DrsReplicaSyncTestCase(drs_base.DrsBaseTestCase):
73 """Intended as a semi-black box test case for DsGetNCChanges
74 implementation for extended operations. It should be testing
75 how DsGetNCChanges handles different input params (mostly invalid).
76 Final goal is to make DsGetNCChanges as binary compatible to
77 Windows implementation as possible"""
80 super(DrsReplicaSyncTestCase, self).setUp()
81 self.base_dn = self.ldb_dc1.get_default_basedn()
82 self.ou = "OU=test_getncchanges,%s" % self.base_dn
85 "objectclass": "organizationalUnit"})
86 (self.drs, self.drs_handle) = self._ds_bind(self.dnsname_dc1)
87 (self.default_hwm, self.default_utdv) = self._get_highest_hwm_utdv(self.ldb_dc1)
91 self.ldb_dc1.delete(self.ou, ["tree_delete:1"])
92 except ldb.LdbError as e:
93 (enum, string) = e.args
94 if enum == ldb.ERR_NO_SUCH_OBJECT:
96 super(DrsReplicaSyncTestCase, self).tearDown()
98 def _determine_fSMORoleOwner(self, fsmo_obj_dn):
99 """Returns (owner, not_owner) pair where:
100 owner: dns name for FSMO owner
101 not_owner: dns name for DC not owning the FSMO"""
102 # collect info to return later
103 fsmo_info_1 = {"dns_name": self.dnsname_dc1,
104 "invocation_id": self.ldb_dc1.get_invocation_id(),
105 "ntds_guid": self.ldb_dc1.get_ntds_GUID(),
106 "server_dn": self.ldb_dc1.get_serverName()}
107 fsmo_info_2 = {"dns_name": self.dnsname_dc2,
108 "invocation_id": self.ldb_dc2.get_invocation_id(),
109 "ntds_guid": self.ldb_dc2.get_ntds_GUID(),
110 "server_dn": self.ldb_dc2.get_serverName()}
112 msgs = self.ldb_dc1.search(scope=ldb.SCOPE_BASE, base=fsmo_info_1["server_dn"], attrs=["serverReference"])
113 fsmo_info_1["server_acct_dn"] = ldb.Dn(self.ldb_dc1, msgs[0]["serverReference"][0].decode('utf8'))
114 fsmo_info_1["rid_set_dn"] = ldb.Dn(self.ldb_dc1, "CN=RID Set") + fsmo_info_1["server_acct_dn"]
116 msgs = self.ldb_dc2.search(scope=ldb.SCOPE_BASE, base=fsmo_info_2["server_dn"], attrs=["serverReference"])
117 fsmo_info_2["server_acct_dn"] = ldb.Dn(self.ldb_dc2, msgs[0]["serverReference"][0].decode('utf8'))
118 fsmo_info_2["rid_set_dn"] = ldb.Dn(self.ldb_dc2, "CN=RID Set") + fsmo_info_2["server_acct_dn"]
120 # determine the owner dc
121 res = self.ldb_dc1.search(fsmo_obj_dn,
122 scope=SCOPE_BASE, attrs=["fSMORoleOwner"])
123 assert len(res) == 1, "Only one fSMORoleOwner value expected for %s!"%fsmo_obj_dn
124 fsmo_owner = res[0]["fSMORoleOwner"][0]
125 if fsmo_owner == self.info_dc1["dsServiceName"][0]:
126 return (fsmo_info_1, fsmo_info_2)
127 return (fsmo_info_2, fsmo_info_1)
129 def _check_exop_failed(self, ctr6, expected_failure):
130 self.assertEqual(ctr6.extended_ret, expected_failure)
131 #self.assertEqual(ctr6.object_count, 0)
132 #self.assertEqual(ctr6.first_object, None)
133 self.assertEqual(ctr6.more_data, False)
134 self.assertEqual(ctr6.nc_object_count, 0)
135 self.assertEqual(ctr6.nc_linked_attributes_count, 0)
136 self.assertEqual(ctr6.linked_attributes_count, 0)
137 self.assertEqual(ctr6.linked_attributes, [])
138 self.assertEqual(ctr6.drs_error[0], 0)
140 def test_do_single_repl(self):
142 Make sure that DRSUAPI_EXOP_REPL_OBJ never replicates more than
143 one object, even when we use DRS_GET_ANC/GET_TGT.
146 ou1 = "OU=get_anc1,%s" % self.ou
149 "objectclass": "organizationalUnit"
151 ou1_id = self._get_identifier(self.ldb_dc1, ou1)
152 ou2 = "OU=get_anc2,%s" % ou1
155 "objectclass": "organizationalUnit"
157 ou2_id = self._get_identifier(self.ldb_dc1, ou2)
158 dc3 = "CN=test_anc_dc_%u,%s" % (random.randint(0, 4294967295), ou2)
161 "objectclass": "computer",
162 "userAccountControl": "%d" % (samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_SERVER_TRUST_ACCOUNT)
164 dc3_id = self._get_identifier(self.ldb_dc1, dc3)
166 # Add some linked attributes (for checking GET_TGT behaviour)
168 m.dn = ldb.Dn(self.ldb_dc2, ou1)
169 m["managedBy"] = ldb.MessageElement(ou2, ldb.FLAG_MOD_ADD, "managedBy")
170 self.ldb_dc1.modify(m)
171 ou1_link = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
172 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
173 ou1_id.guid, ou2_id.guid)
175 m.dn = ldb.Dn(self.ldb_dc2, dc3)
176 m["managedBy"] = ldb.MessageElement(ou2, ldb.FLAG_MOD_ADD, "managedBy")
177 self.ldb_dc1.modify(m)
178 dc3_link = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
179 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
180 dc3_id.guid, ou2_id.guid)
182 req = self._getnc_req10(dest_dsa=None,
183 invocation_id=self.ldb_dc1.get_invocation_id(),
185 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
186 replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP,
187 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT)
188 (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req)
189 self._check_ctr6(ctr, [ou1], expected_links=[ou1_link])
191 # DRSUAPI_DRS_WRIT_REP means that we should only replicate the dn we give (dc3).
192 # DRSUAPI_DRS_GET_ANC means that we should also replicate its ancestors, but
193 # Windows doesn't do this if we use both.
194 req = self._getnc_req10(dest_dsa=None,
195 invocation_id=self.ldb_dc1.get_invocation_id(),
197 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
198 replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP |
199 drsuapi.DRSUAPI_DRS_GET_ANC,
200 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT)
201 (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req)
202 self._check_ctr6(ctr, [dc3], expected_links=[dc3_link])
204 # Even though the ancestor of ou2 (ou1) has changed since last hwm, and we're
205 # sending DRSUAPI_DRS_GET_ANC, the expected response is that it will only try
206 # and replicate the single object still.
207 req = self._getnc_req10(dest_dsa=None,
208 invocation_id=self.ldb_dc1.get_invocation_id(),
210 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
211 replica_flags=drsuapi.DRSUAPI_DRS_CRITICAL_ONLY |
212 drsuapi.DRSUAPI_DRS_GET_ANC,
213 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT)
214 (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 10, req)
215 self._check_ctr6(ctr, [ou2])
217 def test_do_full_repl_on_ou(self):
219 Make sure that a full replication on a not-an-nc fails with
223 non_nc_ou = "OU=not-an-NC,%s" % self.ou
226 "objectclass": "organizationalUnit"
228 req8 = self._exop_req8(dest_dsa=None,
229 invocation_id=self.ldb_dc1.get_invocation_id(),
231 exop=drsuapi.DRSUAPI_EXOP_NONE,
232 replica_flags=drsuapi.DRSUAPI_DRS_WRIT_REP)
235 (level, ctr) = self.drs.DsGetNCChanges(self.drs_handle, 8, req8)
236 self.fail("Expected DsGetNCChanges to fail with WERR_DS_CANT_FIND_EXPECTED_NC")
237 except WERRORError as e1:
238 (enum, estr) = e1.args
239 self.assertEquals(enum, werror.WERR_DS_CANT_FIND_EXPECTED_NC)
241 def test_link_utdv_hwm(self):
242 """Test verify the DRS_GET_ANC behavior."""
244 ou1 = "OU=get_anc1,%s" % self.ou
247 "objectclass": "organizationalUnit"
249 ou1_id = self._get_identifier(self.ldb_dc1, ou1)
250 ou2 = "OU=get_anc2,%s" % ou1
253 "objectclass": "organizationalUnit"
255 ou2_id = self._get_identifier(self.ldb_dc1, ou2)
256 dc3 = "CN=test_anc_dc_%u,%s" % (random.randint(0, 4294967295), ou2)
259 "objectclass": "computer",
260 "userAccountControl": "%d" % (samba.dsdb.UF_ACCOUNTDISABLE | samba.dsdb.UF_SERVER_TRUST_ACCOUNT)
262 dc3_id = self._get_identifier(self.ldb_dc1, dc3)
264 (hwm1, utdv1) = self._check_replication([ou1,ou2,dc3],
265 drsuapi.DRSUAPI_DRS_WRIT_REP)
267 self._check_replication([ou1,ou2,dc3],
268 drsuapi.DRSUAPI_DRS_WRIT_REP|
269 drsuapi.DRSUAPI_DRS_GET_ANC)
271 self._check_replication([dc3],
272 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
274 self._check_replication([ou1,ou2,dc3],
275 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
276 drsuapi.DRSUAPI_DRS_GET_ANC)
279 m.dn = ldb.Dn(self.ldb_dc1, ou1)
280 m["displayName"] = ldb.MessageElement("OU1", ldb.FLAG_MOD_ADD, "displayName")
281 self.ldb_dc1.modify(m)
283 (hwm2, utdv2) = self._check_replication([ou2,dc3,ou1],
284 drsuapi.DRSUAPI_DRS_WRIT_REP)
286 self._check_replication([ou1,ou2,dc3],
287 drsuapi.DRSUAPI_DRS_WRIT_REP|
288 drsuapi.DRSUAPI_DRS_GET_ANC)
290 self._check_replication([dc3],
291 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
293 self._check_replication([ou1,ou2,dc3],
294 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
295 drsuapi.DRSUAPI_DRS_GET_ANC)
297 self._check_replication([ou1],
298 drsuapi.DRSUAPI_DRS_WRIT_REP,
301 self._check_replication([ou1],
302 drsuapi.DRSUAPI_DRS_WRIT_REP|
303 drsuapi.DRSUAPI_DRS_GET_ANC,
306 self._check_replication([ou1],
307 drsuapi.DRSUAPI_DRS_WRIT_REP|
308 drsuapi.DRSUAPI_DRS_GET_ANC,
309 uptodateness_vector=utdv1)
312 m.dn = ldb.Dn(self.ldb_dc1, ou2)
313 m["displayName"] = ldb.MessageElement("OU2", ldb.FLAG_MOD_ADD, "displayName")
314 self.ldb_dc1.modify(m)
316 (hwm3, utdv3) = self._check_replication([dc3,ou1,ou2],
317 drsuapi.DRSUAPI_DRS_WRIT_REP)
319 self._check_replication([ou1,ou2,dc3],
320 drsuapi.DRSUAPI_DRS_WRIT_REP|
321 drsuapi.DRSUAPI_DRS_GET_ANC)
323 self._check_replication([dc3],
324 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
326 self._check_replication([ou1,ou2,dc3],
327 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
328 drsuapi.DRSUAPI_DRS_GET_ANC)
330 self._check_replication([ou1,ou2],
331 drsuapi.DRSUAPI_DRS_WRIT_REP,
334 self._check_replication([ou1,ou2],
335 drsuapi.DRSUAPI_DRS_WRIT_REP|
336 drsuapi.DRSUAPI_DRS_GET_ANC,
339 self._check_replication([ou1,ou2],
340 drsuapi.DRSUAPI_DRS_WRIT_REP|
341 drsuapi.DRSUAPI_DRS_GET_ANC,
342 uptodateness_vector=utdv1)
345 m.dn = ldb.Dn(self.ldb_dc1, self.ou)
346 m["displayName"] = ldb.MessageElement("OU", ldb.FLAG_MOD_ADD, "displayName")
347 self.ldb_dc1.modify(m)
349 (hwm4, utdv4) = self._check_replication([dc3,ou1,ou2,self.ou],
350 drsuapi.DRSUAPI_DRS_WRIT_REP)
352 self._check_replication([self.ou,ou1,ou2,dc3],
353 drsuapi.DRSUAPI_DRS_WRIT_REP|
354 drsuapi.DRSUAPI_DRS_GET_ANC)
356 self._check_replication([dc3],
357 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
359 self._check_replication([self.ou,ou1,ou2,dc3],
360 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
361 drsuapi.DRSUAPI_DRS_GET_ANC)
363 self._check_replication([self.ou,ou2],
364 drsuapi.DRSUAPI_DRS_WRIT_REP|
365 drsuapi.DRSUAPI_DRS_GET_ANC,
366 uptodateness_vector=utdv2)
368 cn3 = "CN=get_anc3,%s" % ou2
371 "objectclass": "container",
373 cn3_id = self._get_identifier(self.ldb_dc1, cn3)
375 (hwm5, utdv5) = self._check_replication([dc3,ou1,ou2,self.ou,cn3],
376 drsuapi.DRSUAPI_DRS_WRIT_REP)
378 self._check_replication([self.ou,ou1,ou2,dc3,cn3],
379 drsuapi.DRSUAPI_DRS_WRIT_REP|
380 drsuapi.DRSUAPI_DRS_GET_ANC)
382 self._check_replication([dc3],
383 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
385 self._check_replication([self.ou,ou1,ou2,dc3],
386 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
387 drsuapi.DRSUAPI_DRS_GET_ANC)
390 m.dn = ldb.Dn(self.ldb_dc1, ou2)
391 m["managedBy"] = ldb.MessageElement(dc3, ldb.FLAG_MOD_ADD, "managedBy")
392 self.ldb_dc1.modify(m)
393 ou2_managedBy_dc3 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
394 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
395 ou2_id.guid, dc3_id.guid)
397 (hwm6, utdv6) = self._check_replication([dc3,ou1,self.ou,cn3,ou2],
398 drsuapi.DRSUAPI_DRS_WRIT_REP,
399 expected_links=[ou2_managedBy_dc3])
401 # Can fail against Windows due to equal precedence of dc3, cn3
402 self._check_replication([self.ou,ou1,ou2,dc3,cn3],
403 drsuapi.DRSUAPI_DRS_WRIT_REP|
404 drsuapi.DRSUAPI_DRS_GET_ANC,
405 expected_links=[ou2_managedBy_dc3])
407 self._check_replication([dc3],
408 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY)
410 self._check_replication([self.ou,ou1,ou2,dc3],
411 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
412 drsuapi.DRSUAPI_DRS_GET_ANC)
414 self._check_replication([],
415 drsuapi.DRSUAPI_DRS_WRIT_REP,
416 uptodateness_vector=utdv5,
417 expected_links=[ou2_managedBy_dc3])
419 self._check_replication([],
420 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
421 uptodateness_vector=utdv5)
423 self._check_replication([],
424 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
425 uptodateness_vector=utdv5)
428 m.dn = ldb.Dn(self.ldb_dc1, dc3)
429 m["managedBy"] = ldb.MessageElement(ou1, ldb.FLAG_MOD_ADD, "managedBy")
430 self.ldb_dc1.modify(m)
431 dc3_managedBy_ou1 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
432 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
433 dc3_id.guid, ou1_id.guid)
435 (hwm7, utdv7) = self._check_replication([ou1,self.ou,cn3,ou2,dc3],
436 drsuapi.DRSUAPI_DRS_WRIT_REP,
437 expected_links=[ou2_managedBy_dc3,dc3_managedBy_ou1])
439 # Can fail against Windows due to equal precedence of dc3, cn3
440 #self._check_replication([self.ou,ou1,ou2,dc3,cn3],
441 # drsuapi.DRSUAPI_DRS_WRIT_REP|
442 # drsuapi.DRSUAPI_DRS_GET_ANC,
443 # expected_links=[ou2_managedBy_dc3,dc3_managedBy_ou1])
445 self._check_replication([dc3],
446 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
447 expected_links=[dc3_managedBy_ou1])
449 self._check_replication([dc3],
450 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
451 expected_links=[dc3_managedBy_ou1])
453 self._check_replication([self.ou,ou1,ou2,dc3],
454 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
455 drsuapi.DRSUAPI_DRS_GET_ANC,
456 expected_links=[dc3_managedBy_ou1])
458 # GET_TGT seems to override DRS_CRITICAL_ONLY and also returns any
459 # object(s) that relate to the linked attributes (similar to GET_ANC)
460 self._check_replication([ou1, dc3],
461 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
462 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT,
463 expected_links=[dc3_managedBy_ou1], dn_ordered=False)
465 # Change DC3's managedBy to OU2 instead of OU1
466 # Note that the OU1 managedBy linked attribute will still exist as
467 # a tombstone object (and so will be returned in the replication still)
469 m.dn = ldb.Dn(self.ldb_dc1, dc3)
470 m["managedBy"] = ldb.MessageElement(ou2, ldb.FLAG_MOD_REPLACE, "managedBy")
471 self.ldb_dc1.modify(m)
472 dc3_managedBy_ou1.flags &= ~drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE
473 dc3_managedBy_ou2 = AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
474 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
475 dc3_id.guid, ou2_id.guid)
477 (hwm8, utdv8) = self._check_replication([ou1,self.ou,cn3,ou2,dc3],
478 drsuapi.DRSUAPI_DRS_WRIT_REP,
479 expected_links=[ou2_managedBy_dc3,dc3_managedBy_ou1,dc3_managedBy_ou2])
481 # Can fail against Windows due to equal precedence of dc3, cn3
482 #self._check_replication([self.ou,ou1,ou2,dc3,cn3],
483 # drsuapi.DRSUAPI_DRS_WRIT_REP|
484 # drsuapi.DRSUAPI_DRS_GET_ANC,
485 # expected_links=[ou2_managedBy_dc3,dc3_managedBy_ou1,dc3_managedBy_ou2])
487 self._check_replication([dc3],
488 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
489 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2])
491 self._check_replication([self.ou,ou1,ou2,dc3],
492 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
493 drsuapi.DRSUAPI_DRS_GET_ANC,
494 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2])
496 # GET_TGT will also return any DNs referenced by the linked attributes
497 # (including the Tombstone attribute)
498 self._check_replication([ou1, ou2, dc3],
499 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
500 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT,
501 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2], dn_ordered=False)
503 # Use the highwater-mark prior to changing ManagedBy - this should
504 # only return the old/Tombstone and new linked attributes (we already
506 self._check_replication([],
507 drsuapi.DRSUAPI_DRS_WRIT_REP,
508 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
511 self._check_replication([],
512 drsuapi.DRSUAPI_DRS_WRIT_REP|
513 drsuapi.DRSUAPI_DRS_GET_ANC,
514 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
517 self._check_replication([],
518 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
519 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
522 self._check_replication([],
523 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
524 drsuapi.DRSUAPI_DRS_GET_ANC,
525 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
528 self._check_replication([],
529 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
530 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT,
531 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
534 # Repeat the above set of tests using the uptodateness_vector
535 # instead of the highwater-mark
536 self._check_replication([],
537 drsuapi.DRSUAPI_DRS_WRIT_REP,
538 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
539 uptodateness_vector=utdv7)
541 self._check_replication([],
542 drsuapi.DRSUAPI_DRS_WRIT_REP|
543 drsuapi.DRSUAPI_DRS_GET_ANC,
544 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
545 uptodateness_vector=utdv7)
547 self._check_replication([],
548 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
549 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
550 uptodateness_vector=utdv7)
552 self._check_replication([],
553 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY|
554 drsuapi.DRSUAPI_DRS_GET_ANC,
555 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
556 uptodateness_vector=utdv7)
558 self._check_replication([],
559 drsuapi.DRSUAPI_DRS_CRITICAL_ONLY,
560 more_flags=drsuapi.DRSUAPI_DRS_GET_TGT,
561 expected_links=[dc3_managedBy_ou1,dc3_managedBy_ou2],
562 uptodateness_vector=utdv7)
564 def test_FSMONotOwner(self):
565 """Test role transfer with against DC not owner of the role"""
566 fsmo_dn = self.ldb_dc1.get_schema_basedn()
567 (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn)
569 req8 = self._exop_req8(dest_dsa=fsmo_owner["ntds_guid"],
570 invocation_id=fsmo_not_owner["invocation_id"],
572 exop=drsuapi.DRSUAPI_EXOP_FSMO_REQ_ROLE)
574 (drs, drs_handle) = self._ds_bind(fsmo_not_owner["dns_name"])
575 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
576 self.assertEqual(level, 6, "Expected level 6 response!")
577 self._check_exop_failed(ctr, drsuapi.DRSUAPI_EXOP_ERR_FSMO_NOT_OWNER)
578 self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_not_owner["ntds_guid"]))
579 self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_not_owner["invocation_id"]))
581 def test_InvalidDestDSA(self):
582 """Test role transfer with invalid destination DSA guid"""
583 fsmo_dn = self.ldb_dc1.get_schema_basedn()
584 (fsmo_owner, fsmo_not_owner) = self._determine_fSMORoleOwner(fsmo_dn)
586 req8 = self._exop_req8(dest_dsa="9c637462-5b8c-4467-aef2-bdb1f57bc4ef",
587 invocation_id=fsmo_owner["invocation_id"],
589 exop=drsuapi.DRSUAPI_EXOP_FSMO_REQ_ROLE)
591 (drs, drs_handle) = self._ds_bind(fsmo_owner["dns_name"])
592 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
593 self.assertEqual(level, 6, "Expected level 6 response!")
594 self._check_exop_failed(ctr, drsuapi.DRSUAPI_EXOP_ERR_UNKNOWN_CALLER)
595 self.assertEqual(ctr.source_dsa_guid, misc.GUID(fsmo_owner["ntds_guid"]))
596 self.assertEqual(ctr.source_dsa_invocation_id, misc.GUID(fsmo_owner["invocation_id"]))
598 class DrsReplicaPrefixMapTestCase(drs_base.DrsBaseTestCase):
600 super(DrsReplicaPrefixMapTestCase, self).setUp()
601 self.base_dn = self.ldb_dc1.get_default_basedn()
602 self.ou = "ou=pfm_exop,%s" % self.base_dn
605 "objectclass": "organizationalUnit"})
606 self.user = "cn=testuser,%s" % self.ou
609 "objectclass": "user"})
612 super(DrsReplicaPrefixMapTestCase, self).tearDown()
614 self.ldb_dc1.delete(self.ou, ["tree_delete:1"])
615 except ldb.LdbError as e2:
616 (enum, string) = e2.args
617 if enum == ldb.ERR_NO_SUCH_OBJECT:
620 def test_missing_prefix_map_dsa(self):
621 partial_attribute_set = self.get_partial_attribute_set()
623 dc_guid_1 = self.ldb_dc1.get_invocation_id()
625 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
627 req8 = self._exop_req8(dest_dsa=None,
628 invocation_id=dc_guid_1,
630 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
631 partial_attribute_set=partial_attribute_set)
634 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
635 self.assertEqual(ctr.extended_ret, drsuapi.DRSUAPI_EXOP_ERR_SUCCESS)
637 self.fail("Missing prefixmap shouldn't have triggered an error")
639 def test_invalid_prefix_map_attid(self):
640 # Request for invalid attid
641 partial_attribute_set = self.get_partial_attribute_set([99999])
643 dc_guid_1 = self.ldb_dc1.get_invocation_id()
644 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
647 pfm = self._samdb_fetch_pfm_and_schi()
649 # On Windows, prefixMap isn't available over LDAP
650 req8 = self._exop_req8(dest_dsa=None,
651 invocation_id=dc_guid_1,
653 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ)
654 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
655 pfm = ctr.mapping_ctr
657 req8 = self._exop_req8(dest_dsa=None,
658 invocation_id=dc_guid_1,
660 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
661 partial_attribute_set=partial_attribute_set,
665 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
666 self.fail("Invalid attid (99999) should have triggered an error")
667 except RuntimeError as e3:
668 (ecode, emsg) = e3.args
669 self.assertEqual(ecode, 0x000020E2, "Error code should have been "
670 "WERR_DS_DRA_SCHEMA_MISMATCH")
672 def test_secret_prefix_map_attid(self):
673 # Request for a secret attid
674 partial_attribute_set = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_unicodePwd])
676 dc_guid_1 = self.ldb_dc1.get_invocation_id()
677 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
680 pfm = self._samdb_fetch_pfm_and_schi()
682 # On Windows, prefixMap isn't available over LDAP
683 req8 = self._exop_req8(dest_dsa=None,
684 invocation_id=dc_guid_1,
686 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ)
687 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
688 pfm = ctr.mapping_ctr
691 req8 = self._exop_req8(dest_dsa=None,
692 invocation_id=dc_guid_1,
694 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
695 partial_attribute_set=partial_attribute_set,
698 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
701 for attr in ctr.first_object.object.attribute_ctr.attributes:
702 if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd:
706 self.assertTrue(found, "Ensure we get the unicodePwd attribute back")
708 for i, mapping in enumerate(pfm.mappings):
710 # objectClass: 2.5.4.0
711 if mapping.oid.binary_oid == [85, 4]:
713 # OID: 1.2.840.113556.1.4.*
714 # unicodePwd: 1.2.840.113556.1.4.90
715 elif mapping.oid.binary_oid == [42, 134, 72, 134, 247, 20, 1, 4]:
718 (pfm.mappings[idx1].id_prefix,
719 pfm.mappings[idx2].id_prefix) = (pfm.mappings[idx2].id_prefix,
720 pfm.mappings[idx1].id_prefix)
723 tmp[idx1], tmp[idx2] = tmp[idx2], tmp[idx1]
726 # 90 for unicodePwd (with new prefix = 0)
727 # 589824, 589827 for objectClass and CN
728 # Use of three ensures sorting is correct
729 partial_attribute_set = self.get_partial_attribute_set([90, 589824, 589827])
730 req8 = self._exop_req8(dest_dsa=None,
731 invocation_id=dc_guid_1,
733 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
734 partial_attribute_set=partial_attribute_set,
737 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
740 for attr in ctr.first_object.object.attribute_ctr.attributes:
741 if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd:
745 self.assertTrue(found, "Ensure we get the unicodePwd attribute back")
747 def test_regular_prefix_map_attid(self):
748 # Request for a regular (non-secret) attid
749 partial_attribute_set = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_name])
751 dc_guid_1 = self.ldb_dc1.get_invocation_id()
752 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
755 pfm = self._samdb_fetch_pfm_and_schi()
757 # On Windows, prefixMap isn't available over LDAP
758 req8 = self._exop_req8(dest_dsa=None,
759 invocation_id=dc_guid_1,
761 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ)
762 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
763 pfm = ctr.mapping_ctr
766 req8 = self._exop_req8(dest_dsa=None,
767 invocation_id=dc_guid_1,
769 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
770 partial_attribute_set=partial_attribute_set,
773 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
776 for attr in ctr.first_object.object.attribute_ctr.attributes:
777 if attr.attid == drsuapi.DRSUAPI_ATTID_name:
781 self.assertTrue(found, "Ensure we get the name attribute back")
783 for i, mapping in enumerate(pfm.mappings):
785 # objectClass: 2.5.4.0
786 if mapping.oid.binary_oid == [85, 4]:
788 # OID: 1.2.840.113556.1.4.*
789 # name: 1.2.840.113556.1.4.1
790 elif mapping.oid.binary_oid == [42, 134, 72, 134, 247, 20, 1, 4]:
793 (pfm.mappings[idx1].id_prefix,
794 pfm.mappings[idx2].id_prefix) = (pfm.mappings[idx2].id_prefix,
795 pfm.mappings[idx1].id_prefix)
798 tmp[idx1], tmp[idx2] = tmp[idx2], tmp[idx1]
801 # 1 for name (with new prefix = 0)
802 partial_attribute_set = self.get_partial_attribute_set([1])
803 req8 = self._exop_req8(dest_dsa=None,
804 invocation_id=dc_guid_1,
806 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
807 partial_attribute_set=partial_attribute_set,
810 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
813 for attr in ctr.first_object.object.attribute_ctr.attributes:
814 if attr.attid == drsuapi.DRSUAPI_ATTID_name:
818 self.assertTrue(found, "Ensure we get the name attribute back")
820 def test_regular_prefix_map_ex_attid(self):
821 # Request for a regular (non-secret) attid
822 partial_attribute_set = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_name])
823 partial_attribute_set_ex = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_unicodePwd])
825 dc_guid_1 = self.ldb_dc1.get_invocation_id()
826 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
829 pfm = self._samdb_fetch_pfm_and_schi()
831 # On Windows, prefixMap isn't available over LDAP
832 req8 = self._exop_req8(dest_dsa=None,
833 invocation_id=dc_guid_1,
835 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ)
836 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
837 pfm = ctr.mapping_ctr
840 req8 = self._exop_req8(dest_dsa=None,
841 invocation_id=dc_guid_1,
843 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
844 partial_attribute_set=partial_attribute_set,
845 partial_attribute_set_ex=partial_attribute_set_ex,
848 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
851 for attr in ctr.first_object.object.attribute_ctr.attributes:
852 if attr.attid == drsuapi.DRSUAPI_ATTID_name:
856 self.assertTrue(found, "Ensure we get the name attribute back")
859 for attr in ctr.first_object.object.attribute_ctr.attributes:
860 if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd:
864 self.assertTrue(found, "Ensure we get the unicodePwd attribute back")
866 for i, mapping in enumerate(pfm.mappings):
868 # objectClass: 2.5.4.0
869 if mapping.oid.binary_oid == [85, 4]:
871 # OID: 1.2.840.113556.1.4.*
872 # name: 1.2.840.113556.1.4.1
873 # unicodePwd: 1.2.840.113556.1.4.90
874 elif mapping.oid.binary_oid == [42, 134, 72, 134, 247, 20, 1, 4]:
877 (pfm.mappings[idx1].id_prefix,
878 pfm.mappings[idx2].id_prefix) = (pfm.mappings[idx2].id_prefix,
879 pfm.mappings[idx1].id_prefix)
882 tmp[idx1], tmp[idx2] = tmp[idx2], tmp[idx1]
885 # 1 for name (with new prefix = 0)
886 partial_attribute_set = self.get_partial_attribute_set([1])
887 # 90 for unicodePwd (with new prefix = 0)
888 # HOWEVER: Windows doesn't seem to respect incoming maps for PartialAttrSetEx
889 partial_attribute_set_ex = self.get_partial_attribute_set([drsuapi.DRSUAPI_ATTID_unicodePwd])
890 req8 = self._exop_req8(dest_dsa=None,
891 invocation_id=dc_guid_1,
893 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ,
894 partial_attribute_set=partial_attribute_set,
895 partial_attribute_set_ex=partial_attribute_set_ex,
898 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
901 for attr in ctr.first_object.object.attribute_ctr.attributes:
902 if attr.attid == drsuapi.DRSUAPI_ATTID_name:
906 self.assertTrue(found, "Ensure we get the name attribute back")
909 for attr in ctr.first_object.object.attribute_ctr.attributes:
910 if attr.attid == drsuapi.DRSUAPI_ATTID_unicodePwd:
914 self.assertTrue(found, "Ensure we get the unicodePwd attribute back")
916 def _samdb_fetch_pfm_and_schi(self):
917 """Fetch prefixMap and schemaInfo stored in SamDB using LDB connection"""
919 res = samdb.search(base=samdb.get_schema_basedn(), scope=SCOPE_BASE,
920 attrs=["prefixMap", "schemaInfo"])
922 pfm = ndr_unpack(drsblobs.prefixMapBlob,
923 str(res[0]['prefixMap']))
925 schi = drsuapi.DsReplicaOIDMapping()
928 if 'schemaInfo' in res[0]:
929 schi.oid.length = len(map(ord, str(res[0]['schemaInfo'])))
930 schi.oid.binary_oid = map(ord, str(res[0]['schemaInfo']))
932 schema_info = drsblobs.schemaInfoBlob()
933 schema_info.revision = 0
934 schema_info.marker = 0xFF
935 schema_info.invocation_id = misc.GUID(samdb.get_invocation_id())
936 schi.oid.length = len(map(ord, ndr_pack(schema_info)))
937 schi.oid.binary_oid = map(ord, ndr_pack(schema_info))
939 pfm.ctr.mappings = pfm.ctr.mappings + [schi]
940 pfm.ctr.num_mappings += 1
943 class DrsReplicaSyncSortTestCase(drs_base.DrsBaseTestCase):
945 super(DrsReplicaSyncSortTestCase, self).setUp()
946 self.base_dn = self.ldb_dc1.get_default_basedn()
947 self.ou = "ou=sort_exop,%s" % self.base_dn
950 "objectclass": "organizationalUnit"})
953 super(DrsReplicaSyncSortTestCase, self).tearDown()
954 # tidyup groups and users
956 self.ldb_dc1.delete(self.ou, ["tree_delete:1"])
957 except ldb.LdbError as e4:
958 (enum, string) = e4.args
959 if enum == ldb.ERR_NO_SUCH_OBJECT:
962 def add_linked_attribute(self, src, dest, attr='member'):
964 m.dn = ldb.Dn(self.ldb_dc1, src)
965 m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_ADD, attr)
966 self.ldb_dc1.modify(m)
968 def remove_linked_attribute(self, src, dest, attr='member'):
970 m.dn = ldb.Dn(self.ldb_dc1, src)
971 m[attr] = ldb.MessageElement(dest, ldb.FLAG_MOD_DELETE, attr)
972 self.ldb_dc1.modify(m)
974 def test_sort_behaviour_single_object(self):
975 """Testing sorting behaviour on single objects"""
977 user1_dn = "cn=test_user1,%s" % self.ou
978 user2_dn = "cn=test_user2,%s" % self.ou
979 user3_dn = "cn=test_user3,%s" % self.ou
980 group_dn = "cn=test_group,%s" % self.ou
982 self.ldb_dc1.add({"dn": user1_dn, "objectclass": "user"})
983 self.ldb_dc1.add({"dn": user2_dn, "objectclass": "user"})
984 self.ldb_dc1.add({"dn": user3_dn, "objectclass": "user"})
985 self.ldb_dc1.add({"dn": group_dn, "objectclass": "group"})
987 u1_guid = misc.GUID(self.ldb_dc1.search(base=user1_dn,
988 attrs=["objectGUID"])[0]['objectGUID'][0])
989 u2_guid = misc.GUID(self.ldb_dc1.search(base=user2_dn,
990 attrs=["objectGUID"])[0]['objectGUID'][0])
991 u3_guid = misc.GUID(self.ldb_dc1.search(base=user3_dn,
992 attrs=["objectGUID"])[0]['objectGUID'][0])
993 g_guid = misc.GUID(self.ldb_dc1.search(base=group_dn,
994 attrs=["objectGUID"])[0]['objectGUID'][0])
996 self.add_linked_attribute(group_dn, user1_dn,
998 self.add_linked_attribute(group_dn, user2_dn,
1000 self.add_linked_attribute(group_dn, user3_dn,
1002 self.add_linked_attribute(group_dn, user1_dn,
1004 self.add_linked_attribute(group_dn, user2_dn,
1005 attr='nonSecurityMember')
1006 self.add_linked_attribute(group_dn, user3_dn,
1007 attr='nonSecurityMember')
1009 set_inactive = AbstractLink(drsuapi.DRSUAPI_ATTID_nonSecurityMember,
1010 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
1013 expected_links = set([set_inactive,
1014 AbstractLink(drsuapi.DRSUAPI_ATTID_member,
1015 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
1018 AbstractLink(drsuapi.DRSUAPI_ATTID_member,
1019 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
1022 AbstractLink(drsuapi.DRSUAPI_ATTID_member,
1023 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
1026 AbstractLink(drsuapi.DRSUAPI_ATTID_managedBy,
1027 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
1030 AbstractLink(drsuapi.DRSUAPI_ATTID_nonSecurityMember,
1031 drsuapi.DRSUAPI_DS_LINKED_ATTRIBUTE_FLAG_ACTIVE,
1036 dc_guid_1 = self.ldb_dc1.get_invocation_id()
1038 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
1040 req8 = self._exop_req8(dest_dsa=None,
1041 invocation_id=dc_guid_1,
1043 exop=drsuapi.DRSUAPI_EXOP_REPL_OBJ)
1045 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
1048 for link in ctr.linked_attributes:
1049 target_guid = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3,
1050 link.value.blob).guid
1051 no_inactive.append((link, target_guid))
1052 self.assertTrue(AbstractLink(link.attid, link.flags,
1053 link.identifier.guid,
1054 target_guid) in expected_links)
1056 no_inactive.sort(cmp=_linked_attribute_compare)
1058 # assert the two arrays are the same
1059 self.assertEqual(len(expected_links), ctr.linked_attributes_count)
1060 self.assertEqual([x[0] for x in no_inactive], ctr.linked_attributes)
1062 self.remove_linked_attribute(group_dn, user3_dn,
1063 attr='nonSecurityMember')
1065 # Set the link inactive
1066 expected_links.remove(set_inactive)
1067 set_inactive.flags = 0
1068 expected_links.add(set_inactive)
1071 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
1072 for link in ctr.linked_attributes:
1073 target_guid = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3,
1074 link.value.blob).guid
1075 has_inactive.append((link, target_guid))
1076 self.assertTrue(AbstractLink(link.attid, link.flags,
1077 link.identifier.guid,
1078 target_guid) in expected_links)
1080 has_inactive.sort(cmp=_linked_attribute_compare)
1082 # assert the two arrays are the same
1083 self.assertEqual(len(expected_links), ctr.linked_attributes_count)
1084 self.assertEqual([x[0] for x in has_inactive], ctr.linked_attributes)
1086 def test_sort_behaviour_ncchanges(self):
1087 """Testing sorting behaviour on a group of objects."""
1088 user1_dn = "cn=test_user1,%s" % self.ou
1089 group_dn = "cn=test_group,%s" % self.ou
1090 self.ldb_dc1.add({"dn": user1_dn, "objectclass": "user"})
1091 self.ldb_dc1.add({"dn": group_dn, "objectclass": "group"})
1093 self.add_linked_attribute(group_dn, user1_dn,
1096 dc_guid_1 = self.ldb_dc1.get_invocation_id()
1098 drs, drs_handle = self._ds_bind(self.dnsname_dc1)
1100 # Make sure the max objects count is high enough
1101 req8 = self._exop_req8(dest_dsa=None,
1102 invocation_id=dc_guid_1,
1103 nc_dn_str=self.base_dn,
1106 exop=drsuapi.DRSUAPI_EXOP_NONE)
1108 # Loop until we get linked attributes, or we get to the end.
1109 # Samba sends linked attributes at the end, unlike Windows.
1111 (level, ctr) = drs.DsGetNCChanges(drs_handle, 8, req8)
1112 if ctr.more_data == 0 or ctr.linked_attributes_count != 0:
1114 req8.highwatermark = ctr.new_highwatermark
1116 self.assertTrue(ctr.linked_attributes_count != 0)
1119 for link in ctr.linked_attributes:
1121 target_guid = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3,
1122 link.value.blob).guid
1124 target_guid = ndr_unpack(drsuapi.DsReplicaObjectIdentifier3Binary,
1125 link.value.blob).guid
1126 no_inactive.append((link, target_guid))
1128 no_inactive.sort(cmp=_linked_attribute_compare)
1130 # assert the two arrays are the same
1131 self.assertEqual([x[0] for x in no_inactive], ctr.linked_attributes)