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 get_applied_guids(self):
239 ''' Return a list of applied ext guids
240 return - List of guids for gpos that have applied settings
244 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
245 if user_obj is not None:
246 apply_log = user_obj.find('applylog')
247 if apply_log is not None:
248 guid_objs = apply_log.findall('guid[@count]')
249 guids_by_count = [(g.get('count'), g.get('value'))
251 guids_by_count.sort(reverse=True)
252 guids.extend(guid for count, guid in guids_by_count)
255 def get_applied_settings(self, guids):
256 ''' Return a list of applied ext guids
257 return - List of tuples containing the guid of a gpo, then
258 a dictionary of policies and their values prior
259 policy application. These are sorted so that the
260 most recently applied settings are removed first.
263 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
265 guid_settings = user_obj.find('guid[@value="%s"]' % guid)
266 exts = guid_settings.findall('gp_ext')
270 attrs = ext.findall('attribute')
272 attr_dict[attr.attrib['name']] = attr.text
273 settings[ext.attrib['name']] = attr_dict
274 ret.append((guid, settings))
277 def delete(self, gp_ext_name, attribute):
278 ''' Remove an attribute from the gp_log
279 param gp_ext_name - name of extension from which to remove the
281 param attribute - attribute to remove
283 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
284 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
285 assert guid_obj is not None, "gpo guid was not set"
286 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
288 attr = ext.find('attribute[@name="%s"]' % attribute)
295 ''' Write gp_log changes to disk '''
296 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
300 def __init__(self, log_file):
301 if os.path.isfile(log_file):
302 self.log = tdb.open(log_file)
304 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT |os.O_RDWR)
307 self.log.transaction_start()
309 def get_int(self, key):
311 return int(self.log.get(key))
316 return self.log.get(key)
318 def get_gplog(self, user):
319 return gp_log(user, self, self.log.get(user))
321 def store(self, key, val):
322 self.log.store(key, val)
325 self.log.transaction_cancel()
327 def delete(self, key):
331 self.log.transaction_commit()
337 class gp_ext(object):
338 __metaclass__ = ABCMeta
340 def __init__(self, logger, lp, creds, store):
344 self.gp_db = store.get_gplog(creds.get_username())
347 def process_group_policy(self, deleted_gpo_list, changed_gpo_list):
351 def read(self, policy):
354 def parse(self, afile):
355 local_path = self.lp.cache_path('gpo_cache')
356 data_file = os.path.join(local_path, check_safe_path(afile).upper())
357 if os.path.exists(data_file):
358 return self.read(open(data_file, 'r').read())
366 class gp_ext_setter(object):
367 __metaclass__ = ABCMeta
369 def __init__(self, logger, gp_db, lp, creds, attribute, val):
371 self.attribute = attribute
380 def update_samba(self):
381 (upd_sam, value) = self.mapper().get(self.attribute)
393 class gp_inf_ext(gp_ext):
394 def read(self, policy):
395 inf_conf = ConfigParser()
396 inf_conf.optionxform = str
398 inf_conf.readfp(StringIO(policy))
400 inf_conf.readfp(StringIO(policy.decode('utf-16')))
404 ''' Fetch the hostname of a writable DC '''
407 def get_dc_hostname(creds, lp):
408 net = Net(creds=creds, lp=lp)
409 cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
411 return cldap_ret.pdc_dns_name
414 ''' Fetch a list of GUIDs for applicable GPOs '''
417 def get_gpo_list(dc_hostname, creds, lp):
419 ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
421 gpos = ads.get_gpo_list(creds.get_username())
425 def cache_gpo_dir(conn, cache, sub_dir):
426 loc_sub_dir = sub_dir.upper()
427 local_dir = os.path.join(cache, loc_sub_dir)
429 os.makedirs(local_dir, mode=0o755)
431 if e.errno != errno.EEXIST:
433 for fdata in conn.list(sub_dir):
434 if fdata['attrib'] & smb.FILE_ATTRIBUTE_DIRECTORY:
435 cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
437 local_name = fdata['name'].upper()
438 f = NamedTemporaryFile(delete=False, dir=local_dir)
439 fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
440 f.write(conn.loadfile(fname))
442 os.rename(f.name, os.path.join(local_dir, local_name))
445 def check_safe_path(path):
446 dirs = re.split('/|\\\\', path)
448 dirs = dirs[dirs.index('sysvol') + 1:]
450 return os.path.join(*dirs)
454 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
455 conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds, sign=True)
456 cache_path = lp.cache_path('gpo_cache')
458 if not gpo.file_sys_path:
460 cache_gpo_dir(conn, cache_path, check_safe_path(gpo.file_sys_path))
463 def gpo_version(lp, path):
464 # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
465 # read from the gpo client cache.
466 gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
467 return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
470 def apply_gp(lp, creds, logger, store, gp_extensions):
471 gp_db = store.get_gplog(creds.get_username())
472 dc_hostname = get_dc_hostname(creds, lp)
473 gpos = get_gpo_list(dc_hostname, creds, lp)
475 check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
477 logger.error('Failed downloading gpt cache from \'%s\' using SMB'
483 if not gpo_obj.file_sys_path:
486 path = check_safe_path(gpo_obj.file_sys_path).upper()
487 version = gpo_version(lp, path)
488 if version != store.get_int(guid):
489 logger.info('GPO %s has changed' % guid)
490 changed_gpos.append(gpo_obj)
493 for ext in gp_extensions:
495 ext.process_group_policy([], changed_gpos)
496 except Exception as e:
497 logger.error('Failed to apply extension %s' % str(ext))
498 logger.error('Message was: ' + str(e))
501 if not gpo_obj.file_sys_path:
504 path = check_safe_path(gpo_obj.file_sys_path).upper()
505 version = gpo_version(lp, path)
506 store.store(guid, '%i' % version)
510 def unapply_log(gp_db):
512 item = gp_db.apply_log_pop()
519 def unapply_gp(lp, creds, logger, store, gp_extensions):
520 gp_db = store.get_gplog(creds.get_username())
521 gp_db.state(GPOSTATE.UNAPPLY)
522 for gpo_guid in unapply_log(gp_db):
523 gp_db.set_guid(gpo_guid)
524 unapply_attributes = gp_db.list(gp_extensions)
525 for attr in unapply_attributes:
526 attr_obj = attr[-1](logger, gp_db, lp, attr[0], attr[1])
527 attr_obj.mapper()[attr[0]][0](attr[1]) # Set the old value
528 gp_db.delete(str(attr_obj), attr[0])
532 def parse_gpext_conf(smb_conf):
534 if smb_conf is not None:
538 ext_conf = lp.state_path('gpext.conf')
539 parser = ConfigParser()
540 parser.read(ext_conf)
544 def atomic_write_conf(lp, parser):
545 ext_conf = lp.state_path('gpext.conf')
546 with NamedTemporaryFile(delete=False, dir=os.path.dirname(ext_conf)) as f:
548 os.rename(f.name, ext_conf)
551 def check_guid(guid):
552 # Check for valid guid with curly braces
553 if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
556 UUID(guid, version=4)
562 def register_gp_extension(guid, name, path,
563 smb_conf=None, machine=True, user=True):
564 # Check that the module exists
565 if not os.path.exists(path):
567 if not check_guid(guid):
570 lp, parser = parse_gpext_conf(smb_conf)
571 if guid not in parser.sections():
572 parser.add_section(guid)
573 parser.set(guid, 'DllName', path)
574 parser.set(guid, 'ProcessGroupPolicy', name)
575 parser.set(guid, 'NoMachinePolicy', 0 if machine else 1)
576 parser.set(guid, 'NoUserPolicy', 0 if user else 1)
578 atomic_write_conf(lp, parser)
583 def list_gp_extensions(smb_conf=None):
584 _, parser = parse_gpext_conf(smb_conf)
586 for guid in parser.sections():
588 results[guid]['DllName'] = parser.get(guid, 'DllName')
589 results[guid]['ProcessGroupPolicy'] = \
590 parser.get(guid, 'ProcessGroupPolicy')
591 results[guid]['MachinePolicy'] = \
592 not int(parser.get(guid, 'NoMachinePolicy'))
593 results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
597 def unregister_gp_extension(guid, smb_conf=None):
598 if not check_guid(guid):
601 lp, parser = parse_gpext_conf(smb_conf)
602 if guid in parser.sections():
603 parser.remove_section(guid)
605 atomic_write_conf(lp, parser)