auth/creds/torture: add a test showing segfault
[Samba.git] / python / samba / gpclass.py
blob0040f235e6e3d1accc5209794516976fe08686a6
1 # Reads important GPO parameters and updates Samba
2 # Copyright (C) Luke Morrison <luc785@.hotmail.com> 2013
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 import sys
19 import os
20 import errno
21 import tdb
22 sys.path.insert(0, "bin/python")
23 from samba import NTSTATUSError
24 from samba.compat import ConfigParser
25 from samba.compat import StringIO
26 from samba.compat import get_bytes
27 from abc import ABCMeta, abstractmethod
28 import xml.etree.ElementTree as etree
29 import re
30 from samba.net import Net
31 from samba.dcerpc import nbt
32 from samba.samba3 import libsmb_samba_internal as libsmb
33 from samba.samba3 import param as s3param
34 import samba.gpo as gpo
35 from samba.param import LoadParm
36 from uuid import UUID
37 from tempfile import NamedTemporaryFile
39 try:
40 from enum import Enum
41 GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
42 except ImportError:
43 class GPOSTATE:
44 APPLY = 1
45 ENFORCE = 2
46 UNAPPLY = 3
49 class gp_log:
50 ''' Log settings overwritten by gpo apply
51 The gp_log is an xml file that stores a history of gpo changes (and the
52 original setting value).
54 The log is organized like so:
56 <gp>
57 <user name="KDC-1$">
58 <applylog>
59 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
60 </applylog>
61 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
62 <gp_ext name="System Access">
63 <attribute name="minPwdAge">-864000000000</attribute>
64 <attribute name="maxPwdAge">-36288000000000</attribute>
65 <attribute name="minPwdLength">7</attribute>
66 <attribute name="pwdProperties">1</attribute>
67 </gp_ext>
68 <gp_ext name="Kerberos Policy">
69 <attribute name="ticket_lifetime">1d</attribute>
70 <attribute name="renew_lifetime" />
71 <attribute name="clockskew">300</attribute>
72 </gp_ext>
73 </guid>
74 </user>
75 </gp>
77 Each guid value contains a list of extensions, which contain a list of
78 attributes. The guid value represents a GPO. The attributes are the values
79 of those settings prior to the application of the GPO.
80 The list of guids is enclosed within a user name, which represents the user
81 the settings were applied to. This user may be the samaccountname of the
82 local computer, which implies that these are machine policies.
83 The applylog keeps track of the order in which the GPOs were applied, so
84 that they can be rolled back in reverse, returning the machine to the state
85 prior to policy application.
86 '''
87 def __init__(self, user, gpostore, db_log=None):
88 ''' Initialize the gp_log
89 param user - the username (or machine name) that policies are
90 being applied to
91 param gpostore - the GPOStorage obj which references the tdb which
92 contains gp_logs
93 param db_log - (optional) a string to initialize the gp_log
94 '''
95 self._state = GPOSTATE.APPLY
96 self.gpostore = gpostore
97 self.username = user
98 if db_log:
99 self.gpdb = etree.fromstring(db_log)
100 else:
101 self.gpdb = etree.Element('gp')
102 self.user = user
103 user_obj = self.gpdb.find('user[@name="%s"]' % user)
104 if user_obj is None:
105 user_obj = etree.SubElement(self.gpdb, 'user')
106 user_obj.attrib['name'] = user
108 def state(self, value):
109 ''' Policy application state
110 param value - APPLY, ENFORCE, or UNAPPLY
112 The behavior of the gp_log depends on whether we are applying policy,
113 enforcing policy, or unapplying policy. During an apply, old settings
114 are recorded in the log. During an enforce, settings are being applied
115 but the gp_log does not change. During an unapply, additions to the log
116 should be ignored (since function calls to apply settings are actually
117 reverting policy), but removals from the log are allowed.
119 # If we're enforcing, but we've unapplied, apply instead
120 if value == GPOSTATE.ENFORCE:
121 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
122 apply_log = user_obj.find('applylog')
123 if apply_log is None or len(apply_log) == 0:
124 self._state = GPOSTATE.APPLY
125 else:
126 self._state = value
127 else:
128 self._state = value
130 def set_guid(self, guid):
131 ''' Log to a different GPO guid
132 param guid - guid value of the GPO from which we're applying
133 policy
135 self.guid = guid
136 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
137 obj = user_obj.find('guid[@value="%s"]' % guid)
138 if obj is None:
139 obj = etree.SubElement(user_obj, 'guid')
140 obj.attrib['value'] = guid
141 if self._state == GPOSTATE.APPLY:
142 apply_log = user_obj.find('applylog')
143 if apply_log is None:
144 apply_log = etree.SubElement(user_obj, 'applylog')
145 prev = apply_log.find('guid[@value="%s"]' % guid)
146 if prev is None:
147 item = etree.SubElement(apply_log, 'guid')
148 item.attrib['count'] = '%d' % (len(apply_log) - 1)
149 item.attrib['value'] = guid
151 def store(self, gp_ext_name, attribute, old_val):
152 ''' Store an attribute in the gp_log
153 param gp_ext_name - Name of the extension applying policy
154 param attribute - The attribute being modified
155 param old_val - The value of the attribute prior to policy
156 application
158 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
159 return None
160 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
161 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
162 assert guid_obj is not None, "gpo guid was not set"
163 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
164 if ext is None:
165 ext = etree.SubElement(guid_obj, 'gp_ext')
166 ext.attrib['name'] = gp_ext_name
167 attr = ext.find('attribute[@name="%s"]' % attribute)
168 if attr is None:
169 attr = etree.SubElement(ext, 'attribute')
170 attr.attrib['name'] = attribute
171 attr.text = old_val
173 def retrieve(self, gp_ext_name, attribute):
174 ''' Retrieve a stored attribute from the gp_log
175 param gp_ext_name - Name of the extension which applied policy
176 param attribute - The attribute being retrieved
177 return - The value of the attribute prior to policy
178 application
180 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
181 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
182 assert guid_obj is not None, "gpo guid was not set"
183 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
184 if ext is not None:
185 attr = ext.find('attribute[@name="%s"]' % attribute)
186 if attr is not None:
187 return attr.text
188 return None
190 def get_applied_guids(self):
191 ''' Return a list of applied ext guids
192 return - List of guids for gpos that have applied settings
193 to the system.
195 guids = []
196 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
197 if user_obj is not None:
198 apply_log = user_obj.find('applylog')
199 if apply_log is not None:
200 guid_objs = apply_log.findall('guid[@count]')
201 guids_by_count = [(g.get('count'), g.get('value'))
202 for g in guid_objs]
203 guids_by_count.sort(reverse=True)
204 guids.extend(guid for count, guid in guids_by_count)
205 return guids
207 def get_applied_settings(self, guids):
208 ''' Return a list of applied ext guids
209 return - List of tuples containing the guid of a gpo, then
210 a dictionary of policies and their values prior
211 policy application. These are sorted so that the
212 most recently applied settings are removed first.
214 ret = []
215 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
216 for guid in guids:
217 guid_settings = user_obj.find('guid[@value="%s"]' % guid)
218 exts = guid_settings.findall('gp_ext')
219 settings = {}
220 for ext in exts:
221 attr_dict = {}
222 attrs = ext.findall('attribute')
223 for attr in attrs:
224 attr_dict[attr.attrib['name']] = attr.text
225 settings[ext.attrib['name']] = attr_dict
226 ret.append((guid, settings))
227 return ret
229 def delete(self, gp_ext_name, attribute):
230 ''' Remove an attribute from the gp_log
231 param gp_ext_name - name of extension from which to remove the
232 attribute
233 param attribute - attribute to remove
235 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
236 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
237 assert guid_obj is not None, "gpo guid was not set"
238 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
239 if ext is not None:
240 attr = ext.find('attribute[@name="%s"]' % attribute)
241 if attr is not None:
242 ext.remove(attr)
243 if len(ext) == 0:
244 guid_obj.remove(ext)
246 def commit(self):
247 ''' Write gp_log changes to disk '''
248 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
251 class GPOStorage:
252 def __init__(self, log_file):
253 if os.path.isfile(log_file):
254 self.log = tdb.open(log_file)
255 else:
256 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT | os.O_RDWR)
258 def start(self):
259 self.log.transaction_start()
261 def get_int(self, key):
262 try:
263 return int(self.log.get(get_bytes(key)))
264 except TypeError:
265 return None
267 def get(self, key):
268 return self.log.get(get_bytes(key))
270 def get_gplog(self, user):
271 return gp_log(user, self, self.log.get(get_bytes(user)))
273 def store(self, key, val):
274 self.log.store(get_bytes(key), get_bytes(val))
276 def cancel(self):
277 self.log.transaction_cancel()
279 def delete(self, key):
280 self.log.delete(get_bytes(key))
282 def commit(self):
283 self.log.transaction_commit()
285 def __del__(self):
286 self.log.close()
289 class gp_ext(object):
290 __metaclass__ = ABCMeta
292 def __init__(self, logger, lp, creds, store):
293 self.logger = logger
294 self.lp = lp
295 self.creds = creds
296 self.gp_db = store.get_gplog(creds.get_username())
298 @abstractmethod
299 def process_group_policy(self, deleted_gpo_list, changed_gpo_list):
300 pass
302 @abstractmethod
303 def read(self, policy):
304 pass
306 def parse(self, afile):
307 local_path = self.lp.cache_path('gpo_cache')
308 data_file = os.path.join(local_path, check_safe_path(afile).upper())
309 if os.path.exists(data_file):
310 return self.read(open(data_file, 'r').read())
311 return None
313 @abstractmethod
314 def __str__(self):
315 pass
318 class gp_ext_setter(object):
319 __metaclass__ = ABCMeta
321 def __init__(self, logger, gp_db, lp, creds, attribute, val):
322 self.logger = logger
323 self.attribute = attribute
324 self.val = val
325 self.lp = lp
326 self.creds = creds
327 self.gp_db = gp_db
329 def explicit(self):
330 return self.val
332 def update_samba(self):
333 (upd_sam, value) = self.mapper().get(self.attribute)
334 upd_sam(value())
336 @abstractmethod
337 def mapper(self):
338 pass
340 def delete(self):
341 upd_sam, _ = self.mapper().get(self.attribute)
342 upd_sam(self.val)
344 @abstractmethod
345 def __str__(self):
346 pass
349 class gp_inf_ext(gp_ext):
350 def read(self, policy):
351 inf_conf = ConfigParser()
352 inf_conf.optionxform = str
353 try:
354 inf_conf.readfp(StringIO(policy))
355 except:
356 inf_conf.readfp(StringIO(policy.decode('utf-16')))
357 return inf_conf
360 ''' Fetch the hostname of a writable DC '''
363 def get_dc_hostname(creds, lp):
364 net = Net(creds=creds, lp=lp)
365 cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
366 nbt.NBT_SERVER_DS))
367 return cldap_ret.pdc_dns_name
370 ''' Fetch a list of GUIDs for applicable GPOs '''
373 def get_gpo_list(dc_hostname, creds, lp):
374 gpos = []
375 ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
376 if ads.connect():
377 gpos = ads.get_gpo_list(creds.get_username())
378 return gpos
381 def cache_gpo_dir(conn, cache, sub_dir):
382 loc_sub_dir = sub_dir.upper()
383 local_dir = os.path.join(cache, loc_sub_dir)
384 try:
385 os.makedirs(local_dir, mode=0o755)
386 except OSError as e:
387 if e.errno != errno.EEXIST:
388 raise
389 for fdata in conn.list(sub_dir):
390 if fdata['attrib'] & libsmb.FILE_ATTRIBUTE_DIRECTORY:
391 cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
392 else:
393 local_name = fdata['name'].upper()
394 f = NamedTemporaryFile(delete=False, dir=local_dir)
395 fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
396 f.write(conn.loadfile(fname))
397 f.close()
398 os.rename(f.name, os.path.join(local_dir, local_name))
401 def check_safe_path(path):
402 dirs = re.split('/|\\\\', path)
403 if 'sysvol' in path:
404 dirs = dirs[dirs.index('sysvol') + 1:]
405 if '..' not in dirs:
406 return os.path.join(*dirs)
407 raise OSError(path)
410 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
411 # the SMB bindings rely on having a s3 loadparm
412 s3_lp = s3param.get_context()
413 s3_lp.load(lp.configfile)
414 conn = libsmb.Conn(dc_hostname, 'sysvol', lp=s3_lp, creds=creds, sign=True)
415 cache_path = lp.cache_path('gpo_cache')
416 for gpo in gpos:
417 if not gpo.file_sys_path:
418 continue
419 cache_gpo_dir(conn, cache_path, check_safe_path(gpo.file_sys_path))
422 def get_deleted_gpos_list(gp_db, gpos):
423 applied_gpos = gp_db.get_applied_guids()
424 current_guids = set([p.name for p in gpos])
425 deleted_gpos = [guid for guid in applied_gpos if guid not in current_guids]
426 return gp_db.get_applied_settings(deleted_gpos)
428 def gpo_version(lp, path):
429 # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
430 # read from the gpo client cache.
431 gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
432 return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
435 def apply_gp(lp, creds, logger, store, gp_extensions, force=False):
436 gp_db = store.get_gplog(creds.get_username())
437 dc_hostname = get_dc_hostname(creds, lp)
438 gpos = get_gpo_list(dc_hostname, creds, lp)
439 del_gpos = get_deleted_gpos_list(gp_db, gpos)
440 try:
441 check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
442 except:
443 logger.error('Failed downloading gpt cache from \'%s\' using SMB'
444 % dc_hostname)
445 return
447 if force:
448 changed_gpos = gpos
449 gp_db.state(GPOSTATE.ENFORCE)
450 else:
451 changed_gpos = []
452 for gpo_obj in gpos:
453 if not gpo_obj.file_sys_path:
454 continue
455 guid = gpo_obj.name
456 path = check_safe_path(gpo_obj.file_sys_path).upper()
457 version = gpo_version(lp, path)
458 if version != store.get_int(guid):
459 logger.info('GPO %s has changed' % guid)
460 changed_gpos.append(gpo_obj)
461 gp_db.state(GPOSTATE.APPLY)
463 store.start()
464 for ext in gp_extensions:
465 try:
466 ext.process_group_policy(del_gpos, changed_gpos)
467 except Exception as e:
468 logger.error('Failed to apply extension %s' % str(ext))
469 logger.error('Message was: ' + str(e))
470 continue
471 for gpo_obj in gpos:
472 if not gpo_obj.file_sys_path:
473 continue
474 guid = gpo_obj.name
475 path = check_safe_path(gpo_obj.file_sys_path).upper()
476 version = gpo_version(lp, path)
477 store.store(guid, '%i' % version)
478 store.commit()
481 def unapply_gp(lp, creds, logger, store, gp_extensions):
482 gp_db = store.get_gplog(creds.get_username())
483 gp_db.state(GPOSTATE.UNAPPLY)
484 # Treat all applied gpos as deleted
485 del_gpos = gp_db.get_applied_settings(gp_db.get_applied_guids())
486 store.start()
487 for ext in gp_extensions:
488 try:
489 ext.process_group_policy(del_gpos, [])
490 except Exception as e:
491 logger.error('Failed to unapply extension %s' % str(ext))
492 logger.error('Message was: ' + str(e))
493 continue
494 store.commit()
497 def parse_gpext_conf(smb_conf):
498 lp = LoadParm()
499 if smb_conf is not None:
500 lp.load(smb_conf)
501 else:
502 lp.load_default()
503 ext_conf = lp.state_path('gpext.conf')
504 parser = ConfigParser()
505 parser.read(ext_conf)
506 return lp, parser
509 def atomic_write_conf(lp, parser):
510 ext_conf = lp.state_path('gpext.conf')
511 with NamedTemporaryFile(mode="w+", delete=False, dir=os.path.dirname(ext_conf)) as f:
512 parser.write(f)
513 os.rename(f.name, ext_conf)
516 def check_guid(guid):
517 # Check for valid guid with curly braces
518 if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
519 return False
520 try:
521 UUID(guid, version=4)
522 except ValueError:
523 return False
524 return True
527 def register_gp_extension(guid, name, path,
528 smb_conf=None, machine=True, user=True):
529 # Check that the module exists
530 if not os.path.exists(path):
531 return False
532 if not check_guid(guid):
533 return False
535 lp, parser = parse_gpext_conf(smb_conf)
536 if guid not in parser.sections():
537 parser.add_section(guid)
538 parser.set(guid, 'DllName', path)
539 parser.set(guid, 'ProcessGroupPolicy', name)
540 parser.set(guid, 'NoMachinePolicy', "0" if machine else "1")
541 parser.set(guid, 'NoUserPolicy', "0" if user else "1")
543 atomic_write_conf(lp, parser)
545 return True
548 def list_gp_extensions(smb_conf=None):
549 _, parser = parse_gpext_conf(smb_conf)
550 results = {}
551 for guid in parser.sections():
552 results[guid] = {}
553 results[guid]['DllName'] = parser.get(guid, 'DllName')
554 results[guid]['ProcessGroupPolicy'] = \
555 parser.get(guid, 'ProcessGroupPolicy')
556 results[guid]['MachinePolicy'] = \
557 not int(parser.get(guid, 'NoMachinePolicy'))
558 results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
559 return results
562 def unregister_gp_extension(guid, smb_conf=None):
563 if not check_guid(guid):
564 return False
566 lp, parser = parse_gpext_conf(smb_conf)
567 if guid in parser.sections():
568 parser.remove_section(guid)
570 atomic_write_conf(lp, parser)
572 return True