1 # Unix SMB/CIFS implementation.
2 # Copyright Matthieu Patou <mat@matws.net> 2011
3 # Copyright Andrew Bartlett <abartlet@samba.org> 2008-2015
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 from ldb
import LdbError
22 from samba
import werror
23 from samba
.ndr
import ndr_unpack
24 from samba
.dcerpc
import misc
, dnsp
25 from samba
.dcerpc
.dnsp
import DNS_TYPE_NS
, DNS_TYPE_A
, DNS_TYPE_AAAA
, \
26 DNS_TYPE_CNAME
, DNS_TYPE_SRV
, DNS_TYPE_PTR
29 class DemoteException(Exception):
30 """Base element for demote errors"""
32 def __init__(self
, value
):
36 return "DemoteException: " + self
.value
39 def remove_sysvol_references(samdb
, logger
, dc_name
):
40 # DNs under the Configuration DN:
41 realm
= samdb
.domain_dns_name()
42 for s
in ("CN=Enterprise,CN=Microsoft System Volumes,CN=System",
43 "CN=%s,CN=Microsoft System Volumes,CN=System" % realm
):
46 # This is verbose, but it is the safe, escape-proof way
47 # to add a base and add an arbitrary RDN.
49 dn
.add_base(samdb
.get_config_basedn())
51 raise DemoteException("Failed constructing DN %s by adding base %s"
52 % (dn
, samdb
.get_config_basedn()))
56 raise DemoteException("Failed constructing DN %s by adding child CN=X"
58 dn
.set_component(0, "CN", dc_name
)
60 logger
.info("Removing Sysvol reference: %s" % dn
)
62 except ldb
.LdbError
as e
:
64 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
69 # DNs under the Domain DN:
70 for s
in ("CN=Domain System Volumes (SYSVOL share),CN=File Replication Service,CN=System",
71 "CN=Topology,CN=Domain System Volume,CN=DFSR-GlobalSettings,CN=System"):
72 # This is verbose, but it is the safe, escape-proof way
73 # to add a base and add an arbitrary RDN.
76 dn
.add_base(samdb
.get_default_basedn())
78 raise DemoteException("Failed constructing DN %s by adding base %s"
79 % (dn
, samdb
.get_default_basedn()))
83 raise DemoteException("Failed constructing DN %s by adding child "
84 "CN=X (soon to be CN=%s)" % (dn
, dc_name
))
85 dn
.set_component(0, "CN", dc_name
)
88 logger
.info("Removing Sysvol reference: %s" % dn
)
90 except ldb
.LdbError
as e1
:
91 (enum
, estr
) = e1
.args
92 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
98 def remove_dns_references(samdb
, logger
, dnsHostName
, ignore_no_name
=False):
100 # Check we are using in-database DNS
101 zones
= samdb
.search(base
="", scope
=ldb
.SCOPE_SUBTREE
,
102 expression
="(&(objectClass=dnsZone)(!(dc=RootDNSServers)))",
104 controls
=["search_options:0:2"])
108 dnsHostNameUpper
= dnsHostName
.upper()
111 (dn
, primary_recs
) = samdb
.dns_lookup(dnsHostName
)
112 except RuntimeError as e4
:
113 (enum
, estr
) = e4
.args
114 if (enum
== werror
.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST
or
115 enum
== werror
.WERR_DNS_ERROR_RCODE_NAME_ERROR
):
117 remove_hanging_dns_references(samdb
, logger
,
121 raise DemoteException("lookup of %s failed: %s" % (dnsHostName
, estr
))
122 samdb
.dns_replace(dnsHostName
, [])
124 res
= samdb
.search("",
125 scope
=ldb
.SCOPE_BASE
, attrs
=["namingContexts"])
127 ncs
= res
[0]["namingContexts"]
129 # Work out the set of names we will likely have an A record on by
130 # default. This is by default all the partitions of type
131 # domainDNS. By finding the canonical name of all the partitions,
132 # we find the likely candidates. We only remove the record if it
133 # matches the IP that was used by the dnsHostName. This avoids us
134 # needing to look at a dns_update_list file from in the demote
137 def dns_name_from_dn(dn
):
138 # The canonical string of DC=example,DC=com is
141 # The canonical string of CN=Configuration,DC=example,DC=com
142 # is example.com/Configuration
143 return ldb
.Dn(samdb
, dn
).canonical_str().split('/', 1)[0]
145 # By using a set here, duplicates via (eg) example.com/Configuration
146 # do not matter, they become just example.com
147 a_names_to_remove_from \
148 = set(dns_name_from_dn(str(dn
)) for dn
in ncs
)
150 def a_rec_to_remove(dnsRecord
):
151 if dnsRecord
.wType
== DNS_TYPE_A
or dnsRecord
.wType
== DNS_TYPE_AAAA
:
152 for rec
in primary_recs
:
153 if rec
.wType
== dnsRecord
.wType
and rec
.data
== dnsRecord
.data
:
157 for a_name
in a_names_to_remove_from
:
159 logger
.debug("checking for DNS records to remove on %s" % a_name
)
160 (a_rec_dn
, a_recs
) = samdb
.dns_lookup(a_name
)
161 except RuntimeError as e2
:
162 (enum
, estr
) = e2
.args
163 if enum
== werror
.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST
:
165 raise DemoteException("lookup of %s failed: %s" % (a_name
, estr
))
167 orig_num_recs
= len(a_recs
)
168 a_recs
= [r
for r
in a_recs
if not a_rec_to_remove(r
)]
170 if len(a_recs
) != orig_num_recs
:
171 logger
.info("updating %s keeping %d values, removing %s values" %
172 (a_name
, len(a_recs
), orig_num_recs
- len(a_recs
)))
173 samdb
.dns_replace(a_name
, a_recs
)
175 remove_hanging_dns_references(samdb
, logger
, dnsHostNameUpper
, zones
)
178 def remove_hanging_dns_references(samdb
, logger
, dnsHostNameUpper
, zones
):
180 # Find all the CNAME, NS, PTR and SRV records that point at the
181 # name we are removing
183 def to_remove(value
):
184 dnsRecord
= ndr_unpack(dnsp
.DnssrvRpcRecord
, value
)
185 if dnsRecord
.wType
== DNS_TYPE_NS \
186 or dnsRecord
.wType
== DNS_TYPE_CNAME \
187 or dnsRecord
.wType
== DNS_TYPE_PTR
:
188 if dnsRecord
.data
.upper() == dnsHostNameUpper
:
190 elif dnsRecord
.wType
== DNS_TYPE_SRV
:
191 if dnsRecord
.data
.nameTarget
.upper() == dnsHostNameUpper
:
196 logger
.debug("checking %s" % zone
.dn
)
197 records
= samdb
.search(base
=zone
.dn
, scope
=ldb
.SCOPE_SUBTREE
,
198 expression
="(&(objectClass=dnsNode)"
199 "(!(dNSTombstoned=TRUE)))",
201 for record
in records
:
203 orig_values
= record
["dnsRecord"]
207 # Remove references to dnsHostName in A, AAAA, NS, CNAME and SRV
208 values
= [ndr_unpack(dnsp
.DnssrvRpcRecord
, v
)
209 for v
in orig_values
if not to_remove(v
)]
211 if len(values
) != len(orig_values
):
212 logger
.info("updating %s keeping %d values, removing %s values"
213 % (record
.dn
, len(values
),
214 len(orig_values
) - len(values
)))
216 # This requires the values to be unpacked, so this
217 # has been done in the list comprehension above
218 samdb
.dns_replace_by_dn(record
.dn
, values
)
221 def offline_remove_server(samdb
, logger
,
223 remove_computer_obj
=False,
224 remove_server_obj
=False,
225 remove_sysvol_obj
=False,
226 remove_dns_names
=False,
227 remove_dns_account
=False):
228 res
= samdb
.search("",
229 scope
=ldb
.SCOPE_BASE
, attrs
=["dsServiceName"])
231 my_serviceName
= res
[0]["dsServiceName"][0]
233 # Confirm this is really a server object
234 msgs
= samdb
.search(base
=server_dn
,
235 attrs
=["serverReference", "cn",
237 scope
=ldb
.SCOPE_BASE
,
238 expression
="(objectClass=server)")
240 dc_name
= str(msg
["cn"][0])
243 computer_dn
= ldb
.Dn(samdb
, msg
["serverReference"][0].decode('utf8'))
248 dnsHostName
= str(msg
["dnsHostName"][0])
252 if remove_server_obj
:
253 # Remove the server DN (do a tree-delete as it could still have a
254 # 'DNS Settings' child object if it's a Windows DC)
255 samdb
.delete(server_dn
, ["tree_delete:0"])
257 if computer_dn
is not None:
258 computer_msgs
= samdb
.search(base
=computer_dn
,
259 expression
="objectclass=computer",
260 attrs
=["msDS-KrbTgtLink",
263 scope
=ldb
.SCOPE_BASE
)
264 if "rIDSetReferences" in computer_msgs
[0]:
265 rid_set_dn
= str(computer_msgs
[0]["rIDSetReferences"][0])
266 logger
.info("Removing RID Set: %s" % rid_set_dn
)
267 samdb
.delete(rid_set_dn
)
268 if "msDS-KrbTgtLink" in computer_msgs
[0]:
269 krbtgt_link_dn
= str(computer_msgs
[0]["msDS-KrbTgtLink"][0])
270 logger
.info("Removing RODC KDC account: %s" % krbtgt_link_dn
)
271 samdb
.delete(krbtgt_link_dn
)
273 if remove_computer_obj
:
274 # Delete the computer tree
275 logger
.info("Removing computer account: %s (and any child objects)" % computer_dn
)
276 samdb
.delete(computer_dn
, ["tree_delete:0"])
278 if "dnsHostName" in msg
:
279 dnsHostName
= str(msg
["dnsHostName"][0])
281 if remove_dns_account
:
282 res
= samdb
.search(expression
="(&(objectclass=user)(cn=dns-%s)(servicePrincipalName=DNS/%s))" %
283 (ldb
.binary_encode(dc_name
), dnsHostName
),
284 attrs
=[], scope
=ldb
.SCOPE_SUBTREE
,
285 base
=samdb
.get_default_basedn())
287 logger
.info("Removing Samba-specific DNS service account: %s" % res
[0].dn
)
288 samdb
.delete(res
[0].dn
)
290 if dnsHostName
is not None and remove_dns_names
:
291 remove_dns_references(samdb
, logger
, dnsHostName
)
293 if remove_sysvol_obj
:
294 remove_sysvol_references(samdb
, logger
, dc_name
)
297 def offline_remove_ntds_dc(samdb
,
300 remove_computer_obj
=False,
301 remove_server_obj
=False,
302 remove_connection_obj
=False,
303 seize_stale_fsmo
=False,
304 remove_sysvol_obj
=False,
305 remove_dns_names
=False,
306 remove_dns_account
=False):
307 res
= samdb
.search("",
308 scope
=ldb
.SCOPE_BASE
, attrs
=["dsServiceName"])
310 my_serviceName
= ldb
.Dn(samdb
, res
[0]["dsServiceName"][0].decode('utf8'))
311 server_dn
= ntds_dn
.parent()
313 if my_serviceName
== ntds_dn
:
314 raise DemoteException("Refusing to demote our own DSA: %s " % my_serviceName
)
317 msgs
= samdb
.search(base
=ntds_dn
, expression
="objectClass=ntdsDSA",
318 attrs
=["objectGUID"], scope
=ldb
.SCOPE_BASE
)
319 except LdbError
as e5
:
320 (enum
, estr
) = e5
.args
321 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
322 raise DemoteException("Given DN %s doesn't exist" % ntds_dn
)
326 raise DemoteException("%s is not an ntdsda in %s"
327 % (ntds_dn
, samdb
.domain_dns_name()))
330 if (msg
.dn
.get_rdn_name() != "CN" or
331 msg
.dn
.get_rdn_value() != "NTDS Settings"):
332 raise DemoteException("Given DN (%s) wasn't the NTDS Settings DN" %
335 ntds_guid
= ndr_unpack(misc
.GUID
, msg
["objectGUID"][0])
337 if remove_connection_obj
:
338 # Find any nTDSConnection objects with that DC as the fromServer.
339 # We use the GUID to avoid issues with any () chars in a server
341 stale_connections
= samdb
.search(base
=samdb
.get_config_basedn(),
342 expression
="(&(objectclass=nTDSConnection)"
343 "(fromServer=<GUID=%s>))" % ntds_guid
)
344 for conn
in stale_connections
:
345 logger
.info("Removing nTDSConnection: %s" % conn
.dn
)
346 samdb
.delete(conn
.dn
)
349 stale_fsmo_roles
= samdb
.search(base
="", scope
=ldb
.SCOPE_SUBTREE
,
350 expression
="(fsmoRoleOwner=<GUID=%s>))"
352 controls
=["search_options:0:2"])
353 # Find any FSMO roles they have, give them to this server
355 for role
in stale_fsmo_roles
:
356 val
= str(my_serviceName
)
359 m
['value'] = ldb
.MessageElement(val
, ldb
.FLAG_MOD_REPLACE
,
361 logger
.warning("Seizing FSMO role on: %s (now owned by %s)"
362 % (role
.dn
, my_serviceName
))
365 # Remove the NTDS setting tree
367 logger
.info("Removing nTDSDSA: %s (and any children)" % ntds_dn
)
368 samdb
.delete(ntds_dn
, ["tree_delete:0"])
369 except LdbError
as e6
:
370 (enum
, estr
) = e6
.args
371 raise DemoteException("Failed to remove the DCs NTDS DSA object: %s"
374 offline_remove_server(samdb
, logger
, server_dn
,
375 remove_computer_obj
=remove_computer_obj
,
376 remove_server_obj
=remove_server_obj
,
377 remove_sysvol_obj
=remove_sysvol_obj
,
378 remove_dns_names
=remove_dns_names
,
379 remove_dns_account
=remove_dns_account
)
382 def remove_dc(samdb
, logger
, dc_name
):
384 # TODO: Check if this is the last server (covered mostly by
385 # refusing to remove our own name)
387 samdb
.transaction_start()
391 # Allow the name to be an nTDS-DSA GUID
393 ntds_guid
= uuid
.UUID(hex=dc_name
)
394 ntds_dn
= "<GUID=%s>" % ntds_guid
397 server_msgs
= samdb
.search(base
=samdb
.get_config_basedn(),
399 expression
="(&(objectClass=server)"
401 % ldb
.binary_encode(dc_name
))
402 except LdbError
as e3
:
403 (enum
, estr
) = e3
.args
404 raise DemoteException("Failure checking if %s is an server "
406 % (dc_name
, samdb
.domain_dns_name(), estr
))
408 if (len(server_msgs
) == 0):
409 samdb
.transaction_cancel()
410 raise DemoteException("%s is not an AD DC in %s"
411 % (dc_name
, samdb
.domain_dns_name()))
412 server_dn
= server_msgs
[0].dn
414 ntds_dn
= ldb
.Dn(samdb
, "CN=NTDS Settings")
415 ntds_dn
.add_base(server_dn
)
417 # Confirm this is really an ntdsDSA object
419 ntds_msgs
= samdb
.search(base
=ntds_dn
, attrs
=[], scope
=ldb
.SCOPE_BASE
,
420 expression
="(objectClass=ntdsdsa)")
421 except LdbError
as e7
:
422 (enum
, estr
) = e7
.args
423 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
426 samdb
.transaction_cancel()
427 raise DemoteException(
428 "Failure checking if %s is an NTDS DSA in %s: %s" %
429 (ntds_dn
, samdb
.domain_dns_name(), estr
))
431 # If the NTDS Settings child DN wasn't found or wasn't an ntdsDSA
432 # object, just remove the server object located above
433 if (len(ntds_msgs
) == 0):
434 if server_dn
is None:
435 samdb
.transaction_cancel()
436 raise DemoteException("%s is not an AD DC in %s"
437 % (dc_name
, samdb
.domain_dns_name()))
439 offline_remove_server(samdb
, logger
,
441 remove_computer_obj
=True,
442 remove_server_obj
=True,
443 remove_sysvol_obj
=True,
444 remove_dns_names
=True,
445 remove_dns_account
=True)
447 offline_remove_ntds_dc(samdb
, logger
,
449 remove_computer_obj
=True,
450 remove_server_obj
=True,
451 remove_connection_obj
=True,
452 seize_stale_fsmo
=True,
453 remove_sysvol_obj
=True,
454 remove_dns_names
=True,
455 remove_dns_account
=True)
457 samdb
.transaction_commit()
460 def offline_remove_dc_RemoveDsServer(samdb
, ntds_dn
):
462 samdb
.start_transaction()
464 offline_remove_ntds_dc(samdb
, ntds_dn
, None)
466 samdb
.commit_transaction()