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/>.
22 sys.path.insert(0, "bin/python")
23 from samba import NTSTATUSError
24 from ConfigParser import ConfigParser
25 from StringIO import StringIO
26 from abc import ABCMeta, abstractmethod
27 import xml.etree.ElementTree as etree
29 from samba.net import Net
30 from samba.dcerpc import nbt
32 import samba.gpo as gpo
33 from samba.param import LoadParm
35 from tempfile import NamedTemporaryFile
39 GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
47 ''' Log settings overwritten by gpo apply
48 The gp_log is an xml file that stores a history of gpo changes (and the
49 original setting value).
51 The log is organized like so:
56 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
58 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
59 <gp_ext name="System Access">
60 <attribute name="minPwdAge">-864000000000</attribute>
61 <attribute name="maxPwdAge">-36288000000000</attribute>
62 <attribute name="minPwdLength">7</attribute>
63 <attribute name="pwdProperties">1</attribute>
65 <gp_ext name="Kerberos Policy">
66 <attribute name="ticket_lifetime">1d</attribute>
67 <attribute name="renew_lifetime" />
68 <attribute name="clockskew">300</attribute>
74 Each guid value contains a list of extensions, which contain a list of
75 attributes. The guid value represents a GPO. The attributes are the values
76 of those settings prior to the application of the GPO.
77 The list of guids is enclosed within a user name, which represents the user
78 the settings were applied to. This user may be the samaccountname of the
79 local computer, which implies that these are machine policies.
80 The applylog keeps track of the order in which the GPOs were applied, so
81 that they can be rolled back in reverse, returning the machine to the state
82 prior to policy application.
84 def __init__(self, user, gpostore, db_log=None):
85 ''' Initialize the gp_log
86 param user - the username (or machine name) that policies are
88 param gpostore - the GPOStorage obj which references the tdb which
90 param db_log - (optional) a string to initialize the gp_log
92 self._state = GPOSTATE.APPLY
93 self.gpostore = gpostore
96 self.gpdb = etree.fromstring(db_log)
98 self.gpdb = etree.Element('gp')
100 user_obj = self.gpdb.find('user[@name="%s"]' % user)
102 user_obj = etree.SubElement(self.gpdb, 'user')
103 user_obj.attrib['name'] = user
105 def state(self, value):
106 ''' Policy application state
107 param value - APPLY, ENFORCE, or UNAPPLY
109 The behavior of the gp_log depends on whether we are applying policy,
110 enforcing policy, or unapplying policy. During an apply, old settings
111 are recorded in the log. During an enforce, settings are being applied
112 but the gp_log does not change. During an unapply, additions to the log
113 should be ignored (since function calls to apply settings are actually
114 reverting policy), but removals from the log are allowed.
116 # If we're enforcing, but we've unapplied, apply instead
117 if value == GPOSTATE.ENFORCE:
118 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
119 apply_log = user_obj.find('applylog')
120 if apply_log is None or len(apply_log) == 0:
121 self._state = GPOSTATE.APPLY
127 def set_guid(self, guid):
128 ''' Log to a different GPO guid
129 param guid - guid value of the GPO from which we're applying
133 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
134 obj = user_obj.find('guid[@value="%s"]' % guid)
136 obj = etree.SubElement(user_obj, 'guid')
137 obj.attrib['value'] = guid
138 if self._state == GPOSTATE.APPLY:
139 apply_log = user_obj.find('applylog')
140 if apply_log is None:
141 apply_log = etree.SubElement(user_obj, 'applylog')
142 item = etree.SubElement(apply_log, 'guid')
143 item.attrib['count'] = '%d' % (len(apply_log)-1)
144 item.attrib['value'] = guid
146 def apply_log_pop(self):
147 ''' Pop a GPO guid from the applylog
148 return - last applied GPO guid
150 Removes the GPO guid last added to the list, which is the most recently
153 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
154 apply_log = user_obj.find('applylog')
155 if apply_log is not None:
156 ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
158 apply_log.remove(ret)
159 return ret.attrib['value']
160 if len(apply_log) == 0 and apply_log in user_obj:
161 user_obj.remove(apply_log)
164 def store(self, gp_ext_name, attribute, old_val):
165 ''' Store an attribute in the gp_log
166 param gp_ext_name - Name of the extension applying policy
167 param attribute - The attribute being modified
168 param old_val - The value of the attribute prior to policy
171 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
173 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
174 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
175 assert guid_obj is not None, "gpo guid was not set"
176 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
178 ext = etree.SubElement(guid_obj, 'gp_ext')
179 ext.attrib['name'] = gp_ext_name
180 attr = ext.find('attribute[@name="%s"]' % attribute)
182 attr = etree.SubElement(ext, 'attribute')
183 attr.attrib['name'] = attribute
186 def retrieve(self, gp_ext_name, attribute):
187 ''' Retrieve a stored attribute from the gp_log
188 param gp_ext_name - Name of the extension which applied policy
189 param attribute - The attribute being retrieved
190 return - The value of the attribute prior to policy
193 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
194 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
195 assert guid_obj is not None, "gpo guid was not set"
196 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
198 attr = ext.find('attribute[@name="%s"]' % attribute)
203 def list(self, gp_extensions):
204 ''' Return a list of attributes, their previous values, and functions
206 param gp_extensions - list of extension objects, for retrieving attr to
208 return - list of (attr, value, apply_func) tuples for
211 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
212 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
213 assert guid_obj is not None, "gpo guid was not set"
216 for gp_ext in gp_extensions:
217 data_maps.update(gp_ext.apply_map())
218 exts = guid_obj.findall('gp_ext')
221 attrs = ext.findall('attribute')
224 if attr.attrib['name'] in data_maps[ext.attrib['name']]:
225 func = data_maps[ext.attrib['name']]\
226 [attr.attrib['name']][-1]
228 for dmap in data_maps[ext.attrib['name']].keys():
229 if data_maps[ext.attrib['name']][dmap][0] == \
231 func = data_maps[ext.attrib['name']][dmap][-1]
233 ret.append((attr.attrib['name'], attr.text, func))
236 def delete(self, gp_ext_name, attribute):
237 ''' Remove an attribute from the gp_log
238 param gp_ext_name - name of extension from which to remove the
240 param attribute - attribute to remove
242 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
243 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
244 assert guid_obj is not None, "gpo guid was not set"
245 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
247 attr = ext.find('attribute[@name="%s"]' % attribute)
254 ''' Write gp_log changes to disk '''
255 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
258 def __init__(self, log_file):
259 if os.path.isfile(log_file):
260 self.log = tdb.open(log_file)
262 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
265 self.log.transaction_start()
267 def get_int(self, key):
269 return int(self.log.get(key))
274 return self.log.get(key)
276 def get_gplog(self, user):
277 return gp_log(user, self, self.log.get(user))
279 def store(self, key, val):
280 self.log.store(key, val)
283 self.log.transaction_cancel()
285 def delete(self, key):
289 self.log.transaction_commit()
294 class gp_ext(object):
295 __metaclass__ = ABCMeta
297 def __init__(self, logger):
301 def list(self, rootpath):
309 def read(self, policy):
312 def parse(self, afile, ldb, gp_db, lp):
317 local_path = self.lp.cache_path('gpo_cache')
318 data_file = os.path.join(local_path, check_safe_path(afile).upper())
319 if os.path.exists(data_file):
320 return self.read(open(data_file, 'r').read())
327 class gp_ext_setter():
328 __metaclass__ = ABCMeta
330 def __init__(self, logger, ldb, gp_db, lp, attribute, val):
333 self.attribute = attribute
341 def update_samba(self):
342 (upd_sam, value) = self.mapper().get(self.attribute)
353 class gp_inf_ext(gp_ext):
355 def list(self, rootpath):
362 def read(self, policy):
364 inftable = self.apply_map()
366 current_section = None
368 # So here we would declare a boolean,
369 # that would get changed to TRUE.
371 # If at any point in time a GPO was applied,
372 # then we return that boolean at the end.
374 inf_conf = ConfigParser()
375 inf_conf.optionxform=str
377 inf_conf.readfp(StringIO(policy))
379 inf_conf.readfp(StringIO(policy.decode('utf-16')))
381 for section in inf_conf.sections():
382 current_section = inftable.get(section)
383 if not current_section:
385 for key, value in inf_conf.items(section):
386 if current_section.get(key):
387 (att, setter) = current_section.get(key)
388 value = value.encode('ascii', 'ignore')
390 setter(self.logger, self.ldb, self.gp_db, self.lp, att,
391 value).update_samba()
399 ''' Fetch the hostname of a writable DC '''
400 def get_dc_hostname(creds, lp):
401 net = Net(creds=creds, lp=lp)
402 cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
404 return cldap_ret.pdc_dns_name
406 ''' Fetch a list of GUIDs for applicable GPOs '''
407 def get_gpo_list(dc_hostname, creds, lp):
409 ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
411 gpos = ads.get_gpo_list(creds.get_username())
415 def cache_gpo_dir(conn, cache, sub_dir):
416 loc_sub_dir = sub_dir.upper()
417 local_dir = os.path.join(cache, loc_sub_dir)
419 os.makedirs(local_dir, mode=0o755)
421 if e.errno != errno.EEXIST:
423 for fdata in conn.list(sub_dir):
424 if fdata['attrib'] & smb.FILE_ATTRIBUTE_DIRECTORY:
425 cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
427 local_name = fdata['name'].upper()
428 f = NamedTemporaryFile(delete=False, dir=local_dir)
429 fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
430 f.write(conn.loadfile(fname))
432 os.rename(f.name, os.path.join(local_dir, local_name))
435 def check_safe_path(path):
436 dirs = re.split('/|\\\\', path)
438 dirs = dirs[dirs.index('sysvol')+1:]
440 return os.path.join(*dirs)
443 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
444 conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds, sign=True)
445 cache_path = lp.cache_path('gpo_cache')
447 if not gpo.file_sys_path:
449 cache_gpo_dir(conn, cache_path, check_safe_path(gpo.file_sys_path))
451 def gpo_version(lp, path):
452 # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
453 # read from the gpo client cache.
454 gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
455 return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
457 def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
458 gp_db = store.get_gplog(creds.get_username())
459 dc_hostname = get_dc_hostname(creds, lp)
460 gpos = get_gpo_list(dc_hostname, creds, lp)
462 check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
464 logger.error('Failed downloading gpt cache from \'%s\' using SMB' \
470 if guid == 'Local Policy':
472 path = os.path.join(lp.get('realm'), 'Policies', guid).upper()
473 version = gpo_version(lp, path)
474 if version != store.get_int(guid):
475 logger.info('GPO %s has changed' % guid)
476 gp_db.state(GPOSTATE.APPLY)
478 gp_db.state(GPOSTATE.ENFORCE)
481 for ext in gp_extensions:
483 ext.parse(ext.list(path), test_ldb, gp_db, lp)
484 except Exception as e:
485 logger.error('Failed to parse gpo %s for extension %s' % \
487 logger.error('Message was: ' + str(e))
490 store.store(guid, '%i' % version)
493 def unapply_log(gp_db):
495 item = gp_db.apply_log_pop()
501 def unapply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
502 gp_db = store.get_gplog(creds.get_username())
503 gp_db.state(GPOSTATE.UNAPPLY)
504 for gpo_guid in unapply_log(gp_db):
505 gp_db.set_guid(gpo_guid)
506 unapply_attributes = gp_db.list(gp_extensions)
507 for attr in unapply_attributes:
508 attr_obj = attr[-1](logger, test_ldb, gp_db, lp, attr[0], attr[1])
509 attr_obj.mapper()[attr[0]][0](attr[1]) # Set the old value
510 gp_db.delete(str(attr_obj), attr[0])
513 def parse_gpext_conf(smb_conf):
515 if smb_conf is not None:
519 ext_conf = lp.state_path('gpext.conf')
520 parser = ConfigParser()
521 parser.read(ext_conf)
524 def atomic_write_conf(lp, parser):
525 ext_conf = lp.state_path('gpext.conf')
526 with NamedTemporaryFile(delete=False, dir=os.path.dirname(ext_conf)) as f:
528 os.rename(f.name, ext_conf)
530 def check_guid(guid):
531 # Check for valid guid with curly braces
532 if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
535 UUID(guid, version=4)
540 def register_gp_extension(guid, name, path,
541 smb_conf=None, machine=True, user=True):
542 # Check that the module exists
543 if not os.path.exists(path):
545 if not check_guid(guid):
548 lp, parser = parse_gpext_conf(smb_conf)
549 if not guid in parser.sections():
550 parser.add_section(guid)
551 parser.set(guid, 'DllName', path)
552 parser.set(guid, 'ProcessGroupPolicy', name)
553 parser.set(guid, 'NoMachinePolicy', 0 if machine else 1)
554 parser.set(guid, 'NoUserPolicy', 0 if user else 1)
556 atomic_write_conf(lp, parser)
560 def list_gp_extensions(smb_conf=None):
561 _, parser = parse_gpext_conf(smb_conf)
563 for guid in parser.sections():
565 results[guid]['DllName'] = parser.get(guid, 'DllName')
566 results[guid]['ProcessGroupPolicy'] = \
567 parser.get(guid, 'ProcessGroupPolicy')
568 results[guid]['MachinePolicy'] = \
569 not int(parser.get(guid, 'NoMachinePolicy'))
570 results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
573 def unregister_gp_extension(guid, smb_conf=None):
574 if not check_guid(guid):
577 lp, parser = parse_gpext_conf(smb_conf)
578 if guid in parser.sections():
579 parser.remove_section(guid)
581 atomic_write_conf(lp, parser)