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 samba.compat 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')
48 ''' Log settings overwritten by gpo apply
49 The gp_log is an xml file that stores a history of gpo changes (and the
50 original setting value).
52 The log is organized like so:
57 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
59 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
60 <gp_ext name="System Access">
61 <attribute name="minPwdAge">-864000000000</attribute>
62 <attribute name="maxPwdAge">-36288000000000</attribute>
63 <attribute name="minPwdLength">7</attribute>
64 <attribute name="pwdProperties">1</attribute>
66 <gp_ext name="Kerberos Policy">
67 <attribute name="ticket_lifetime">1d</attribute>
68 <attribute name="renew_lifetime" />
69 <attribute name="clockskew">300</attribute>
75 Each guid value contains a list of extensions, which contain a list of
76 attributes. The guid value represents a GPO. The attributes are the values
77 of those settings prior to the application of the GPO.
78 The list of guids is enclosed within a user name, which represents the user
79 the settings were applied to. This user may be the samaccountname of the
80 local computer, which implies that these are machine policies.
81 The applylog keeps track of the order in which the GPOs were applied, so
82 that they can be rolled back in reverse, returning the machine to the state
83 prior to policy application.
85 def __init__(self, user, gpostore, db_log=None):
86 ''' Initialize the gp_log
87 param user - the username (or machine name) that policies are
89 param gpostore - the GPOStorage obj which references the tdb which
91 param db_log - (optional) a string to initialize the gp_log
93 self._state = GPOSTATE.APPLY
94 self.gpostore = gpostore
97 self.gpdb = etree.fromstring(db_log)
99 self.gpdb = etree.Element('gp')
101 user_obj = self.gpdb.find('user[@name="%s"]' % user)
103 user_obj = etree.SubElement(self.gpdb, 'user')
104 user_obj.attrib['name'] = user
106 def state(self, value):
107 ''' Policy application state
108 param value - APPLY, ENFORCE, or UNAPPLY
110 The behavior of the gp_log depends on whether we are applying policy,
111 enforcing policy, or unapplying policy. During an apply, old settings
112 are recorded in the log. During an enforce, settings are being applied
113 but the gp_log does not change. During an unapply, additions to the log
114 should be ignored (since function calls to apply settings are actually
115 reverting policy), but removals from the log are allowed.
117 # If we're enforcing, but we've unapplied, apply instead
118 if value == GPOSTATE.ENFORCE:
119 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
120 apply_log = user_obj.find('applylog')
121 if apply_log is None or len(apply_log) == 0:
122 self._state = GPOSTATE.APPLY
128 def set_guid(self, guid):
129 ''' Log to a different GPO guid
130 param guid - guid value of the GPO from which we're applying
134 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
135 obj = user_obj.find('guid[@value="%s"]' % guid)
137 obj = etree.SubElement(user_obj, 'guid')
138 obj.attrib['value'] = guid
139 if self._state == GPOSTATE.APPLY:
140 apply_log = user_obj.find('applylog')
141 if apply_log is None:
142 apply_log = etree.SubElement(user_obj, 'applylog')
143 prev = apply_log.find('guid[@value="%s"]' % guid)
145 item = etree.SubElement(apply_log, 'guid')
146 item.attrib['count'] = '%d' % (len(apply_log) - 1)
147 item.attrib['value'] = guid
149 def apply_log_pop(self):
150 ''' Pop a GPO guid from the applylog
151 return - last applied GPO guid
153 Removes the GPO guid last added to the list, which is the most recently
156 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
157 apply_log = user_obj.find('applylog')
158 if apply_log is not None:
159 ret = apply_log.find('guid[@count="%d"]' % (len(apply_log) - 1))
161 apply_log.remove(ret)
162 return ret.attrib['value']
163 if len(apply_log) == 0 and apply_log in user_obj:
164 user_obj.remove(apply_log)
167 def store(self, gp_ext_name, attribute, old_val):
168 ''' Store an attribute in the gp_log
169 param gp_ext_name - Name of the extension applying policy
170 param attribute - The attribute being modified
171 param old_val - The value of the attribute prior to policy
174 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
176 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
177 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
178 assert guid_obj is not None, "gpo guid was not set"
179 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
181 ext = etree.SubElement(guid_obj, 'gp_ext')
182 ext.attrib['name'] = gp_ext_name
183 attr = ext.find('attribute[@name="%s"]' % attribute)
185 attr = etree.SubElement(ext, 'attribute')
186 attr.attrib['name'] = attribute
189 def retrieve(self, gp_ext_name, attribute):
190 ''' Retrieve a stored attribute from the gp_log
191 param gp_ext_name - Name of the extension which applied policy
192 param attribute - The attribute being retrieved
193 return - The value of the attribute prior to policy
196 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
197 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
198 assert guid_obj is not None, "gpo guid was not set"
199 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
201 attr = ext.find('attribute[@name="%s"]' % attribute)
206 def list(self, gp_extensions):
207 ''' Return a list of attributes, their previous values, and functions
209 param gp_extensions - list of extension objects, for retrieving attr to
211 return - list of (attr, value, apply_func) tuples for
214 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
215 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
216 assert guid_obj is not None, "gpo guid was not set"
219 for gp_ext in gp_extensions:
220 data_maps.update(gp_ext.apply_map())
221 exts = guid_obj.findall('gp_ext')
224 attrs = ext.findall('attribute')
227 if attr.attrib['name'] in data_maps[ext.attrib['name']]:
228 func = data_maps[ext.attrib['name']][attr.attrib['name']][-1]
230 for dmap in data_maps[ext.attrib['name']].keys():
231 if data_maps[ext.attrib['name']][dmap][0] == \
233 func = data_maps[ext.attrib['name']][dmap][-1]
235 ret.append((attr.attrib['name'], attr.text, func))
238 def delete(self, gp_ext_name, attribute):
239 ''' Remove an attribute from the gp_log
240 param gp_ext_name - name of extension from which to remove the
242 param attribute - attribute to remove
244 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
245 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
246 assert guid_obj is not None, "gpo guid was not set"
247 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
249 attr = ext.find('attribute[@name="%s"]' % attribute)
256 ''' Write gp_log changes to disk '''
257 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
261 def __init__(self, log_file):
262 if os.path.isfile(log_file):
263 self.log = tdb.open(log_file)
265 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT |os.O_RDWR)
268 self.log.transaction_start()
270 def get_int(self, key):
272 return int(self.log.get(key))
277 return self.log.get(key)
279 def get_gplog(self, user):
280 return gp_log(user, self, self.log.get(user))
282 def store(self, key, val):
283 self.log.store(key, val)
286 self.log.transaction_cancel()
288 def delete(self, key):
292 self.log.transaction_commit()
298 class gp_ext(object):
299 __metaclass__ = ABCMeta
301 def __init__(self, logger):
305 def list(self, rootpath):
313 def read(self, policy):
316 def parse(self, afile, ldb, gp_db, lp):
321 local_path = self.lp.cache_path('gpo_cache')
322 data_file = os.path.join(local_path, check_safe_path(afile).upper())
323 if os.path.exists(data_file):
324 return self.read(open(data_file, 'r').read())
332 class gp_ext_setter():
333 __metaclass__ = ABCMeta
335 def __init__(self, logger, ldb, gp_db, lp, attribute, val):
338 self.attribute = attribute
346 def update_samba(self):
347 (upd_sam, value) = self.mapper().get(self.attribute)
359 class gp_inf_ext(gp_ext):
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()
394 ''' Fetch the hostname of a writable DC '''
397 def get_dc_hostname(creds, lp):
398 net = Net(creds=creds, lp=lp)
399 cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
401 return cldap_ret.pdc_dns_name
404 ''' 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)
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))
453 def gpo_version(lp, path):
454 # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
455 # read from the gpo client cache.
456 gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
457 return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
460 def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
461 gp_db = store.get_gplog(creds.get_username())
462 dc_hostname = get_dc_hostname(creds, lp)
463 gpos = get_gpo_list(dc_hostname, creds, lp)
465 check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
467 logger.error('Failed downloading gpt cache from \'%s\' using SMB'
473 if guid == 'Local Policy':
475 path = os.path.join(lp.get('realm'), 'Policies', guid).upper()
476 version = gpo_version(lp, path)
477 if version != store.get_int(guid):
478 logger.info('GPO %s has changed' % guid)
479 gp_db.state(GPOSTATE.APPLY)
481 gp_db.state(GPOSTATE.ENFORCE)
484 for ext in gp_extensions:
486 ext.parse(ext.list(path), test_ldb, gp_db, lp)
487 except Exception as e:
488 logger.error('Failed to parse gpo %s for extension %s' %
490 logger.error('Message was: ' + str(e))
493 store.store(guid, '%i' % version)
497 def unapply_log(gp_db):
499 item = gp_db.apply_log_pop()
506 def unapply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
507 gp_db = store.get_gplog(creds.get_username())
508 gp_db.state(GPOSTATE.UNAPPLY)
509 for gpo_guid in unapply_log(gp_db):
510 gp_db.set_guid(gpo_guid)
511 unapply_attributes = gp_db.list(gp_extensions)
512 for attr in unapply_attributes:
513 attr_obj = attr[-1](logger, test_ldb, gp_db, lp, attr[0], attr[1])
514 attr_obj.mapper()[attr[0]][0](attr[1]) # Set the old value
515 gp_db.delete(str(attr_obj), attr[0])
519 def parse_gpext_conf(smb_conf):
521 if smb_conf is not None:
525 ext_conf = lp.state_path('gpext.conf')
526 parser = ConfigParser()
527 parser.read(ext_conf)
531 def atomic_write_conf(lp, parser):
532 ext_conf = lp.state_path('gpext.conf')
533 with NamedTemporaryFile(delete=False, dir=os.path.dirname(ext_conf)) as f:
535 os.rename(f.name, ext_conf)
538 def check_guid(guid):
539 # Check for valid guid with curly braces
540 if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
543 UUID(guid, version=4)
549 def register_gp_extension(guid, name, path,
550 smb_conf=None, machine=True, user=True):
551 # Check that the module exists
552 if not os.path.exists(path):
554 if not check_guid(guid):
557 lp, parser = parse_gpext_conf(smb_conf)
558 if guid not in parser.sections():
559 parser.add_section(guid)
560 parser.set(guid, 'DllName', path)
561 parser.set(guid, 'ProcessGroupPolicy', name)
562 parser.set(guid, 'NoMachinePolicy', 0 if machine else 1)
563 parser.set(guid, 'NoUserPolicy', 0 if user else 1)
565 atomic_write_conf(lp, parser)
570 def list_gp_extensions(smb_conf=None):
571 _, parser = parse_gpext_conf(smb_conf)
573 for guid in parser.sections():
575 results[guid]['DllName'] = parser.get(guid, 'DllName')
576 results[guid]['ProcessGroupPolicy'] = \
577 parser.get(guid, 'ProcessGroupPolicy')
578 results[guid]['MachinePolicy'] = \
579 not int(parser.get(guid, 'NoMachinePolicy'))
580 results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
584 def unregister_gp_extension(guid, smb_conf=None):
585 if not check_guid(guid):
588 lp, parser = parse_gpext_conf(smb_conf)
589 if guid in parser.sections():
590 parser.remove_section(guid)
592 atomic_write_conf(lp, parser)