lib: Move the "expired" for gencache_parse calculation into gencache.c
[Samba.git] / python / samba / forest_update.py
blobba6f859a9438909b325db31a37eebd14bffff595
1 # Samba4 Forest update checker
3 # Copyright (C) Andrew Bartlett <abarlet@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 re
20 import ldb
21 import samba
22 import time
23 from base64 import b64decode
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 get_wellknown_sds, get_diff_sds
35 from samba.auth import system_session, admin_session
36 from samba.netcmd import CommandError
37 from samba.netcmd.fsmo import get_fsmo_roleowner
38 from samba.provision.common import setup_path
39 from samba.dsdb import (
40 DS_DOMAIN_FUNCTION_2008,
41 DS_DOMAIN_FUNCTION_2008_R2,
42 DS_DOMAIN_FUNCTION_2012,
43 DS_DOMAIN_FUNCTION_2012_R2,
44 DS_DOMAIN_FUNCTION_2016,
47 MIN_UPDATE = 45
48 MAX_UPDATE = 135
50 update_map = {
51 # Missing updates from 2008 + 2008 R2
52 53: "134428a8-0043-48a6-bcda-63310d9ec4dd",
53 79: "21ae657c-6649-43c4-bbb3-7f184fdf58c1",
54 80: "dca8f425-baae-47cd-b424-e3f6c76ed08b",
55 81: "a662b036-dbbe-4166-b4ba-21abea17f9cc",
56 82: "9d17b863-18c3-497d-9bde-45ddb95fcb65",
57 83: "11c39bed-4bee-45f5-b195-8da0e05b573a",
58 # Windows Server 2012 - version 11
59 84: "4664e973-cb20-4def-b3d5-559d6fe123e0",
60 85: "2972d92d-a07a-44ac-9cb0-bf243356f345",
61 86: "09a49cb3-6c54-4b83-ab20-8370838ba149",
62 87: "77283e65-ce02-4dc3-8c1e-bf99b22527c2",
63 88: "0afb7f53-96bd-404b-a659-89e65c269420",
64 89: "c7f717ef-fdbe-4b4b-8dfc-fa8b839fbcfa",
65 90: "00232167-f3a4-43c6-b503-9acb7a81b01c",
66 91: "73a9515b-511c-44d2-822b-444a33d3bd33",
67 92: "e0c60003-2ed7-4fd3-8659-7655a7e79397",
68 93: "ed0c8cca-80ab-4b6b-ac5a-59b1d317e11f",
69 94: "b6a6c19a-afc9-476b-8994-61f5b14b3f05",
70 95: "defc28cd-6cb6-4479-8bcb-aabfb41e9713",
71 96: "d6bd96d4-e66b-4a38-9c6b-e976ff58c56d",
72 97: "bb8efc40-3090-4fa2-8a3f-7cd1d380e695",
73 98: "2d6abe1b-4326-489e-920c-76d5337d2dc5",
74 99: "6b13dfb5-cecc-4fb8-b28d-0505cea24175",
75 100: "92e73422-c68b-46c9-b0d5-b55f9c741410",
76 101: "c0ad80b4-8e84-4cc4-9163-2f84649bcc42",
77 102: "992fe1d0-6591-4f24-a163-c820fcb7f308",
78 103: "ede85f96-7061-47bf-b11b-0c0d999595b5",
79 104: "ee0f3271-eb51-414a-bdac-8f9ba6397a39",
80 105: "587d52e0-507e-440e-9d67-e6129f33bb68",
81 106: "ce24f0f6-237e-43d6-ac04-1e918ab04aac",
82 107: "7f77d431-dd6a-434f-ae4d-ce82928e498f",
83 108: "ba14e1f6-7cd1-4739-804f-57d0ea74edf4",
84 109: "156ffa2a-e07c-46fb-a5c4-fbd84a4e5cce",
85 110: "7771d7dd-2231-4470-aa74-84a6f56fc3b6",
86 111: "49b2ae86-839a-4ea0-81fe-9171c1b98e83",
87 112: "1b1de989-57ec-4e96-b933-8279a8119da4",
88 113: "281c63f0-2c9a-4cce-9256-a238c23c0db9",
89 114: "4c47881a-f15a-4f6c-9f49-2742f7a11f4b",
90 115: "2aea2dc6-d1d3-4f0c-9994-66c1da21de0f",
91 116: "ae78240c-43b9-499e-ae65-2b6e0f0e202a",
92 117: "261b5bba-3438-4d5c-a3e9-7b871e5f57f0",
93 118: "3fb79c05-8ea1-438c-8c7a-81f213aa61c2",
94 119: "0b2be39a-d463-4c23-8290-32186759d3b1",
95 120: "f0842b44-bc03-46a1-a860-006e8527fccd",
96 121: "93efec15-4dd9-4850-bc86-a1f2c8e2ebb9",
97 122: "9e108d96-672f-40f0-b6bd-69ee1f0b7ac4",
98 123: "1e269508-f862-4c4a-b01f-420d26c4ff8c",
99 125: "e1ab17ed-5efb-4691-ad2d-0424592c5755",
100 126: "0e848bd4-7c70-48f2-b8fc-00fbaa82e360",
101 127: "016f23f7-077d-41fa-a356-de7cfdb01797",
102 128: "49c140db-2de3-44c2-a99a-bab2e6d2ba81",
103 129: "e0b11c80-62c5-47f7-ad0d-3734a71b8312",
104 130: "2ada1a2d-b02f-4731-b4fe-59f955e24f71",
105 # Windows Server 2012 R2 - version 15
106 131: "b83818c1-01a6-4f39-91b7-a3bb581c3ae3",
107 132: "bbbb9db0-4009-4368-8c40-6674e980d3c3",
108 133: "f754861c-3692-4a7b-b2c2-d0fa28ed0b0b",
109 134: "d32f499f-3026-4af0-a5bd-13fe5a331bd2",
110 135: "38618886-98ee-4e42-8cf1-d9a2cd9edf8b",
111 # Windows Server 2016 - version 16
112 136: "328092FB-16E7-4453-9AB8-7592DB56E9C4",
113 137: "3A1C887F-DF0A-489F-B3F2-2D0409095F6E",
114 138: "232E831F-F988-4444-8E3E-8A352E2FD411",
115 139: "DDDDCF0C-BEC9-4A5A-AE86-3CFE6CC6E110",
116 140: "A0A45AAC-5550-42DF-BB6A-3CC5C46B52F2",
117 141: "3E7645F3-3EA5-4567-B35A-87630449C70C",
118 142: "E634067B-E2C4-4D79-B6E8-73C619324D5E"
121 functional_level_to_max_update = {
122 DS_DOMAIN_FUNCTION_2008: 78,
123 DS_DOMAIN_FUNCTION_2008_R2: 83,
124 DS_DOMAIN_FUNCTION_2012: 130,
125 DS_DOMAIN_FUNCTION_2012_R2: 135,
126 DS_DOMAIN_FUNCTION_2016: 142,
129 functional_level_to_version = {
130 DS_DOMAIN_FUNCTION_2008: 2,
131 DS_DOMAIN_FUNCTION_2008_R2: 5,
132 DS_DOMAIN_FUNCTION_2012: 11,
133 DS_DOMAIN_FUNCTION_2012_R2: 15,
134 DS_DOMAIN_FUNCTION_2016: 16,
137 # Documentation says that this update was deprecated
138 missing_updates = [124]
141 class ForestUpdateException(Exception):
142 pass
145 class ForestUpdate(object):
146 """Check and update a SAM database for forest updates"""
148 def __init__(self, samdb, verbose=False, fix=False,
149 add_update_container=True):
151 :param samdb: LDB database
152 :param verbose: Show the ldif changes
153 :param fix: Apply the update if the container is missing
154 :param add_update_container: Add the container at the end of the change
155 :raise ForestUpdateException:
157 from samba.ms_forest_updates_markdown import read_ms_markdown
159 self.samdb = samdb
160 self.fix = fix
161 self.verbose = verbose
162 self.add_update_container = add_update_container
163 # TODO In future we should check for inconsistencies when it claims it has been done
164 self.check_update_applied = False
166 self.config_dn = self.samdb.get_config_basedn()
167 self.domain_dn = self.samdb.domain_dn()
168 self.schema_dn = self.samdb.get_schema_basedn()
170 self.sd_utils = sd_utils.SDUtils(samdb)
171 self.domain_sid = security.dom_sid(samdb.get_domain_sid())
173 self.forestupdate_container = self.samdb.get_config_basedn()
174 if not self.forestupdate_container.add_child("CN=Operations,CN=ForestUpdates"):
175 raise ForestUpdateException("Failed to add forest update container child")
177 self.revision_object = self.samdb.get_config_basedn()
178 if not self.revision_object.add_child("CN=ActiveDirectoryUpdate,CN=ForestUpdates"):
179 raise ForestUpdateException("Failed to add revision object child")
181 # Store the result of parsing the markdown in a dictionary
182 self.stored_ldif = {}
183 read_ms_markdown(setup_path("adprep/WindowsServerDocs/Forest-Wide-Updates.md"),
184 out_dict=self.stored_ldif)
186 def check_updates_functional_level(self, functional_level,
187 old_functional_level=None,
188 update_revision=False):
190 Apply all updates for a given old and new functional level
191 :param functional_level: constant
192 :param old_functional_level: constant
193 :param update_revision: modify the stored version
194 :raise ForestUpdateException:
196 res = self.samdb.search(base=self.revision_object,
197 attrs=["revision"], scope=ldb.SCOPE_BASE)
199 expected_update = functional_level_to_max_update[functional_level]
201 if old_functional_level:
202 min_update = functional_level_to_max_update[old_functional_level]
203 min_update += 1
204 else:
205 min_update = MIN_UPDATE
207 self.check_updates_range(min_update, expected_update)
209 expected_version = functional_level_to_version[functional_level]
210 found_version = int(res[0]['revision'][0])
211 if update_revision and found_version < expected_version:
212 if not self.fix:
213 raise ForestUpdateException("Revision is not high enough. Fix is set to False."
214 "\nExpected: %dGot: %d" % (expected_version,
215 found_version))
216 self.samdb.modify_ldif("""dn: %s
217 changetype: modify
218 replace: revision
219 revision: %d
220 """ % (str(self.revision_object), expected_version))
222 def check_updates_iterator(self, iterator):
224 Apply a list of updates which must be within the valid range of updates
225 :param iterator: Iterable specifying integer update numbers to apply
226 :raise ForestUpdateException:
228 for op in iterator:
229 if op < MIN_UPDATE or op > MAX_UPDATE:
230 raise ForestUpdateException("Update number invalid.")
232 if 84 <= op <= 87:
233 self.operation_ldif(op)
234 elif 91 <= op <= 126:
235 self.operation_ldif(op)
236 elif 131 <= op <= 134:
237 self.operation_ldif(op)
238 else:
239 # No LDIF file exists for the change
240 getattr(self, "operation_%d" % op)(op)
242 def check_updates_range(self, start=0, end=0):
244 Apply a range of updates which must be within the valid range of updates
245 :param start: integer update to begin
246 :param end: integer update to end (inclusive)
247 :raise ForestUpdateException:
249 op = start
250 if start < MIN_UPDATE or start > end or end > MAX_UPDATE:
251 raise ForestUpdateException("Update number invalid.")
252 while op <= end:
253 if op in missing_updates:
254 pass
255 elif 84 <= op <= 87:
256 self.operation_ldif(op)
257 elif 91 <= op <= 126:
258 self.operation_ldif(op)
259 elif 131 <= op <= 134:
260 self.operation_ldif(op)
261 else:
262 # No LDIF file exists for the change
263 getattr(self, "operation_%d" % op)(op)
265 op += 1
267 def update_exists(self, op):
269 :param op: Integer update number
270 :return: True if update exists else False
272 try:
273 res = self.samdb.search(base=self.forestupdate_container,
274 expression="(CN=%s)" % update_map[op])
275 except ldb.LdbError:
276 return False
278 return len(res) == 1
280 def update_add(self, op):
282 Add the corresponding container object for the given update
283 :param op: Integer update
285 self.samdb.add_ldif("""dn: CN=%s,%s
286 objectClass: container
287 """ % (update_map[op], str(self.forestupdate_container)))
289 def operation_ldif(self, op):
290 if self.update_exists(op):
291 # Assume we have applied it (we have no double checks for these)
292 return True
294 ldif = self.stored_ldif[update_map[op]]
296 sub_ldif = samba.substitute_var(ldif, {"CONFIG_DN":
297 str(self.config_dn),
298 "FOREST_ROOT_DOMAIN":
299 str(self.domain_dn),
300 "SCHEMA_DN":
301 str(self.schema_dn)})
302 if self.verbose:
303 print("UPDATE (LDIF) ------ OPERATION %d" % op)
304 print(sub_ldif)
306 self.samdb.modify_ldif(sub_ldif)
307 if self.add_update_container:
308 self.update_add(op)
310 def insert_ace_into_dacl(self, dn, existing_sddl, ace):
312 Add an ACE to a DACL, checking if it already exists with a simple string search.
314 :param dn: DN to modify
315 :param existing_sddl: existing sddl as string
316 :param ace: string ace to insert
317 :return: True if modified else False
319 index = existing_sddl.rfind("S:")
320 if index != -1:
321 new_sddl = existing_sddl[:index] + ace + existing_sddl[index:]
322 else:
323 # Insert it at the end if no S: section
324 new_sddl = existing_sddl + ace
326 if ace in existing_sddl:
327 return False
329 self.sd_utils.modify_sd_on_dn(dn, new_sddl,
330 controls=["sd_flags:1:%d" % SECINFO_DACL])
332 return True
334 def insert_ace_into_string(self, dn, ace, attr):
336 Insert an ACE into a string attribute like defaultSecurityDescriptor.
337 This also checks if it already exists using a simple string search.
339 :param dn: DN to modify
340 :param ace: string ace to insert
341 :param attr: attribute to modify
342 :return: True if modified else False
344 msg = self.samdb.search(base=dn,
345 attrs=[attr],
346 controls=["search_options:1:2"])
348 assert len(msg) == 1
349 existing_sddl = msg[0][attr][0]
350 index = existing_sddl.rfind("S:")
351 if index != -1:
352 new_sddl = existing_sddl[:index] + ace + existing_sddl[index:]
353 else:
354 # Insert it at the end if no S: section
355 new_sddl = existing_sddl + ace
357 if ace in existing_sddl:
358 return False
360 m = ldb.Message()
361 m.dn = dn
362 m[attr] = ldb.MessageElement(new_sddl, ldb.FLAG_MOD_REPLACE,
363 attr)
365 self.samdb.modify(m, controls=["relax:0"])
367 return True
369 def raise_if_not_fix(self, op):
371 Raises an exception if not set to fix.
372 :param op: Integer operation
373 :raise ForestUpdateException:
375 if not self.fix:
376 raise ForestUpdateException("Missing operation %d. Fix is currently set to False" % op)
379 # Created a new object CN=Sam-Domain in the Schema partition
381 # Created the following access control entry (ACE) to grant Write Property
382 # to Principal Self on the object: ...
384 def operation_88(self, op):
385 if self.update_exists(op):
386 return
387 self.raise_if_not_fix(op)
389 ace = "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"
391 schema_dn = ldb.Dn(self.samdb, "CN=Sam-Domain,%s" % str(self.schema_dn))
393 self.insert_ace_into_string(schema_dn, ace,
394 attr="defaultSecurityDescriptor")
396 res = self.samdb.search(expression="(objectClass=samDomain)",
397 attrs=["nTSecurityDescriptor"],
398 controls=["search_options:1:2"])
399 for msg in res:
400 existing_sd = ndr_unpack(security.descriptor, msg["nTSecurityDescriptor"][0])
401 existing_sddl = existing_sd.as_sddl(self.domain_sid)
403 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
405 if self.add_update_container:
406 self.update_add(op)
409 # Created a new object CN=Domain-DNS in the Schema partition
411 # Created the following access control entry (ACE) to grant Write Property
412 # to Principal Self on the object: ...
414 def operation_89(self, op):
415 if self.update_exists(op):
416 return
417 self.raise_if_not_fix(op)
419 ace = "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"
421 schema_dn = ldb.Dn(self.samdb, "CN=Domain-DNS,%s" % str(self.schema_dn))
422 self.insert_ace_into_string(schema_dn, ace,
423 attr="defaultSecurityDescriptor")
425 res = self.samdb.search(expression="(objectClass=domainDNS)",
426 attrs=["nTSecurityDescriptor"],
427 controls=["search_options:1:2",
428 "sd_flags:1:%d" % SECINFO_DACL])
430 for msg in res:
431 existing_sd = ndr_unpack(security.descriptor, msg["nTSecurityDescriptor"][0])
432 existing_sddl = existing_sd.as_sddl(self.domain_sid)
434 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
436 if self.add_update_container:
437 self.update_add(op)
439 # Update display specifiers
440 def operation_90(self, op):
441 if self.add_update_container and not self.update_exists(op):
442 self.update_add(op)
444 # Update display specifiers
445 def operation_127(self, op):
446 if self.add_update_container and not self.update_exists(op):
447 self.update_add(op)
449 # Update appears to already be applied in documentation
450 def operation_128(self, op):
451 if self.add_update_container and not self.update_exists(op):
452 self.update_add(op)
454 # Grant ACE (OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS) to samDomain
455 def operation_129(self, op):
456 if self.update_exists(op):
457 return
458 self.raise_if_not_fix(op)
460 ace = "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)"
462 schema_dn = ldb.Dn(self.samdb, "CN=Sam-Domain,%s" % str(self.schema_dn))
463 self.insert_ace_into_string(schema_dn, ace,
464 attr='defaultSecurityDescriptor')
466 res = self.samdb.search(expression="(objectClass=samDomain)",
467 attrs=["nTSecurityDescriptor"],
468 controls=["search_options:1:2"])
469 for msg in res:
470 existing_sd = ndr_unpack(security.descriptor, msg["nTSecurityDescriptor"][0])
471 existing_sddl = existing_sd.as_sddl(self.domain_sid)
473 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
475 if self.add_update_container:
476 self.update_add(op)
478 # Grant ACE (OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS) to domainDNS
479 def operation_130(self, op):
480 if self.update_exists(op):
481 return
482 self.raise_if_not_fix(op)
484 ace = "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)"
486 schema_dn = ldb.Dn(self.samdb, "CN=Domain-DNS,%s" % str(self.schema_dn))
487 self.insert_ace_into_string(schema_dn, ace,
488 attr='defaultSecurityDescriptor')
490 res = self.samdb.search(expression="(objectClass=domainDNS)",
491 attrs=["nTSecurityDescriptor"],
492 controls=["search_options:1:2"])
494 for msg in res:
495 existing_sd = ndr_unpack(security.descriptor, msg["nTSecurityDescriptor"][0])
496 existing_sddl = existing_sd.as_sddl(self.domain_sid)
498 self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
500 if self.add_update_container:
501 self.update_add(op)
503 # Set msDS-ClaimIsValueSpaceRestricted on ad://ext/AuthenticationSilo to FALSE
504 def operation_135(self, op):
505 if self.update_exists(op):
506 return
507 self.raise_if_not_fix(op)
509 self.samdb.modify_ldif("""dn: CN=ad://ext/AuthenticationSilo,CN=Claim Types,CN=Claims Configuration,CN=Services,%s
510 changetype: modify
511 replace: msDS-ClaimIsValueSpaceRestricted
512 msDS-ClaimIsValueSpaceRestricted: FALSE
513 """ % self.config_dn,
514 controls=["relax:0", "provision:0"])
516 if self.add_update_container:
517 self.update_add(op)
520 # THE FOLLOWING ARE MISSING UPDATES FROM 2008 + 2008 R2
523 def operation_53(self, op):
524 if self.add_update_container and not self.update_exists(op):
525 self.update_add(op)
527 def operation_79(self, op):
528 if self.add_update_container and not self.update_exists(op):
529 self.update_add(op)
531 def operation_80(self, op):
532 if self.add_update_container and not self.update_exists(op):
533 self.update_add(op)
535 def operation_81(self, op):
536 if self.add_update_container and not self.update_exists(op):
537 self.update_add(op)
539 def operation_82(self, op):
540 if self.add_update_container and not self.update_exists(op):
541 self.update_add(op)
543 def operation_83(self, op):
544 if self.add_update_container and not self.update_exists(op):
545 self.update_add(op)