1# Unix SMB/CIFS implementation. 2# Copyright Matthieu Patou <mat@matws.net> 2011 3# Copyright Andrew Bartlett <abartlet@samba.org> 2008-2015 4# 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. 9# 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. 14# 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/>. 17# 18 19import uuid 20import ldb 21from ldb import LdbError 22from samba import werror 23from samba.ndr import ndr_unpack 24from samba.dcerpc import misc, dnsp 25from samba.dcerpc.dnsp import DNS_TYPE_NS, DNS_TYPE_A, DNS_TYPE_AAAA, \ 26 DNS_TYPE_CNAME, DNS_TYPE_SRV, DNS_TYPE_PTR 27 28 29class DemoteException(Exception): 30 """Base element for demote errors""" 31 32 def __init__(self, value): 33 self.value = value 34 35 def __str__(self): 36 return "DemoteException: " + self.value 37 38 39def 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): 44 dn = ldb.Dn(samdb, s) 45 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" 53 % (dn)) 54 dn.set_component(0, "CN", dc_name) 55 try: 56 logger.info("Removing Sysvol reference: %s" % dn) 57 samdb.delete(dn) 58 except ldb.LdbError as e: 59 (enum, estr) = e.args 60 if enum == ldb.ERR_NO_SUCH_OBJECT: 61 pass 62 else: 63 raise 64 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. 70 dn = ldb.Dn(samdb, s) 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) 78 79 try: 80 logger.info("Removing Sysvol reference: %s" % dn) 81 samdb.delete(dn) 82 except ldb.LdbError as e1: 83 (enum, estr) = e1.args 84 if enum == ldb.ERR_NO_SUCH_OBJECT: 85 pass 86 else: 87 raise 88 89 90def remove_dns_references(samdb, logger, dnsHostName, ignore_no_name=False): 91 92 # Check we are using in-database DNS 93 zones = samdb.search(base="", scope=ldb.SCOPE_SUBTREE, 94 expression="(&(objectClass=dnsZone)(!(dc=RootDNSServers)))", 95 attrs=[], 96 controls=["search_options:0:2"]) 97 if len(zones) == 0: 98 return 99 100 dnsHostNameUpper = dnsHostName.upper() 101 102 try: 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): 108 if ignore_no_name: 109 remove_hanging_dns_references(samdb, logger, 110 dnsHostNameUpper, 111 zones) 112 return 113 raise DemoteException("lookup of %s failed: %s" % (dnsHostName, estr)) 114 samdb.dns_replace(dnsHostName, []) 115 116 res = samdb.search("", 117 scope=ldb.SCOPE_BASE, attrs=["namingContexts"]) 118 assert len(res) == 1 119 ncs = res[0]["namingContexts"] 120 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 127 # script. 128 129 def dns_name_from_dn(dn): 130 # The canonical string of DC=example,DC=com is 131 # example.com/ 132 # 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] 136 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) 141 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: 146 return True 147 return False 148 149 for a_name in a_names_to_remove_from: 150 try: 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: 156 return 157 raise DemoteException("lookup of %s failed: %s" % (a_name, estr)) 158 159 orig_num_recs = len(a_recs) 160 a_recs = [r for r in a_recs if not a_rec_to_remove(r)] 161 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) 166 167 remove_hanging_dns_references(samdb, logger, dnsHostNameUpper, zones) 168 169 170def remove_hanging_dns_references(samdb, logger, dnsHostNameUpper, zones): 171 172 # Find all the CNAME, NS, PTR and SRV records that point at the 173 # name we are removing 174 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: 181 return True 182 elif dnsRecord.wType == DNS_TYPE_SRV: 183 if dnsRecord.data.nameTarget.upper() == dnsHostNameUpper: 184 return True 185 return False 186 187 for zone in zones: 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)))", 192 attrs=["dnsRecord"]) 193 for record in records: 194 try: 195 orig_values = record["dnsRecord"] 196 except KeyError: 197 continue 198 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)] 202 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))) 207 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) 211 212 213def offline_remove_server(samdb, logger, 214 server_dn, 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"]) 222 assert len(res) == 1 223 my_serviceName = res[0]["dsServiceName"][0] 224 225 # Confirm this is really a server object 226 msgs = samdb.search(base=server_dn, 227 attrs=["serverReference", "cn", 228 "dnsHostName"], 229 scope=ldb.SCOPE_BASE, 230 expression="(objectClass=server)") 231 msg = msgs[0] 232 dc_name = str(msg["cn"][0]) 233 234 try: 235 computer_dn = ldb.Dn(samdb, msg["serverReference"][0].decode('utf8')) 236 except KeyError: 237 computer_dn = None 238 239 try: 240 dnsHostName = str(msg["dnsHostName"][0]) 241 except KeyError: 242 dnsHostName = None 243 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"]) 248 249 if computer_dn is not None: 250 computer_msgs = samdb.search(base=computer_dn, 251 expression="objectclass=computer", 252 attrs=["msDS-KrbTgtLink", 253 "rIDSetReferences", 254 "cn"], 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) 264 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"]) 269 270 if "dnsHostName" in msg: 271 dnsHostName = str(msg["dnsHostName"][0]) 272 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()) 278 if len(res) == 1: 279 logger.info("Removing Samba-specific DNS service account: %s" % res[0].dn) 280 samdb.delete(res[0].dn) 281 282 if dnsHostName is not None and remove_dns_names: 283 remove_dns_references(samdb, logger, dnsHostName) 284 285 if remove_sysvol_obj: 286 remove_sysvol_references(samdb, logger, dc_name) 287 288 289def offline_remove_ntds_dc(samdb, 290 logger, 291 ntds_dn, 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"]) 301 assert len(res) == 1 302 my_serviceName = ldb.Dn(samdb, res[0]["dsServiceName"][0].decode('utf8')) 303 server_dn = ntds_dn.parent() 304 305 if my_serviceName == ntds_dn: 306 raise DemoteException("Refusing to demote our own DSA: %s " % my_serviceName) 307 308 try: 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) 315 else: 316 raise 317 if (len(msgs) == 0): 318 raise DemoteException("%s is not an ntdsda in %s" 319 % (ntds_dn, samdb.domain_dns_name())) 320 321 msg = msgs[0] 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" % 325 ntds_dn) 326 327 ntds_guid = ndr_unpack(misc.GUID, msg["objectGUID"][0]) 328 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 332 # name. 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) 339 340 if seize_stale_fsmo: 341 stale_fsmo_roles = samdb.search(base="", scope=ldb.SCOPE_SUBTREE, 342 expression="(fsmoRoleOwner=<GUID=%s>))" 343 % ntds_guid, 344 controls=["search_options:0:2"]) 345 # Find any FSMO roles they have, give them to this server 346 347 for role in stale_fsmo_roles: 348 val = str(my_serviceName) 349 m = ldb.Message() 350 m.dn = role.dn 351 m['value'] = ldb.MessageElement(val, ldb.FLAG_MOD_REPLACE, 352 'fsmoRoleOwner') 353 logger.warning("Seizing FSMO role on: %s (now owned by %s)" 354 % (role.dn, my_serviceName)) 355 samdb.modify(m) 356 357 # Remove the NTDS setting tree 358 try: 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" 364 % estr) 365 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) 372 373 374def remove_dc(samdb, logger, dc_name): 375 376 # TODO: Check if this is the last server (covered mostly by 377 # refusing to remove our own name) 378 379 samdb.transaction_start() 380 381 server_dn = None 382 383 # Allow the name to be a the nTDS-DSA GUID 384 try: 385 ntds_guid = uuid.UUID(hex=dc_name) 386 ntds_dn = "<GUID=%s>" % ntds_guid 387 except ValueError: 388 try: 389 server_msgs = samdb.search(base=samdb.get_config_basedn(), 390 attrs=[], 391 expression="(&(objectClass=server)" 392 "(cn=%s))" 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 " 397 "object in %s: %s" 398 % (dc_name, samdb.domain_dns_name(), estr)) 399 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 405 406 ntds_dn = ldb.Dn(samdb, "CN=NTDS Settings") 407 ntds_dn.add_base(server_dn) 408 pass 409 410 # Confirm this is really an ntdsDSA object 411 try: 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: 417 ntds_msgs = [] 418 pass 419 else: 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)) 424 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())) 432 433 offline_remove_server(samdb, logger, 434 server_dn, 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) 440 else: 441 offline_remove_ntds_dc(samdb, logger, 442 ntds_msgs[0].dn, 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) 450 451 samdb.transaction_commit() 452 453 454def offline_remove_dc_RemoveDsServer(samdb, ntds_dn): 455 456 samdb.start_transaction() 457 458 offline_remove_ntds_dc(samdb, ntds_dn, None) 459 460 samdb.commit_transaction() 461