python: Remove unused imports
[Samba.git] / python / samba / domain_update.py
blobe91bdf40dbbb35e3f2163d135b85be2e93d3d755
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 from base64 import b64encode
21 from samba import sd_utils
22 from samba.dcerpc import security
23 from samba.descriptor import (
24 get_managed_service_accounts_descriptor,
26 from samba.dsdb import (
27 DS_DOMAIN_FUNCTION_2008,
28 DS_DOMAIN_FUNCTION_2008_R2,
29 DS_DOMAIN_FUNCTION_2012,
30 DS_DOMAIN_FUNCTION_2012_R2,
31 DS_DOMAIN_FUNCTION_2016,
34 MIN_UPDATE = 75
35 MAX_UPDATE = 89
37 update_map = {
38 # Missing updates from 2008 R2 - version 5
39 75: "5e1574f6-55df-493e-a671-aaeffca6a100",
40 76: "d262aae8-41f7-48ed-9f35-56bbb677573d",
41 77: "82112ba0-7e4c-4a44-89d9-d46c9612bf91",
42 # Windows Server 2012 - version 9
43 78: "c3c927a6-cc1d-47c0-966b-be8f9b63d991",
44 79: "54afcfb9-637a-4251-9f47-4d50e7021211",
45 80: "f4728883-84dd-483c-9897-274f2ebcf11e",
46 81: "ff4f9d27-7157-4cb0-80a9-5d6f2b14c8ff",
47 # Windows Server 2012 R2 - version 10
48 # No updates
49 # Windows Server 2016 - version 15
50 82: "83c53da7-427e-47a4-a07a-a324598b88f7",
51 # from the documentation and a fresh installation
52 # 83 is this:
53 # c81fc9cc-0130-4fd1-b272-634d74818133
54 # adprep will use this on the wire:
55 # c81fc9cc-0130-f4d1-b272-634d74818133
56 83: "c81fc9cc-0130-4fd1-b272-634d74818133",
57 84: "e5f9e791-d96d-4fc9-93c9-d53e1dc439ba",
58 85: "e6d5fd00-385d-4e65-b02d-9da3493ed850",
59 86: "3a6b3fbf-3168-4312-a10d-dd5b3393952d",
60 87: "7f950403-0ab3-47f9-9730-5d7b0269f9bd",
61 88: "434bb40d-dbc9-4fe7-81d4-d57229f7b080",
62 # Windows Server 2016 - version 16
63 89: "a0c238ba-9e30-4ee6-80a6-43f731e9a5cd",
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: 89,
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: 16,
83 # No update numbers have been skipped over
84 missing_updates = []
87 class DomainUpdateException(Exception):
88 pass
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):
96 """
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:
102 self.samdb = samdb
103 self.fix = fix
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 try:
117 self.domainupdate_container.add_child("CN=Operations,CN=DomainUpdates,CN=System")
118 except ldb.LdbError:
119 raise DomainUpdateException("Failed to add domain update container child")
121 self.revision_object = self.samdb.get_root_basedn()
122 try:
123 self.revision_object.add_child("CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System")
124 except ldb.LdbError:
125 raise DomainUpdateException("Failed to add revision object child")
127 def check_updates_functional_level(self, functional_level,
128 old_functional_level=None,
129 update_revision=False):
131 Apply all updates for a given old and new functional level
132 :param functional_level: constant
133 :param old_functional_level: constant
134 :param update_revision: modify the stored version
135 :raise DomainUpdateException:
137 res = self.samdb.search(base=self.revision_object,
138 attrs=["revision"], scope=ldb.SCOPE_BASE)
140 expected_update = functional_level_to_max_update[functional_level]
142 if old_functional_level:
143 min_update = functional_level_to_max_update[old_functional_level]
144 min_update += 1
145 else:
146 min_update = MIN_UPDATE
148 self.check_updates_range(min_update, expected_update)
150 expected_version = functional_level_to_version[functional_level]
151 found_version = int(res[0]['revision'][0])
152 if update_revision and found_version < expected_version:
153 if not self.fix:
154 raise DomainUpdateException("Revision is not high enough. Fix is set to False."
155 "\nExpected: %dGot: %d" % (expected_version,
156 found_version))
157 self.samdb.modify_ldif("""dn: %s
158 changetype: modify
159 replace: revision
160 revision: %d
161 """ % (str(self.revision_object), expected_version))
163 def check_updates_iterator(self, iterator):
165 Apply a list of updates which must be within the valid range of updates
166 :param iterator: Iterable specifying integer update numbers to apply
167 :raise DomainUpdateException:
169 for op in iterator:
170 if op < MIN_UPDATE or op > MAX_UPDATE:
171 raise DomainUpdateException("Update number invalid.")
173 # No LDIF file exists for the change
174 getattr(self, "operation_%d" % op)(op)
176 def check_updates_range(self, start=0, end=0):
178 Apply a range of updates which must be within the valid range of updates
179 :param start: integer update to begin
180 :param end: integer update to end (inclusive)
181 :raise DomainUpdateException:
183 op = start
184 if start < MIN_UPDATE or start > end or end > MAX_UPDATE:
185 raise DomainUpdateException("Update number invalid.")
186 while op <= end:
187 if op not in missing_updates:
188 # No LDIF file exists for the change
189 getattr(self, "operation_%d" % op)(op)
191 op += 1
193 def update_exists(self, op):
195 :param op: Integer update number
196 :return: True if update exists else False
198 update_dn = "CN=%s,%s" % (update_map[op], self.domainupdate_container)
199 try:
200 res = self.samdb.search(base=update_dn,
201 scope=ldb.SCOPE_BASE,
202 attrs=[])
203 except ldb.LdbError as e:
204 (num, msg) = e.args
205 if num != ldb.ERR_NO_SUCH_OBJECT:
206 raise
207 return False
209 assert len(res) == 1
210 print("Skip Domain Update %u: %s" % (op, update_map[op]))
211 return True
213 def update_add(self, op):
215 Add the corresponding container object for the given update
216 :param op: Integer update
218 self.samdb.add_ldif("""dn: CN=%s,%s
219 objectClass: container
220 """ % (update_map[op], str(self.domainupdate_container)))
221 print("Applied Domain Update %u: %s" % (op, update_map[op]))
223 def raise_if_not_fix(self, op):
225 Raises an exception if not set to fix.
226 :param op: Integer operation
227 :raise DomainUpdateException:
229 if not self.fix:
230 raise DomainUpdateException("Missing operation %d. Fix is currently set to False" % op)
232 # Create a new object CN=TPM Devices in the Domain partition.
233 def operation_78(self, op):
234 if self.update_exists(op):
235 return
236 self.raise_if_not_fix(op)
238 self.samdb.add_ldif("""dn: CN=TPM Devices,%s
239 objectClass: top
240 objectClass: msTPM-InformationObjectsContainer
241 """ % self.domain_dn,
242 controls=["relax:0", "provision:0"])
244 if self.add_update_container:
245 self.update_add(op)
247 # Created an access control entry for the TPM service.
248 def operation_79(self, op):
249 if self.update_exists(op):
250 return
251 self.raise_if_not_fix(op)
253 ace = "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"
255 self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=[ace])
257 if self.add_update_container:
258 self.update_add(op)
260 # Grant "Clone DC" extended right to Cloneable Domain Controllers group
261 def operation_80(self, op):
262 if self.update_exists(op):
263 return
264 self.raise_if_not_fix(op)
266 ace = "(OA;;CR;3e0f7e18-2c7a-4c10-ba82-4d926db99a3e;;CN)"
268 self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=[ace])
270 if self.add_update_container:
271 self.update_add(op)
273 # Grant ms-DS-Allowed-To-Act-On-Behalf-Of-Other-Identity to Principal Self
274 # on all objects
275 def operation_81(self, op):
276 if self.update_exists(op):
277 return
278 self.raise_if_not_fix(op)
280 ace = "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)"
282 self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=[ace])
284 if self.add_update_container:
285 self.update_add(op)
288 # THE FOLLOWING ARE MISSING UPDATES FROM 2008 R2
291 # Add Managed Service Accounts container
292 def operation_75(self, op):
293 if self.update_exists(op):
294 return
295 self.raise_if_not_fix(op)
297 descriptor = get_managed_service_accounts_descriptor(self.domain_sid)
298 managedservice_descr = b64encode(descriptor).decode('utf8')
299 managed_service_dn = "CN=Managed Service Accounts,%s" % \
300 str(self.domain_dn)
302 self.samdb.modify_ldif("""dn: %s
303 changetype: add
304 objectClass: container
305 description: Default container for managed service accounts
306 showInAdvancedViewOnly: FALSE
307 nTSecurityDescriptor:: %s""" % (managed_service_dn, managedservice_descr),
308 controls=["relax:0", "provision:0"])
310 if self.add_update_container:
311 self.update_add(op)
313 # Add the otherWellKnownObjects reference to MSA
314 def operation_76(self, op):
315 if self.update_exists(op):
316 return
317 self.raise_if_not_fix(op)
319 managed_service_dn = "CN=Managed Service Accounts,%s" % \
320 str(self.domain_dn)
322 self.samdb.modify_ldif("""dn: %s
323 changetype: modify
324 add: otherWellKnownObjects
325 otherWellKnownObjects: B:32:1EB93889E40C45DF9F0C64D23BBB6237:%s
326 """ % (str(self.domain_dn), managed_service_dn), controls=["relax:0",
327 "provision:0"])
329 if self.add_update_container:
330 self.update_add(op)
332 # Add the PSPs object in the System container
333 def operation_77(self, op):
334 if self.update_exists(op):
335 return
336 self.raise_if_not_fix(op)
338 self.samdb.add_ldif("""dn: CN=PSPs,CN=System,%s
339 objectClass: top
340 objectClass: msImaging-PSPs
341 """ % str(self.domain_dn), controls=["relax:0", "provision:0"])
343 if self.add_update_container:
344 self.update_add(op)
346 ## ## Windows Server 2016: Domain-wide updates
348 ## After the operations that are performed by domainprep in Windows
349 ## Server 2016 (operations 82-88) complete, the revision attribute for the
350 ## CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System,DC=ForestRootDomain
351 ## object is set to 15.
353 ## Operation 82: {83c53da7-427e-47a4-a07a-a324598b88f7}
355 ## Create CN=Keys container at root of domain
357 ## - objectClass: container
358 ## - description: Default container for key credential objects
359 ## - ShowInAdvancedViewOnly: TRUE
361 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EA)
362 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DA)
363 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;SY)
364 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DD)
365 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;ED)
367 def operation_82(self, op):
368 if self.update_exists(op):
369 return
370 self.raise_if_not_fix(op)
372 keys_dn = "CN=Keys,%s" % str(self.domain_dn)
374 sddl = "O:DA"
375 sddl += "D:"
376 sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EA)"
377 sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DA)"
378 sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;SY)"
379 sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DD)"
380 sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;ED)"
382 ldif = """
383 dn: %s
384 objectClass: container
385 description: Default container for key credential objects
386 ShowInAdvancedViewOnly: TRUE
387 nTSecurityDescriptor: %s
388 """ % (keys_dn, sddl)
390 self.samdb.add_ldif(ldif)
392 if self.add_update_container:
393 self.update_add(op)
395 ## Operation 83: {c81fc9cc-0130-4fd1-b272-634d74818133}
397 ## Add Full Control allow aces to CN=Keys container for "domain\Key Admins"
398 ## and "rootdomain\Enterprise Key Admins".
400 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Key Admins)
401 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins)
403 def operation_83(self, op):
404 if self.update_exists(op):
405 return
406 self.raise_if_not_fix(op)
408 keys_dn = "CN=Keys,%s" % str(self.domain_dn)
410 aces = ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;KA)"]
411 aces += ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EK)"]
413 self.sd_utils.update_aces_in_dacl(keys_dn, add_aces=aces)
415 if self.add_update_container:
416 self.update_add(op)
419 ## Operation 84: {e5f9e791-d96d-4fc9-93c9-d53e1dc439ba}
421 ## Modify otherWellKnownObjects attribute to point to the CN=Keys container.
423 ## - otherWellKnownObjects: B:32:683A24E2E8164BD3AF86AC3C2CF3F981:CN=Keys,%ws
424 def operation_84(self, op):
425 if self.update_exists(op):
426 return
427 self.raise_if_not_fix(op)
429 keys_dn = "CN=Keys,%s" % str(self.domain_dn)
431 ldif = """
432 dn: %s
433 changetype: modify
434 add: otherWellKnownObjects
435 otherWellKnownObjects: B:32:683A24E2E8164BD3AF86AC3C2CF3F981:%s
436 """ % (str(self.domain_dn), keys_dn)
438 self.samdb.modify_ldif(ldif)
440 if self.add_update_container:
441 self.update_add(op)
444 ## Operation 85: {e6d5fd00-385d-4e65-b02d-9da3493ed850}
446 ## Modify the domain NC to permit "domain\Key Admins" and
447 ## "rootdomain\Enterprise Key Admins"
448 ## to modify the msds-KeyCredentialLink attribute.
450 ## (OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Key Admins)
451 ## (OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Enterprise Key Admins)
452 ## in root domain, but in non-root domains resulted in a bogus domain-relative
453 ## ACE with a non-resolvable -527 SID
455 def operation_85(self, op):
456 if self.update_exists(op):
457 return
458 self.raise_if_not_fix(op)
460 aces = ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;KA)"]
461 # we use an explicit sid in order to replay the windows mistake
462 aces += ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;%s-527)" %
463 str(self.domain_sid)]
465 self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=aces)
467 if self.add_update_container:
468 self.update_add(op)
471 ## Operation 86: {3a6b3fbf-3168-4312-a10d-dd5b3393952d}
473 ## Grant the DS-Validated-Write-Computer CAR to creator owner and self
475 ## (OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;PS)
476 ## (OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;CO)
478 def operation_86(self, op):
479 if self.update_exists(op):
480 return
481 self.raise_if_not_fix(op)
483 aces = ["(OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"]
484 aces += ["(OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;CO)"]
486 self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=aces)
488 if self.add_update_container:
489 self.update_add(op)
491 ## Operation 87: {7f950403-0ab3-47f9-9730-5d7b0269f9bd}
493 ## Delete the ACE granting Full Control to the incorrect
494 ## domain-relative Enterprise Key Admins group, and add
495 ## an ACE granting Full Control to Enterprise Key Admins group.
497 ## Delete (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins)
498 ## Add (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins)
500 def operation_87(self, op):
501 if self.update_exists(op):
502 return
503 self.raise_if_not_fix(op)
505 # we use an explicit sid in order to replay the windows mistake
506 # note this is also strange for a 2nd reason because it doesn't
507 # delete: ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;%s-527)"
508 # which was added in operation_85, so the del is basically a noop
509 # and the result is one additional ace
510 del_aces = ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;%s-527)" %
511 str(self.domain_sid)]
512 add_aces = ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EK)"]
514 self.sd_utils.update_aces_in_dacl(self.domain_dn,
515 del_aces=del_aces,
516 add_aces=add_aces)
518 if self.add_update_container:
519 self.update_add(op)
521 ## Operation 88: {434bb40d-dbc9-4fe7-81d4-d57229f7b080}
523 ## Add "msDS-ExpirePasswordsOnSmartCardOnlyAccounts" on the domain NC object
524 ## and set default value to FALSE
526 def operation_88(self, op):
527 if self.update_exists(op):
528 return
529 self.raise_if_not_fix(op)
531 ldif = """
532 dn: %s
533 changetype: modify
534 add: msDS-ExpirePasswordsOnSmartCardOnlyAccounts
535 msDS-ExpirePasswordsOnSmartCardOnlyAccounts: FALSE
536 """ % str(self.domain_dn)
538 self.samdb.modify_ldif(ldif)
540 if self.add_update_container:
541 self.update_add(op)
543 ## Windows Server 2016 (operation 89) complete, the **revision** attribute for the
544 ## CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System,DC=ForestRootDomain object
545 ## is set to **16**.
548 ## Operation 89: {a0c238ba-9e30-4ee6-80a6-43f731e9a5cd}
550 ## Delete the ACE granting Full Control to Enterprise Key Admins and
551 ## add an ACE granting Enterprise Key Admins Full Control over just
552 ## the msdsKeyCredentialLink attribute.
554 ## Delete (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins)
555 ## Add (OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Enterprise Key Admins)|
557 def operation_89(self, op):
558 if self.update_exists(op):
559 return
560 self.raise_if_not_fix(op)
562 # Note this only fixes the mistake from operation_87
563 # but leaves the mistake of operation_85 if we're
564 # not in the root domain...
565 del_aces = ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EK)"]
566 add_aces = ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;EK)"]
568 self.sd_utils.update_aces_in_dacl(self.domain_dn,
569 del_aces=del_aces,
570 add_aces=add_aces)
572 if self.add_update_container:
573 self.update_add(op)