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