selftest: enable py3 for samba.tests.kcc.graph_utils
[samba.git] / python / samba / domain_update.py
1 # Samba4 Domain update checker
2 #
3 # Copyright (C) Andrew Bartlett <abartlet@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 b64encode
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 (
35     get_wellknown_sds,
36     get_diff_sds,
37     get_managed_service_accounts_descriptor,
38 )
39 from samba.auth import system_session, admin_session
40 from samba.netcmd import CommandError
41 from samba.netcmd.fsmo import get_fsmo_roleowner
42 from samba.dsdb import (
43     DS_DOMAIN_FUNCTION_2008,
44     DS_DOMAIN_FUNCTION_2008_R2,
45     DS_DOMAIN_FUNCTION_2012,
46     DS_DOMAIN_FUNCTION_2012_R2,
47     DS_DOMAIN_FUNCTION_2016,
48 )
49
50 MIN_UPDATE = 75
51 MAX_UPDATE = 81
52
53 update_map = {
54     # Missing updates from 2008 R2 - version 5
55     75: "5e1574f6-55df-493e-a671-aaeffca6a100",
56     76: "d262aae8-41f7-48ed-9f35-56bbb677573d",
57     77: "82112ba0-7e4c-4a44-89d9-d46c9612bf91",
58     # Windows Server 2012 - version 9
59     78: "c3c927a6-cc1d-47c0-966b-be8f9b63d991",
60     79: "54afcfb9-637a-4251-9f47-4d50e7021211",
61     80: "f4728883-84dd-483c-9897-274f2ebcf11e",
62     81: "ff4f9d27-7157-4cb0-80a9-5d6f2b14c8ff",
63     # Windows Server 2012 R2 - version 10
64     # No updates
65 }
66
67 functional_level_to_max_update = {
68     DS_DOMAIN_FUNCTION_2008: 74,
69     DS_DOMAIN_FUNCTION_2008_R2: 77,
70     DS_DOMAIN_FUNCTION_2012: 81,
71     DS_DOMAIN_FUNCTION_2012_R2: 81,
72     DS_DOMAIN_FUNCTION_2016: 88,
73 }
74
75 functional_level_to_version = {
76     DS_DOMAIN_FUNCTION_2008: 3,
77     DS_DOMAIN_FUNCTION_2008_R2: 5,
78     DS_DOMAIN_FUNCTION_2012: 9,
79     DS_DOMAIN_FUNCTION_2012_R2: 10,
80     DS_DOMAIN_FUNCTION_2016: 15,
81 }
82
83 # No update numbers have been skipped over
84 missing_updates = []
85
86
87 class DomainUpdateException(Exception):
88     pass
89
90
91 class DomainUpdate(object):
92     """Check and update a SAM database for domain updates"""
93
94     def __init__(self, samdb, fix=False,
95                  add_update_container=True):
96         """
97         :param samdb: LDB database
98         :param fix: Apply the update if the container is missing
99         :param add_update_container: Add the container at the end of the change
100         :raise DomainUpdateException:
101         """
102         self.samdb = samdb
103         self.fix = fix
104         self.add_update_container = add_update_container
105         # TODO: In future we should check for inconsistencies when it claims it has been done
106         self.check_update_applied = False
107
108         self.config_dn = self.samdb.get_config_basedn()
109         self.domain_dn = self.samdb.domain_dn()
110         self.schema_dn = self.samdb.get_schema_basedn()
111
112         self.sd_utils = sd_utils.SDUtils(samdb)
113         self.domain_sid = security.dom_sid(samdb.get_domain_sid())
114
115         self.domainupdate_container = self.samdb.get_root_basedn()
116         if not self.domainupdate_container.add_child("CN=Operations,CN=DomainUpdates,CN=System"):
117             raise DomainUpdateException("Failed to add domain update container child")
118
119         self.revision_object = self.samdb.get_root_basedn()
120         if not self.revision_object.add_child("CN=ActiveDirectoryUpdate,CN=DomainUpdates,CN=System"):
121             raise DomainUpdateException("Failed to add revision object child")
122
123     def check_updates_functional_level(self, functional_level,
124                                        old_functional_level=None,
125                                        update_revision=False):
126         """
127         Apply all updates for a given old and new functional level
128         :param functional_level: constant
129         :param old_functional_level: constant
130         :param update_revision: modify the stored version
131         :raise DomainUpdateException:
132         """
133         res = self.samdb.search(base=self.revision_object,
134                                 attrs=["revision"], scope=ldb.SCOPE_BASE)
135
136         expected_update = functional_level_to_max_update[functional_level]
137
138         if old_functional_level:
139             min_update = functional_level_to_max_update[old_functional_level]
140             min_update += 1
141         else:
142             min_update = MIN_UPDATE
143
144         self.check_updates_range(min_update, expected_update)
145
146         expected_version = functional_level_to_version[functional_level]
147         found_version = int(res[0]['revision'][0])
148         if update_revision and found_version < expected_version:
149             if not self.fix:
150                 raise DomainUpdateException("Revision is not high enough. Fix is set to False."
151                                             "\nExpected: %dGot: %d" % (expected_version,
152                                                                        found_version))
153             self.samdb.modify_ldif("""dn: %s
154 changetype: modify
155 replace: revision
156 revision: %d
157 """ % (str(self.revision_object), expected_version))
158
159     def check_updates_iterator(self, iterator):
160         """
161         Apply a list of updates which must be within the valid range of updates
162         :param iterator: Iterable specifying integer update numbers to apply
163         :raise DomainUpdateException:
164         """
165         for op in iterator:
166             if op < MIN_UPDATE or op > MAX_UPDATE:
167                 raise DomainUpdateException("Update number invalid.")
168
169             # No LDIF file exists for the change
170             getattr(self, "operation_%d" % op)(op)
171
172     def check_updates_range(self, start=0, end=0):
173         """
174         Apply a range of updates which must be within the valid range of updates
175         :param start: integer update to begin
176         :param end: integer update to end (inclusive)
177         :raise DomainUpdateException:
178         """
179         op = start
180         if start < MIN_UPDATE or start > end or end > MAX_UPDATE:
181             raise DomainUpdateException("Update number invalid.")
182         while op <= end:
183             if op not in missing_updates:
184                 # No LDIF file exists for the change
185                 getattr(self, "operation_%d" % op)(op)
186
187             op += 1
188
189     def update_exists(self, op):
190         """
191         :param op: Integer update number
192         :return: True if update exists else False
193         """
194         try:
195             res = self.samdb.search(base=self.domainupdate_container,
196                                     expression="(CN=%s)" % update_map[op])
197         except ldb.LdbError:
198             return False
199
200         return len(res) == 1
201
202     def update_add(self, op):
203         """
204         Add the corresponding container object for the given update
205         :param op: Integer update
206         """
207         self.samdb.add_ldif("""dn: CN=%s,%s
208 objectClass: container
209 """ % (update_map[op], str(self.domainupdate_container)))
210
211     def insert_ace_into_dacl(self, dn, existing_sddl, ace):
212         """
213         Add an ACE to a DACL, checking if it already exists with a simple string search.
214
215         :param dn: DN to modify
216         :param existing_sddl: existing sddl as string
217         :param ace: string ace to insert
218         :return: True if modified else False
219         """
220         index = existing_sddl.rfind("S:")
221         if index != -1:
222             new_sddl = existing_sddl[:index] + ace + existing_sddl[index:]
223         else:
224             # Insert it at the end if no S: section
225             new_sddl = existing_sddl + ace
226
227         if ace in existing_sddl:
228             return False
229
230         self.sd_utils.modify_sd_on_dn(dn, new_sddl,
231                                       controls=["sd_flags:1:%d" % SECINFO_DACL])
232
233         return True
234
235     def insert_ace_into_string(self, dn, ace, attr):
236         """
237         Insert an ACE into a string attribute like defaultSecurityDescriptor.
238         This also checks if it already exists using a simple string search.
239
240         :param dn: DN to modify
241         :param ace: string ace to insert
242         :param attr: attribute to modify
243         :return: True if modified else False
244         """
245         msg = self.samdb.search(base=dn,
246                                 attrs=[attr],
247                                 controls=["search_options:1:2"])
248
249         assert len(msg) == 1
250         existing_sddl = msg[0][attr][0]
251         index = existing_sddl.rfind("S:")
252         if index != -1:
253             new_sddl = existing_sddl[:index] + ace + existing_sddl[index:]
254         else:
255             # Insert it at the end if no S: section
256             new_sddl = existing_sddl + ace
257
258         if ace in existing_sddl:
259             return False
260
261         m = ldb.Message()
262         m.dn = dn
263         m[attr] = ldb.MessageElement(new_sddl, ldb.FLAG_MOD_REPLACE,
264                                      attr)
265
266         self.samdb.modify(m, controls=["relax:0"])
267
268         return True
269
270     def raise_if_not_fix(self, op):
271         """
272         Raises an exception if not set to fix.
273         :param op: Integer operation
274         :raise DomainUpdateException:
275         """
276         if not self.fix:
277             raise DomainUpdateException("Missing operation %d. Fix is currently set to False" % op)
278
279     # Create a new object CN=TPM Devices in the Domain partition.
280     def operation_78(self, op):
281         if self.update_exists(op):
282             return
283         self.raise_if_not_fix(op)
284
285         self.samdb.add_ldif("""dn: CN=TPM Devices,%s
286 objectClass: top
287 objectClass: msTPM-InformationObjectsContainer
288 """ % self.domain_dn,
289                             controls=["relax:0", "provision:0"])
290
291         if self.add_update_container:
292             self.update_add(op)
293
294     # Created an access control entry for the TPM service.
295     def operation_79(self, op):
296         if self.update_exists(op):
297             return
298         self.raise_if_not_fix(op)
299
300         ace = "(OA;CIIO;WP;ea1b7b93-5e48-46d5-bc6c-4df4fda78a35;bf967a86-0de6-11d0-a285-00aa003049e2;PS)"
301
302         res = self.samdb.search(expression="(objectClass=samDomain)",
303                                 attrs=["nTSecurityDescriptor"],
304                                 controls=["search_options:1:2"])
305         for msg in res:
306             existing_sd = ndr_unpack(security.descriptor,
307                                      msg["nTSecurityDescriptor"][0])
308             existing_sddl = existing_sd.as_sddl(self.domain_sid)
309
310             self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
311
312         res = self.samdb.search(expression="(objectClass=domainDNS)",
313                                 attrs=["nTSecurityDescriptor"],
314                                 controls=["search_options:1:2"])
315         for msg in res:
316             existing_sd = ndr_unpack(security.descriptor,
317                                      msg["nTSecurityDescriptor"][0])
318             existing_sddl = existing_sd.as_sddl(self.domain_sid)
319
320             self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
321
322         if self.add_update_container:
323             self.update_add(op)
324
325     # Grant "Clone DC" extended right to Cloneable Domain Controllers group
326     def operation_80(self, op):
327         if self.update_exists(op):
328             return
329         self.raise_if_not_fix(op)
330
331         ace = "(OA;;CR;3e0f7e18-2c7a-4c10-ba82-4d926db99a3e;;%s-522)" % str(self.domain_sid)
332
333         res = self.samdb.search(base=self.domain_dn,
334                                 scope=ldb.SCOPE_BASE,
335                                 attrs=["nTSecurityDescriptor"],
336                                 controls=["search_options:1:2",
337                                           "sd_flags:1:%d" % SECINFO_DACL])
338         msg = res[0]
339
340         existing_sd = ndr_unpack(security.descriptor,
341                                  msg["nTSecurityDescriptor"][0])
342         existing_sddl = existing_sd.as_sddl(self.domain_sid)
343
344         self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
345
346         if self.add_update_container:
347             self.update_add(op)
348
349     # Grant ms-DS-Allowed-To-Act-On-Behalf-Of-Other-Identity to Principal Self
350     # on all objects
351     def operation_81(self, op):
352         if self.update_exists(op):
353             return
354         self.raise_if_not_fix(op)
355
356         ace = "(OA;CIOI;RPWP;3f78c3e5-f79a-46bd-a0b8-9d18116ddc79;;PS)"
357
358         res = self.samdb.search(expression="(objectClass=samDomain)",
359                                 attrs=["nTSecurityDescriptor"],
360                                 controls=["search_options:1:2"])
361         for msg in res:
362             existing_sd = ndr_unpack(security.descriptor,
363                                      msg["nTSecurityDescriptor"][0])
364             existing_sddl = existing_sd.as_sddl(self.domain_sid)
365
366             self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
367
368         res = self.samdb.search(expression="(objectClass=domainDNS)",
369                                 attrs=["nTSecurityDescriptor"],
370                                 controls=["search_options:1:2"])
371
372         for msg in res:
373             existing_sd = ndr_unpack(security.descriptor,
374                                      msg["nTSecurityDescriptor"][0])
375             existing_sddl = existing_sd.as_sddl(self.domain_sid)
376
377             self.insert_ace_into_dacl(msg.dn, existing_sddl, ace)
378
379         if self.add_update_container:
380             self.update_add(op)
381
382     #
383     # THE FOLLOWING ARE MISSING UPDATES FROM 2008 R2
384     #
385
386     # Add Managed Service Accounts container
387     def operation_75(self, op):
388         if self.update_exists(op):
389             return
390         self.raise_if_not_fix(op)
391
392         descriptor = get_managed_service_accounts_descriptor(self.domain_sid)
393         managedservice_descr = b64encode(descriptor)
394         managed_service_dn = "CN=Managed Service Accounts,%s" % \
395             str(self.domain_dn)
396
397         self.samdb.modify_ldif("""dn: %s
398 changetype: add
399 objectClass: container
400 description: Default container for managed service accounts
401 showInAdvancedViewOnly: FALSE
402 nTSecurityDescriptor:: %s""" % (managed_service_dn, managedservice_descr),
403                                controls=["relax:0", "provision:0"])
404
405         if self.add_update_container:
406             self.update_add(op)
407
408     # Add the otherWellKnownObjects reference to MSA
409     def operation_76(self, op):
410         if self.update_exists(op):
411             return
412         self.raise_if_not_fix(op)
413
414         managed_service_dn = "CN=Managed Service Accounts,%s" % \
415             str(self.domain_dn)
416
417         self.samdb.modify_ldif("""dn: %s
418 changetype: modify
419 add: otherWellKnownObjects
420 otherWellKnownObjects: B:32:1EB93889E40C45DF9F0C64D23BBB6237:%s
421 """ % (str(self.domain_dn), managed_service_dn), controls=["relax:0",
422                                                            "provision:0"])
423
424         if self.add_update_container:
425             self.update_add(op)
426
427     # Add the PSPs object in the System container
428     def operation_77(self, op):
429         if self.update_exists(op):
430             return
431         self.raise_if_not_fix(op)
432
433         self.samdb.add_ldif("""dn: CN=PSPs,CN=System,%s
434 objectClass: top
435 objectClass: msImaging-PSPs
436 """ % str(self.domain_dn), controls=["relax:0", "provision:0"])
437
438         if self.add_update_container:
439             self.update_add(op)