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 tempfile import NamedTemporaryFile
37 GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
45 ''' Log settings overwritten by gpo apply
46 The gp_log is an xml file that stores a history of gpo changes (and the
47 original setting value).
49 The log is organized like so:
54 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
56 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
57 <gp_ext name="System Access">
58 <attribute name="minPwdAge">-864000000000</attribute>
59 <attribute name="maxPwdAge">-36288000000000</attribute>
60 <attribute name="minPwdLength">7</attribute>
61 <attribute name="pwdProperties">1</attribute>
63 <gp_ext name="Kerberos Policy">
64 <attribute name="ticket_lifetime">1d</attribute>
65 <attribute name="renew_lifetime" />
66 <attribute name="clockskew">300</attribute>
72 Each guid value contains a list of extensions, which contain a list of
73 attributes. The guid value represents a GPO. The attributes are the values
74 of those settings prior to the application of the GPO.
75 The list of guids is enclosed within a user name, which represents the user
76 the settings were applied to. This user may be the samaccountname of the
77 local computer, which implies that these are machine policies.
78 The applylog keeps track of the order in which the GPOs were applied, so
79 that they can be rolled back in reverse, returning the machine to the state
80 prior to policy application.
82 def __init__(self, user, gpostore, db_log=None):
83 ''' Initialize the gp_log
84 param user - the username (or machine name) that policies are
86 param gpostore - the GPOStorage obj which references the tdb which
88 param db_log - (optional) a string to initialize the gp_log
90 self._state = GPOSTATE.APPLY
91 self.gpostore = gpostore
94 self.gpdb = etree.fromstring(db_log)
96 self.gpdb = etree.Element('gp')
98 user_obj = self.gpdb.find('user[@name="%s"]' % user)
100 user_obj = etree.SubElement(self.gpdb, 'user')
101 user_obj.attrib['name'] = user
103 def state(self, value):
104 ''' Policy application state
105 param value - APPLY, ENFORCE, or UNAPPLY
107 The behavior of the gp_log depends on whether we are applying policy,
108 enforcing policy, or unapplying policy. During an apply, old settings
109 are recorded in the log. During an enforce, settings are being applied
110 but the gp_log does not change. During an unapply, additions to the log
111 should be ignored (since function calls to apply settings are actually
112 reverting policy), but removals from the log are allowed.
114 # If we're enforcing, but we've unapplied, apply instead
115 if value == GPOSTATE.ENFORCE:
116 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
117 apply_log = user_obj.find('applylog')
118 if apply_log is None or len(apply_log) == 0:
119 self._state = GPOSTATE.APPLY
125 def set_guid(self, guid):
126 ''' Log to a different GPO guid
127 param guid - guid value of the GPO from which we're applying
131 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
132 obj = user_obj.find('guid[@value="%s"]' % guid)
134 obj = etree.SubElement(user_obj, 'guid')
135 obj.attrib['value'] = guid
136 if self._state == GPOSTATE.APPLY:
137 apply_log = user_obj.find('applylog')
138 if apply_log is None:
139 apply_log = etree.SubElement(user_obj, 'applylog')
140 item = etree.SubElement(apply_log, 'guid')
141 item.attrib['count'] = '%d' % (len(apply_log)-1)
142 item.attrib['value'] = guid
144 def apply_log_pop(self):
145 ''' Pop a GPO guid from the applylog
146 return - last applied GPO guid
148 Removes the GPO guid last added to the list, which is the most recently
151 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
152 apply_log = user_obj.find('applylog')
153 if apply_log is not None:
154 ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
156 apply_log.remove(ret)
157 return ret.attrib['value']
158 if len(apply_log) == 0 and apply_log in user_obj:
159 user_obj.remove(apply_log)
162 def store(self, gp_ext_name, attribute, old_val):
163 ''' Store an attribute in the gp_log
164 param gp_ext_name - Name of the extension applying policy
165 param attribute - The attribute being modified
166 param old_val - The value of the attribute prior to policy
169 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
171 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
172 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
173 assert guid_obj is not None, "gpo guid was not set"
174 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
176 ext = etree.SubElement(guid_obj, 'gp_ext')
177 ext.attrib['name'] = gp_ext_name
178 attr = ext.find('attribute[@name="%s"]' % attribute)
180 attr = etree.SubElement(ext, 'attribute')
181 attr.attrib['name'] = attribute
184 def retrieve(self, gp_ext_name, attribute):
185 ''' Retrieve a stored attribute from the gp_log
186 param gp_ext_name - Name of the extension which applied policy
187 param attribute - The attribute being retrieved
188 return - The value of the attribute prior to policy
191 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
192 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
193 assert guid_obj is not None, "gpo guid was not set"
194 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
196 attr = ext.find('attribute[@name="%s"]' % attribute)
201 def list(self, gp_extensions):
202 ''' Return a list of attributes, their previous values, and functions
204 param gp_extensions - list of extension objects, for retrieving attr to
206 return - list of (attr, value, apply_func) tuples for
209 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
210 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
211 assert guid_obj is not None, "gpo guid was not set"
214 for gp_ext in gp_extensions:
215 data_maps.update(gp_ext.apply_map())
216 exts = guid_obj.findall('gp_ext')
219 attrs = ext.findall('attribute')
222 if attr.attrib['name'] in data_maps[ext.attrib['name']]:
223 func = data_maps[ext.attrib['name']]\
224 [attr.attrib['name']][-1]
226 for dmap in data_maps[ext.attrib['name']].keys():
227 if data_maps[ext.attrib['name']][dmap][0] == \
229 func = data_maps[ext.attrib['name']][dmap][-1]
231 ret.append((attr.attrib['name'], attr.text, func))
234 def delete(self, gp_ext_name, attribute):
235 ''' Remove an attribute from the gp_log
236 param gp_ext_name - name of extension from which to remove the
238 param attribute - attribute to remove
240 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
241 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
242 assert guid_obj is not None, "gpo guid was not set"
243 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
245 attr = ext.find('attribute[@name="%s"]' % attribute)
252 ''' Write gp_log changes to disk '''
253 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
256 def __init__(self, log_file):
257 if os.path.isfile(log_file):
258 self.log = tdb.open(log_file)
260 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
263 self.log.transaction_start()
265 def get_int(self, key):
267 return int(self.log.get(key))
272 return self.log.get(key)
274 def get_gplog(self, user):
275 return gp_log(user, self, self.log.get(user))
277 def store(self, key, val):
278 self.log.store(key, val)
281 self.log.transaction_cancel()
283 def delete(self, key):
287 self.log.transaction_commit()
292 class gp_ext(object):
293 __metaclass__ = ABCMeta
295 def __init__(self, logger):
299 def list(self, rootpath):
307 def read(self, policy):
310 def parse(self, afile, ldb, gp_db, lp):
315 local_path = self.lp.cache_path('gpo_cache')
316 data_file = os.path.join(local_path, check_safe_path(afile).upper())
317 if os.path.exists(data_file):
318 return self.read(open(data_file, 'r').read())
325 class gp_ext_setter():
326 __metaclass__ = ABCMeta
328 def __init__(self, logger, ldb, gp_db, lp, attribute, val):
331 self.attribute = attribute
339 def update_samba(self):
340 (upd_sam, value) = self.mapper().get(self.attribute)
351 class gp_inf_ext(gp_ext):
353 def list(self, rootpath):
360 def read(self, policy):
362 inftable = self.apply_map()
364 current_section = None
366 # So here we would declare a boolean,
367 # that would get changed to TRUE.
369 # If at any point in time a GPO was applied,
370 # then we return that boolean at the end.
372 inf_conf = ConfigParser()
373 inf_conf.optionxform=str
375 inf_conf.readfp(StringIO(policy))
377 inf_conf.readfp(StringIO(policy.decode('utf-16')))
379 for section in inf_conf.sections():
380 current_section = inftable.get(section)
381 if not current_section:
383 for key, value in inf_conf.items(section):
384 if current_section.get(key):
385 (att, setter) = current_section.get(key)
386 value = value.encode('ascii', 'ignore')
388 setter(self.logger, self.ldb, self.gp_db, self.lp, att,
389 value).update_samba()
397 ''' Fetch the hostname of a writable DC '''
398 def get_dc_hostname(creds, lp):
399 net = Net(creds=creds, lp=lp)
400 cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
402 return cldap_ret.pdc_dns_name
404 ''' Fetch a list of GUIDs for applicable GPOs '''
405 def get_gpo_list(dc_hostname, creds, lp):
407 ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
409 gpos = ads.get_gpo_list(creds.get_username())
413 def cache_gpo_dir(conn, cache, sub_dir):
414 loc_sub_dir = sub_dir.upper()
415 local_dir = os.path.join(cache, loc_sub_dir)
417 os.makedirs(local_dir, mode=0o755)
419 if e.errno != errno.EEXIST:
421 for fdata in conn.list(sub_dir):
422 if fdata['attrib'] & smb.FILE_ATTRIBUTE_DIRECTORY:
423 cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
425 local_name = fdata['name'].upper()
426 f = NamedTemporaryFile(delete=False, dir=local_dir)
427 fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
428 f.write(conn.loadfile(fname))
430 os.rename(f.name, os.path.join(local_dir, local_name))
433 def check_safe_path(path):
434 dirs = re.split('/|\\\\', path)
436 dirs = dirs[dirs.index('sysvol')+1:]
438 return os.path.join(*dirs)
441 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
442 conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds, sign=True)
443 cache_path = lp.cache_path('gpo_cache')
445 if not gpo.file_sys_path:
447 cache_gpo_dir(conn, cache_path, check_safe_path(gpo.file_sys_path))
449 def gpo_version(lp, path):
450 # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
451 # read from the gpo client cache.
452 gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
453 return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
455 def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
456 gp_db = store.get_gplog(creds.get_username())
457 dc_hostname = get_dc_hostname(creds, lp)
458 gpos = get_gpo_list(dc_hostname, creds, lp)
460 check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
462 logger.error('Failed downloading gpt cache from \'%s\' using SMB' \
468 if guid == 'Local Policy':
470 path = os.path.join(lp.get('realm'), 'Policies', guid).upper()
471 version = gpo_version(lp, path)
472 if version != store.get_int(guid):
473 logger.info('GPO %s has changed' % guid)
474 gp_db.state(GPOSTATE.APPLY)
476 gp_db.state(GPOSTATE.ENFORCE)
479 for ext in gp_extensions:
481 ext.parse(ext.list(path), test_ldb, gp_db, lp)
482 except Exception as e:
483 logger.error('Failed to parse gpo %s for extension %s' % \
485 logger.error('Message was: ' + str(e))
488 store.store(guid, '%i' % version)
491 def unapply_log(gp_db):
493 item = gp_db.apply_log_pop()
499 def unapply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
500 gp_db = store.get_gplog(creds.get_username())
501 gp_db.state(GPOSTATE.UNAPPLY)
502 for gpo_guid in unapply_log(gp_db):
503 gp_db.set_guid(gpo_guid)
504 unapply_attributes = gp_db.list(gp_extensions)
505 for attr in unapply_attributes:
506 attr_obj = attr[-1](logger, test_ldb, gp_db, lp, attr[0], attr[1])
507 attr_obj.mapper()[attr[0]][0](attr[1]) # Set the old value
508 gp_db.delete(str(attr_obj), attr[0])