netcmd: models: update docstring of Computer.find method
[samba.git] / python / samba / policies.py
1 # Utilities for working with policies in SYSVOL Registry.pol files
2 #
3 # Copyright (C) David Mulder <dmulder@samba.org> 2022
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 from io import StringIO
19 import ldb
20 from samba.ndr import ndr_unpack, ndr_pack
21 from samba.dcerpc import preg
22 from samba.netcmd.common import netcmd_finddc
23 from samba.netcmd.gpcommon import (
24     create_directory_hier,
25     smb_connection,
26     get_gpo_dn
27 )
28 from samba import NTSTATUSError
29 from numbers import Number
30 from samba.registry import str_regtype
31 from samba.ntstatus import (
32     NT_STATUS_OBJECT_NAME_INVALID,
33     NT_STATUS_OBJECT_NAME_NOT_FOUND,
34     NT_STATUS_OBJECT_PATH_NOT_FOUND,
35     NT_STATUS_INVALID_PARAMETER
36 )
37 from samba.gp_parse.gp_ini import GPTIniParser
38 from samba.common import get_string
39 from samba.dcerpc import security
40 from samba.ntacls import dsacl2fsacl
41 from samba.dcerpc.misc import REG_BINARY, REG_MULTI_SZ, REG_SZ, GUID
42
43 GPT_EMPTY = \
44 """
45 [General]
46 Version=0
47 """
48
49 class RegistryGroupPolicies(object):
50     def __init__(self, gpo, lp, creds, samdb, host=None):
51         self.gpo = gpo
52         self.lp = lp
53         self.creds = creds
54         self.samdb = samdb
55         realm = self.lp.get('realm')
56         self.pol_dir = '\\'.join([realm.lower(), 'Policies', gpo, '%s'])
57         self.pol_file = '\\'.join([self.pol_dir, 'Registry.pol'])
58         self.policy_dn = get_gpo_dn(self.samdb, self.gpo)
59
60         if host and host.startswith('ldap://'):
61             dc_hostname = host[7:]
62         else:
63             dc_hostname = netcmd_finddc(self.lp, self.creds)
64
65         self.conn = smb_connection(dc_hostname,
66                                    'sysvol',
67                                    lp=self.lp,
68                                    creds=self.creds)
69
70         # Get new security descriptor
71         ds_sd_flags = (security.SECINFO_OWNER |
72                        security.SECINFO_GROUP |
73                        security.SECINFO_DACL)
74         msg = self.samdb.search(base=self.policy_dn, scope=ldb.SCOPE_BASE,
75                                 attrs=['nTSecurityDescriptor'])[0]
76         ds_sd_ndr = msg['nTSecurityDescriptor'][0]
77         ds_sd = ndr_unpack(security.descriptor, ds_sd_ndr).as_sddl()
78
79         # Create a file system security descriptor
80         domain_sid = security.dom_sid(self.samdb.get_domain_sid())
81         sddl = dsacl2fsacl(ds_sd, domain_sid)
82         self.fs_sd = security.descriptor.from_sddl(sddl, domain_sid)
83
84     def __load_registry_pol(self, pol_file):
85         try:
86             pol_data = ndr_unpack(preg.file, self.conn.loadfile(pol_file))
87         except NTSTATUSError as e:
88             if e.args[0] in [NT_STATUS_OBJECT_NAME_INVALID,
89                              NT_STATUS_OBJECT_NAME_NOT_FOUND,
90                              NT_STATUS_OBJECT_PATH_NOT_FOUND]:
91                 pol_data = preg.file() # The file doesn't exist
92             else:
93                 raise
94         return pol_data
95
96     def __save_file(self, file_dir, file_name, data):
97         create_directory_hier(self.conn, file_dir)
98         self.conn.savefile(file_name, data)
99         self.conn.set_acl(file_name, self.fs_sd)
100
101     def __save_registry_pol(self, pol_dir, pol_file, pol_data):
102         self.__save_file(pol_dir, pol_file, ndr_pack(pol_data))
103
104     def __validate_json(self, json_input, remove=False):
105         if type(json_input) != list:
106             raise SyntaxError('JSON not formatted correctly')
107         for entry in json_input:
108             if type(entry) != dict:
109                 raise SyntaxError('JSON not formatted correctly')
110             keys = ['keyname', 'valuename', 'class']
111             if not remove:
112                 keys.extend(['data', 'type'])
113             if not all([k in entry for k in keys]):
114                 raise SyntaxError('JSON not formatted correctly')
115
116     def __determine_data_type(self, entry):
117         if isinstance(entry['type'], Number):
118             return entry['type']
119         else:
120             for i in range(12):
121                 if str_regtype(i) == entry['type'].upper():
122                     return i
123         raise TypeError('Unknown type %s' % entry['type'])
124
125     def __set_data(self, rtype, data):
126         # JSON can't store bytes, and have to be set via an int array
127         if rtype == REG_BINARY and type(data) == list:
128             return bytes(data)
129         elif rtype == REG_MULTI_SZ and type(data) == list:
130             data = ('\x00').join(data) + '\x00\x00'
131             return data.encode('utf-16-le')
132         elif rtype == REG_SZ and type(data) == str:
133             return data.encode('utf-8')
134         return data
135
136     def __pol_replace(self, pol_data, entry):
137         for e in pol_data.entries:
138             if e.keyname == entry['keyname'] and \
139                e.valuename == entry['valuename']:
140                 e.data = self.__set_data(e.type, entry['data'])
141                 break
142         else:
143             e = preg.entry()
144             e.keyname = entry['keyname']
145             e.valuename = entry['valuename']
146             e.type = self.__determine_data_type(entry)
147             e.data = self.__set_data(e.type, entry['data'])
148             entries = list(pol_data.entries)
149             entries.append(e)
150             pol_data.entries = entries
151             pol_data.num_entries = len(entries)
152
153     def __pol_remove(self, pol_data, entry):
154         entries = []
155         for e in pol_data.entries:
156             if not (e.keyname == entry['keyname'] and
157                     e.valuename == entry['valuename']):
158                 entries.append(e)
159         pol_data.entries = entries
160         pol_data.num_entries = len(entries)
161
162     def increment_gpt_ini(self, machine_changed=False, user_changed=False):
163         if not machine_changed and not user_changed:
164             return
165         GPT_INI = self.pol_dir % 'GPT.INI'
166         try:
167             data = self.conn.loadfile(GPT_INI)
168         except NTSTATUSError as e:
169             if e.args[0] in [NT_STATUS_OBJECT_NAME_INVALID,
170                              NT_STATUS_OBJECT_NAME_NOT_FOUND,
171                              NT_STATUS_OBJECT_PATH_NOT_FOUND]:
172                 data = GPT_EMPTY
173             else:
174                 raise
175         parser = GPTIniParser()
176         parser.parse(data)
177         version = 0
178         machine_version = 0
179         user_version = 0
180         if parser.ini_conf.has_option('General', 'Version'):
181             version = int(parser.ini_conf.get('General',
182                                               'Version').encode('utf-8'))
183             machine_version = version & 0x0000FFFF
184             user_version = version >> 16
185         if machine_changed:
186             machine_version += 1
187         if user_changed:
188             user_version += 1
189         version = (user_version << 16) + machine_version
190
191         # Set the new version in the GPT.INI
192         if not parser.ini_conf.has_section('General'):
193             parser.ini_conf.add_section('General')
194         parser.ini_conf.set('General', 'Version', str(version))
195         with StringIO() as out_data:
196             parser.ini_conf.write(out_data)
197             out_data.seek(0)
198             self.__save_file(self.pol_dir % '', GPT_INI,
199                              out_data.read().encode('utf-8'))
200
201         # Set the new versionNumber on the ldap object
202         m = ldb.Message()
203         m.dn = self.policy_dn
204         m['new_value'] = ldb.MessageElement(str(version), ldb.FLAG_MOD_REPLACE,
205                                             'versionNumber')
206         self.samdb.modify(m)
207
208     def __validate_extension_registration(self, ext_name, ext_attr):
209         try:
210             ext_name_guid = GUID(ext_name)
211         except NTSTATUSError as e:
212             if e.args[0] == NT_STATUS_INVALID_PARAMETER:
213                 raise SyntaxError('Extension name not formatted correctly')
214             raise
215         if ext_attr not in ['gPCMachineExtensionNames',
216                             'gPCUserExtensionNames']:
217             raise SyntaxError('Extension attribute incorrect')
218         return '{%s}' % ext_name_guid
219
220     def register_extension_name(self, ext_name, ext_attr):
221         ext_name = self.__validate_extension_registration(ext_name, ext_attr)
222         res = self.samdb.search(base=self.policy_dn, scope=ldb.SCOPE_BASE,
223                                 attrs=[ext_attr])
224         if len(res) == 0 or ext_attr not in res[0]:
225             ext_names = '[]'
226         else:
227             ext_names = get_string(res[0][ext_attr][-1])
228         if ext_name not in ext_names:
229             ext_names = '[' + ext_names.strip('[]') + ext_name + ']'
230         else:
231             return
232
233         m = ldb.Message()
234         m.dn = self.policy_dn
235         m['new_value'] = ldb.MessageElement(ext_names, ldb.FLAG_MOD_REPLACE,
236                                             ext_attr)
237         self.samdb.modify(m)
238
239     def unregister_extension_name(self, ext_name, ext_attr):
240         ext_name = self.__validate_extension_registration(ext_name, ext_attr)
241         res = self.samdb.search(base=self.policy_dn, scope=ldb.SCOPE_BASE,
242                                 attrs=[ext_attr])
243         if len(res) == 0 or ext_attr not in res[0]:
244             return
245         else:
246             ext_names = get_string(res[0][ext_attr][-1])
247         if ext_name in ext_names:
248             ext_names = ext_names.replace(ext_name, '')
249         else:
250             return
251
252         m = ldb.Message()
253         m.dn = self.policy_dn
254         m['new_value'] = ldb.MessageElement(ext_names, ldb.FLAG_MOD_REPLACE,
255                                             ext_attr)
256         self.samdb.modify(m)
257
258     def remove_s(self, json_input):
259         """remove_s
260         json_input: JSON list of entries to remove from GPO
261
262         Example json_input:
263         [
264             {
265                 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
266                 "valuename": "StartPage",
267                 "class": "USER",
268             },
269             {
270                 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
271                 "valuename": "URL",
272                 "class": "USER",
273             },
274         ]
275         """
276         self.__validate_json(json_input, remove=True)
277         user_pol_data = self.__load_registry_pol(self.pol_file % 'User')
278         machine_pol_data = self.__load_registry_pol(self.pol_file % 'Machine')
279
280         machine_changed = False
281         user_changed = False
282         for entry in json_input:
283             cls = entry['class'].lower()
284             if cls == 'machine' or cls == 'both':
285                 machine_changed = True
286                 self.__pol_remove(machine_pol_data, entry)
287             if cls == 'user' or cls == 'both':
288                 user_changed = True
289                 self.__pol_remove(user_pol_data, entry)
290         if user_changed:
291             self.__save_registry_pol(self.pol_dir % 'User',
292                                      self.pol_file % 'User',
293                                      user_pol_data)
294         if machine_changed:
295             self.__save_registry_pol(self.pol_dir % 'Machine',
296                                      self.pol_file % 'Machine',
297                                      machine_pol_data)
298         self.increment_gpt_ini(machine_changed, user_changed)
299
300     def merge_s(self, json_input):
301         """merge_s
302         json_input: JSON list of entries to merge into GPO
303
304         Example json_input:
305         [
306             {
307                 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
308                 "valuename": "StartPage",
309                 "class": "USER",
310                 "type": "REG_SZ",
311                 "data": "homepage"
312             },
313             {
314                 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
315                 "valuename": "URL",
316                 "class": "USER",
317                 "type": "REG_SZ",
318                 "data": "google.com"
319             },
320         ]
321         """
322         self.__validate_json(json_input)
323         user_pol_data = self.__load_registry_pol(self.pol_file % 'User')
324         machine_pol_data = self.__load_registry_pol(self.pol_file % 'Machine')
325
326         machine_changed = False
327         user_changed = False
328         for entry in json_input:
329             cls = entry['class'].lower()
330             if cls == 'machine' or cls == 'both':
331                 machine_changed = True
332                 self.__pol_replace(machine_pol_data, entry)
333             if cls == 'user' or cls == 'both':
334                 user_changed = True
335                 self.__pol_replace(user_pol_data, entry)
336         if user_changed:
337             self.__save_registry_pol(self.pol_dir % 'User',
338                                      self.pol_file % 'User',
339                                      user_pol_data)
340         if machine_changed:
341             self.__save_registry_pol(self.pol_dir % 'Machine',
342                                      self.pol_file % 'Machine',
343                                      machine_pol_data)
344         self.increment_gpt_ini(machine_changed, user_changed)
345
346     def replace_s(self, json_input):
347         """replace_s
348         json_input: JSON list of entries to replace entries in GPO
349
350         Example json_input:
351         [
352             {
353                 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
354                 "valuename": "StartPage",
355                 "class": "USER",
356                 "data": "homepage"
357             },
358             {
359                 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
360                 "valuename": "URL",
361                 "class": "USER",
362                 "data": "google.com"
363             },
364         ]
365         """
366         self.__validate_json(json_input)
367         user_pol_data = preg.file()
368         machine_pol_data = preg.file()
369
370         machine_changed = False
371         user_changed = False
372         for entry in json_input:
373             cls = entry['class'].lower()
374             if cls == 'machine' or cls == 'both':
375                 machine_changed = True
376                 self.__pol_replace(machine_pol_data, entry)
377             if cls == 'user' or cls == 'both':
378                 user_changed = True
379                 self.__pol_replace(user_pol_data, entry)
380         if user_changed:
381             self.__save_registry_pol(self.pol_dir % 'User',
382                                      self.pol_file % 'User',
383                                      user_pol_data)
384         if machine_changed:
385             self.__save_registry_pol(self.pol_dir % 'Machine',
386                                      self.pol_file % 'Machine',
387                                      machine_pol_data)
388         self.increment_gpt_ini(machine_changed, user_changed)