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/>.
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
,
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):
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
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
]
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
:
213 raise ForestUpdateException("Revision is not high enough. Fix is set to False."
214 "\nExpected: %dGot: %d" % (expected_version
,
216 self
.samdb
.modify_ldif("""dn: %s
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:
229 if op
< MIN_UPDATE
or op
> MAX_UPDATE
:
230 raise ForestUpdateException("Update number invalid.")
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
)
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:
250 if start
< MIN_UPDATE
or start
> end
or end
> MAX_UPDATE
:
251 raise ForestUpdateException("Update number invalid.")
253 if op
in missing_updates
:
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
)
262 # No LDIF file exists for the change
263 getattr(self
, "operation_%d" % op
)(op
)
267 def update_exists(self
, op
):
269 :param op: Integer update number
270 :return: True if update exists else False
273 res
= self
.samdb
.search(base
=self
.forestupdate_container
,
274 expression
="(CN=%s)" % update_map
[op
])
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)
294 ldif
= self
.stored_ldif
[update_map
[op
]]
296 sub_ldif
= samba
.substitute_var(ldif
, {"CONFIG_DN":
298 "FOREST_ROOT_DOMAIN":
301 str(self
.schema_dn
)})
303 print("UPDATE (LDIF) ------ OPERATION %d" % op
)
306 self
.samdb
.modify_ldif(sub_ldif
)
307 if self
.add_update_container
:
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:")
321 new_sddl
= existing_sddl
[:index
] + ace
+ existing_sddl
[index
:]
323 # Insert it at the end if no S: section
324 new_sddl
= existing_sddl
+ ace
326 if ace
in existing_sddl
:
329 self
.sd_utils
.modify_sd_on_dn(dn
, new_sddl
,
330 controls
=["sd_flags:1:%d" % SECINFO_DACL
])
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
,
346 controls
=["search_options:1:2"])
349 existing_sddl
= msg
[0][attr
][0]
350 index
= existing_sddl
.rfind("S:")
352 new_sddl
= existing_sddl
[:index
] + ace
+ existing_sddl
[index
:]
354 # Insert it at the end if no S: section
355 new_sddl
= existing_sddl
+ ace
357 if ace
in existing_sddl
:
362 m
[attr
] = ldb
.MessageElement(new_sddl
, ldb
.FLAG_MOD_REPLACE
,
365 self
.samdb
.modify(m
, controls
=["relax:0"])
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:
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
):
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"])
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
:
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
):
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
])
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
:
439 # Update display specifiers
440 def operation_90(self
, op
):
441 if self
.add_update_container
and not self
.update_exists(op
):
444 # Update display specifiers
445 def operation_127(self
, op
):
446 if self
.add_update_container
and not self
.update_exists(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
):
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
):
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"])
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
:
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
):
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"])
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
:
503 # Set msDS-ClaimIsValueSpaceRestricted on ad://ext/AuthenticationSilo to FALSE
504 def operation_135(self
, op
):
505 if self
.update_exists(op
):
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
511 replace: msDS-ClaimIsValueSpaceRestricted
512 msDS-ClaimIsValueSpaceRestricted: FALSE
513 """ % self
.config_dn
,
514 controls
=["relax:0", "provision:0"])
516 if self
.add_update_container
:
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
):
527 def operation_79(self
, op
):
528 if self
.add_update_container
and not self
.update_exists(op
):
531 def operation_80(self
, op
):
532 if self
.add_update_container
and not self
.update_exists(op
):
535 def operation_81(self
, op
):
536 if self
.add_update_container
and not self
.update_exists(op
):
539 def operation_82(self
, op
):
540 if self
.add_update_container
and not self
.update_exists(op
):
543 def operation_83(self
, op
):
544 if self
.add_update_container
and not self
.update_exists(op
):