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 samba.compat 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 store(self, gp_ext_name, attribute, old_val):
150 ''' Store an attribute in the gp_log
151 param gp_ext_name - Name of the extension applying policy
152 param attribute - The attribute being modified
153 param old_val - The value of the attribute prior to policy
156 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
158 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
159 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
160 assert guid_obj is not None, "gpo guid was not set"
161 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
163 ext = etree.SubElement(guid_obj, 'gp_ext')
164 ext.attrib['name'] = gp_ext_name
165 attr = ext.find('attribute[@name="%s"]' % attribute)
167 attr = etree.SubElement(ext, 'attribute')
168 attr.attrib['name'] = attribute
171 def retrieve(self, gp_ext_name, attribute):
172 ''' Retrieve a stored attribute from the gp_log
173 param gp_ext_name - Name of the extension which applied policy
174 param attribute - The attribute being retrieved
175 return - The value of the attribute prior to policy
178 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
179 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
180 assert guid_obj is not None, "gpo guid was not set"
181 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
183 attr = ext.find('attribute[@name="%s"]' % attribute)
188 def get_applied_guids(self):
189 ''' Return a list of applied ext guids
190 return - List of guids for gpos that have applied settings
194 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
195 if user_obj is not None:
196 apply_log = user_obj.find('applylog')
197 if apply_log is not None:
198 guid_objs = apply_log.findall('guid[@count]')
199 guids_by_count = [(g.get('count'), g.get('value'))
201 guids_by_count.sort(reverse=True)
202 guids.extend(guid for count, guid in guids_by_count)
205 def get_applied_settings(self, guids):
206 ''' Return a list of applied ext guids
207 return - List of tuples containing the guid of a gpo, then
208 a dictionary of policies and their values prior
209 policy application. These are sorted so that the
210 most recently applied settings are removed first.
213 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
215 guid_settings = user_obj.find('guid[@value="%s"]' % guid)
216 exts = guid_settings.findall('gp_ext')
220 attrs = ext.findall('attribute')
222 attr_dict[attr.attrib['name']] = attr.text
223 settings[ext.attrib['name']] = attr_dict
224 ret.append((guid, settings))
227 def delete(self, gp_ext_name, attribute):
228 ''' Remove an attribute from the gp_log
229 param gp_ext_name - name of extension from which to remove the
231 param attribute - attribute to remove
233 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
234 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
235 assert guid_obj is not None, "gpo guid was not set"
236 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
238 attr = ext.find('attribute[@name="%s"]' % attribute)
245 ''' Write gp_log changes to disk '''
246 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
250 def __init__(self, log_file):
251 if os.path.isfile(log_file):
252 self.log = tdb.open(log_file)
254 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT | os.O_RDWR)
257 self.log.transaction_start()
259 def get_int(self, key):
261 return int(self.log.get(key))
266 return self.log.get(key)
268 def get_gplog(self, user):
269 return gp_log(user, self, self.log.get(user))
271 def store(self, key, val):
272 self.log.store(key, val)
275 self.log.transaction_cancel()
277 def delete(self, key):
281 self.log.transaction_commit()
287 class gp_ext(object):
288 __metaclass__ = ABCMeta
290 def __init__(self, logger, lp, creds, store):
294 self.gp_db = store.get_gplog(creds.get_username())
297 def process_group_policy(self, deleted_gpo_list, changed_gpo_list):
301 def read(self, policy):
304 def parse(self, afile):
305 local_path = self.lp.cache_path('gpo_cache')
306 data_file = os.path.join(local_path, check_safe_path(afile).upper())
307 if os.path.exists(data_file):
308 return self.read(open(data_file, 'r').read())
316 class gp_ext_setter(object):
317 __metaclass__ = ABCMeta
319 def __init__(self, logger, gp_db, lp, creds, attribute, val):
321 self.attribute = attribute
330 def update_samba(self):
331 (upd_sam, value) = self.mapper().get(self.attribute)
339 upd_sam, _ = self.mapper().get(self.attribute)
347 class gp_inf_ext(gp_ext):
348 def read(self, policy):
349 inf_conf = ConfigParser()
350 inf_conf.optionxform = str
352 inf_conf.readfp(StringIO(policy))
354 inf_conf.readfp(StringIO(policy.decode('utf-16')))
358 ''' Fetch the hostname of a writable DC '''
361 def get_dc_hostname(creds, lp):
362 net = Net(creds=creds, lp=lp)
363 cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
365 return cldap_ret.pdc_dns_name
368 ''' Fetch a list of GUIDs for applicable GPOs '''
371 def get_gpo_list(dc_hostname, creds, lp):
373 ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
375 gpos = ads.get_gpo_list(creds.get_username())
379 def cache_gpo_dir(conn, cache, sub_dir):
380 loc_sub_dir = sub_dir.upper()
381 local_dir = os.path.join(cache, loc_sub_dir)
383 os.makedirs(local_dir, mode=0o755)
385 if e.errno != errno.EEXIST:
387 for fdata in conn.list(sub_dir):
388 if fdata['attrib'] & smb.FILE_ATTRIBUTE_DIRECTORY:
389 cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
391 local_name = fdata['name'].upper()
392 f = NamedTemporaryFile(delete=False, dir=local_dir)
393 fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
394 f.write(conn.loadfile(fname))
396 os.rename(f.name, os.path.join(local_dir, local_name))
399 def check_safe_path(path):
400 dirs = re.split('/|\\\\', path)
402 dirs = dirs[dirs.index('sysvol') + 1:]
404 return os.path.join(*dirs)
408 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
409 conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds, sign=True)
410 cache_path = lp.cache_path('gpo_cache')
412 if not gpo.file_sys_path:
414 cache_gpo_dir(conn, cache_path, check_safe_path(gpo.file_sys_path))
417 def get_deleted_gpos_list(gp_db, gpos):
418 applied_gpos = gp_db.get_applied_guids()
419 current_guids = set([p.name for p in gpos])
420 deleted_gpos = [guid for guid in applied_gpos if guid not in current_guids]
421 return gp_db.get_applied_settings(deleted_gpos)
423 def gpo_version(lp, path):
424 # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
425 # read from the gpo client cache.
426 gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
427 return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
430 def apply_gp(lp, creds, logger, store, gp_extensions, force=False):
431 gp_db = store.get_gplog(creds.get_username())
432 dc_hostname = get_dc_hostname(creds, lp)
433 gpos = get_gpo_list(dc_hostname, creds, lp)
434 del_gpos = get_deleted_gpos_list(gp_db, gpos)
436 check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
438 logger.error('Failed downloading gpt cache from \'%s\' using SMB'
444 gp_db.state(GPOSTATE.ENFORCE)
448 if not gpo_obj.file_sys_path:
451 path = check_safe_path(gpo_obj.file_sys_path).upper()
452 version = gpo_version(lp, path)
453 if version != store.get_int(guid):
454 logger.info('GPO %s has changed' % guid)
455 changed_gpos.append(gpo_obj)
456 gp_db.state(GPOSTATE.APPLY)
459 for ext in gp_extensions:
461 ext.process_group_policy(del_gpos, changed_gpos)
462 except Exception as e:
463 logger.error('Failed to apply extension %s' % str(ext))
464 logger.error('Message was: ' + str(e))
467 if not gpo_obj.file_sys_path:
470 path = check_safe_path(gpo_obj.file_sys_path).upper()
471 version = gpo_version(lp, path)
472 store.store(guid, '%i' % version)
476 def unapply_gp(lp, creds, logger, store, gp_extensions):
477 gp_db = store.get_gplog(creds.get_username())
478 gp_db.state(GPOSTATE.UNAPPLY)
479 # Treat all applied gpos as deleted
480 del_gpos = gp_db.get_applied_settings(gp_db.get_applied_guids())
482 for ext in gp_extensions:
484 ext.process_group_policy(del_gpos, [])
485 except Exception as e:
486 logger.error('Failed to unapply extension %s' % str(ext))
487 logger.error('Message was: ' + str(e))
492 def parse_gpext_conf(smb_conf):
494 if smb_conf is not None:
498 ext_conf = lp.state_path('gpext.conf')
499 parser = ConfigParser()
500 parser.read(ext_conf)
504 def atomic_write_conf(lp, parser):
505 ext_conf = lp.state_path('gpext.conf')
506 with NamedTemporaryFile(delete=False, dir=os.path.dirname(ext_conf)) as f:
508 os.rename(f.name, ext_conf)
511 def check_guid(guid):
512 # Check for valid guid with curly braces
513 if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
516 UUID(guid, version=4)
522 def register_gp_extension(guid, name, path,
523 smb_conf=None, machine=True, user=True):
524 # Check that the module exists
525 if not os.path.exists(path):
527 if not check_guid(guid):
530 lp, parser = parse_gpext_conf(smb_conf)
531 if guid not in parser.sections():
532 parser.add_section(guid)
533 parser.set(guid, 'DllName', path)
534 parser.set(guid, 'ProcessGroupPolicy', name)
535 parser.set(guid, 'NoMachinePolicy', 0 if machine else 1)
536 parser.set(guid, 'NoUserPolicy', 0 if user else 1)
538 atomic_write_conf(lp, parser)
543 def list_gp_extensions(smb_conf=None):
544 _, parser = parse_gpext_conf(smb_conf)
546 for guid in parser.sections():
548 results[guid]['DllName'] = parser.get(guid, 'DllName')
549 results[guid]['ProcessGroupPolicy'] = \
550 parser.get(guid, 'ProcessGroupPolicy')
551 results[guid]['MachinePolicy'] = \
552 not int(parser.get(guid, 'NoMachinePolicy'))
553 results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
557 def unregister_gp_extension(guid, smb_conf=None):
558 if not check_guid(guid):
561 lp, parser = parse_gpext_conf(smb_conf)
562 if guid in parser.sections():
563 parser.remove_section(guid)
565 atomic_write_conf(lp, parser)