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
28 class DemoteException(Exception):
29 """Base element for demote errors"""
31 def __init__(self
, value
):
35 return "DemoteException: " + self
.value
38 def remove_sysvol_references(samdb
, logger
, dc_name
):
39 # DNs under the Configuration DN:
40 realm
= samdb
.domain_dns_name()
41 for s
in ("CN=Enterprise,CN=Microsoft System Volumes,CN=System",
42 "CN=%s,CN=Microsoft System Volumes,CN=System" % realm
):
45 # This is verbose, but it is the safe, escape-proof way
46 # to add a base and add an arbitrary RDN.
47 if dn
.add_base(samdb
.get_config_basedn()) == False:
48 raise DemoteException("Failed constructing DN %s by adding base %s" \
49 % (dn
, samdb
.get_config_basedn()))
50 if dn
.add_child("CN=X") == False:
51 raise DemoteException("Failed constructing DN %s by adding child CN=X"\
53 dn
.set_component(0, "CN", dc_name
)
55 logger
.info("Removing Sysvol reference: %s" % dn
)
57 except ldb
.LdbError
as e
:
59 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
64 # DNs under the Domain DN:
65 for s
in ("CN=Domain System Volumes (SYSVOL share),CN=File Replication Service,CN=System",
66 "CN=Topology,CN=Domain System Volume,CN=DFSR-GlobalSettings,CN=System"):
67 # This is verbose, but it is the safe, escape-proof way
68 # to add a base and add an arbitrary RDN.
70 if dn
.add_base(samdb
.get_default_basedn()) == False:
71 raise DemoteException("Failed constructing DN %s by adding base" % \
72 (dn
, samdb
.get_default_basedn()))
73 if dn
.add_child("CN=X") == False:
74 raise DemoteException("Failed constructing DN %s by adding child "
75 "CN=X (soon to be CN=%s)" % (dn
, dc_name
))
76 dn
.set_component(0, "CN", dc_name
)
79 logger
.info("Removing Sysvol reference: %s" % dn
)
81 except ldb
.LdbError
as e1
:
82 (enum
, estr
) = e1
.args
83 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
89 def remove_dns_references(samdb
, logger
, dnsHostName
, ignore_no_name
=False):
91 # Check we are using in-database DNS
92 zones
= samdb
.search(base
="", scope
=ldb
.SCOPE_SUBTREE
,
93 expression
="(&(objectClass=dnsZone)(!(dc=RootDNSServers)))",
95 controls
=["search_options:0:2"])
99 dnsHostNameUpper
= dnsHostName
.upper()
102 (dn
, primary_recs
) = samdb
.dns_lookup(dnsHostName
)
103 except RuntimeError as e4
:
104 (enum
, estr
) = e4
.args
105 if enum
== werror
.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST
:
107 remove_hanging_dns_references(samdb
, logger
,
111 raise DemoteException("lookup of %s failed: %s" % (dnsHostName
, estr
))
112 samdb
.dns_replace(dnsHostName
, [])
114 res
= samdb
.search("",
115 scope
=ldb
.SCOPE_BASE
, attrs
=["namingContexts"])
117 ncs
= res
[0]["namingContexts"]
119 # Work out the set of names we will likely have an A record on by
120 # default. This is by default all the partitions of type
121 # domainDNS. By finding the canocial name of all the partitions,
122 # we find the likely candidates. We only remove the record if it
123 # maches the IP that was used by the dnsHostName. This avoids us
124 # needing to look a the dns_update_list file from in the demote
127 def dns_name_from_dn(dn
):
128 # The canonical string of DC=example,DC=com is
131 # The canonical string of CN=Configuration,DC=example,DC=com
132 # is example.com/Configuration
133 return ldb
.Dn(samdb
, dn
).canonical_str().split('/', 1)[0]
135 # By using a set here, duplicates via (eg) example.com/Configuration
136 # do not matter, they become just example.com
137 a_names_to_remove_from \
138 = set(dns_name_from_dn(dn
) for dn
in ncs
)
140 def a_rec_to_remove(dnsRecord
):
141 if dnsRecord
.wType
== DNS_TYPE_A
or dnsRecord
.wType
== DNS_TYPE_AAAA
:
142 for rec
in primary_recs
:
143 if rec
.wType
== dnsRecord
.wType
and rec
.data
== dnsRecord
.data
:
147 for a_name
in a_names_to_remove_from
:
149 logger
.debug("checking for DNS records to remove on %s" % a_name
)
150 (a_rec_dn
, a_recs
) = samdb
.dns_lookup(a_name
)
151 except RuntimeError as e2
:
152 (enum
, estr
) = e2
.args
153 if enum
== werror
.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST
:
155 raise DemoteException("lookup of %s failed: %s" % (a_name
, estr
))
157 orig_num_recs
= len(a_recs
)
158 a_recs
= [ r
for r
in a_recs
if not a_rec_to_remove(r
) ]
160 if len(a_recs
) != orig_num_recs
:
161 logger
.info("updating %s keeping %d values, removing %s values" % \
162 (a_name
, len(a_recs
), orig_num_recs
- len(a_recs
)))
163 samdb
.dns_replace(a_name
, a_recs
)
165 remove_hanging_dns_references(samdb
, logger
, dnsHostNameUpper
, zones
)
168 def remove_hanging_dns_references(samdb
, logger
, dnsHostNameUpper
, zones
):
170 # Find all the CNAME, NS, PTR and SRV records that point at the
171 # name we are removing
173 def to_remove(value
):
174 dnsRecord
= ndr_unpack(dnsp
.DnssrvRpcRecord
, value
)
175 if dnsRecord
.wType
== DNS_TYPE_NS \
176 or dnsRecord
.wType
== DNS_TYPE_CNAME \
177 or dnsRecord
.wType
== DNS_TYPE_PTR
:
178 if dnsRecord
.data
.upper() == dnsHostNameUpper
:
180 elif dnsRecord
.wType
== DNS_TYPE_SRV
:
181 if dnsRecord
.data
.nameTarget
.upper() == dnsHostNameUpper
:
186 logger
.debug("checking %s" % zone
.dn
)
187 records
= samdb
.search(base
=zone
.dn
, scope
=ldb
.SCOPE_SUBTREE
,
188 expression
="(&(objectClass=dnsNode)"
189 "(!(dNSTombstoned=TRUE)))",
191 for record
in records
:
193 orig_values
= record
["dnsRecord"]
197 # Remove references to dnsHostName in A, AAAA, NS, CNAME and SRV
198 values
= [ ndr_unpack(dnsp
.DnssrvRpcRecord
, v
)
199 for v
in orig_values
if not to_remove(v
) ]
201 if len(values
) != len(orig_values
):
202 logger
.info("updating %s keeping %d values, removing %s values" \
203 % (record
.dn
, len(values
),
204 len(orig_values
) - len(values
)))
206 # This requires the values to be unpacked, so this
207 # has been done in the list comprehension above
208 samdb
.dns_replace_by_dn(record
.dn
, values
)
211 def offline_remove_server(samdb
, logger
,
213 remove_computer_obj
=False,
214 remove_server_obj
=False,
215 remove_sysvol_obj
=False,
216 remove_dns_names
=False,
217 remove_dns_account
=False):
218 res
= samdb
.search("",
219 scope
=ldb
.SCOPE_BASE
, attrs
=["dsServiceName"])
221 my_serviceName
= res
[0]["dsServiceName"][0]
223 # Confirm this is really a server object
224 msgs
= samdb
.search(base
=server_dn
,
225 attrs
=["serverReference", "cn",
227 scope
=ldb
.SCOPE_BASE
,
228 expression
="(objectClass=server)")
230 dc_name
= str(msgs
[0]["cn"][0])
233 computer_dn
= ldb
.Dn(samdb
, msgs
[0]["serverReference"][0])
238 dnsHostName
= msgs
[0]["dnsHostName"][0]
242 if remove_server_obj
:
243 # Remove the server DN
244 samdb
.delete(server_dn
)
246 if computer_dn
is not None:
247 computer_msgs
= samdb
.search(base
=computer_dn
,
248 expression
="objectclass=computer",
249 attrs
=["msDS-KrbTgtLink",
252 scope
=ldb
.SCOPE_BASE
)
253 if "rIDSetReferences" in computer_msgs
[0]:
254 rid_set_dn
= str(computer_msgs
[0]["rIDSetReferences"][0])
255 logger
.info("Removing RID Set: %s" % rid_set_dn
)
256 samdb
.delete(rid_set_dn
)
257 if "msDS-KrbTgtLink" in computer_msgs
[0]:
258 krbtgt_link_dn
= str(computer_msgs
[0]["msDS-KrbTgtLink"][0])
259 logger
.info("Removing RODC KDC account: %s" % krbtgt_link_dn
)
260 samdb
.delete(krbtgt_link_dn
)
262 if remove_computer_obj
:
263 # Delete the computer tree
264 logger
.info("Removing computer account: %s (and any child objects)" % computer_dn
)
265 samdb
.delete(computer_dn
, ["tree_delete:0"])
267 if "dnsHostName" in msgs
[0]:
268 dnsHostName
= msgs
[0]["dnsHostName"][0]
270 if remove_dns_account
:
271 res
= samdb
.search(expression
="(&(objectclass=user)(cn=dns-%s)(servicePrincipalName=DNS/%s))" %
272 (ldb
.binary_encode(dc_name
), dnsHostName
),
273 attrs
=[], scope
=ldb
.SCOPE_SUBTREE
,
274 base
=samdb
.get_default_basedn())
276 logger
.info("Removing Samba-specific DNS service account: %s" % res
[0].dn
)
277 samdb
.delete(res
[0].dn
)
279 if dnsHostName
is not None and remove_dns_names
:
280 remove_dns_references(samdb
, logger
, dnsHostName
)
282 if remove_sysvol_obj
:
283 remove_sysvol_references(samdb
, logger
, dc_name
)
285 def offline_remove_ntds_dc(samdb
,
288 remove_computer_obj
=False,
289 remove_server_obj
=False,
290 remove_connection_obj
=False,
291 seize_stale_fsmo
=False,
292 remove_sysvol_obj
=False,
293 remove_dns_names
=False,
294 remove_dns_account
=False):
295 res
= samdb
.search("",
296 scope
=ldb
.SCOPE_BASE
, attrs
=["dsServiceName"])
298 my_serviceName
= ldb
.Dn(samdb
, res
[0]["dsServiceName"][0])
299 server_dn
= ntds_dn
.parent()
301 if my_serviceName
== ntds_dn
:
302 raise DemoteException("Refusing to demote our own DSA: %s " % my_serviceName
)
305 msgs
= samdb
.search(base
=ntds_dn
, expression
="objectClass=ntdsDSA",
306 attrs
=["objectGUID"], scope
=ldb
.SCOPE_BASE
)
307 except LdbError
as e5
:
308 (enum
, estr
) = e5
.args
309 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
310 raise DemoteException("Given DN %s doesn't exist" % ntds_dn
)
314 raise DemoteException("%s is not an ntdsda in %s"
315 % (ntds_dn
, samdb
.domain_dns_name()))
318 if (msg
.dn
.get_rdn_name() != "CN" or
319 msg
.dn
.get_rdn_value() != "NTDS Settings"):
320 raise DemoteException("Given DN (%s) wasn't the NTDS Settings DN" %
323 ntds_guid
= ndr_unpack(misc
.GUID
, msg
["objectGUID"][0])
325 if remove_connection_obj
:
326 # Find any nTDSConnection objects with that DC as the fromServer.
327 # We use the GUID to avoid issues with any () chars in a server
329 stale_connections
= samdb
.search(base
=samdb
.get_config_basedn(),
330 expression
="(&(objectclass=nTDSConnection)"
331 "(fromServer=<GUID=%s>))" % ntds_guid
)
332 for conn
in stale_connections
:
333 logger
.info("Removing nTDSConnection: %s" % conn
.dn
)
334 samdb
.delete(conn
.dn
)
337 stale_fsmo_roles
= samdb
.search(base
="", scope
=ldb
.SCOPE_SUBTREE
,
338 expression
="(fsmoRoleOwner=<GUID=%s>))"
340 controls
=["search_options:0:2"])
341 # Find any FSMO roles they have, give them to this server
343 for role
in stale_fsmo_roles
:
344 val
= str(my_serviceName
)
347 m
['value'] = ldb
.MessageElement(val
, ldb
.FLAG_MOD_REPLACE
,
349 logger
.warning("Seizing FSMO role on: %s (now owned by %s)"
350 % (role
.dn
, my_serviceName
))
353 # Remove the NTDS setting tree
355 logger
.info("Removing nTDSDSA: %s (and any children)" % ntds_dn
)
356 samdb
.delete(ntds_dn
, ["tree_delete:0"])
357 except LdbError
as e6
:
358 (enum
, estr
) = e6
.args
359 raise DemoteException("Failed to remove the DCs NTDS DSA object: %s"
362 offline_remove_server(samdb
, logger
, server_dn
,
363 remove_computer_obj
=remove_computer_obj
,
364 remove_server_obj
=remove_server_obj
,
365 remove_sysvol_obj
=remove_sysvol_obj
,
366 remove_dns_names
=remove_dns_names
,
367 remove_dns_account
=remove_dns_account
)
370 def remove_dc(samdb
, logger
, dc_name
):
372 # TODO: Check if this is the last server (covered mostly by
373 # refusing to remove our own name)
375 samdb
.transaction_start()
379 # Allow the name to be a the nTDS-DSA GUID
381 ntds_guid
= uuid
.UUID(hex=dc_name
)
382 ntds_dn
= "<GUID=%s>" % ntds_guid
385 server_msgs
= samdb
.search(base
=samdb
.get_config_basedn(),
387 expression
="(&(objectClass=server)"
389 % ldb
.binary_encode(dc_name
))
390 except LdbError
as e3
:
391 (enum
, estr
) = e3
.args
392 raise DemoteException("Failure checking if %s is an server "
394 % (dc_name
, samdb
.domain_dns_name()), estr
)
396 if (len(server_msgs
) == 0):
397 samdb
.transaction_cancel()
398 raise DemoteException("%s is not an AD DC in %s"
399 % (dc_name
, samdb
.domain_dns_name()))
400 server_dn
= server_msgs
[0].dn
402 ntds_dn
= ldb
.Dn(samdb
, "CN=NTDS Settings")
403 ntds_dn
.add_base(server_dn
)
406 # Confirm this is really an ntdsDSA object
408 ntds_msgs
= samdb
.search(base
=ntds_dn
, attrs
=[], scope
=ldb
.SCOPE_BASE
,
409 expression
="(objectClass=ntdsdsa)")
410 except LdbError
as e7
:
411 (enum
, estr
) = e7
.args
412 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
416 samdb
.transaction_cancel()
417 raise DemoteException("Failure checking if %s is an NTDS DSA in %s: "
418 % (ntds_dn
, samdb
.domain_dns_name()), estr
)
420 # If the NTDS Settings child DN wasn't found or wasnt an ntdsDSA
421 # object, just remove the server object located above
422 if (len(ntds_msgs
) == 0):
423 if server_dn
is None:
424 samdb
.transaction_cancel()
425 raise DemoteException("%s is not an AD DC in %s"
426 % (dc_name
, samdb
.domain_dns_name()))
428 offline_remove_server(samdb
, logger
,
430 remove_computer_obj
=True,
431 remove_server_obj
=True,
432 remove_sysvol_obj
=True,
433 remove_dns_names
=True,
434 remove_dns_account
=True)
436 offline_remove_ntds_dc(samdb
, logger
,
438 remove_computer_obj
=True,
439 remove_server_obj
=True,
440 remove_connection_obj
=True,
441 seize_stale_fsmo
=True,
442 remove_sysvol_obj
=True,
443 remove_dns_names
=True,
444 remove_dns_account
=True)
446 samdb
.transaction_commit()
450 def offline_remove_dc_RemoveDsServer(samdb
, ntds_dn
):
452 samdb
.start_transaction()
454 offline_remove_ntds_dc(samdb
, ntds_dn
, None)
456 samdb
.commit_transaction()