s4:kdc: adjust formatting of samba_kdc_update_pac() documentation
[Samba.git] / python / samba / domain_update.py
blobfeb03de860cd9fa12e5de9e860457f9abe31d368
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 = 89
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
52 # Windows Server 2016 - version 15
53 82: "83c53da7-427e-47a4-a07a-a324598b88f7",
54 # from the documentation and a fresh installation
55 # 83 is this:
56 # c81fc9cc-0130-4fd1-b272-634d74818133
57 # adprep will use this on the wire:
58 # c81fc9cc-0130-f4d1-b272-634d74818133
59 83: "c81fc9cc-0130-4fd1-b272-634d74818133",
60 84: "e5f9e791-d96d-4fc9-93c9-d53e1dc439ba",
61 85: "e6d5fd00-385d-4e65-b02d-9da3493ed850",
62 86: "3a6b3fbf-3168-4312-a10d-dd5b3393952d",
63 87: "7f950403-0ab3-47f9-9730-5d7b0269f9bd",
64 88: "434bb40d-dbc9-4fe7-81d4-d57229f7b080",
65 # Windows Server 2016 - version 16
66 89: "a0c238ba-9e30-4ee6-80a6-43f731e9a5cd",
70 functional_level_to_max_update = {
71 DS_DOMAIN_FUNCTION_2008: 74,
72 DS_DOMAIN_FUNCTION_2008_R2: 77,
73 DS_DOMAIN_FUNCTION_2012: 81,
74 DS_DOMAIN_FUNCTION_2012_R2: 81,
75 DS_DOMAIN_FUNCTION_2016: 89,
78 functional_level_to_version = {
79 DS_DOMAIN_FUNCTION_2008: 3,
80 DS_DOMAIN_FUNCTION_2008_R2: 5,
81 DS_DOMAIN_FUNCTION_2012: 9,
82 DS_DOMAIN_FUNCTION_2012_R2: 10,
83 DS_DOMAIN_FUNCTION_2016: 16,
86 # No update numbers have been skipped over
87 missing_updates = []
90 class DomainUpdateException(Exception):
91 pass
94 class DomainUpdate(object):
95 """Check and update a SAM database for domain updates"""
97 def __init__(self, samdb, fix=False,
98 add_update_container=True):
99 """
100 :param samdb: LDB database
101 :param fix: Apply the update if the container is missing
102 :param add_update_container: Add the container at the end of the change
103 :raise DomainUpdateException:
105 self.samdb = samdb
106 self.fix = fix
107 self.add_update_container = add_update_container
108 # TODO: In future we should check for inconsistencies when it claims it has been done
109 self.check_update_applied = False
111 self.config_dn = self.samdb.get_config_basedn()
112 self.domain_dn = self.samdb.domain_dn()
113 self.schema_dn = self.samdb.get_schema_basedn()
115 self.sd_utils = sd_utils.SDUtils(samdb)
116 self.domain_sid = security.dom_sid(samdb.get_domain_sid())
118 self.domainupdate_container = self.samdb.get_root_basedn()
119 try:
120 self.domainupdate_container.add_child("CN=Operations,CN=DomainUpdates,CN=System")
121 except ldb.LdbError:
122 raise DomainUpdateException("Failed to add domain update container child")
124 self.revision_object = self.samdb.get_root_basedn()
125 try:
126 self.revision_object.add_child("CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System")
127 except ldb.LdbError:
128 raise DomainUpdateException("Failed to add revision object child")
130 def check_updates_functional_level(self, functional_level,
131 old_functional_level=None,
132 update_revision=False):
134 Apply all updates for a given old and new functional level
135 :param functional_level: constant
136 :param old_functional_level: constant
137 :param update_revision: modify the stored version
138 :raise DomainUpdateException:
140 res = self.samdb.search(base=self.revision_object,
141 attrs=["revision"], scope=ldb.SCOPE_BASE)
143 expected_update = functional_level_to_max_update[functional_level]
145 if old_functional_level:
146 min_update = functional_level_to_max_update[old_functional_level]
147 min_update += 1
148 else:
149 min_update = MIN_UPDATE
151 self.check_updates_range(min_update, expected_update)
153 expected_version = functional_level_to_version[functional_level]
154 found_version = int(res[0]['revision'][0])
155 if update_revision and found_version < expected_version:
156 if not self.fix:
157 raise DomainUpdateException("Revision is not high enough. Fix is set to False."
158 "\nExpected: %dGot: %d" % (expected_version,
159 found_version))
160 self.samdb.modify_ldif("""dn: %s
161 changetype: modify
162 replace: revision
163 revision: %d
164 """ % (str(self.revision_object), expected_version))
166 def check_updates_iterator(self, iterator):
168 Apply a list of updates which must be within the valid range of updates
169 :param iterator: Iterable specifying integer update numbers to apply
170 :raise DomainUpdateException:
172 for op in iterator:
173 if op < MIN_UPDATE or op > MAX_UPDATE:
174 raise DomainUpdateException("Update number invalid.")
176 # No LDIF file exists for the change
177 getattr(self, "operation_%d" % op)(op)
179 def check_updates_range(self, start=0, end=0):
181 Apply a range of updates which must be within the valid range of updates
182 :param start: integer update to begin
183 :param end: integer update to end (inclusive)
184 :raise DomainUpdateException:
186 op = start
187 if start < MIN_UPDATE or start > end or end > MAX_UPDATE:
188 raise DomainUpdateException("Update number invalid.")
189 while op <= end:
190 if op not in missing_updates:
191 # No LDIF file exists for the change
192 getattr(self, "operation_%d" % op)(op)
194 op += 1
196 def update_exists(self, op):
198 :param op: Integer update number
199 :return: True if update exists else False
201 update_dn = "CN=%s,%s" % (update_map[op], self.domainupdate_container)
202 try:
203 res = self.samdb.search(base=update_dn,
204 scope=ldb.SCOPE_BASE,
205 attrs=[])
206 except ldb.LdbError as e:
207 (num, msg) = e.args
208 if num != ldb.ERR_NO_SUCH_OBJECT:
209 raise
210 return False
212 assert len(res) == 1
213 print("Skip Domain Update %u: %s" % (op, update_map[op]))
214 return True
216 def update_add(self, op):
218 Add the corresponding container object for the given update
219 :param op: Integer update
221 self.samdb.add_ldif("""dn: CN=%s,%s
222 objectClass: container
223 """ % (update_map[op], str(self.domainupdate_container)))
224 print("Applied Domain Update %u: %s" % (op, update_map[op]))
226 def raise_if_not_fix(self, op):
228 Raises an exception if not set to fix.
229 :param op: Integer operation
230 :raise DomainUpdateException:
232 if not self.fix:
233 raise DomainUpdateException("Missing operation %d. Fix is currently set to False" % op)
235 # Create a new object CN=TPM Devices in the Domain partition.
236 def operation_78(self, op):
237 if self.update_exists(op):
238 return
239 self.raise_if_not_fix(op)
241 self.samdb.add_ldif("""dn: CN=TPM Devices,%s
242 objectClass: top
243 objectClass: msTPM-InformationObjectsContainer
244 """ % self.domain_dn,
245 controls=["relax:0", "provision:0"])
247 if self.add_update_container:
248 self.update_add(op)
250 # Created an access control entry for the TPM service.
251 def operation_79(self, op):
252 if self.update_exists(op):
253 return
254 self.raise_if_not_fix(op)
256 ace = "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"
258 self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=[ace])
260 if self.add_update_container:
261 self.update_add(op)
263 # Grant "Clone DC" extended right to Cloneable Domain Controllers group
264 def operation_80(self, op):
265 if self.update_exists(op):
266 return
267 self.raise_if_not_fix(op)
269 ace = "(OA;;CR;3e0f7e18-2c7a-4c10-ba82-4d926db99a3e;;CN)"
271 self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=[ace])
273 if self.add_update_container:
274 self.update_add(op)
276 # Grant ms-DS-Allowed-To-Act-On-Behalf-Of-Other-Identity to Principal Self
277 # on all objects
278 def operation_81(self, op):
279 if self.update_exists(op):
280 return
281 self.raise_if_not_fix(op)
283 ace = "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)"
285 self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=[ace])
287 if self.add_update_container:
288 self.update_add(op)
291 # THE FOLLOWING ARE MISSING UPDATES FROM 2008 R2
294 # Add Managed Service Accounts container
295 def operation_75(self, op):
296 if self.update_exists(op):
297 return
298 self.raise_if_not_fix(op)
300 descriptor = get_managed_service_accounts_descriptor(self.domain_sid)
301 managedservice_descr = b64encode(descriptor).decode('utf8')
302 managed_service_dn = "CN=Managed Service Accounts,%s" % \
303 str(self.domain_dn)
305 self.samdb.modify_ldif("""dn: %s
306 changetype: add
307 objectClass: container
308 description: Default container for managed service accounts
309 showInAdvancedViewOnly: FALSE
310 nTSecurityDescriptor:: %s""" % (managed_service_dn, managedservice_descr),
311 controls=["relax:0", "provision:0"])
313 if self.add_update_container:
314 self.update_add(op)
316 # Add the otherWellKnownObjects reference to MSA
317 def operation_76(self, op):
318 if self.update_exists(op):
319 return
320 self.raise_if_not_fix(op)
322 managed_service_dn = "CN=Managed Service Accounts,%s" % \
323 str(self.domain_dn)
325 self.samdb.modify_ldif("""dn: %s
326 changetype: modify
327 add: otherWellKnownObjects
328 otherWellKnownObjects: B:32:1EB93889E40C45DF9F0C64D23BBB6237:%s
329 """ % (str(self.domain_dn), managed_service_dn), controls=["relax:0",
330 "provision:0"])
332 if self.add_update_container:
333 self.update_add(op)
335 # Add the PSPs object in the System container
336 def operation_77(self, op):
337 if self.update_exists(op):
338 return
339 self.raise_if_not_fix(op)
341 self.samdb.add_ldif("""dn: CN=PSPs,CN=System,%s
342 objectClass: top
343 objectClass: msImaging-PSPs
344 """ % str(self.domain_dn), controls=["relax:0", "provision:0"])
346 if self.add_update_container:
347 self.update_add(op)
349 ## ## Windows Server 2016: Domain-wide updates
351 ## After the operations that are performed by domainprep in Windows
352 ## Server 2016 (operations 82-88) complete, the revision attribute for the
353 ## CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System,DC=ForestRootDomain
354 ## object is set to 15.
356 ## Operation 82: {83c53da7-427e-47a4-a07a-a324598b88f7}
358 ## Create CN=Keys container at root of domain
360 ## - objectClass: container
361 ## - description: Default container for key credential objects
362 ## - ShowInAdvancedViewOnly: TRUE
364 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EA)
365 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DA)
366 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;SY)
367 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DD)
368 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;ED)
370 def operation_82(self, op):
371 if self.update_exists(op):
372 return
373 self.raise_if_not_fix(op)
375 keys_dn = "CN=Keys,%s" % str(self.domain_dn)
377 sddl = "O:DA"
378 sddl += "D:"
379 sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EA)"
380 sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DA)"
381 sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;SY)"
382 sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;DD)"
383 sddl += "(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;ED)"
385 ldif = """
386 dn: %s
387 objectClass: container
388 description: Default container for key credential objects
389 ShowInAdvancedViewOnly: TRUE
390 nTSecurityDescriptor: %s
391 """ % (keys_dn, sddl)
393 self.samdb.add_ldif(ldif)
395 if self.add_update_container:
396 self.update_add(op)
398 ## Operation 83: {c81fc9cc-0130-4fd1-b272-634d74818133}
400 ## Add Full Control allow aces to CN=Keys container for "domain\Key Admins"
401 ## and "rootdomain\Enterprise Key Admins".
403 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Key Admins)
404 ## (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins)
406 def operation_83(self, op):
407 if self.update_exists(op):
408 return
409 self.raise_if_not_fix(op)
411 keys_dn = "CN=Keys,%s" % str(self.domain_dn)
413 aces = ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;KA)"]
414 aces += ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EK)"]
416 self.sd_utils.update_aces_in_dacl(keys_dn, add_aces=aces)
418 if self.add_update_container:
419 self.update_add(op)
422 ## Operation 84: {e5f9e791-d96d-4fc9-93c9-d53e1dc439ba}
424 ## Modify otherWellKnownObjects attribute to point to the CN=Keys container.
426 ## - otherWellKnownObjects: B:32:683A24E2E8164BD3AF86AC3C2CF3F981:CN=Keys,%ws
427 def operation_84(self, op):
428 if self.update_exists(op):
429 return
430 self.raise_if_not_fix(op)
432 keys_dn = "CN=Keys,%s" % str(self.domain_dn)
434 ldif = """
435 dn: %s
436 changetype: modify
437 add: otherWellKnownObjects
438 otherWellKnownObjects: B:32:683A24E2E8164BD3AF86AC3C2CF3F981:%s
439 """ % (str(self.domain_dn), keys_dn)
441 self.samdb.modify_ldif(ldif)
443 if self.add_update_container:
444 self.update_add(op)
447 ## Operation 85: {e6d5fd00-385d-4e65-b02d-9da3493ed850}
449 ## Modify the domain NC to permit "domain\Key Admins" and
450 ## "rootdomain\Enterprise Key Admins"
451 ## to modify the msds-KeyCredentialLink attribute.
453 ## (OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Key Admins)
454 ## (OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Enterprise Key Admins)
455 ## in root domain, but in non-root domains resulted in a bogus domain-relative
456 ## ACE with a non-resolvable -527 SID
458 def operation_85(self, op):
459 if self.update_exists(op):
460 return
461 self.raise_if_not_fix(op)
463 aces = ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;KA)"]
464 # we use an explicit sid in order to replay the windows mistake
465 aces += ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;%s-527)" %
466 str(self.domain_sid)]
468 self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=aces)
470 if self.add_update_container:
471 self.update_add(op)
474 ## Operation 86: {3a6b3fbf-3168-4312-a10d-dd5b3393952d}
476 ## Grant the DS-Validated-Write-Computer CAR to creator owner and self
478 ## (OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;PS)
479 ## (OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;CO)
481 def operation_86(self, op):
482 if self.update_exists(op):
483 return
484 self.raise_if_not_fix(op)
486 aces = ["(OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"]
487 aces += ["(OA;CIIO;SW;9b026da6-0d3c-465c-8bee-5199d7165cba;bf967a86-0de6-11d0-a285-00aa003049e2;CO)"]
489 self.sd_utils.update_aces_in_dacl(self.domain_dn, add_aces=aces)
491 if self.add_update_container:
492 self.update_add(op)
494 ## Operation 87: {7f950403-0ab3-47f9-9730-5d7b0269f9bd}
496 ## Delete the ACE granting Full Control to the incorrect
497 ## domain-relative Enterprise Key Admins group, and add
498 ## an ACE granting Full Control to Enterprise Key Admins group.
500 ## Delete (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins)
501 ## Add (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins)
503 def operation_87(self, op):
504 if self.update_exists(op):
505 return
506 self.raise_if_not_fix(op)
508 # we use an explicit sid in order to replay the windows mistake
509 # note this is also strange for a 2nd reason because it doesn't
510 # delete: ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;%s-527)"
511 # which was added in operation_85, so the del is basically a noop
512 # and the result is one additional ace
513 del_aces = ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;%s-527)" %
514 str(self.domain_sid)]
515 add_aces = ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EK)"]
517 self.sd_utils.update_aces_in_dacl(self.domain_dn,
518 del_aces=del_aces,
519 add_aces=add_aces)
521 if self.add_update_container:
522 self.update_add(op)
524 ## Operation 88: {434bb40d-dbc9-4fe7-81d4-d57229f7b080}
526 ## Add "msDS-ExpirePasswordsOnSmartCardOnlyAccounts" on the domain NC object
527 ## and set default value to FALSE
529 def operation_88(self, op):
530 if self.update_exists(op):
531 return
532 self.raise_if_not_fix(op)
534 ldif = """
535 dn: %s
536 changetype: modify
537 add: msDS-ExpirePasswordsOnSmartCardOnlyAccounts
538 msDS-ExpirePasswordsOnSmartCardOnlyAccounts: FALSE
539 """ % str(self.domain_dn)
541 self.samdb.modify_ldif(ldif)
543 if self.add_update_container:
544 self.update_add(op)
546 ## Windows Server 2016 (operation 89) complete, the **revision** attribute for the
547 ## CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System,DC=ForestRootDomain object
548 ## is set to **16**.
551 ## Operation 89: {a0c238ba-9e30-4ee6-80a6-43f731e9a5cd}
553 ## Delete the ACE granting Full Control to Enterprise Key Admins and
554 ## add an ACE granting Enterprise Key Admins Full Control over just
555 ## the msdsKeyCredentialLink attribute.
557 ## Delete (A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;Enterprise Key Admins)
558 ## Add (OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;Enterprise Key Admins)|
560 def operation_89(self, op):
561 if self.update_exists(op):
562 return
563 self.raise_if_not_fix(op)
565 # Note this only fixes the mistake from operation_87
566 # but leaves the mistake of operation_85 if we're
567 # not in the root domain...
568 del_aces = ["(A;CI;RPWPCRLCLOCCDCRCWDWOSDDTSW;;;EK)"]
569 add_aces = ["(OA;CI;RPWP;5b47d60f-6090-40b2-9f37-2a4de88f3063;;EK)"]
571 self.sd_utils.update_aces_in_dacl(self.domain_dn,
572 del_aces=del_aces,
573 add_aces=add_aces)
575 if self.add_update_container:
576 self.update_add(op)