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.
48 if dn
.add_base(samdb
.get_config_basedn()) == False:
49 raise DemoteException("Failed constructing DN %s by adding base %s"
50 % (dn
, samdb
.get_config_basedn()))
51 if dn
.add_child("CN=X") == False:
52 raise DemoteException("Failed constructing DN %s by adding child CN=X"
54 dn
.set_component(0, "CN", dc_name
)
56 logger
.info("Removing Sysvol reference: %s" % dn
)
58 except ldb
.LdbError
as e
:
60 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
65 # DNs under the Domain DN:
66 for s
in ("CN=Domain System Volumes (SYSVOL share),CN=File Replication Service,CN=System",
67 "CN=Topology,CN=Domain System Volume,CN=DFSR-GlobalSettings,CN=System"):
68 # This is verbose, but it is the safe, escape-proof way
69 # to add a base and add an arbitrary RDN.
71 if dn
.add_base(samdb
.get_default_basedn()) == False:
72 raise DemoteException("Failed constructing DN %s by adding base %s"
73 % (dn
, samdb
.get_default_basedn()))
74 if dn
.add_child("CN=X") == False:
75 raise DemoteException("Failed constructing DN %s by adding child "
76 "CN=X (soon to be CN=%s)" % (dn
, dc_name
))
77 dn
.set_component(0, "CN", dc_name
)
80 logger
.info("Removing Sysvol reference: %s" % dn
)
82 except ldb
.LdbError
as e1
:
83 (enum
, estr
) = e1
.args
84 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
90 def remove_dns_references(samdb
, logger
, dnsHostName
, ignore_no_name
=False):
92 # Check we are using in-database DNS
93 zones
= samdb
.search(base
="", scope
=ldb
.SCOPE_SUBTREE
,
94 expression
="(&(objectClass=dnsZone)(!(dc=RootDNSServers)))",
96 controls
=["search_options:0:2"])
100 dnsHostNameUpper
= dnsHostName
.upper()
103 (dn
, primary_recs
) = samdb
.dns_lookup(dnsHostName
)
104 except RuntimeError as e4
:
105 (enum
, estr
) = e4
.args
106 if (enum
== werror
.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST
or
107 enum
== werror
.WERR_DNS_ERROR_RCODE_NAME_ERROR
):
109 remove_hanging_dns_references(samdb
, logger
,
113 raise DemoteException("lookup of %s failed: %s" % (dnsHostName
, estr
))
114 samdb
.dns_replace(dnsHostName
, [])
116 res
= samdb
.search("",
117 scope
=ldb
.SCOPE_BASE
, attrs
=["namingContexts"])
119 ncs
= res
[0]["namingContexts"]
121 # Work out the set of names we will likely have an A record on by
122 # default. This is by default all the partitions of type
123 # domainDNS. By finding the canocial name of all the partitions,
124 # we find the likely candidates. We only remove the record if it
125 # maches the IP that was used by the dnsHostName. This avoids us
126 # needing to look a the dns_update_list file from in the demote
129 def dns_name_from_dn(dn
):
130 # The canonical string of DC=example,DC=com is
133 # The canonical string of CN=Configuration,DC=example,DC=com
134 # is example.com/Configuration
135 return ldb
.Dn(samdb
, dn
).canonical_str().split('/', 1)[0]
137 # By using a set here, duplicates via (eg) example.com/Configuration
138 # do not matter, they become just example.com
139 a_names_to_remove_from \
140 = set(dns_name_from_dn(str(dn
)) for dn
in ncs
)
142 def a_rec_to_remove(dnsRecord
):
143 if dnsRecord
.wType
== DNS_TYPE_A
or dnsRecord
.wType
== DNS_TYPE_AAAA
:
144 for rec
in primary_recs
:
145 if rec
.wType
== dnsRecord
.wType
and rec
.data
== dnsRecord
.data
:
149 for a_name
in a_names_to_remove_from
:
151 logger
.debug("checking for DNS records to remove on %s" % a_name
)
152 (a_rec_dn
, a_recs
) = samdb
.dns_lookup(a_name
)
153 except RuntimeError as e2
:
154 (enum
, estr
) = e2
.args
155 if enum
== werror
.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST
:
157 raise DemoteException("lookup of %s failed: %s" % (a_name
, estr
))
159 orig_num_recs
= len(a_recs
)
160 a_recs
= [r
for r
in a_recs
if not a_rec_to_remove(r
)]
162 if len(a_recs
) != orig_num_recs
:
163 logger
.info("updating %s keeping %d values, removing %s values" %
164 (a_name
, len(a_recs
), orig_num_recs
- len(a_recs
)))
165 samdb
.dns_replace(a_name
, a_recs
)
167 remove_hanging_dns_references(samdb
, logger
, dnsHostNameUpper
, zones
)
170 def remove_hanging_dns_references(samdb
, logger
, dnsHostNameUpper
, zones
):
172 # Find all the CNAME, NS, PTR and SRV records that point at the
173 # name we are removing
175 def to_remove(value
):
176 dnsRecord
= ndr_unpack(dnsp
.DnssrvRpcRecord
, value
)
177 if dnsRecord
.wType
== DNS_TYPE_NS \
178 or dnsRecord
.wType
== DNS_TYPE_CNAME \
179 or dnsRecord
.wType
== DNS_TYPE_PTR
:
180 if dnsRecord
.data
.upper() == dnsHostNameUpper
:
182 elif dnsRecord
.wType
== DNS_TYPE_SRV
:
183 if dnsRecord
.data
.nameTarget
.upper() == dnsHostNameUpper
:
188 logger
.debug("checking %s" % zone
.dn
)
189 records
= samdb
.search(base
=zone
.dn
, scope
=ldb
.SCOPE_SUBTREE
,
190 expression
="(&(objectClass=dnsNode)"
191 "(!(dNSTombstoned=TRUE)))",
193 for record
in records
:
195 orig_values
= record
["dnsRecord"]
199 # Remove references to dnsHostName in A, AAAA, NS, CNAME and SRV
200 values
= [ndr_unpack(dnsp
.DnssrvRpcRecord
, v
)
201 for v
in orig_values
if not to_remove(v
)]
203 if len(values
) != len(orig_values
):
204 logger
.info("updating %s keeping %d values, removing %s values"
205 % (record
.dn
, len(values
),
206 len(orig_values
) - len(values
)))
208 # This requires the values to be unpacked, so this
209 # has been done in the list comprehension above
210 samdb
.dns_replace_by_dn(record
.dn
, values
)
213 def offline_remove_server(samdb
, logger
,
215 remove_computer_obj
=False,
216 remove_server_obj
=False,
217 remove_sysvol_obj
=False,
218 remove_dns_names
=False,
219 remove_dns_account
=False):
220 res
= samdb
.search("",
221 scope
=ldb
.SCOPE_BASE
, attrs
=["dsServiceName"])
223 my_serviceName
= res
[0]["dsServiceName"][0]
225 # Confirm this is really a server object
226 msgs
= samdb
.search(base
=server_dn
,
227 attrs
=["serverReference", "cn",
229 scope
=ldb
.SCOPE_BASE
,
230 expression
="(objectClass=server)")
232 dc_name
= str(msg
["cn"][0])
235 computer_dn
= ldb
.Dn(samdb
, msg
["serverReference"][0].decode('utf8'))
240 dnsHostName
= str(msg
["dnsHostName"][0])
244 if remove_server_obj
:
245 # Remove the server DN (do a tree-delete as it could still have a
246 # 'DNS Settings' child object if it's a Windows DC)
247 samdb
.delete(server_dn
, ["tree_delete:0"])
249 if computer_dn
is not None:
250 computer_msgs
= samdb
.search(base
=computer_dn
,
251 expression
="objectclass=computer",
252 attrs
=["msDS-KrbTgtLink",
255 scope
=ldb
.SCOPE_BASE
)
256 if "rIDSetReferences" in computer_msgs
[0]:
257 rid_set_dn
= str(computer_msgs
[0]["rIDSetReferences"][0])
258 logger
.info("Removing RID Set: %s" % rid_set_dn
)
259 samdb
.delete(rid_set_dn
)
260 if "msDS-KrbTgtLink" in computer_msgs
[0]:
261 krbtgt_link_dn
= str(computer_msgs
[0]["msDS-KrbTgtLink"][0])
262 logger
.info("Removing RODC KDC account: %s" % krbtgt_link_dn
)
263 samdb
.delete(krbtgt_link_dn
)
265 if remove_computer_obj
:
266 # Delete the computer tree
267 logger
.info("Removing computer account: %s (and any child objects)" % computer_dn
)
268 samdb
.delete(computer_dn
, ["tree_delete:0"])
270 if "dnsHostName" in msg
:
271 dnsHostName
= str(msg
["dnsHostName"][0])
273 if remove_dns_account
:
274 res
= samdb
.search(expression
="(&(objectclass=user)(cn=dns-%s)(servicePrincipalName=DNS/%s))" %
275 (ldb
.binary_encode(dc_name
), dnsHostName
),
276 attrs
=[], scope
=ldb
.SCOPE_SUBTREE
,
277 base
=samdb
.get_default_basedn())
279 logger
.info("Removing Samba-specific DNS service account: %s" % res
[0].dn
)
280 samdb
.delete(res
[0].dn
)
282 if dnsHostName
is not None and remove_dns_names
:
283 remove_dns_references(samdb
, logger
, dnsHostName
)
285 if remove_sysvol_obj
:
286 remove_sysvol_references(samdb
, logger
, dc_name
)
289 def offline_remove_ntds_dc(samdb
,
292 remove_computer_obj
=False,
293 remove_server_obj
=False,
294 remove_connection_obj
=False,
295 seize_stale_fsmo
=False,
296 remove_sysvol_obj
=False,
297 remove_dns_names
=False,
298 remove_dns_account
=False):
299 res
= samdb
.search("",
300 scope
=ldb
.SCOPE_BASE
, attrs
=["dsServiceName"])
302 my_serviceName
= ldb
.Dn(samdb
, res
[0]["dsServiceName"][0].decode('utf8'))
303 server_dn
= ntds_dn
.parent()
305 if my_serviceName
== ntds_dn
:
306 raise DemoteException("Refusing to demote our own DSA: %s " % my_serviceName
)
309 msgs
= samdb
.search(base
=ntds_dn
, expression
="objectClass=ntdsDSA",
310 attrs
=["objectGUID"], scope
=ldb
.SCOPE_BASE
)
311 except LdbError
as e5
:
312 (enum
, estr
) = e5
.args
313 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
314 raise DemoteException("Given DN %s doesn't exist" % ntds_dn
)
318 raise DemoteException("%s is not an ntdsda in %s"
319 % (ntds_dn
, samdb
.domain_dns_name()))
322 if (msg
.dn
.get_rdn_name() != "CN" or
323 msg
.dn
.get_rdn_value() != "NTDS Settings"):
324 raise DemoteException("Given DN (%s) wasn't the NTDS Settings DN" %
327 ntds_guid
= ndr_unpack(misc
.GUID
, msg
["objectGUID"][0])
329 if remove_connection_obj
:
330 # Find any nTDSConnection objects with that DC as the fromServer.
331 # We use the GUID to avoid issues with any () chars in a server
333 stale_connections
= samdb
.search(base
=samdb
.get_config_basedn(),
334 expression
="(&(objectclass=nTDSConnection)"
335 "(fromServer=<GUID=%s>))" % ntds_guid
)
336 for conn
in stale_connections
:
337 logger
.info("Removing nTDSConnection: %s" % conn
.dn
)
338 samdb
.delete(conn
.dn
)
341 stale_fsmo_roles
= samdb
.search(base
="", scope
=ldb
.SCOPE_SUBTREE
,
342 expression
="(fsmoRoleOwner=<GUID=%s>))"
344 controls
=["search_options:0:2"])
345 # Find any FSMO roles they have, give them to this server
347 for role
in stale_fsmo_roles
:
348 val
= str(my_serviceName
)
351 m
['value'] = ldb
.MessageElement(val
, ldb
.FLAG_MOD_REPLACE
,
353 logger
.warning("Seizing FSMO role on: %s (now owned by %s)"
354 % (role
.dn
, my_serviceName
))
357 # Remove the NTDS setting tree
359 logger
.info("Removing nTDSDSA: %s (and any children)" % ntds_dn
)
360 samdb
.delete(ntds_dn
, ["tree_delete:0"])
361 except LdbError
as e6
:
362 (enum
, estr
) = e6
.args
363 raise DemoteException("Failed to remove the DCs NTDS DSA object: %s"
366 offline_remove_server(samdb
, logger
, server_dn
,
367 remove_computer_obj
=remove_computer_obj
,
368 remove_server_obj
=remove_server_obj
,
369 remove_sysvol_obj
=remove_sysvol_obj
,
370 remove_dns_names
=remove_dns_names
,
371 remove_dns_account
=remove_dns_account
)
374 def remove_dc(samdb
, logger
, dc_name
):
376 # TODO: Check if this is the last server (covered mostly by
377 # refusing to remove our own name)
379 samdb
.transaction_start()
383 # Allow the name to be a the nTDS-DSA GUID
385 ntds_guid
= uuid
.UUID(hex=dc_name
)
386 ntds_dn
= "<GUID=%s>" % ntds_guid
389 server_msgs
= samdb
.search(base
=samdb
.get_config_basedn(),
391 expression
="(&(objectClass=server)"
393 % ldb
.binary_encode(dc_name
))
394 except LdbError
as e3
:
395 (enum
, estr
) = e3
.args
396 raise DemoteException("Failure checking if %s is an server "
398 % (dc_name
, samdb
.domain_dns_name(), estr
))
400 if (len(server_msgs
) == 0):
401 samdb
.transaction_cancel()
402 raise DemoteException("%s is not an AD DC in %s"
403 % (dc_name
, samdb
.domain_dns_name()))
404 server_dn
= server_msgs
[0].dn
406 ntds_dn
= ldb
.Dn(samdb
, "CN=NTDS Settings")
407 ntds_dn
.add_base(server_dn
)
410 # Confirm this is really an ntdsDSA object
412 ntds_msgs
= samdb
.search(base
=ntds_dn
, attrs
=[], scope
=ldb
.SCOPE_BASE
,
413 expression
="(objectClass=ntdsdsa)")
414 except LdbError
as e7
:
415 (enum
, estr
) = e7
.args
416 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
420 samdb
.transaction_cancel()
421 raise DemoteException(
422 "Failure checking if %s is an NTDS DSA in %s: %s" %
423 (ntds_dn
, samdb
.domain_dns_name(), estr
))
425 # If the NTDS Settings child DN wasn't found or wasnt an ntdsDSA
426 # object, just remove the server object located above
427 if (len(ntds_msgs
) == 0):
428 if server_dn
is None:
429 samdb
.transaction_cancel()
430 raise DemoteException("%s is not an AD DC in %s"
431 % (dc_name
, samdb
.domain_dns_name()))
433 offline_remove_server(samdb
, logger
,
435 remove_computer_obj
=True,
436 remove_server_obj
=True,
437 remove_sysvol_obj
=True,
438 remove_dns_names
=True,
439 remove_dns_account
=True)
441 offline_remove_ntds_dc(samdb
, logger
,
443 remove_computer_obj
=True,
444 remove_server_obj
=True,
445 remove_connection_obj
=True,
446 seize_stale_fsmo
=True,
447 remove_sysvol_obj
=True,
448 remove_dns_names
=True,
449 remove_dns_account
=True)
451 samdb
.transaction_commit()
454 def offline_remove_dc_RemoveDsServer(samdb
, ntds_dn
):
456 samdb
.start_transaction()
458 offline_remove_ntds_dc(samdb
, ntds_dn
, None)
460 samdb
.commit_transaction()