mirror of
https://github.com/samba-team/samba.git
synced 2024-12-22 13:34:15 +03:00
domain_update: Create a module to apply domain prep updates
These updates are referenced in documentation much like our Forest-Wide-Updates.md file under the same MIT and CC attribution licenses. https://github.com/MicrosoftDocs/windowsserverdocs/blob/master/WindowsServerDocs/identity/ad-ds/deploy/Domain-Wide-Updates.md Signed-off-by: Garming Sam <garming@catalyst.net.nz> Reviewed-by: Andrew Bartlett <abartlet@samba.org>
This commit is contained in:
parent
8a4085fc2b
commit
23dbcb403a
406
python/samba/domain_update.py
Normal file
406
python/samba/domain_update.py
Normal file
@ -0,0 +1,406 @@
|
||||
# Samba4 Domain update checker
|
||||
#
|
||||
# Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import re
|
||||
import ldb
|
||||
import samba
|
||||
import time
|
||||
from base64 import b64encode
|
||||
from samba import dsdb
|
||||
from samba import common
|
||||
from samba import sd_utils
|
||||
from samba.dcerpc import misc
|
||||
from samba.dcerpc import drsuapi
|
||||
from samba.ndr import ndr_unpack, ndr_pack
|
||||
from samba.dcerpc import drsblobs
|
||||
from samba.common import dsdb_Dn
|
||||
from samba.dcerpc import security
|
||||
from samba.dcerpc.security import SECINFO_DACL
|
||||
from samba.descriptor import (
|
||||
get_wellknown_sds,
|
||||
get_diff_sds,
|
||||
get_managed_service_accounts_descriptor,
|
||||
)
|
||||
from samba.auth import system_session, admin_session
|
||||
from samba.netcmd import CommandError
|
||||
from samba.netcmd.fsmo import get_fsmo_roleowner
|
||||
from samba.dsdb import (
|
||||
DS_DOMAIN_FUNCTION_2008,
|
||||
DS_DOMAIN_FUNCTION_2008_R2,
|
||||
DS_DOMAIN_FUNCTION_2012,
|
||||
DS_DOMAIN_FUNCTION_2012_R2,
|
||||
DS_DOMAIN_FUNCTION_2016,
|
||||
)
|
||||
|
||||
MIN_UPDATE = 75
|
||||
MAX_UPDATE = 81
|
||||
|
||||
update_map = {
|
||||
# Missing updates from 2008 R2 - version 5
|
||||
75: "5e1574f6-55df-493e-a671-aaeffca6a100",
|
||||
76: "d262aae8-41f7-48ed-9f35-56bbb677573d",
|
||||
77: "82112ba0-7e4c-4a44-89d9-d46c9612bf91",
|
||||
# Windows Server 2012 - version 9
|
||||
78: "c3c927a6-cc1d-47c0-966b-be8f9b63d991",
|
||||
79: "54afcfb9-637a-4251-9f47-4d50e7021211",
|
||||
80: "f4728883-84dd-483c-9897-274f2ebcf11e",
|
||||
81: "ff4f9d27-7157-4cb0-80a9-5d6f2b14c8ff",
|
||||
# Windows Server 2012 R2 - version 10
|
||||
# No updates
|
||||
}
|
||||
|
||||
functional_level_to_max_update = {
|
||||
DS_DOMAIN_FUNCTION_2008: 74,
|
||||
DS_DOMAIN_FUNCTION_2008_R2: 77,
|
||||
DS_DOMAIN_FUNCTION_2012: 81,
|
||||
DS_DOMAIN_FUNCTION_2012_R2: 81,
|
||||
DS_DOMAIN_FUNCTION_2016: 88,
|
||||
}
|
||||
|
||||
functional_level_to_version = {
|
||||
DS_DOMAIN_FUNCTION_2008: 3,
|
||||
DS_DOMAIN_FUNCTION_2008_R2: 5,
|
||||
DS_DOMAIN_FUNCTION_2012: 9,
|
||||
DS_DOMAIN_FUNCTION_2012_R2: 10,
|
||||
DS_DOMAIN_FUNCTION_2016: 15,
|
||||
}
|
||||
|
||||
# No update numbers have been skipped over
|
||||
missing_updates = []
|
||||
|
||||
|
||||
class DomainUpdateException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DomainUpdate(object):
|
||||
"""Check and update a SAM database for domain updates"""
|
||||
|
||||
def __init__(self, samdb, fix=False,
|
||||
add_update_container=True):
|
||||
"""
|
||||
:param samdb: LDB database
|
||||
:param fix: Apply the update if the container is missing
|
||||
:param add_update_container: Add the container at the end of the change
|
||||
:raise DomainUpdateException:
|
||||
"""
|
||||
self.samdb = samdb
|
||||
self.fix = fix
|
||||
self.add_update_container = add_update_container
|
||||
# TODO: In future we should check for inconsistencies when it claims it has been done
|
||||
self.check_update_applied = False
|
||||
|
||||
self.config_dn = self.samdb.get_config_basedn()
|
||||
self.domain_dn = self.samdb.domain_dn()
|
||||
self.schema_dn = self.samdb.get_schema_basedn()
|
||||
|
||||
self.sd_utils = sd_utils.SDUtils(samdb)
|
||||
self.domain_sid = security.dom_sid(samdb.get_domain_sid())
|
||||
|
||||
self.domainupdate_container = self.samdb.get_root_basedn()
|
||||
if not self.domainupdate_container.add_child("CN=Operations,CN=DomainUpdates,CN=System"):
|
||||
raise DomainUpdateException("Failed to add domain update container child")
|
||||
|
||||
self.revision_object = self.samdb.get_root_basedn()
|
||||
if not self.revision_object.add_child("CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System"):
|
||||
raise DomainUpdateException("Failed to add revision object child")
|
||||
|
||||
def check_updates_functional_level(self, functional_level,
|
||||
old_functional_level=None):
|
||||
res = self.samdb.search(base=self.revision_object,
|
||||
attrs=["revision"], scope=ldb.SCOPE_BASE)
|
||||
|
||||
expected_update = functional_level_to_max_update[functional_level]
|
||||
|
||||
if old_functional_level:
|
||||
min_update = functional_level_to_max_update[old_functional_level]
|
||||
min_update += 1
|
||||
else:
|
||||
min_update = MIN_UPDATE
|
||||
|
||||
self.check_updates_range(min_update, expected_update)
|
||||
|
||||
expected_version = functional_level_to_version[functional_level]
|
||||
if int(res[0]['revision'][0]) < expected_version:
|
||||
pass
|
||||
|
||||
def check_updates_iterator(self, iterator):
|
||||
"""
|
||||
Apply a list of updates which must be within the valid range of updates
|
||||
:param iterator: Iterable specifying integer update numbers to apply
|
||||
:raise DomainUpdateException:
|
||||
"""
|
||||
for op in iterator:
|
||||
if op < MIN_UPDATE or op > MAX_UPDATE:
|
||||
raise DomainUpdateException("Update number invalid.")
|
||||
|
||||
# No LDIF file exists for the change
|
||||
getattr(self, "operation_%d" % op)(op)
|
||||
|
||||
def check_updates_range(self, start=0, end=0):
|
||||
"""
|
||||
Apply a range of updates which must be within the valid range of updates
|
||||
:param start: integer update to begin
|
||||
:param end: integer update to end (inclusive)
|
||||
:raise DomainUpdateException:
|
||||
"""
|
||||
op = start
|
||||
if start < MIN_UPDATE or start > end or end > MAX_UPDATE:
|
||||
raise DomainUpdateException("Update number invalid.")
|
||||
while op <= end:
|
||||
if op not in missing_updates:
|
||||
# No LDIF file exists for the change
|
||||
getattr(self, "operation_%d" % op)(op)
|
||||
|
||||
op += 1
|
||||
|
||||
def update_exists(self, op):
|
||||
"""
|
||||
:param op: Integer update number
|
||||
:return: True if update exists else False
|
||||
"""
|
||||
try:
|
||||
res = self.samdb.search(base=self.domainupdate_container,
|
||||
expression="(CN=%s)" % update_map[op])
|
||||
except ldb.LdbError:
|
||||
return False
|
||||
|
||||
return len(res) == 1
|
||||
|
||||
def update_add(self, op):
|
||||
"""
|
||||
Add the corresponding container object for the given update
|
||||
:param op: Integer update
|
||||
"""
|
||||
self.samdb.add_ldif("""dn: CN=%s,%s
|
||||
objectClass: container
|
||||
""" % (update_map[op], str(self.domainupdate_container)))
|
||||
|
||||
def insert_ace_into_dacl(self, dn, existing_sddl, ace):
|
||||
"""
|
||||
Add an ACE to a DACL, checking if it already exists with a simple string search.
|
||||
|
||||
:param dn: DN to modify
|
||||
:param existing_sddl: existing sddl as string
|
||||
:param ace: string ace to insert
|
||||
:return: True if modified else False
|
||||
"""
|
||||
index = existing_sddl.rfind("S:")
|
||||
if index != -1:
|
||||
new_sddl = existing_sddl[:index] + ace + existing_sddl[index:]
|
||||
else:
|
||||
# Insert it at the end if no S: section
|
||||
new_sddl = existing_sddl + ace
|
||||
|
||||
if ace in existing_sddl:
|
||||
return False
|
||||
|
||||
self.sd_utils.modify_sd_on_dn(dn, new_sddl,
|
||||
controls=["sd_flags:1:%d" % SECINFO_DACL])
|
||||
|
||||
return True
|
||||
|
||||
def insert_ace_into_string(self, dn, ace, attr):
|
||||
"""
|
||||
Insert an ACE into a string attribute like defaultSecurityDescriptor.
|
||||
This also checks if it already exists using a simple string search.
|
||||
|
||||
:param dn: DN to modify
|
||||
:param ace: string ace to insert
|
||||
:param attr: attribute to modify
|
||||
:return: True if modified else False
|
||||
"""
|
||||
msg = self.samdb.search(base=dn,
|
||||
attrs=[attr],
|
||||
controls=["search_options:1:2"])
|
||||
|
||||
assert len(msg) == 1
|
||||
existing_sddl = msg[0][attr][0]
|
||||
index = existing_sddl.rfind("S:")
|
||||
if index != -1:
|
||||
new_sddl = existing_sddl[:index] + ace + existing_sddl[index:]
|
||||
else:
|
||||
# Insert it at the end if no S: section
|
||||
new_sddl = existing_sddl + ace
|
||||
|
||||
if ace in existing_sddl:
|
||||
return False
|
||||
|
||||
m = ldb.Message()
|
||||
m.dn = dn
|
||||
m[attr] = ldb.MessageElement(new_sddl, ldb.FLAG_MOD_REPLACE,
|
||||
attr)
|
||||
|
||||
self.samdb.modify(m, controls=["relax:0"])
|
||||
|
||||
return True
|
||||
|
||||
# Create a new object CN=TPM Devices in the Domain partition.
|
||||
def operation_78(self, op):
|
||||
if self.update_exists(op):
|
||||
return
|
||||
|
||||
self.samdb.add_ldif("""dn: CN=TPM Devices,%s
|
||||
objectClass: top
|
||||
objectClass: msTPM-InformationObjectsContainer
|
||||
""" % self.domain_dn,
|
||||
controls=["relax:0", "provision:0"])
|
||||
|
||||
if self.add_update_container:
|
||||
self.update_add(op)
|
||||
|
||||
# Created an access control entry for the TPM service.
|
||||
def operation_79(self, op):
|
||||
if self.update_exists(op):
|
||||
return
|
||||
|
||||
ace = "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"
|
||||
|
||||
res = self.samdb.search(expression="(objectClass=samDomain)",
|
||||
attrs=["nTSecurityDescriptor"],
|
||||
controls=["search_options:1:2"])
|
||||
for msg in res:
|
||||
existing_sd = ndr_unpack(security.descriptor,
|
||||
msg["nTSecurityDescriptor"][0])
|
||||
existing_sddl = existing_sd.as_sddl(self.domain_sid)
|
||||
|
||||
self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
|
||||
|
||||
res = self.samdb.search(expression="(objectClass=domainDNS)",
|
||||
attrs=["nTSecurityDescriptor"],
|
||||
controls=["search_options:1:2"])
|
||||
for msg in res:
|
||||
existing_sd = ndr_unpack(security.descriptor,
|
||||
msg["nTSecurityDescriptor"][0])
|
||||
existing_sddl = existing_sd.as_sddl(self.domain_sid)
|
||||
|
||||
self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
|
||||
|
||||
if self.add_update_container:
|
||||
self.update_add(op)
|
||||
|
||||
# Grant "Clone DC" extended right to Cloneable Domain Controllers group
|
||||
def operation_80(self, op):
|
||||
if self.update_exists(op):
|
||||
return
|
||||
|
||||
ace = "(OA;;CR;3e0f7e18-2c7a-4c10-ba82-4d926db99a3e;;%s-522)" % str(self.domain_sid)
|
||||
|
||||
res = self.samdb.search(base=self.domain_dn,
|
||||
scope=ldb.SCOPE_BASE,
|
||||
attrs=["nTSecurityDescriptor"],
|
||||
controls=["search_options:1:2",
|
||||
"sd_flags:1:%d" % SECINFO_DACL])
|
||||
msg = res[0]
|
||||
|
||||
existing_sd = ndr_unpack(security.descriptor,
|
||||
msg["nTSecurityDescriptor"][0])
|
||||
existing_sddl = existing_sd.as_sddl(self.domain_sid)
|
||||
|
||||
self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
|
||||
|
||||
if self.add_update_container:
|
||||
self.update_add(op)
|
||||
|
||||
# Grant ms-DS-Allowed-To-Act-On-Behalf-Of-Other-Identity to Principal Self
|
||||
# on all objects
|
||||
def operation_81(self, op):
|
||||
if self.update_exists(op):
|
||||
return
|
||||
|
||||
ace = "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)"
|
||||
|
||||
res = self.samdb.search(expression="(objectClass=samDomain)",
|
||||
attrs=["nTSecurityDescriptor"],
|
||||
controls=["search_options:1:2"])
|
||||
for msg in res:
|
||||
existing_sd = ndr_unpack(security.descriptor,
|
||||
msg["nTSecurityDescriptor"][0])
|
||||
existing_sddl = existing_sd.as_sddl(self.domain_sid)
|
||||
|
||||
self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
|
||||
|
||||
res = self.samdb.search(expression="(objectClass=domainDNS)",
|
||||
attrs=["nTSecurityDescriptor"],
|
||||
controls=["search_options:1:2"])
|
||||
|
||||
for msg in res:
|
||||
existing_sd = ndr_unpack(security.descriptor,
|
||||
msg["nTSecurityDescriptor"][0])
|
||||
existing_sddl = existing_sd.as_sddl(self.domain_sid)
|
||||
|
||||
self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
|
||||
|
||||
if self.add_update_container:
|
||||
self.update_add(op)
|
||||
|
||||
#
|
||||
# THE FOLLOWING ARE MISSING UPDATES FROM 2008 R2
|
||||
#
|
||||
|
||||
# Add Managed Service Accounts container
|
||||
def operation_75(self, op):
|
||||
if self.update_exists(op):
|
||||
return
|
||||
|
||||
descriptor = get_managed_service_accounts_descriptor(self.domain_sid)
|
||||
managedservice_descr = b64encode(descriptor)
|
||||
managed_service_dn = "CN=Managed Service Accounts,%s" % \
|
||||
str(self.domain_dn)
|
||||
|
||||
self.samdb.modify_ldif("""dn: %s
|
||||
changetype: add
|
||||
objectClass: container
|
||||
description: Default container for managed service accounts
|
||||
showInAdvancedViewOnly: FALSE
|
||||
nTSecurityDescriptor:: %s""" % (managed_service_dn, managedservice_descr),
|
||||
controls=["relax:0", "provision:0"])
|
||||
|
||||
if self.add_update_container:
|
||||
self.update_add(op)
|
||||
|
||||
# Add the otherWellKnownObjects reference to MSA
|
||||
def operation_76(self, op):
|
||||
if self.update_exists(op):
|
||||
return
|
||||
|
||||
managed_service_dn = "CN=Managed Service Accounts,%s" % \
|
||||
str(self.domain_dn)
|
||||
|
||||
self.samdb.modify_ldif("""dn: %s
|
||||
changetype: modify
|
||||
add: otherWellKnownObjects
|
||||
otherWellKnownObjects: B:32:1EB93889E40C45DF9F0C64D23BBB6237:%s
|
||||
""" % (str(self.domain_dn), managed_service_dn), controls=["relax:0",
|
||||
"provision:0"])
|
||||
|
||||
if self.add_update_container:
|
||||
self.update_add(op)
|
||||
|
||||
# Add the PSPs object in the System container
|
||||
def operation_77(self, op):
|
||||
if self.update_exists(op):
|
||||
return
|
||||
|
||||
self.samdb.add_ldif("""dn: CN=PSPs,CN=System,%s
|
||||
objectClass: top
|
||||
objectClass: msImaging-PSPs
|
||||
""" % str(self.domain_dn), controls=["relax:0", "provision:0"])
|
||||
|
||||
if self.add_update_container:
|
||||
self.update_add(op)
|
Loading…
Reference in New Issue
Block a user