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 orig_values
= record
["dnsRecord"]
183 # Remove references to dnsHostName in A, AAAA, NS, CNAME and SRV
184 values
= [ ndr_unpack(dnsp
.DnssrvRpcRecord
, v
)
185 for v
in orig_values
if not to_remove(v
) ]
187 if len(values
) != len(orig_values
):
188 logger
.info("updating %s keeping %d values, removing %s values" \
189 % (record
.dn
, len(values
),
190 len(orig_values
) - len(values
)))
192 # This requires the values to be unpacked, so this
193 # has been done in the list comprehension above
194 samdb
.dns_replace_by_dn(record
.dn
, values
)
196 def offline_remove_server(samdb
, logger
,
198 remove_computer_obj
=False,
199 remove_server_obj
=False,
200 remove_sysvol_obj
=False,
201 remove_dns_names
=False,
202 remove_dns_account
=False):
203 res
= samdb
.search("",
204 scope
=ldb
.SCOPE_BASE
, attrs
=["dsServiceName"])
206 my_serviceName
= res
[0]["dsServiceName"][0]
208 # Confirm this is really a server object
209 msgs
= samdb
.search(base
=server_dn
,
210 attrs
=["serverReference", "cn",
212 scope
=ldb
.SCOPE_BASE
,
213 expression
="(objectClass=server)")
215 dc_name
= str(msgs
[0]["cn"][0])
218 computer_dn
= ldb
.Dn(samdb
, msgs
[0]["serverReference"][0])
223 dnsHostName
= msgs
[0]["dnsHostName"][0]
227 if remove_server_obj
:
228 # Remove the server DN
229 samdb
.delete(server_dn
)
231 if computer_dn
is not None:
232 computer_msgs
= samdb
.search(base
=computer_dn
,
233 expression
="objectclass=computer",
234 attrs
=["msDS-KrbTgtLink",
237 scope
=ldb
.SCOPE_BASE
)
238 if "rIDSetReferences" in computer_msgs
[0]:
239 rid_set_dn
= str(computer_msgs
[0]["rIDSetReferences"][0])
240 logger
.info("Removing RID Set: %s" % rid_set_dn
)
241 samdb
.delete(rid_set_dn
)
242 if "msDS-KrbTgtLink" in computer_msgs
[0]:
243 krbtgt_link_dn
= str(computer_msgs
[0]["msDS-KrbTgtLink"][0])
244 logger
.info("Removing RODC KDC account: %s" % krbtgt_link_dn
)
245 samdb
.delete(krbtgt_link_dn
)
247 if remove_computer_obj
:
248 # Delete the computer tree
249 logger
.info("Removing computer account: %s (and any child objects)" % computer_dn
)
250 samdb
.delete(computer_dn
, ["tree_delete:0"])
252 if "dnsHostName" in msgs
[0]:
253 dnsHostName
= msgs
[0]["dnsHostName"][0]
255 if remove_dns_account
:
256 res
= samdb
.search(expression
="(&(objectclass=user)(cn=dns-%s)(servicePrincipalName=DNS/%s))" %
257 (ldb
.binary_encode(dc_name
), dnsHostName
),
258 attrs
=[], scope
=ldb
.SCOPE_SUBTREE
,
259 base
=samdb
.get_default_basedn())
261 logger
.info("Removing Samba-specific DNS service account: %s" % res
[0].dn
)
262 samdb
.delete(res
[0].dn
)
264 if dnsHostName
is not None and remove_dns_names
:
265 remove_dns_references(samdb
, logger
, dnsHostName
)
267 if remove_sysvol_obj
:
268 remove_sysvol_references(samdb
, logger
, dc_name
)
270 def offline_remove_ntds_dc(samdb
,
273 remove_computer_obj
=False,
274 remove_server_obj
=False,
275 remove_connection_obj
=False,
276 seize_stale_fsmo
=False,
277 remove_sysvol_obj
=False,
278 remove_dns_names
=False,
279 remove_dns_account
=False):
280 res
= samdb
.search("",
281 scope
=ldb
.SCOPE_BASE
, attrs
=["dsServiceName"])
283 my_serviceName
= ldb
.Dn(samdb
, res
[0]["dsServiceName"][0])
284 server_dn
= ntds_dn
.parent()
286 if my_serviceName
== ntds_dn
:
287 raise DemoteException("Refusing to demote our own DSA: %s " % my_serviceName
)
290 msgs
= samdb
.search(base
=ntds_dn
, expression
="objectClass=ntdsDSA",
291 attrs
=["objectGUID"], scope
=ldb
.SCOPE_BASE
)
292 except LdbError
as (enum
, estr
):
293 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
294 raise DemoteException("Given DN %s doesn't exist" % ntds_dn
)
298 raise DemoteException("%s is not an ntdsda in %s"
299 % (ntds_dn
, samdb
.domain_dns_name()))
302 if (msg
.dn
.get_rdn_name() != "CN" or
303 msg
.dn
.get_rdn_value() != "NTDS Settings"):
304 raise DemoteException("Given DN (%s) wasn't the NTDS Settings DN" %
307 ntds_guid
= ndr_unpack(misc
.GUID
, msg
["objectGUID"][0])
309 if remove_connection_obj
:
310 # Find any nTDSConnection objects with that DC as the fromServer.
311 # We use the GUID to avoid issues with any () chars in a server
313 stale_connections
= samdb
.search(base
=samdb
.get_config_basedn(),
314 expression
="(&(objectclass=nTDSConnection)"
315 "(fromServer=<GUID=%s>))" % ntds_guid
)
316 for conn
in stale_connections
:
317 logger
.info("Removing nTDSConnection: %s" % conn
.dn
)
318 samdb
.delete(conn
.dn
)
321 stale_fsmo_roles
= samdb
.search(base
="", scope
=ldb
.SCOPE_SUBTREE
,
322 expression
="(fsmoRoleOwner=<GUID=%s>))"
324 controls
=["search_options:0:2"])
325 # Find any FSMO roles they have, give them to this server
327 for role
in stale_fsmo_roles
:
328 val
= str(my_serviceName
)
331 m
['value'] = ldb
.MessageElement(val
, ldb
.FLAG_MOD_REPLACE
,
333 logger
.warning("Seizing FSMO role on: %s (now owned by %s)"
334 % (role
.dn
, my_serviceName
))
337 # Remove the NTDS setting tree
339 logger
.info("Removing nTDSDSA: %s (and any children)" % ntds_dn
)
340 samdb
.delete(ntds_dn
, ["tree_delete:0"])
341 except LdbError
as (enum
, estr
):
342 raise DemoteException("Failed to remove the DCs NTDS DSA object: %s"
345 offline_remove_server(samdb
, logger
, server_dn
,
346 remove_computer_obj
=remove_computer_obj
,
347 remove_server_obj
=remove_server_obj
,
348 remove_sysvol_obj
=remove_sysvol_obj
,
349 remove_dns_names
=remove_dns_names
,
350 remove_dns_account
=remove_dns_account
)
353 def remove_dc(samdb
, logger
, dc_name
):
355 # TODO: Check if this is the last server (covered mostly by
356 # refusing to remove our own name)
358 samdb
.transaction_start()
362 # Allow the name to be a the nTDS-DSA GUID
364 ntds_guid
= uuid
.UUID(hex=dc_name
)
365 ntds_dn
= "<GUID=%s>" % ntds_guid
368 server_msgs
= samdb
.search(base
=samdb
.get_config_basedn(),
370 expression
="(&(objectClass=server)"
372 % ldb
.binary_encode(dc_name
))
373 except LdbError
as (enum
, estr
):
374 raise DemoteException("Failure checking if %s is an server "
376 % (dc_name
, samdb
.domain_dns_name()), estr
)
378 if (len(server_msgs
) == 0):
379 raise DemoteException("%s is not an AD DC in %s"
380 % (dc_name
, samdb
.domain_dns_name()))
381 server_dn
= server_msgs
[0].dn
383 ntds_dn
= ldb
.Dn(samdb
, "CN=NTDS Settings")
384 ntds_dn
.add_base(server_dn
)
387 # Confirm this is really an ntdsDSA object
389 ntds_msgs
= samdb
.search(base
=ntds_dn
, attrs
=[], scope
=ldb
.SCOPE_BASE
,
390 expression
="(objectClass=ntdsdsa)")
391 except LdbError
as (enum
, estr
):
392 if enum
== ldb
.ERR_NO_SUCH_OBJECT
:
396 raise DemoteException("Failure checking if %s is an NTDS DSA in %s: "
397 % (ntds_dn
, samdb
.domain_dns_name()), estr
)
399 # If the NTDS Settings child DN wasn't found or wasnt an ntdsDSA
400 # object, just remove the server object located above
401 if (len(ntds_msgs
) == 0):
402 if server_dn
is None:
403 raise DemoteException("%s is not an AD DC in %s"
404 % (dc_name
, samdb
.domain_dns_name()))
406 offline_remove_server(samdb
, logger
,
408 remove_computer_obj
=True,
409 remove_server_obj
=True,
410 remove_sysvol_obj
=True,
411 remove_dns_names
=True,
412 remove_dns_account
=True)
414 offline_remove_ntds_dc(samdb
, logger
,
416 remove_computer_obj
=True,
417 remove_server_obj
=True,
418 remove_connection_obj
=True,
419 seize_stale_fsmo
=True,
420 remove_sysvol_obj
=True,
421 remove_dns_names
=True,
422 remove_dns_account
=True)
424 samdb
.transaction_commit()
428 def offline_remove_dc_RemoveDsServer(samdb
, ntds_dn
):
430 samdb
.start_transaction()
432 offline_remove_ntds_dc(samdb
, ntds_dn
, None)
434 samdb
.commit_transaction()