s3:printing: Allow to run samba-bgqd as a standalone systemd service
[Samba.git] / python / samba / policies.py
blob45392322b3e01c0df1ed682e0f3257d573fb67ec
1 # Utilities for working with policies in SYSVOL Registry.pol files
3 # Copyright (C) David Mulder <dmulder@samba.org> 2022
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.
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.
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/>.
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
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
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
43 GPT_EMPTY = \
44 """
45 [General]
46 Version=0
47 """
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)
60 if host and host.startswith('ldap://'):
61 dc_hostname = host[7:]
62 else:
63 dc_hostname = netcmd_finddc(self.lp, self.creds)
65 self.conn = smb_connection(dc_hostname,
66 'sysvol',
67 lp=self.lp,
68 creds=self.creds)
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()
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)
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
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)
101 def __save_registry_pol(self, pol_dir, pol_file, pol_data):
102 self.__save_file(pol_dir, pol_file, ndr_pack(pol_data))
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')
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'])
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
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)
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)
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
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'))
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)
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
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
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)
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
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)
258 def remove_s(self, json_input):
259 """remove_s
260 json_input: JSON list of entries to remove from GPO
262 Example json_input:
265 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
266 "valuename": "StartPage",
267 "class": "USER",
270 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
271 "valuename": "URL",
272 "class": "USER",
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')
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)
300 def merge_s(self, json_input):
301 """merge_s
302 json_input: JSON list of entries to merge into GPO
304 Example json_input:
307 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
308 "valuename": "StartPage",
309 "class": "USER",
310 "type": "REG_SZ",
311 "data": "homepage"
314 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
315 "valuename": "URL",
316 "class": "USER",
317 "type": "REG_SZ",
318 "data": "google.com"
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')
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)
346 def replace_s(self, json_input):
347 """replace_s
348 json_input: JSON list of entries to replace entries in GPO
350 Example json_input:
353 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
354 "valuename": "StartPage",
355 "class": "USER",
356 "data": "homepage"
359 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
360 "valuename": "URL",
361 "class": "USER",
362 "data": "google.com"
366 self.__validate_json(json_input)
367 user_pol_data = preg.file()
368 machine_pol_data = preg.file()
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)