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