selftest: Confirm new and old SDDL strings differ after a samba-tool dsacl set
[Samba.git] / python / samba / domain_update.py
blobae5446a583701bf5ccdf8aac0303ec5ed5f9bce4
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/>.
19 import ldb
20 import samba
21 from base64 import b64encode
22 from samba import sd_utils
23 from samba.ndr import ndr_unpack, ndr_pack
24 from samba.dcerpc import security
25 from samba.dcerpc.security import SECINFO_DACL
26 from samba.descriptor import (
27 get_managed_service_accounts_descriptor,
29 from samba.dsdb import (
30 DS_DOMAIN_FUNCTION_2008,
31 DS_DOMAIN_FUNCTION_2008_R2,
32 DS_DOMAIN_FUNCTION_2012,
33 DS_DOMAIN_FUNCTION_2012_R2,
34 DS_DOMAIN_FUNCTION_2016,
37 MIN_UPDATE = 75
38 MAX_UPDATE = 81
40 update_map = {
41 # Missing updates from 2008 R2 - version 5
42 75: "5e1574f6-55df-493e-a671-aaeffca6a100",
43 76: "d262aae8-41f7-48ed-9f35-56bbb677573d",
44 77: "82112ba0-7e4c-4a44-89d9-d46c9612bf91",
45 # Windows Server 2012 - version 9
46 78: "c3c927a6-cc1d-47c0-966b-be8f9b63d991",
47 79: "54afcfb9-637a-4251-9f47-4d50e7021211",
48 80: "f4728883-84dd-483c-9897-274f2ebcf11e",
49 81: "ff4f9d27-7157-4cb0-80a9-5d6f2b14c8ff",
50 # Windows Server 2012 R2 - version 10
51 # No updates
54 functional_level_to_max_update = {
55 DS_DOMAIN_FUNCTION_2008: 74,
56 DS_DOMAIN_FUNCTION_2008_R2: 77,
57 DS_DOMAIN_FUNCTION_2012: 81,
58 DS_DOMAIN_FUNCTION_2012_R2: 81,
59 DS_DOMAIN_FUNCTION_2016: 88,
62 functional_level_to_version = {
63 DS_DOMAIN_FUNCTION_2008: 3,
64 DS_DOMAIN_FUNCTION_2008_R2: 5,
65 DS_DOMAIN_FUNCTION_2012: 9,
66 DS_DOMAIN_FUNCTION_2012_R2: 10,
67 DS_DOMAIN_FUNCTION_2016: 15,
70 # No update numbers have been skipped over
71 missing_updates = []
74 class DomainUpdateException(Exception):
75 pass
78 class DomainUpdate(object):
79 """Check and update a SAM database for domain updates"""
81 def __init__(self, samdb, fix=False,
82 add_update_container=True):
83 """
84 :param samdb: LDB database
85 :param fix: Apply the update if the container is missing
86 :param add_update_container: Add the container at the end of the change
87 :raise DomainUpdateException:
88 """
89 self.samdb = samdb
90 self.fix = fix
91 self.add_update_container = add_update_container
92 # TODO: In future we should check for inconsistencies when it claims it has been done
93 self.check_update_applied = False
95 self.config_dn = self.samdb.get_config_basedn()
96 self.domain_dn = self.samdb.domain_dn()
97 self.schema_dn = self.samdb.get_schema_basedn()
99 self.sd_utils = sd_utils.SDUtils(samdb)
100 self.domain_sid = security.dom_sid(samdb.get_domain_sid())
102 self.domainupdate_container = self.samdb.get_root_basedn()
103 if not self.domainupdate_container.add_child("CN=Operations,CN=DomainUpdates,CN=System"):
104 raise DomainUpdateException("Failed to add domain update container child")
106 self.revision_object = self.samdb.get_root_basedn()
107 if not self.revision_object.add_child("CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System"):
108 raise DomainUpdateException("Failed to add revision object child")
110 def check_updates_functional_level(self, functional_level,
111 old_functional_level=None,
112 update_revision=False):
114 Apply all updates for a given old and new functional level
115 :param functional_level: constant
116 :param old_functional_level: constant
117 :param update_revision: modify the stored version
118 :raise DomainUpdateException:
120 res = self.samdb.search(base=self.revision_object,
121 attrs=["revision"], scope=ldb.SCOPE_BASE)
123 expected_update = functional_level_to_max_update[functional_level]
125 if old_functional_level:
126 min_update = functional_level_to_max_update[old_functional_level]
127 min_update += 1
128 else:
129 min_update = MIN_UPDATE
131 self.check_updates_range(min_update, expected_update)
133 expected_version = functional_level_to_version[functional_level]
134 found_version = int(res[0]['revision'][0])
135 if update_revision and found_version < expected_version:
136 if not self.fix:
137 raise DomainUpdateException("Revision is not high enough. Fix is set to False."
138 "\nExpected: %dGot: %d" % (expected_version,
139 found_version))
140 self.samdb.modify_ldif("""dn: %s
141 changetype: modify
142 replace: revision
143 revision: %d
144 """ % (str(self.revision_object), expected_version))
146 def check_updates_iterator(self, iterator):
148 Apply a list of updates which must be within the valid range of updates
149 :param iterator: Iterable specifying integer update numbers to apply
150 :raise DomainUpdateException:
152 for op in iterator:
153 if op < MIN_UPDATE or op > MAX_UPDATE:
154 raise DomainUpdateException("Update number invalid.")
156 # No LDIF file exists for the change
157 getattr(self, "operation_%d" % op)(op)
159 def check_updates_range(self, start=0, end=0):
161 Apply a range of updates which must be within the valid range of updates
162 :param start: integer update to begin
163 :param end: integer update to end (inclusive)
164 :raise DomainUpdateException:
166 op = start
167 if start < MIN_UPDATE or start > end or end > MAX_UPDATE:
168 raise DomainUpdateException("Update number invalid.")
169 while op <= end:
170 if op not in missing_updates:
171 # No LDIF file exists for the change
172 getattr(self, "operation_%d" % op)(op)
174 op += 1
176 def update_exists(self, op):
178 :param op: Integer update number
179 :return: True if update exists else False
181 try:
182 res = self.samdb.search(base=self.domainupdate_container,
183 expression="(CN=%s)" % update_map[op])
184 except ldb.LdbError:
185 return False
187 return len(res) == 1
189 def update_add(self, op):
191 Add the corresponding container object for the given update
192 :param op: Integer update
194 self.samdb.add_ldif("""dn: CN=%s,%s
195 objectClass: container
196 """ % (update_map[op], str(self.domainupdate_container)))
198 def insert_ace_into_dacl(self, dn, existing_sddl, ace):
200 Add an ACE to a DACL, checking if it already exists with a simple string search.
202 :param dn: DN to modify
203 :param existing_sddl: existing sddl as string
204 :param ace: string ace to insert
205 :return: True if modified else False
207 index = existing_sddl.rfind("S:")
208 if index != -1:
209 new_sddl = existing_sddl[:index] + ace + existing_sddl[index:]
210 else:
211 # Insert it at the end if no S: section
212 new_sddl = existing_sddl + ace
214 if ace in existing_sddl:
215 return False
217 self.sd_utils.modify_sd_on_dn(dn, new_sddl,
218 controls=["sd_flags:1:%d" % SECINFO_DACL])
220 return True
222 def insert_ace_into_string(self, dn, ace, attr):
224 Insert an ACE into a string attribute like defaultSecurityDescriptor.
225 This also checks if it already exists using a simple string search.
227 :param dn: DN to modify
228 :param ace: string ace to insert
229 :param attr: attribute to modify
230 :return: True if modified else False
232 msg = self.samdb.search(base=dn,
233 attrs=[attr],
234 controls=["search_options:1:2"])
236 assert len(msg) == 1
237 existing_sddl = msg[0][attr][0]
238 index = existing_sddl.rfind("S:")
239 if index != -1:
240 new_sddl = existing_sddl[:index] + ace + existing_sddl[index:]
241 else:
242 # Insert it at the end if no S: section
243 new_sddl = existing_sddl + ace
245 if ace in existing_sddl:
246 return False
248 m = ldb.Message()
249 m.dn = dn
250 m[attr] = ldb.MessageElement(new_sddl, ldb.FLAG_MOD_REPLACE,
251 attr)
253 self.samdb.modify(m, controls=["relax:0"])
255 return True
257 def raise_if_not_fix(self, op):
259 Raises an exception if not set to fix.
260 :param op: Integer operation
261 :raise DomainUpdateException:
263 if not self.fix:
264 raise DomainUpdateException("Missing operation %d. Fix is currently set to False" % op)
266 # Create a new object CN=TPM Devices in the Domain partition.
267 def operation_78(self, op):
268 if self.update_exists(op):
269 return
270 self.raise_if_not_fix(op)
272 self.samdb.add_ldif("""dn: CN=TPM Devices,%s
273 objectClass: top
274 objectClass: msTPM-InformationObjectsContainer
275 """ % self.domain_dn,
276 controls=["relax:0", "provision:0"])
278 if self.add_update_container:
279 self.update_add(op)
281 # Created an access control entry for the TPM service.
282 def operation_79(self, op):
283 if self.update_exists(op):
284 return
285 self.raise_if_not_fix(op)
287 ace = "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"
289 res = self.samdb.search(expression="(objectClass=samDomain)",
290 attrs=["nTSecurityDescriptor"],
291 controls=["search_options:1:2"])
292 for msg in res:
293 existing_sd = ndr_unpack(security.descriptor,
294 msg["nTSecurityDescriptor"][0])
295 existing_sddl = existing_sd.as_sddl(self.domain_sid)
297 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
299 res = self.samdb.search(expression="(objectClass=domainDNS)",
300 attrs=["nTSecurityDescriptor"],
301 controls=["search_options:1:2"])
302 for msg in res:
303 existing_sd = ndr_unpack(security.descriptor,
304 msg["nTSecurityDescriptor"][0])
305 existing_sddl = existing_sd.as_sddl(self.domain_sid)
307 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
309 if self.add_update_container:
310 self.update_add(op)
312 # Grant "Clone DC" extended right to Cloneable Domain Controllers group
313 def operation_80(self, op):
314 if self.update_exists(op):
315 return
316 self.raise_if_not_fix(op)
318 ace = "(OA;;CR;3e0f7e18-2c7a-4c10-ba82-4d926db99a3e;;%s-522)" % str(self.domain_sid)
320 res = self.samdb.search(base=self.domain_dn,
321 scope=ldb.SCOPE_BASE,
322 attrs=["nTSecurityDescriptor"],
323 controls=["search_options:1:2",
324 "sd_flags:1:%d" % SECINFO_DACL])
325 msg = res[0]
327 existing_sd = ndr_unpack(security.descriptor,
328 msg["nTSecurityDescriptor"][0])
329 existing_sddl = existing_sd.as_sddl(self.domain_sid)
331 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
333 if self.add_update_container:
334 self.update_add(op)
336 # Grant ms-DS-Allowed-To-Act-On-Behalf-Of-Other-Identity to Principal Self
337 # on all objects
338 def operation_81(self, op):
339 if self.update_exists(op):
340 return
341 self.raise_if_not_fix(op)
343 ace = "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)"
345 res = self.samdb.search(expression="(objectClass=samDomain)",
346 attrs=["nTSecurityDescriptor"],
347 controls=["search_options:1:2"])
348 for msg in res:
349 existing_sd = ndr_unpack(security.descriptor,
350 msg["nTSecurityDescriptor"][0])
351 existing_sddl = existing_sd.as_sddl(self.domain_sid)
353 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
355 res = self.samdb.search(expression="(objectClass=domainDNS)",
356 attrs=["nTSecurityDescriptor"],
357 controls=["search_options:1:2"])
359 for msg in res:
360 existing_sd = ndr_unpack(security.descriptor,
361 msg["nTSecurityDescriptor"][0])
362 existing_sddl = existing_sd.as_sddl(self.domain_sid)
364 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
366 if self.add_update_container:
367 self.update_add(op)
370 # THE FOLLOWING ARE MISSING UPDATES FROM 2008 R2
373 # Add Managed Service Accounts container
374 def operation_75(self, op):
375 if self.update_exists(op):
376 return
377 self.raise_if_not_fix(op)
379 descriptor = get_managed_service_accounts_descriptor(self.domain_sid)
380 managedservice_descr = b64encode(descriptor).decode('utf8')
381 managed_service_dn = "CN=Managed Service Accounts,%s" % \
382 str(self.domain_dn)
384 self.samdb.modify_ldif("""dn: %s
385 changetype: add
386 objectClass: container
387 description: Default container for managed service accounts
388 showInAdvancedViewOnly: FALSE
389 nTSecurityDescriptor:: %s""" % (managed_service_dn, managedservice_descr),
390 controls=["relax:0", "provision:0"])
392 if self.add_update_container:
393 self.update_add(op)
395 # Add the otherWellKnownObjects reference to MSA
396 def operation_76(self, op):
397 if self.update_exists(op):
398 return
399 self.raise_if_not_fix(op)
401 managed_service_dn = "CN=Managed Service Accounts,%s" % \
402 str(self.domain_dn)
404 self.samdb.modify_ldif("""dn: %s
405 changetype: modify
406 add: otherWellKnownObjects
407 otherWellKnownObjects: B:32:1EB93889E40C45DF9F0C64D23BBB6237:%s
408 """ % (str(self.domain_dn), managed_service_dn), controls=["relax:0",
409 "provision:0"])
411 if self.add_update_container:
412 self.update_add(op)
414 # Add the PSPs object in the System container
415 def operation_77(self, op):
416 if self.update_exists(op):
417 return
418 self.raise_if_not_fix(op)
420 self.samdb.add_ldif("""dn: CN=PSPs,CN=System,%s
421 objectClass: top
422 objectClass: msImaging-PSPs
423 """ % str(self.domain_dn), controls=["relax:0", "provision:0"])
425 if self.add_update_container:
426 self.update_add(op)