python/samba: PY3 port samba4.blackbox.functionalprep
[amitay/samba.git] / python / samba / forest_update.py
1 # Samba4 Forest update checker
2 #
3 # Copyright (C) Andrew Bartlett <abarlet@samba.org> 2017
4 #
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.
9 #
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.
14 #
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/>.
17 #
18
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,
45 )
46
47 MIN_UPDATE = 45
48 MAX_UPDATE = 135
49
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"
119 }
120
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,
127 }
128
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,
135 }
136
137 # Documentation says that this update was deprecated
138 missing_updates = [124]
139
140
141 class ForestUpdateException(Exception):
142     pass
143
144
145 class ForestUpdate(object):
146     """Check and update a SAM database for forest updates"""
147
148     def __init__(self, samdb, verbose=False, fix=False,
149                  add_update_container=True):
150         """
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:
156         """
157         from samba.ms_forest_updates_markdown import read_ms_markdown
158
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
165
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()
169
170         self.sd_utils = sd_utils.SDUtils(samdb)
171         self.domain_sid = security.dom_sid(samdb.get_domain_sid())
172
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")
176
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")
180
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)
185
186     def check_updates_functional_level(self, functional_level,
187                                        old_functional_level=None,
188                                        update_revision=False):
189         """
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:
195         """
196         res = self.samdb.search(base=self.revision_object,
197                                 attrs=["revision"], scope=ldb.SCOPE_BASE)
198
199         expected_update = functional_level_to_max_update[functional_level]
200
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
206
207         self.check_updates_range(min_update, expected_update)
208
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))
221
222     def check_updates_iterator(self, iterator):
223         """
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:
227         """
228         for op in iterator:
229             if op < MIN_UPDATE or op > MAX_UPDATE:
230                 raise ForestUpdateException("Update number invalid.")
231
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)
241
242     def check_updates_range(self, start=0, end=0):
243         """
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:
248         """
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)
264
265             op += 1
266
267     def update_exists(self, op):
268         """
269         :param op: Integer update number
270         :return: True if update exists else False
271         """
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
277
278         return len(res) == 1
279
280     def update_add(self, op):
281         """
282         Add the corresponding container object for the given update
283         :param op: Integer update
284         """
285         self.samdb.add_ldif("""dn: CN=%s,%s
286 objectClass: container
287 """ % (update_map[op], str(self.forestupdate_container)))
288
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
293
294         ldif = self.stored_ldif[update_map[op]]
295
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)
305
306         self.samdb.modify_ldif(sub_ldif)
307         if self.add_update_container:
308             self.update_add(op)
309
310     def insert_ace_into_dacl(self, dn, existing_sddl, ace):
311         """
312         Add an ACE to a DACL, checking if it already exists with a simple string search.
313
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
318         """
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
325
326         if ace in existing_sddl:
327             return False
328
329         self.sd_utils.modify_sd_on_dn(dn, new_sddl,
330                                       controls=["sd_flags:1:%d" % SECINFO_DACL])
331
332         return True
333
334     def insert_ace_into_string(self, dn, ace, attr):
335         """
336         Insert an ACE into a string attribute like defaultSecurityDescriptor.
337         This also checks if it already exists using a simple string search.
338
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
343         """
344         msg = self.samdb.search(base=dn,
345                                 attrs=[attr],
346                                 controls=["search_options:1:2"])
347
348         assert len(msg) == 1
349         existing_sddl = str(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
356
357         if ace in existing_sddl:
358             return False
359
360         m = ldb.Message()
361         m.dn = dn
362         m[attr] = ldb.MessageElement(new_sddl, ldb.FLAG_MOD_REPLACE,
363                                      attr)
364
365         self.samdb.modify(m, controls=["relax:0"])
366
367         return True
368
369     def raise_if_not_fix(self, op):
370         """
371         Raises an exception if not set to fix.
372         :param op: Integer operation
373         :raise ForestUpdateException:
374         """
375         if not self.fix:
376             raise ForestUpdateException("Missing operation %d. Fix is currently set to False" % op)
377
378     #
379     # Created a new object CN=Sam-Domain in the Schema partition
380     #
381     # Created the following access control entry (ACE) to grant Write Property
382     # to Principal Self on the object: ...
383     #
384     def operation_88(self, op):
385         if self.update_exists(op):
386             return
387         self.raise_if_not_fix(op)
388
389         ace = "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"
390
391         schema_dn = ldb.Dn(self.samdb, "CN=Sam-Domain,%s" % str(self.schema_dn))
392
393         self.insert_ace_into_string(schema_dn, ace,
394                                     attr="defaultSecurityDescriptor")
395
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)
402
403             self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
404
405         if self.add_update_container:
406             self.update_add(op)
407
408     #
409     # Created a new object CN=Domain-DNS in the Schema partition
410     #
411     # Created the following access control entry (ACE) to grant Write Property
412     # to Principal Self on the object: ...
413     #
414     def operation_89(self, op):
415         if self.update_exists(op):
416             return
417         self.raise_if_not_fix(op)
418
419         ace = "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"
420
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")
424
425         res = self.samdb.search(expression="(objectClass=domainDNS)",
426                                 attrs=["nTSecurityDescriptor"],
427                                 controls=["search_options:1:2",
428                                           "sd_flags:1:%d" % SECINFO_DACL])
429
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)
433
434             self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
435
436         if self.add_update_container:
437             self.update_add(op)
438
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)
443
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)
448
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)
453
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)
459
460         ace = "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)"
461
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')
465
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)
472
473             self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
474
475         if self.add_update_container:
476             self.update_add(op)
477
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)
483
484         ace = "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)"
485
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')
489
490         res = self.samdb.search(expression="(objectClass=domainDNS)",
491                                 attrs=["nTSecurityDescriptor"],
492                                 controls=["search_options:1:2"])
493
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)
497
498             self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
499
500         if self.add_update_container:
501             self.update_add(op)
502
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)
508
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"])
515
516         if self.add_update_container:
517             self.update_add(op)
518
519     #
520     # THE FOLLOWING ARE MISSING UPDATES FROM 2008 + 2008 R2
521     #
522
523     def operation_53(self, op):
524         if self.add_update_container and not self.update_exists(op):
525             self.update_add(op)
526
527     def operation_79(self, op):
528         if self.add_update_container and not self.update_exists(op):
529             self.update_add(op)
530
531     def operation_80(self, op):
532         if self.add_update_container and not self.update_exists(op):
533             self.update_add(op)
534
535     def operation_81(self, op):
536         if self.add_update_container and not self.update_exists(op):
537             self.update_add(op)
538
539     def operation_82(self, op):
540         if self.add_update_container and not self.update_exists(op):
541             self.update_add(op)
542
543     def operation_83(self, op):
544         if self.add_update_container and not self.update_exists(op):
545             self.update_add(op)