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 prev = apply_log.find('guid[@value="%s"]' % guid)
144 item = etree.SubElement(apply_log, 'guid')
145 item.attrib['count'] = '%d' % (len(apply_log) - 1)
146 item.attrib['value'] = guid
148 def apply_log_pop(self):
149 ''' Pop a GPO guid from the applylog
150 return - last applied GPO guid
152 Removes the GPO guid last added to the list, which is the most recently
155 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
156 apply_log = user_obj.find('applylog')
157 if apply_log is not None:
158 ret = apply_log.find('guid[@count="%d"]' % (len(apply_log) - 1))
160 apply_log.remove(ret)
161 return ret.attrib['value']
162 if len(apply_log) == 0 and apply_log in user_obj:
163 user_obj.remove(apply_log)
166 def store(self, gp_ext_name, attribute, old_val):
167 ''' Store an attribute in the gp_log
168 param gp_ext_name - Name of the extension applying policy
169 param attribute - The attribute being modified
170 param old_val - The value of the attribute prior to policy
173 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
175 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
176 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
177 assert guid_obj is not None, "gpo guid was not set"
178 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
180 ext = etree.SubElement(guid_obj, 'gp_ext')
181 ext.attrib['name'] = gp_ext_name
182 attr = ext.find('attribute[@name="%s"]' % attribute)
184 attr = etree.SubElement(ext, 'attribute')
185 attr.attrib['name'] = attribute
188 def retrieve(self, gp_ext_name, attribute):
189 ''' Retrieve a stored attribute from the gp_log
190 param gp_ext_name - Name of the extension which applied policy
191 param attribute - The attribute being retrieved
192 return - The value of the attribute prior to policy
195 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
196 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
197 assert guid_obj is not None, "gpo guid was not set"
198 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
200 attr = ext.find('attribute[@name="%s"]' % attribute)
205 def list(self, gp_extensions):
206 ''' Return a list of attributes, their previous values, and functions
208 param gp_extensions - list of extension objects, for retrieving attr to
210 return - list of (attr, value, apply_func) tuples for
213 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
214 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
215 assert guid_obj is not None, "gpo guid was not set"
218 for gp_ext in gp_extensions:
219 data_maps.update(gp_ext.apply_map())
220 exts = guid_obj.findall('gp_ext')
223 attrs = ext.findall('attribute')
226 if attr.attrib['name'] in data_maps[ext.attrib['name']]:
227 func = data_maps[ext.attrib['name']][attr.attrib['name']][-1]
229 for dmap in data_maps[ext.attrib['name']].keys():
230 if data_maps[ext.attrib['name']][dmap][0] == \
232 func = data_maps[ext.attrib['name']][dmap][-1]
234 ret.append((attr.attrib['name'], attr.text, func))
237 def delete(self, gp_ext_name, attribute):
238 ''' Remove an attribute from the gp_log
239 param gp_ext_name - name of extension from which to remove the
241 param attribute - attribute to remove
243 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
244 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
245 assert guid_obj is not None, "gpo guid was not set"
246 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
248 attr = ext.find('attribute[@name="%s"]' % attribute)
255 ''' Write gp_log changes to disk '''
256 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
259 def __init__(self, log_file):
260 if os.path.isfile(log_file):
261 self.log = tdb.open(log_file)
263 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT |os.O_RDWR)
266 self.log.transaction_start()
268 def get_int(self, key):
270 return int(self.log.get(key))
275 return self.log.get(key)
277 def get_gplog(self, user):
278 return gp_log(user, self, self.log.get(user))
280 def store(self, key, val):
281 self.log.store(key, val)
284 self.log.transaction_cancel()
286 def delete(self, key):
290 self.log.transaction_commit()
295 class gp_ext(object):
296 __metaclass__ = ABCMeta
298 def __init__(self, logger):
302 def list(self, rootpath):
310 def read(self, policy):
313 def parse(self, afile, ldb, gp_db, lp):
318 local_path = self.lp.cache_path('gpo_cache')
319 data_file = os.path.join(local_path, check_safe_path(afile).upper())
320 if os.path.exists(data_file):
321 return self.read(open(data_file, 'r').read())
328 class gp_ext_setter():
329 __metaclass__ = ABCMeta
331 def __init__(self, logger, ldb, gp_db, lp, attribute, val):
334 self.attribute = attribute
342 def update_samba(self):
343 (upd_sam, value) = self.mapper().get(self.attribute)
354 class gp_inf_ext(gp_ext):
356 def list(self, rootpath):
363 def read(self, policy):
365 inftable = self.apply_map()
367 current_section = None
369 # So here we would declare a boolean,
370 # that would get changed to TRUE.
372 # If at any point in time a GPO was applied,
373 # then we return that boolean at the end.
375 inf_conf = ConfigParser()
376 inf_conf.optionxform = str
378 inf_conf.readfp(StringIO(policy))
380 inf_conf.readfp(StringIO(policy.decode('utf-16')))
382 for section in inf_conf.sections():
383 current_section = inftable.get(section)
384 if not current_section:
386 for key, value in inf_conf.items(section):
387 if current_section.get(key):
388 (att, setter) = current_section.get(key)
389 value = value.encode('ascii', 'ignore')
391 setter(self.logger, self.ldb, self.gp_db, self.lp, att,
392 value).update_samba()
400 ''' Fetch the hostname of a writable DC '''
401 def get_dc_hostname(creds, lp):
402 net = Net(creds=creds, lp=lp)
403 cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
405 return cldap_ret.pdc_dns_name
407 ''' Fetch a list of GUIDs for applicable GPOs '''
408 def get_gpo_list(dc_hostname, creds, lp):
410 ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
412 gpos = ads.get_gpo_list(creds.get_username())
416 def cache_gpo_dir(conn, cache, sub_dir):
417 loc_sub_dir = sub_dir.upper()
418 local_dir = os.path.join(cache, loc_sub_dir)
420 os.makedirs(local_dir, mode=0o755)
422 if e.errno != errno.EEXIST:
424 for fdata in conn.list(sub_dir):
425 if fdata['attrib'] & smb.FILE_ATTRIBUTE_DIRECTORY:
426 cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
428 local_name = fdata['name'].upper()
429 f = NamedTemporaryFile(delete=False, dir=local_dir)
430 fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
431 f.write(conn.loadfile(fname))
433 os.rename(f.name, os.path.join(local_dir, local_name))
436 def check_safe_path(path):
437 dirs = re.split('/|\\\\', path)
439 dirs = dirs[dirs.index('sysvol') + 1:]
441 return os.path.join(*dirs)
444 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
445 conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds, sign=True)
446 cache_path = lp.cache_path('gpo_cache')
448 if not gpo.file_sys_path:
450 cache_gpo_dir(conn, cache_path, check_safe_path(gpo.file_sys_path))
452 def gpo_version(lp, path):
453 # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
454 # read from the gpo client cache.
455 gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
456 return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
458 def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
459 gp_db = store.get_gplog(creds.get_username())
460 dc_hostname = get_dc_hostname(creds, lp)
461 gpos = get_gpo_list(dc_hostname, creds, lp)
463 check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
465 logger.error('Failed downloading gpt cache from \'%s\' using SMB' \
471 if guid == 'Local Policy':
473 path = os.path.join(lp.get('realm'), 'Policies', guid).upper()
474 version = gpo_version(lp, path)
475 if version != store.get_int(guid):
476 logger.info('GPO %s has changed' % guid)
477 gp_db.state(GPOSTATE.APPLY)
479 gp_db.state(GPOSTATE.ENFORCE)
482 for ext in gp_extensions:
484 ext.parse(ext.list(path), test_ldb, gp_db, lp)
485 except Exception as e:
486 logger.error('Failed to parse gpo %s for extension %s' % \
488 logger.error('Message was: ' + str(e))
491 store.store(guid, '%i' % version)
494 def unapply_log(gp_db):
496 item = gp_db.apply_log_pop()
502 def unapply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
503 gp_db = store.get_gplog(creds.get_username())
504 gp_db.state(GPOSTATE.UNAPPLY)
505 for gpo_guid in unapply_log(gp_db):
506 gp_db.set_guid(gpo_guid)
507 unapply_attributes = gp_db.list(gp_extensions)
508 for attr in unapply_attributes:
509 attr_obj = attr[-1](logger, test_ldb, gp_db, lp, attr[0], attr[1])
510 attr_obj.mapper()[attr[0]][0](attr[1]) # Set the old value
511 gp_db.delete(str(attr_obj), attr[0])
514 def parse_gpext_conf(smb_conf):
516 if smb_conf is not None:
520 ext_conf = lp.state_path('gpext.conf')
521 parser = ConfigParser()
522 parser.read(ext_conf)
525 def atomic_write_conf(lp, parser):
526 ext_conf = lp.state_path('gpext.conf')
527 with NamedTemporaryFile(delete=False, dir=os.path.dirname(ext_conf)) as f:
529 os.rename(f.name, ext_conf)
531 def check_guid(guid):
532 # Check for valid guid with curly braces
533 if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
536 UUID(guid, version=4)
541 def register_gp_extension(guid, name, path,
542 smb_conf=None, machine=True, user=True):
543 # Check that the module exists
544 if not os.path.exists(path):
546 if not check_guid(guid):
549 lp, parser = parse_gpext_conf(smb_conf)
550 if not guid in parser.sections():
551 parser.add_section(guid)
552 parser.set(guid, 'DllName', path)
553 parser.set(guid, 'ProcessGroupPolicy', name)
554 parser.set(guid, 'NoMachinePolicy', 0 if machine else 1)
555 parser.set(guid, 'NoUserPolicy', 0 if user else 1)
557 atomic_write_conf(lp, parser)
561 def list_gp_extensions(smb_conf=None):
562 _, parser = parse_gpext_conf(smb_conf)
564 for guid in parser.sections():
566 results[guid]['DllName'] = parser.get(guid, 'DllName')
567 results[guid]['ProcessGroupPolicy'] = \
568 parser.get(guid, 'ProcessGroupPolicy')
569 results[guid]['MachinePolicy'] = \
570 not int(parser.get(guid, 'NoMachinePolicy'))
571 results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
574 def unregister_gp_extension(guid, smb_conf=None):
575 if not check_guid(guid):
578 lp, parser = parse_gpext_conf(smb_conf)
579 if guid in parser.sections():
580 parser.remove_section(guid)
582 atomic_write_conf(lp, parser)