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
.ndr
import ndr_unpack
23 from samba
.dcerpc
import misc
, dnsp
24 from samba
.dcerpc
.dnsp
import DNS_TYPE_NS
, DNS_TYPE_A
, DNS_TYPE_AAAA
, \
25 DNS_TYPE_CNAME
, DNS_TYPE_SRV
, DNS_TYPE_PTR
27 class DemoteException(Exception):
28 """Base element for demote errors"""
30 def __init__(self
, value
):
34 return "DemoteException: " + self
.value
37 def remove_sysvol_references(samdb
, logger
, dc_name
):
38 # DNs under the Configuration DN:
39 realm
= samdb
.domain_dns_name()
40 for s
in ("CN=Enterprise,CN=Microsoft System Volumes,CN=System",
41 "CN=%s,CN=Microsoft System Volumes,CN=System" % realm
):
44 # This is verbose, but it is the safe, escape-proof way
45 # to add a base and add an arbitrary RDN.
46 if dn
.add_base(samdb
.get_config_basedn()) == False:
47 raise DemoteException("Failed constructing DN %s by adding base %s" \
48 % (dn
, samdb
.get_config_basedn()))
49 if dn
.add_child("CN=X") == False:
50 raise DemoteException("Failed constructing DN %s by adding child CN=X"\
52 dn
.set_component(0, "CN", dc_name
)
54 logger
.info("Removing Sysvol reference: %s" % dn
)
56 except ldb
.LdbError
as (enum
, estr
):
57 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
62 # DNs under the Domain DN:
63 for s
in ("CN=Domain System Volumes (SYSVOL share),CN=File Replication Service,CN=System",
64 "CN=Topology,CN=Domain System Volume,CN=DFSR-GlobalSettings,CN=System"):
65 # This is verbose, but it is the safe, escape-proof way
66 # to add a base and add an arbitrary RDN.
68 if dn
.add_base(samdb
.get_default_basedn()) == False:
69 raise DemoteException("Failed constructing DN %s by adding base" % \
70 (dn
, samdb
.get_default_basedn()))
71 if dn
.add_child("CN=X") == False:
72 raise DemoteException("Failed constructing DN %s by adding child %s"\
74 dn
.set_component(0, "CN", dc_name
)
77 logger
.info("Removing Sysvol reference: %s" % dn
)
79 except ldb
.LdbError
as (enum
, estr
):
80 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
86 def remove_dns_references(samdb
, logger
, dnsHostName
):
88 # Check we are using in-database DNS
89 zones
= samdb
.search(base
="", scope
=ldb
.SCOPE_SUBTREE
,
90 expression
="(&(objectClass=dnsZone)(!(dc=RootDNSServers)))",
92 controls
=["search_options:0:2"])
96 dnsHostNameUpper
= dnsHostName
.upper()
99 primary_recs
= samdb
.dns_lookup(dnsHostName
)
100 except RuntimeError as (enum
, estr
):
101 if enum
== 0x000025F2: #WERR_DNS_ERROR_NAME_DOES_NOT_EXIST
103 raise DemoteException("lookup of %s failed: %s" % (dnsHostName
, estr
))
104 samdb
.dns_replace(dnsHostName
, [])
106 res
= samdb
.search("",
107 scope
=ldb
.SCOPE_BASE
, attrs
=["namingContexts"])
109 ncs
= res
[0]["namingContexts"]
111 # Work out the set of names we will likely have an A record on by
112 # default. This is by default all the partitions of type
113 # domainDNS. By finding the canocial name of all the partitions,
114 # we find the likely candidates. We only remove the record if it
115 # maches the IP that was used by the dnsHostName. This avoids us
116 # needing to look a the dns_update_list file from in the demote
119 def dns_name_from_dn(dn
):
120 # The canonical string of DC=example,DC=com is
123 # The canonical string of CN=Configuration,DC=example,DC=com
124 # is example.com/Configuration
125 return ldb
.Dn(samdb
, dn
).canonical_str().split('/', 1)[0]
127 # By using a set here, duplicates via (eg) example.com/Configuration
128 # do not matter, they become just example.com
129 a_names_to_remove_from \
130 = set(dns_name_from_dn(dn
) for dn
in ncs
)
132 def a_rec_to_remove(dnsRecord
):
133 if dnsRecord
.wType
== DNS_TYPE_A
or dnsRecord
.wType
== DNS_TYPE_AAAA
:
134 for rec
in primary_recs
:
135 if rec
.wType
== dnsRecord
.wType
and rec
.data
== dnsRecord
.data
:
139 for a_name
in a_names_to_remove_from
:
141 logger
.debug("checking for DNS records to remove on %s" % a_name
)
142 a_recs
= samdb
.dns_lookup(a_name
)
143 except RuntimeError as (enum
, estr
):
144 if enum
== 0x000025F2: #WERR_DNS_ERROR_NAME_DOES_NOT_EXIST
146 raise DemoteException("lookup of %s failed: %s" % (a_name
, estr
))
148 orig_num_recs
= len(a_recs
)
149 a_recs
= [ r
for r
in a_recs
if not a_rec_to_remove(r
) ]
151 if len(a_recs
) != orig_num_recs
:
152 logger
.info("updating %s keeping %d values, removing %s values" % \
153 (a_name
, len(a_recs
), orig_num_recs
- len(a_recs
)))
154 samdb
.dns_replace(a_name
, a_recs
)
156 # Find all the CNAME, NS, PTR and SRV records that point at the
157 # name we are removing
159 def to_remove(value
):
160 dnsRecord
= ndr_unpack(dnsp
.DnssrvRpcRecord
, value
)
161 if dnsRecord
.wType
== DNS_TYPE_NS \
162 or dnsRecord
.wType
== DNS_TYPE_CNAME \
163 or dnsRecord
.wType
== DNS_TYPE_PTR
:
164 if dnsRecord
.data
.upper() == dnsHostNameUpper
:
166 elif dnsRecord
.wType
== DNS_TYPE_SRV
:
167 if dnsRecord
.data
.nameTarget
.upper() == dnsHostNameUpper
:
172 logger
.debug("checking %s" % zone
.dn
)
173 records
= samdb
.search(base
=zone
.dn
, scope
=ldb
.SCOPE_SUBTREE
,
174 expression
="(&(objectClass=dnsNode)"
175 "(!(dNSTombstoned=TRUE)))",
177 for record
in records
:
179 values
= record
["dnsRecord"]
182 orig_num_values
= len(values
)
184 # Remove references to dnsHostName in A, AAAA, NS, CNAME and SRV
185 values
= [ ndr_unpack(dnsp
.DnssrvRpcRecord
, v
)
186 for v
in values
if not to_remove(v
) ]
188 if len(values
) != orig_num_values
:
189 logger
.info("updating %s keeping %d values, removing %s values" \
190 % (record
.dn
, len(values
),
191 orig_num_values
- len(values
)))
193 # This requires the values to be unpacked, so this
194 # has been done in the list comprehension above
195 samdb
.dns_replace_by_dn(record
.dn
, values
)
197 def offline_remove_server(samdb
, logger
,
199 remove_computer_obj
=False,
200 remove_server_obj
=False,
201 remove_sysvol_obj
=False,
202 remove_dns_names
=False,
203 remove_dns_account
=False):
204 res
= samdb
.search("",
205 scope
=ldb
.SCOPE_BASE
, attrs
=["dsServiceName"])
207 my_serviceName
= res
[0]["dsServiceName"][0]
209 # Confirm this is really a server object
210 msgs
= samdb
.search(base
=server_dn
,
211 attrs
=["serverReference", "cn",
213 scope
=ldb
.SCOPE_BASE
,
214 expression
="(objectClass=server)")
216 dc_name
= str(msgs
[0]["cn"][0])
219 computer_dn
= ldb
.Dn(samdb
, msgs
[0]["serverReference"][0])
224 dnsHostName
= msgs
[0]["dnsHostName"][0]
228 if remove_server_obj
:
229 # Remove the server DN
230 samdb
.delete(server_dn
)
232 if computer_dn
is not None:
233 computer_msgs
= samdb
.search(base
=computer_dn
,
234 expression
="objectclass=computer",
235 attrs
=["msDS-KrbTgtLink",
238 scope
=ldb
.SCOPE_BASE
)
239 if "rIDSetReferences" in computer_msgs
[0]:
240 rid_set_dn
= str(computer_msgs
[0]["rIDSetReferences"][0])
241 logger
.info("Removing RID Set: %s" % rid_set_dn
)
242 samdb
.delete(rid_set_dn
)
243 if "msDS-KrbTgtLink" in computer_msgs
[0]:
244 krbtgt_link_dn
= str(computer_msgs
[0]["msDS-KrbTgtLink"][0])
245 logger
.info("Removing RODC KDC account: %s" % krbtgt_link_dn
)
246 samdb
.delete(krbtgt_link_dn
)
248 if remove_computer_obj
:
249 # Delete the computer tree
250 logger
.info("Removing computer account: %s (and any child objects)" % computer_dn
)
251 samdb
.delete(computer_dn
, ["tree_delete:0"])
253 if "dnsHostName" in msgs
[0]:
254 dnsHostName
= msgs
[0]["dnsHostName"][0]
256 if remove_dns_account
:
257 res
= samdb
.search(expression
="(&(objectclass=user)(cn=dns-%s)(servicePrincipalName=DNS/%s))" %
258 (ldb
.binary_encode(dc_name
), dnsHostName
),
259 attrs
=[], scope
=ldb
.SCOPE_SUBTREE
,
260 base
=samdb
.get_default_basedn())
262 logger
.info("Removing Samba-specific DNS service account: %s" % res
[0].dn
)
263 samdb
.delete(res
[0].dn
)
265 if dnsHostName
is not None and remove_dns_names
:
266 remove_dns_references(samdb
, logger
, dnsHostName
)
268 if remove_sysvol_obj
:
269 remove_sysvol_references(samdb
, logger
, dc_name
)
271 def offline_remove_ntds_dc(samdb
,
274 remove_computer_obj
=False,
275 remove_server_obj
=False,
276 remove_connection_obj
=False,
277 seize_stale_fsmo
=False,
278 remove_sysvol_obj
=False,
279 remove_dns_names
=False,
280 remove_dns_account
=False):
281 res
= samdb
.search("",
282 scope
=ldb
.SCOPE_BASE
, attrs
=["dsServiceName"])
284 my_serviceName
= ldb
.Dn(samdb
, res
[0]["dsServiceName"][0])
285 server_dn
= ntds_dn
.parent()
287 if my_serviceName
== ntds_dn
:
288 raise DemoteException("Refusing to demote our own DSA: %s " % my_serviceName
)
291 msgs
= samdb
.search(base
=ntds_dn
, expression
="objectClass=ntdsDSA",
292 attrs
=["objectGUID"], scope
=ldb
.SCOPE_BASE
)
293 except LdbError
as (enum
, estr
):
294 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
295 raise DemoteException("Given DN %s doesn't exist" % ntds_dn
)
299 raise DemoteException("%s is not an ntdsda in %s"
300 % (ntds_dn
, samdb
.domain_dns_name()))
303 if (msg
.dn
.get_rdn_name() != "CN" or
304 msg
.dn
.get_rdn_value() != "NTDS Settings"):
305 raise DemoteException("Given DN (%s) wasn't the NTDS Settings DN" %
308 ntds_guid
= ndr_unpack(misc
.GUID
, msg
["objectGUID"][0])
310 if remove_connection_obj
:
311 # Find any nTDSConnection objects with that DC as the fromServer.
312 # We use the GUID to avoid issues with any () chars in a server
314 stale_connections
= samdb
.search(base
=samdb
.get_config_basedn(),
315 expression
="(&(objectclass=nTDSConnection)"
316 "(fromServer=<GUID=%s>))" % ntds_guid
)
317 for conn
in stale_connections
:
318 logger
.info("Removing nTDSConnection: %s" % conn
.dn
)
319 samdb
.delete(conn
.dn
)
322 stale_fsmo_roles
= samdb
.search(base
="", scope
=ldb
.SCOPE_SUBTREE
,
323 expression
="(fsmoRoleOwner=<GUID=%s>))"
325 controls
=["search_options:0:2"])
326 # Find any FSMO roles they have, give them to this server
328 for role
in stale_fsmo_roles
:
329 val
= str(my_serviceName
)
332 m
['value'] = ldb
.MessageElement(val
, ldb
.FLAG_MOD_REPLACE
,
334 logger
.warning("Seizing FSMO role on: %s (now owned by %s)"
335 % (role
.dn
, my_serviceName
))
338 # Remove the NTDS setting tree
340 logger
.info("Removing nTDSDSA: %s (and any children)" % ntds_dn
)
341 samdb
.delete(ntds_dn
, ["tree_delete:0"])
342 except LdbError
as (enum
, estr
):
343 raise DemoteException("Failed to remove the DCs NTDS DSA object: %s"
346 offline_remove_server(samdb
, logger
, server_dn
,
347 remove_computer_obj
=remove_computer_obj
,
348 remove_server_obj
=remove_server_obj
,
349 remove_sysvol_obj
=remove_sysvol_obj
,
350 remove_dns_names
=remove_dns_names
,
351 remove_dns_account
=remove_dns_account
)
354 def remove_dc(samdb
, logger
, dc_name
):
356 # TODO: Check if this is the last server (covered mostly by
357 # refusing to remove our own name)
359 samdb
.transaction_start()
363 # Allow the name to be a the nTDS-DSA GUID
365 ntds_guid
= uuid
.UUID(hex=dc_name
)
366 ntds_dn
= "<GUID=%s>" % ntds_guid
369 server_msgs
= samdb
.search(base
=samdb
.get_config_basedn(),
371 expression
="(&(objectClass=server)"
373 % ldb
.binary_encode(dc_name
))
374 except LdbError
as (enum
, estr
):
375 raise DemoteException("Failure checking if %s is an server "
377 % (dc_name
, samdb
.domain_dns_name()), estr
)
379 if (len(server_msgs
) == 0):
380 raise DemoteException("%s is not an AD DC in %s"
381 % (dc_name
, samdb
.domain_dns_name()))
382 server_dn
= server_msgs
[0].dn
384 ntds_dn
= ldb
.Dn(samdb
, "CN=NTDS Settings")
385 ntds_dn
.add_base(server_dn
)
388 # Confirm this is really an ntdsDSA object
390 ntds_msgs
= samdb
.search(base
=ntds_dn
, attrs
=[], scope
=ldb
.SCOPE_BASE
,
391 expression
="(objectClass=ntdsdsa)")
392 except LdbError
as (enum
, estr
):
393 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
397 raise DemoteException("Failure checking if %s is an NTDS DSA in %s: "
398 % (ntds_dn
, samdb
.domain_dns_name()), estr
)
400 # If the NTDS Settings child DN wasn't found or wasnt an ntdsDSA
401 # object, just remove the server object located above
402 if (len(ntds_msgs
) == 0):
403 if server_dn
is None:
404 raise DemoteException("%s is not an AD DC in %s"
405 % (dc_name
, samdb
.domain_dns_name()))
407 offline_remove_server(samdb
, logger
,
409 remove_computer_obj
=True,
410 remove_server_obj
=True,
411 remove_sysvol_obj
=True,
412 remove_dns_names
=True,
413 remove_dns_account
=True)
415 offline_remove_ntds_dc(samdb
, logger
,
417 remove_computer_obj
=True,
418 remove_server_obj
=True,
419 remove_connection_obj
=True,
420 seize_stale_fsmo
=True,
421 remove_sysvol_obj
=True,
422 remove_dns_names
=True,
423 remove_dns_account
=True)
425 samdb
.transaction_commit()
429 def offline_remove_dc_RemoveDsServer(samdb
, ntds_dn
):
431 samdb
.start_transaction()
433 offline_remove_ntds_dc(samdb
, ntds_dn
, None)
435 samdb
.commit_transaction()