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 samba.compat import get_bytes
27 from abc import ABCMeta, abstractmethod
28 import xml.etree.ElementTree as etree
30 from samba.net import Net
31 from samba.dcerpc import nbt
33 import samba.gpo as gpo
34 from samba.param import LoadParm
36 from tempfile import NamedTemporaryFile
40 GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
49 ''' Log settings overwritten by gpo apply
50 The gp_log is an xml file that stores a history of gpo changes (and the
51 original setting value).
53 The log is organized like so:
58 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
60 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
61 <gp_ext name="System Access">
62 <attribute name="minPwdAge">-864000000000</attribute>
63 <attribute name="maxPwdAge">-36288000000000</attribute>
64 <attribute name="minPwdLength">7</attribute>
65 <attribute name="pwdProperties">1</attribute>
67 <gp_ext name="Kerberos Policy">
68 <attribute name="ticket_lifetime">1d</attribute>
69 <attribute name="renew_lifetime" />
70 <attribute name="clockskew">300</attribute>
76 Each guid value contains a list of extensions, which contain a list of
77 attributes. The guid value represents a GPO. The attributes are the values
78 of those settings prior to the application of the GPO.
79 The list of guids is enclosed within a user name, which represents the user
80 the settings were applied to. This user may be the samaccountname of the
81 local computer, which implies that these are machine policies.
82 The applylog keeps track of the order in which the GPOs were applied, so
83 that they can be rolled back in reverse, returning the machine to the state
84 prior to policy application.
86 def __init__(self, user, gpostore, db_log=None):
87 ''' Initialize the gp_log
88 param user - the username (or machine name) that policies are
90 param gpostore - the GPOStorage obj which references the tdb which
92 param db_log - (optional) a string to initialize the gp_log
94 self._state = GPOSTATE.APPLY
95 self.gpostore = gpostore
98 self.gpdb = etree.fromstring(db_log)
100 self.gpdb = etree.Element('gp')
102 user_obj = self.gpdb.find('user[@name="%s"]' % user)
104 user_obj = etree.SubElement(self.gpdb, 'user')
105 user_obj.attrib['name'] = user
107 def state(self, value):
108 ''' Policy application state
109 param value - APPLY, ENFORCE, or UNAPPLY
111 The behavior of the gp_log depends on whether we are applying policy,
112 enforcing policy, or unapplying policy. During an apply, old settings
113 are recorded in the log. During an enforce, settings are being applied
114 but the gp_log does not change. During an unapply, additions to the log
115 should be ignored (since function calls to apply settings are actually
116 reverting policy), but removals from the log are allowed.
118 # If we're enforcing, but we've unapplied, apply instead
119 if value == GPOSTATE.ENFORCE:
120 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
121 apply_log = user_obj.find('applylog')
122 if apply_log is None or len(apply_log) == 0:
123 self._state = GPOSTATE.APPLY
129 def set_guid(self, guid):
130 ''' Log to a different GPO guid
131 param guid - guid value of the GPO from which we're applying
135 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
136 obj = user_obj.find('guid[@value="%s"]' % guid)
138 obj = etree.SubElement(user_obj, 'guid')
139 obj.attrib['value'] = guid
140 if self._state == GPOSTATE.APPLY:
141 apply_log = user_obj.find('applylog')
142 if apply_log is None:
143 apply_log = etree.SubElement(user_obj, 'applylog')
144 prev = apply_log.find('guid[@value="%s"]' % guid)
146 item = etree.SubElement(apply_log, 'guid')
147 item.attrib['count'] = '%d' % (len(apply_log) - 1)
148 item.attrib['value'] = guid
150 def store(self, gp_ext_name, attribute, old_val):
151 ''' Store an attribute in the gp_log
152 param gp_ext_name - Name of the extension applying policy
153 param attribute - The attribute being modified
154 param old_val - The value of the attribute prior to policy
157 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
159 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
160 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
161 assert guid_obj is not None, "gpo guid was not set"
162 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
164 ext = etree.SubElement(guid_obj, 'gp_ext')
165 ext.attrib['name'] = gp_ext_name
166 attr = ext.find('attribute[@name="%s"]' % attribute)
168 attr = etree.SubElement(ext, 'attribute')
169 attr.attrib['name'] = attribute
172 def retrieve(self, gp_ext_name, attribute):
173 ''' Retrieve a stored attribute from the gp_log
174 param gp_ext_name - Name of the extension which applied policy
175 param attribute - The attribute being retrieved
176 return - The value of the attribute prior to policy
179 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
180 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
181 assert guid_obj is not None, "gpo guid was not set"
182 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
184 attr = ext.find('attribute[@name="%s"]' % attribute)
189 def get_applied_guids(self):
190 ''' Return a list of applied ext guids
191 return - List of guids for gpos that have applied settings
195 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
196 if user_obj is not None:
197 apply_log = user_obj.find('applylog')
198 if apply_log is not None:
199 guid_objs = apply_log.findall('guid[@count]')
200 guids_by_count = [(g.get('count'), g.get('value'))
202 guids_by_count.sort(reverse=True)
203 guids.extend(guid for count, guid in guids_by_count)
206 def get_applied_settings(self, guids):
207 ''' Return a list of applied ext guids
208 return - List of tuples containing the guid of a gpo, then
209 a dictionary of policies and their values prior
210 policy application. These are sorted so that the
211 most recently applied settings are removed first.
214 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
216 guid_settings = user_obj.find('guid[@value="%s"]' % guid)
217 exts = guid_settings.findall('gp_ext')
221 attrs = ext.findall('attribute')
223 attr_dict[attr.attrib['name']] = attr.text
224 settings[ext.attrib['name']] = attr_dict
225 ret.append((guid, settings))
228 def delete(self, gp_ext_name, attribute):
229 ''' Remove an attribute from the gp_log
230 param gp_ext_name - name of extension from which to remove the
232 param attribute - attribute to remove
234 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
235 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
236 assert guid_obj is not None, "gpo guid was not set"
237 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
239 attr = ext.find('attribute[@name="%s"]' % attribute)
246 ''' Write gp_log changes to disk '''
247 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
251 def __init__(self, log_file):
252 if os.path.isfile(log_file):
253 self.log = tdb.open(log_file)
255 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT | os.O_RDWR)
258 self.log.transaction_start()
260 def get_int(self, key):
262 return int(self.log.get(get_bytes(key)))
267 return self.log.get(get_bytes(key))
269 def get_gplog(self, user):
270 return gp_log(user, self, self.log.get(get_bytes(user)))
272 def store(self, key, val):
273 self.log.store(get_bytes(key), get_bytes(val))
276 self.log.transaction_cancel()
278 def delete(self, key):
279 self.log.delete(get_bytes(key))
282 self.log.transaction_commit()
288 class gp_ext(object):
289 __metaclass__ = ABCMeta
291 def __init__(self, logger, lp, creds, store):
295 self.gp_db = store.get_gplog(creds.get_username())
298 def process_group_policy(self, deleted_gpo_list, changed_gpo_list):
302 def read(self, policy):
305 def parse(self, afile):
306 local_path = self.lp.cache_path('gpo_cache')
307 data_file = os.path.join(local_path, check_safe_path(afile).upper())
308 if os.path.exists(data_file):
309 return self.read(open(data_file, 'r').read())
317 class gp_ext_setter(object):
318 __metaclass__ = ABCMeta
320 def __init__(self, logger, gp_db, lp, creds, attribute, val):
322 self.attribute = attribute
331 def update_samba(self):
332 (upd_sam, value) = self.mapper().get(self.attribute)
340 upd_sam, _ = self.mapper().get(self.attribute)
348 class gp_inf_ext(gp_ext):
349 def read(self, policy):
350 inf_conf = ConfigParser()
351 inf_conf.optionxform = str
353 inf_conf.readfp(StringIO(policy))
355 inf_conf.readfp(StringIO(policy.decode('utf-16')))
359 ''' Fetch the hostname of a writable DC '''
362 def get_dc_hostname(creds, lp):
363 net = Net(creds=creds, lp=lp)
364 cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
366 return cldap_ret.pdc_dns_name
369 ''' Fetch a list of GUIDs for applicable GPOs '''
372 def get_gpo_list(dc_hostname, creds, lp):
374 ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
376 gpos = ads.get_gpo_list(creds.get_username())
380 def cache_gpo_dir(conn, cache, sub_dir):
381 loc_sub_dir = sub_dir.upper()
382 local_dir = os.path.join(cache, loc_sub_dir)
384 os.makedirs(local_dir, mode=0o755)
386 if e.errno != errno.EEXIST:
388 for fdata in conn.list(sub_dir):
389 if fdata['attrib'] & smb.FILE_ATTRIBUTE_DIRECTORY:
390 cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
392 local_name = fdata['name'].upper()
393 f = NamedTemporaryFile(delete=False, dir=local_dir)
394 fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
395 f.write(conn.loadfile(fname))
397 os.rename(f.name, os.path.join(local_dir, local_name))
400 def check_safe_path(path):
401 dirs = re.split('/|\\\\', path)
403 dirs = dirs[dirs.index('sysvol') + 1:]
405 return os.path.join(*dirs)
409 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
410 conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds, sign=True)
411 cache_path = lp.cache_path('gpo_cache')
413 if not gpo.file_sys_path:
415 cache_gpo_dir(conn, cache_path, check_safe_path(gpo.file_sys_path))
418 def get_deleted_gpos_list(gp_db, gpos):
419 applied_gpos = gp_db.get_applied_guids()
420 current_guids = set([p.name for p in gpos])
421 deleted_gpos = [guid for guid in applied_gpos if guid not in current_guids]
422 return gp_db.get_applied_settings(deleted_gpos)
424 def gpo_version(lp, path):
425 # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
426 # read from the gpo client cache.
427 gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
428 return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
431 def apply_gp(lp, creds, logger, store, gp_extensions, force=False):
432 gp_db = store.get_gplog(creds.get_username())
433 dc_hostname = get_dc_hostname(creds, lp)
434 gpos = get_gpo_list(dc_hostname, creds, lp)
435 del_gpos = get_deleted_gpos_list(gp_db, gpos)
437 check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
439 logger.error('Failed downloading gpt cache from \'%s\' using SMB'
445 gp_db.state(GPOSTATE.ENFORCE)
449 if not gpo_obj.file_sys_path:
452 path = check_safe_path(gpo_obj.file_sys_path).upper()
453 version = gpo_version(lp, path)
454 if version != store.get_int(guid):
455 logger.info('GPO %s has changed' % guid)
456 changed_gpos.append(gpo_obj)
457 gp_db.state(GPOSTATE.APPLY)
460 for ext in gp_extensions:
462 ext.process_group_policy(del_gpos, changed_gpos)
463 except Exception as e:
464 logger.error('Failed to apply extension %s' % str(ext))
465 logger.error('Message was: ' + str(e))
468 if not gpo_obj.file_sys_path:
471 path = check_safe_path(gpo_obj.file_sys_path).upper()
472 version = gpo_version(lp, path)
473 store.store(guid, '%i' % version)
477 def unapply_gp(lp, creds, logger, store, gp_extensions):
478 gp_db = store.get_gplog(creds.get_username())
479 gp_db.state(GPOSTATE.UNAPPLY)
480 # Treat all applied gpos as deleted
481 del_gpos = gp_db.get_applied_settings(gp_db.get_applied_guids())
483 for ext in gp_extensions:
485 ext.process_group_policy(del_gpos, [])
486 except Exception as e:
487 logger.error('Failed to unapply extension %s' % str(ext))
488 logger.error('Message was: ' + str(e))
493 def parse_gpext_conf(smb_conf):
495 if smb_conf is not None:
499 ext_conf = lp.state_path('gpext.conf')
500 parser = ConfigParser()
501 parser.read(ext_conf)
505 def atomic_write_conf(lp, parser):
506 ext_conf = lp.state_path('gpext.conf')
507 with NamedTemporaryFile(mode="w+", delete=False, dir=os.path.dirname(ext_conf)) as f:
509 os.rename(f.name, ext_conf)
512 def check_guid(guid):
513 # Check for valid guid with curly braces
514 if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
517 UUID(guid, version=4)
523 def register_gp_extension(guid, name, path,
524 smb_conf=None, machine=True, user=True):
525 # Check that the module exists
526 if not os.path.exists(path):
528 if not check_guid(guid):
531 lp, parser = parse_gpext_conf(smb_conf)
532 if guid not in parser.sections():
533 parser.add_section(guid)
534 parser.set(guid, 'DllName', path)
535 parser.set(guid, 'ProcessGroupPolicy', name)
536 parser.set(guid, 'NoMachinePolicy', "0" if machine else "1")
537 parser.set(guid, 'NoUserPolicy', "0" if user else "1")
539 atomic_write_conf(lp, parser)
544 def list_gp_extensions(smb_conf=None):
545 _, parser = parse_gpext_conf(smb_conf)
547 for guid in parser.sections():
549 results[guid]['DllName'] = parser.get(guid, 'DllName')
550 results[guid]['ProcessGroupPolicy'] = \
551 parser.get(guid, 'ProcessGroupPolicy')
552 results[guid]['MachinePolicy'] = \
553 not int(parser.get(guid, 'NoMachinePolicy'))
554 results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
558 def unregister_gp_extension(guid, smb_conf=None):
559 if not check_guid(guid):
562 lp, parser = parse_gpext_conf(smb_conf)
563 if guid in parser.sections():
564 parser.remove_section(guid)
566 atomic_write_conf(lp, parser)