1 # Samba4 Domain update checker
3 # Copyright (C) Andrew Bartlett <abartlet@samba.org> 2017
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/>.
23 from base64 import b64encode
24 from samba import dsdb
25 from samba import common
26 from samba import sd_utils
27 from samba.dcerpc import misc
28 from samba.dcerpc import drsuapi
29 from samba.ndr import ndr_unpack, ndr_pack
30 from samba.dcerpc import drsblobs
31 from samba.common import dsdb_Dn
32 from samba.dcerpc import security
33 from samba.dcerpc.security import SECINFO_DACL
34 from samba.descriptor import (
37 get_managed_service_accounts_descriptor,
39 from samba.auth import system_session, admin_session
40 from samba.netcmd import CommandError
41 from samba.netcmd.fsmo import get_fsmo_roleowner
42 from samba.dsdb import (
43 DS_DOMAIN_FUNCTION_2008,
44 DS_DOMAIN_FUNCTION_2008_R2,
45 DS_DOMAIN_FUNCTION_2012,
46 DS_DOMAIN_FUNCTION_2012_R2,
47 DS_DOMAIN_FUNCTION_2016,
54 # Missing updates from 2008 R2 - version 5
55 75: "5e1574f6-55df-493e-a671-aaeffca6a100",
56 76: "d262aae8-41f7-48ed-9f35-56bbb677573d",
57 77: "82112ba0-7e4c-4a44-89d9-d46c9612bf91",
58 # Windows Server 2012 - version 9
59 78: "c3c927a6-cc1d-47c0-966b-be8f9b63d991",
60 79: "54afcfb9-637a-4251-9f47-4d50e7021211",
61 80: "f4728883-84dd-483c-9897-274f2ebcf11e",
62 81: "ff4f9d27-7157-4cb0-80a9-5d6f2b14c8ff",
63 # Windows Server 2012 R2 - version 10
67 functional_level_to_max_update = {
68 DS_DOMAIN_FUNCTION_2008: 74,
69 DS_DOMAIN_FUNCTION_2008_R2: 77,
70 DS_DOMAIN_FUNCTION_2012: 81,
71 DS_DOMAIN_FUNCTION_2012_R2: 81,
72 DS_DOMAIN_FUNCTION_2016: 88,
75 functional_level_to_version = {
76 DS_DOMAIN_FUNCTION_2008: 3,
77 DS_DOMAIN_FUNCTION_2008_R2: 5,
78 DS_DOMAIN_FUNCTION_2012: 9,
79 DS_DOMAIN_FUNCTION_2012_R2: 10,
80 DS_DOMAIN_FUNCTION_2016: 15,
83 # No update numbers have been skipped over
87 class DomainUpdateException(Exception):
91 class DomainUpdate(object):
92 """Check and update a SAM database for domain updates"""
94 def __init__(self, samdb, fix=False,
95 add_update_container=True):
97 :param samdb: LDB database
98 :param fix: Apply the update if the container is missing
99 :param add_update_container: Add the container at the end of the change
100 :raise DomainUpdateException:
104 self.add_update_container = add_update_container
105 # TODO: In future we should check for inconsistencies when it claims it has been done
106 self.check_update_applied = False
108 self.config_dn = self.samdb.get_config_basedn()
109 self.domain_dn = self.samdb.domain_dn()
110 self.schema_dn = self.samdb.get_schema_basedn()
112 self.sd_utils = sd_utils.SDUtils(samdb)
113 self.domain_sid = security.dom_sid(samdb.get_domain_sid())
115 self.domainupdate_container = self.samdb.get_root_basedn()
116 if not self.domainupdate_container.add_child("CN=Operations,CN=DomainUpdates,CN=System"):
117 raise DomainUpdateException("Failed to add domain update container child")
119 self.revision_object = self.samdb.get_root_basedn()
120 if not self.revision_object.add_child("CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System"):
121 raise DomainUpdateException("Failed to add revision object child")
123 def check_updates_functional_level(self, functional_level,
124 old_functional_level=None,
125 update_revision=False):
127 Apply all updates for a given old and new functional level
128 :param functional_level: constant
129 :param old_functional_level: constant
130 :param update_revision: modify the stored version
131 :raise DomainUpdateException:
133 res = self.samdb.search(base=self.revision_object,
134 attrs=["revision"], scope=ldb.SCOPE_BASE)
136 expected_update = functional_level_to_max_update[functional_level]
138 if old_functional_level:
139 min_update = functional_level_to_max_update[old_functional_level]
142 min_update = MIN_UPDATE
144 self.check_updates_range(min_update, expected_update)
146 expected_version = functional_level_to_version[functional_level]
147 found_version = int(res[0]['revision'][0])
148 if update_revision and found_version < expected_version:
150 raise DomainUpdateException("Revision is not high enough. Fix is set to False."
151 "\nExpected: %dGot: %d" % (expected_version,
153 self.samdb.modify_ldif("""dn: %s
157 """ % (str(self.revision_object), expected_version))
159 def check_updates_iterator(self, iterator):
161 Apply a list of updates which must be within the valid range of updates
162 :param iterator: Iterable specifying integer update numbers to apply
163 :raise DomainUpdateException:
166 if op < MIN_UPDATE or op > MAX_UPDATE:
167 raise DomainUpdateException("Update number invalid.")
169 # No LDIF file exists for the change
170 getattr(self, "operation_%d" % op)(op)
172 def check_updates_range(self, start=0, end=0):
174 Apply a range of updates which must be within the valid range of updates
175 :param start: integer update to begin
176 :param end: integer update to end (inclusive)
177 :raise DomainUpdateException:
180 if start < MIN_UPDATE or start > end or end > MAX_UPDATE:
181 raise DomainUpdateException("Update number invalid.")
183 if op not in missing_updates:
184 # No LDIF file exists for the change
185 getattr(self, "operation_%d" % op)(op)
189 def update_exists(self, op):
191 :param op: Integer update number
192 :return: True if update exists else False
195 res = self.samdb.search(base=self.domainupdate_container,
196 expression="(CN=%s)" % update_map[op])
202 def update_add(self, op):
204 Add the corresponding container object for the given update
205 :param op: Integer update
207 self.samdb.add_ldif("""dn: CN=%s,%s
208 objectClass: container
209 """ % (update_map[op], str(self.domainupdate_container)))
211 def insert_ace_into_dacl(self, dn, existing_sddl, ace):
213 Add an ACE to a DACL, checking if it already exists with a simple string search.
215 :param dn: DN to modify
216 :param existing_sddl: existing sddl as string
217 :param ace: string ace to insert
218 :return: True if modified else False
220 index = existing_sddl.rfind("S:")
222 new_sddl = existing_sddl[:index] + ace + existing_sddl[index:]
224 # Insert it at the end if no S: section
225 new_sddl = existing_sddl + ace
227 if ace in existing_sddl:
230 self.sd_utils.modify_sd_on_dn(dn, new_sddl,
231 controls=["sd_flags:1:%d" % SECINFO_DACL])
235 def insert_ace_into_string(self, dn, ace, attr):
237 Insert an ACE into a string attribute like defaultSecurityDescriptor.
238 This also checks if it already exists using a simple string search.
240 :param dn: DN to modify
241 :param ace: string ace to insert
242 :param attr: attribute to modify
243 :return: True if modified else False
245 msg = self.samdb.search(base=dn,
247 controls=["search_options:1:2"])
250 existing_sddl = msg[0][attr][0]
251 index = existing_sddl.rfind("S:")
253 new_sddl = existing_sddl[:index] + ace + existing_sddl[index:]
255 # Insert it at the end if no S: section
256 new_sddl = existing_sddl + ace
258 if ace in existing_sddl:
263 m[attr] = ldb.MessageElement(new_sddl, ldb.FLAG_MOD_REPLACE,
266 self.samdb.modify(m, controls=["relax:0"])
270 def raise_if_not_fix(self, op):
272 Raises an exception if not set to fix.
273 :param op: Integer operation
274 :raise DomainUpdateException:
277 raise DomainUpdateException("Missing operation %d. Fix is currently set to False" % op)
279 # Create a new object CN=TPM Devices in the Domain partition.
280 def operation_78(self, op):
281 if self.update_exists(op):
283 self.raise_if_not_fix(op)
285 self.samdb.add_ldif("""dn: CN=TPM Devices,%s
287 objectClass: msTPM-InformationObjectsContainer
288 """ % self.domain_dn,
289 controls=["relax:0", "provision:0"])
291 if self.add_update_container:
294 # Created an access control entry for the TPM service.
295 def operation_79(self, op):
296 if self.update_exists(op):
298 self.raise_if_not_fix(op)
300 ace = "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"
302 res = self.samdb.search(expression="(objectClass=samDomain)",
303 attrs=["nTSecurityDescriptor"],
304 controls=["search_options:1:2"])
306 existing_sd = ndr_unpack(security.descriptor,
307 msg["nTSecurityDescriptor"][0])
308 existing_sddl = existing_sd.as_sddl(self.domain_sid)
310 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
312 res = self.samdb.search(expression="(objectClass=domainDNS)",
313 attrs=["nTSecurityDescriptor"],
314 controls=["search_options:1:2"])
316 existing_sd = ndr_unpack(security.descriptor,
317 msg["nTSecurityDescriptor"][0])
318 existing_sddl = existing_sd.as_sddl(self.domain_sid)
320 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
322 if self.add_update_container:
325 # Grant "Clone DC" extended right to Cloneable Domain Controllers group
326 def operation_80(self, op):
327 if self.update_exists(op):
329 self.raise_if_not_fix(op)
331 ace = "(OA;;CR;3e0f7e18-2c7a-4c10-ba82-4d926db99a3e;;%s-522)" % str(self.domain_sid)
333 res = self.samdb.search(base=self.domain_dn,
334 scope=ldb.SCOPE_BASE,
335 attrs=["nTSecurityDescriptor"],
336 controls=["search_options:1:2",
337 "sd_flags:1:%d" % SECINFO_DACL])
340 existing_sd = ndr_unpack(security.descriptor,
341 msg["nTSecurityDescriptor"][0])
342 existing_sddl = existing_sd.as_sddl(self.domain_sid)
344 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
346 if self.add_update_container:
349 # Grant ms-DS-Allowed-To-Act-On-Behalf-Of-Other-Identity to Principal Self
351 def operation_81(self, op):
352 if self.update_exists(op):
354 self.raise_if_not_fix(op)
356 ace = "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)"
358 res = self.samdb.search(expression="(objectClass=samDomain)",
359 attrs=["nTSecurityDescriptor"],
360 controls=["search_options:1:2"])
362 existing_sd = ndr_unpack(security.descriptor,
363 msg["nTSecurityDescriptor"][0])
364 existing_sddl = existing_sd.as_sddl(self.domain_sid)
366 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
368 res = self.samdb.search(expression="(objectClass=domainDNS)",
369 attrs=["nTSecurityDescriptor"],
370 controls=["search_options:1:2"])
373 existing_sd = ndr_unpack(security.descriptor,
374 msg["nTSecurityDescriptor"][0])
375 existing_sddl = existing_sd.as_sddl(self.domain_sid)
377 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
379 if self.add_update_container:
383 # THE FOLLOWING ARE MISSING UPDATES FROM 2008 R2
386 # Add Managed Service Accounts container
387 def operation_75(self, op):
388 if self.update_exists(op):
390 self.raise_if_not_fix(op)
392 descriptor = get_managed_service_accounts_descriptor(self.domain_sid)
393 managedservice_descr = b64encode(descriptor)
394 managed_service_dn = "CN=Managed Service Accounts,%s" % \
397 self.samdb.modify_ldif("""dn: %s
399 objectClass: container
400 description: Default container for managed service accounts
401 showInAdvancedViewOnly: FALSE
402 nTSecurityDescriptor:: %s""" % (managed_service_dn, managedservice_descr),
403 controls=["relax:0", "provision:0"])
405 if self.add_update_container:
408 # Add the otherWellKnownObjects reference to MSA
409 def operation_76(self, op):
410 if self.update_exists(op):
412 self.raise_if_not_fix(op)
414 managed_service_dn = "CN=Managed Service Accounts,%s" % \
417 self.samdb.modify_ldif("""dn: %s
419 add: otherWellKnownObjects
420 otherWellKnownObjects: B:32:1EB93889E40C45DF9F0C64D23BBB6237:%s
421 """ % (str(self.domain_dn), managed_service_dn), controls=["relax:0",
424 if self.add_update_container:
427 # Add the PSPs object in the System container
428 def operation_77(self, op):
429 if self.update_exists(op):
431 self.raise_if_not_fix(op)
433 self.samdb.add_ldif("""dn: CN=PSPs,CN=System,%s
435 objectClass: msImaging-PSPs
436 """ % str(self.domain_dn), controls=["relax:0", "provision:0"])
438 if self.add_update_container: