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
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
,
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
49 class RegistryGroupPolicies(object):
50 def __init__(self
, gpo
, lp
, creds
, samdb
, host
=None):
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:]
63 dc_hostname
= netcmd_finddc(self
.lp
, self
.creds
)
65 self
.conn
= smb_connection(dc_hostname
,
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
):
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
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']
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
):
121 if str_regtype(i
) == entry
['type'].upper():
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:
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')
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'])
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
)
150 pol_data
.entries
= entries
151 pol_data
.num_entries
= len(entries
)
153 def __pol_remove(self
, pol_data
, entry
):
155 for e
in pol_data
.entries
:
156 if not (e
.keyname
== entry
['keyname'] and
157 e
.valuename
== entry
['valuename']):
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
:
165 GPT_INI
= self
.pol_dir
% 'GPT.INI'
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
]:
175 parser
= GPTIniParser()
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
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
)
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
203 m
.dn
= self
.policy_dn
204 m
['new_value'] = ldb
.MessageElement(str(version
), ldb
.FLAG_MOD_REPLACE
,
208 def __validate_extension_registration(self
, ext_name
, ext_attr
):
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')
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
,
224 if len(res
) == 0 or ext_attr
not in res
[0]:
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
+ ']'
234 m
.dn
= self
.policy_dn
235 m
['new_value'] = ldb
.MessageElement(ext_names
, ldb
.FLAG_MOD_REPLACE
,
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
,
243 if len(res
) == 0 or ext_attr
not in res
[0]:
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
, '')
253 m
.dn
= self
.policy_dn
254 m
['new_value'] = ldb
.MessageElement(ext_names
, ldb
.FLAG_MOD_REPLACE
,
258 def remove_s(self
, json_input
):
260 json_input: JSON list of entries to remove from GPO
265 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
266 "valuename": "StartPage",
270 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
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
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':
289 self
.__pol
_remove
(user_pol_data
, entry
)
291 self
.__save
_registry
_pol
(self
.pol_dir
% 'User',
292 self
.pol_file
% 'User',
295 self
.__save
_registry
_pol
(self
.pol_dir
% 'Machine',
296 self
.pol_file
% 'Machine',
298 self
.increment_gpt_ini(machine_changed
, user_changed
)
300 def merge_s(self
, json_input
):
302 json_input: JSON list of entries to merge into GPO
307 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
308 "valuename": "StartPage",
314 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
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
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':
335 self
.__pol
_replace
(user_pol_data
, entry
)
337 self
.__save
_registry
_pol
(self
.pol_dir
% 'User',
338 self
.pol_file
% 'User',
341 self
.__save
_registry
_pol
(self
.pol_dir
% 'Machine',
342 self
.pol_file
% 'Machine',
344 self
.increment_gpt_ini(machine_changed
, user_changed
)
346 def replace_s(self
, json_input
):
348 json_input: JSON list of entries to replace entries in GPO
353 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
354 "valuename": "StartPage",
359 "keyname": "Software\\Policies\\Mozilla\\Firefox\\Homepage",
366 self
.__validate
_json
(json_input
)
367 user_pol_data
= preg
.file()
368 machine_pol_data
= preg
.file()
370 machine_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':
379 self
.__pol
_replace
(user_pol_data
, entry
)
381 self
.__save
_registry
_pol
(self
.pol_dir
% 'User',
382 self
.pol_file
% 'User',
385 self
.__save
_registry
_pol
(self
.pol_dir
% 'Machine',
386 self
.pol_file
% 'Machine',
388 self
.increment_gpt_ini(machine_changed
, user_changed
)