3 # Copyright Andrew Tridgell 2010
4 # Copyright Andrew Bartlett 2017
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 from samba
.dcerpc
import drsuapi
, misc
, drsblobs
21 from samba
.net
import Net
22 from samba
.ndr
import ndr_unpack
23 from samba
import dsdb
24 from samba
import werror
25 from samba
import WERRORError
28 from samba
.dcerpc
.drsuapi
import (DRSUAPI_ATTID_name
,
29 DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8
,
30 DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V10
)
34 class drsException(Exception):
35 """Base element for drs errors"""
37 def __init__(self
, value
):
41 return "drsException: " + self
.value
44 def drsuapi_connect(server
, lp
, creds
, ip
=None):
45 """Make a DRSUAPI connection to the server.
47 :param server: the name of the server to connect to
48 :param lp: a samba line parameter object
49 :param creds: credential used for the connection
50 :param ip: Forced target server name
51 :return: A tuple with the drsuapi bind object, the drsuapi handle
52 and the supported extensions.
53 :raise drsException: if the connection fails
56 binding_options
= "seal"
57 if lp
.log_level() >= 9:
58 binding_options
+= ",print"
60 # Allow forcing the IP
62 binding_options
+= f
",target_hostname={server}"
63 binding_string
= f
"ncacn_ip_tcp:{ip}[{binding_options}]"
65 binding_string
= "ncacn_ip_tcp:%s[%s]" % (server
, binding_options
)
68 drsuapiBind
= drsuapi
.drsuapi(binding_string
, lp
, creds
)
69 (drsuapiHandle
, bindSupportedExtensions
) = drs_DsBind(drsuapiBind
)
70 except Exception as e
:
71 raise drsException("DRS connection to %s failed: %s" % (server
, e
))
73 return (drsuapiBind
, drsuapiHandle
, bindSupportedExtensions
)
76 def sendDsReplicaSync(drsuapiBind
, drsuapi_handle
, source_dsa_guid
,
77 naming_context
, req_option
):
78 """Send DS replica sync request.
80 :param drsuapiBind: a drsuapi Bind object
81 :param drsuapi_handle: a drsuapi handle on the drsuapi connection
82 :param source_dsa_guid: the guid of the source dsa for the replication
83 :param naming_context: the DN of the naming context to replicate
84 :param req_options: replication options for the DsReplicaSync call
85 :raise drsException: if any error occur while sending and receiving the
86 reply for the dsReplicaSync
89 nc
= drsuapi
.DsReplicaObjectIdentifier()
90 nc
.dn
= naming_context
92 req1
= drsuapi
.DsReplicaSyncRequest1()
93 req1
.naming_context
= nc
94 req1
.options
= req_option
95 req1
.source_dsa_guid
= misc
.GUID(source_dsa_guid
)
98 drsuapiBind
.DsReplicaSync(drsuapi_handle
, 1, req1
)
99 except Exception as estr
:
100 raise drsException("DsReplicaSync failed %s" % estr
)
104 '''make a DsBind call, returning the binding handle'''
105 bind_info
= drsuapi
.DsBindInfoCtr()
106 bind_info
.length
= 28
107 bind_info
.info
= drsuapi
.DsBindInfo28()
108 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_BASE
109 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_ASYNC_REPLICATION
110 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_REMOVEAPI
111 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_MOVEREQ_V2
112 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_GETCHG_COMPRESS
113 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V1
114 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_RESTORE_USN_OPTIMIZATION
115 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_KCC_EXECUTE
116 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_ADDENTRY_V2
117 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_LINKED_VALUE_REPLICATION
118 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V2
119 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_INSTANCE_TYPE_NOT_REQ_ON_MOD
120 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_CRYPTO_BIND
121 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_GET_REPL_INFO
122 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_STRONG_ENCRYPTION
123 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_DCINFO_V01
124 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_TRANSITIVE_MEMBERSHIP
125 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_ADD_SID_HISTORY
126 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_POST_BETA3
127 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_GET_MEMBERSHIPS2
128 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V6
129 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_NONDOMAIN_NCS
130 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8
131 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V5
132 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V6
133 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_ADDENTRYREPLY_V3
134 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_GETCHGREPLY_V7
135 bind_info
.info
.supported_extensions |
= drsuapi
.DRSUAPI_SUPPORTED_EXTENSION_VERIFY_OBJECT
136 (info
, handle
) = drs
.DsBind(misc
.GUID(drsuapi
.DRSUAPI_DS_BIND_GUID
), bind_info
)
138 return (handle
, info
.info
.supported_extensions
)
141 def drs_get_rodc_partial_attribute_set(samdb
):
142 '''get a list of attributes for RODC replication'''
143 partial_attribute_set
= drsuapi
.DsPartialAttributeSet()
144 partial_attribute_set
.version
= 1
148 # the exact list of attids we send is quite critical. Note that
149 # we do ask for the secret attributes, but set SPECIAL_SECRET_PROCESSING
151 schema_dn
= samdb
.get_schema_basedn()
152 res
= samdb
.search(base
=schema_dn
, scope
=ldb
.SCOPE_SUBTREE
,
153 expression
="objectClass=attributeSchema",
154 attrs
=["lDAPDisplayName", "systemFlags",
158 ldap_display_name
= str(r
["lDAPDisplayName"][0])
159 if "systemFlags" in r
:
160 system_flags
= r
["systemFlags"][0]
161 if (int(system_flags
) & (samba
.dsdb
.DS_FLAG_ATTR_NOT_REPLICATED |
162 samba
.dsdb
.DS_FLAG_ATTR_IS_CONSTRUCTED
)):
164 if "searchFlags" in r
:
165 search_flags
= r
["searchFlags"][0]
166 if (int(search_flags
) & samba
.dsdb
.SEARCH_FLAG_RODC_ATTRIBUTE
):
168 attid
= samdb
.get_attid_from_lDAPDisplayName(ldap_display_name
)
169 attids
.append(int(attid
))
171 # the attids do need to be sorted, or windows doesn't return
172 # all the attributes we need
174 partial_attribute_set
.attids
= attids
175 partial_attribute_set
.num_attids
= len(attids
)
176 return partial_attribute_set
179 def drs_copy_highwater_mark(hwm
, new_hwm
):
181 Copies the highwater mark by value, rather than by object reference. (This
182 avoids lingering talloc references to old GetNCChanges reply messages).
184 hwm
.tmp_highest_usn
= new_hwm
.tmp_highest_usn
185 hwm
.reserved_usn
= new_hwm
.reserved_usn
186 hwm
.highest_usn
= new_hwm
.highest_usn
189 class drs_Replicate(object):
190 '''DRS replication calls'''
192 def __init__(self
, binding_string
, lp
, creds
, samdb
, invocation_id
):
193 self
.drs
= drsuapi
.drsuapi(binding_string
, lp
, creds
)
194 (self
.drs_handle
, self
.supports_ext
) = drs_DsBind(self
.drs
)
195 self
.net
= Net(creds
=creds
, lp
=lp
)
197 if not isinstance(invocation_id
, misc
.GUID
):
198 raise RuntimeError("Must supply GUID for invocation_id")
199 if invocation_id
== misc
.GUID("00000000-0000-0000-0000-000000000000"):
200 raise RuntimeError("Must not set GUID 00000000-0000-0000-0000-000000000000 as invocation_id")
201 self
.replication_state
= self
.net
.replicate_init(self
.samdb
, lp
, self
.drs
, invocation_id
)
204 def _should_retry_with_get_tgt(self
, error_code
, req
):
206 # If the error indicates we fail to resolve a target object for a
207 # linked attribute, then we should retry the request with GET_TGT
208 # (if we support it and haven't already tried that)
209 supports_ext
= self
.supports_ext
211 return (error_code
== werror
.WERR_DS_DRA_RECYCLED_TARGET
and
212 supports_ext
& DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V10
and
213 (req
.more_flags
& drsuapi
.DRSUAPI_DRS_GET_TGT
) == 0)
216 def _should_calculate_missing_anc_locally(error_code
, req
):
217 # If the error indicates we fail to resolve the parent object
218 # for a new object, then we assume we are replicating from a
219 # buggy server (Samba 4.5 and earlier) that doesn't really
220 # understand how to implement GET_ANC
222 return ((error_code
== werror
.WERR_DS_DRA_MISSING_PARENT
) and
223 (req
.replica_flags
& drsuapi
.DRSUAPI_DRS_GET_ANC
) != 0)
226 def _calculate_missing_anc_locally(self
, ctr
):
227 self
.guids_seen
= set()
229 # walk objects in ctr, add to guid_seen as we see them
230 # note if an object doesn't have a parent
232 object_to_check
= ctr
.first_object
235 if object_to_check
is None:
238 self
.guids_seen
.add(str(object_to_check
.object.identifier
.guid
))
240 if object_to_check
.parent_object_guid
is not None \
241 and object_to_check
.parent_object_guid \
242 != misc
.GUID("00000000-0000-0000-0000-000000000000") \
243 and str(object_to_check
.parent_object_guid
) not in self
.guids_seen
:
244 obj_dn
= ldb
.Dn(self
.samdb
, object_to_check
.object.identifier
.dn
)
245 parent_dn
= obj_dn
.parent()
246 print(f
"Object {parent_dn} with "
247 f
"GUID {object_to_check.parent_object_guid} "
248 "was not sent by the server in this chunk")
250 object_to_check
= object_to_check
.next_object
253 def process_chunk(self
, level
, ctr
, schema
, req_level
, req
, first_chunk
):
254 '''Processes a single chunk of received replication data'''
255 # pass the replication into the py_net.c python bindings for processing
256 self
.net
.replicate_chunk(self
.replication_state
, level
, ctr
,
257 schema
=schema
, req_level
=req_level
, req
=req
)
259 def replicate(self
, dn
, source_dsa_invocation_id
, destination_dsa_guid
,
260 schema
=False, exop
=drsuapi
.DRSUAPI_EXOP_NONE
, rodc
=False,
261 replica_flags
=None, full_sync
=True, sync_forced
=False, more_flags
=0):
262 '''replicate a single DN'''
264 # setup for a GetNCChanges call
265 if self
.supports_ext
& DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V10
:
266 req
= drsuapi
.DsGetNCChangesRequest10()
267 req
.more_flags
= (more_flags | self
.more_flags
)
271 req
= drsuapi
.DsGetNCChangesRequest8()
273 req
.destination_dsa_guid
= destination_dsa_guid
274 req
.source_dsa_invocation_id
= source_dsa_invocation_id
275 req
.naming_context
= drsuapi
.DsReplicaObjectIdentifier()
276 req
.naming_context
.dn
= dn
278 # Default to a full replication if we don't find an upToDatenessVector
280 hwm
= drsuapi
.DsReplicaHighWaterMark()
281 hwm
.tmp_highest_usn
= 0
286 res
= self
.samdb
.search(base
=dn
, scope
=ldb
.SCOPE_BASE
,
288 if "repsFrom" in res
[0]:
289 for reps_from_packed
in res
[0]["repsFrom"]:
290 reps_from_obj
= ndr_unpack(drsblobs
.repsFromToBlob
, reps_from_packed
)
291 if reps_from_obj
.ctr
.source_dsa_invocation_id
== source_dsa_invocation_id
:
292 hwm
= reps_from_obj
.ctr
.highwatermark
294 udv
= drsuapi
.DsReplicaCursorCtrEx()
300 cursors_v2
= dsdb
._dsdb
_load
_udv
_v
2(self
.samdb
,
301 self
.samdb
.get_default_basedn())
302 for cursor_v2
in cursors_v2
:
303 cursor_v1
= drsuapi
.DsReplicaCursor()
304 cursor_v1
.source_dsa_invocation_id
= cursor_v2
.source_dsa_invocation_id
305 cursor_v1
.highest_usn
= cursor_v2
.highest_usn
306 cursors_v1
.append(cursor_v1
)
308 udv
.cursors
= cursors_v1
309 udv
.count
= len(cursors_v1
)
311 req
.highwatermark
= hwm
312 req
.uptodateness_vector
= udv
314 if replica_flags
is not None:
315 req
.replica_flags
= replica_flags
316 elif exop
== drsuapi
.DRSUAPI_EXOP_REPL_SECRET
:
317 req
.replica_flags
= 0
319 req
.replica_flags
= (drsuapi
.DRSUAPI_DRS_INIT_SYNC |
320 drsuapi
.DRSUAPI_DRS_PER_SYNC |
321 drsuapi
.DRSUAPI_DRS_GET_ANC |
322 drsuapi
.DRSUAPI_DRS_NEVER_SYNCED |
323 drsuapi
.DRSUAPI_DRS_GET_ALL_GROUP_MEMBERSHIP
)
325 req
.replica_flags |
= (
326 drsuapi
.DRSUAPI_DRS_SPECIAL_SECRET_PROCESSING
)
328 req
.replica_flags |
= drsuapi
.DRSUAPI_DRS_WRIT_REP
331 req
.replica_flags |
= drsuapi
.DRSUAPI_DRS_SYNC_FORCED
333 req
.max_object_count
= 402
334 req
.max_ndr_size
= 402116
335 req
.extended_op
= exop
337 req
.partial_attribute_set
= None
338 req
.partial_attribute_set_ex
= None
339 req
.mapping_ctr
.num_mappings
= 0
340 req
.mapping_ctr
.mappings
= None
342 if not schema
and rodc
:
343 req
.partial_attribute_set
= drs_get_rodc_partial_attribute_set(self
.samdb
)
345 if not self
.supports_ext
& DRSUAPI_SUPPORTED_EXTENSION_GETCHGREQ_V8
:
347 req5
= drsuapi
.DsGetNCChangesRequest5()
350 setattr(req5
, a
, getattr(req
, a
))
358 (level
, ctr
) = self
.drs
.DsGetNCChanges(self
.drs_handle
, req_level
, req
)
359 if ctr
.first_object
is None and ctr
.object_count
!= 0:
360 raise RuntimeError("DsGetNCChanges: NULL first_object with object_count=%u" % (ctr
.object_count
))
363 self
.process_chunk(level
, ctr
, schema
, req_level
, req
, first_chunk
)
364 except WERRORError
as e
:
365 # Check if retrying with the GET_TGT flag set might resolve this error
366 if self
._should
_retry
_with
_get
_tgt
(e
.args
[0], req
):
368 print("Missing target object - retrying with DRS_GET_TGT")
369 req
.more_flags |
= drsuapi
.DRSUAPI_DRS_GET_TGT
371 # try sending the request again (this has the side-effect
372 # of causing the DC to restart the replication from scratch)
376 if self
._should
_calculate
_missing
_anc
_locally
(e
.args
[0],
378 print("Missing parent object - calculating missing objects locally")
380 self
._calculate
_missing
_anc
_locally
(ctr
)
384 num_objects
+= ctr
.object_count
386 # Cope with servers that do not return level 6, so do not return any links
388 num_links
+= ctr
.linked_attributes_count
389 except AttributeError:
392 if ctr
.more_data
== 0:
395 # update the request's HWM so we get the next chunk
396 drs_copy_highwater_mark(req
.highwatermark
, ctr
.new_highwatermark
)
398 return (num_objects
, num_links
)
401 # Handles the special case of creating a new clone of a DB, while also renaming
402 # the entire DB's objects on the way through
403 class drs_ReplicateRenamer(drs_Replicate
):
404 '''Uses DRS replication to rename the entire DB'''
406 def __init__(self
, binding_string
, lp
, creds
, samdb
, invocation_id
,
407 old_base_dn
, new_base_dn
):
408 super(drs_ReplicateRenamer
, self
).__init
__(binding_string
, lp
, creds
,
409 samdb
, invocation_id
)
410 self
.old_base_dn
= old_base_dn
411 self
.new_base_dn
= new_base_dn
413 # because we're renaming the DNs, we know we're going to have trouble
414 # resolving link targets. Normally we'd get to the end of replication
415 # only to find we need to retry the whole replication with the GET_TGT
416 # flag set. Always setting the GET_TGT flag avoids this extra work.
417 self
.more_flags
= drsuapi
.DRSUAPI_DRS_GET_TGT
419 def rename_dn(self
, dn_str
):
420 '''Uses string substitution to replace the base DN'''
421 return re
.sub('%s$' % self
.old_base_dn
, self
.new_base_dn
, dn_str
)
423 def update_name_attr(self
, base_obj
):
424 '''Updates the 'name' attribute for the base DN object'''
425 for attr
in base_obj
.attribute_ctr
.attributes
:
426 if attr
.attid
== DRSUAPI_ATTID_name
:
427 base_dn
= ldb
.Dn(self
.samdb
, base_obj
.identifier
.dn
)
428 new_name
= base_dn
.get_rdn_value()
429 attr
.value_ctr
.values
[0].blob
= new_name
.encode('utf-16-le')
431 def rename_top_level_object(self
, first_obj
):
432 '''Renames the first/top-level object in a partition'''
433 old_dn
= first_obj
.identifier
.dn
434 first_obj
.identifier
.dn
= self
.rename_dn(first_obj
.identifier
.dn
)
435 print("Renaming partition %s --> %s" % (old_dn
,
436 first_obj
.identifier
.dn
))
438 # we also need to fix up the 'name' attribute for the base DN,
439 # otherwise the RDNs won't match
440 if first_obj
.identifier
.dn
== self
.new_base_dn
:
441 self
.update_name_attr(first_obj
)
443 def process_chunk(self
, level
, ctr
, schema
, req_level
, req
, first_chunk
):
444 '''Processes a single chunk of received replication data'''
446 # we need to rename the NC in every chunk - this gets used in searches
447 # when applying the chunk
448 if ctr
.naming_context
:
449 ctr
.naming_context
.dn
= self
.rename_dn(ctr
.naming_context
.dn
)
451 # rename the first object in each partition. This will cause every
452 # subsequent object in the partiton to be renamed as a side-effect
453 if first_chunk
and ctr
.object_count
!= 0:
454 self
.rename_top_level_object(ctr
.first_object
.object)
456 # then do the normal repl processing to apply this chunk to our DB
457 super(drs_ReplicateRenamer
, self
).process_chunk(level
, ctr
, schema
,