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/>.
21 sys.path.insert(0, "bin/python")
22 from samba import NTSTATUSError
23 from ConfigParser import ConfigParser
24 from StringIO import StringIO
25 from abc import ABCMeta, abstractmethod
26 import xml.etree.ElementTree as etree
28 from samba.net import Net
29 from samba.dcerpc import nbt
31 import samba.gpo as gpo
35 GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
43 ''' Log settings overwritten by gpo apply
44 The gp_log is an xml file that stores a history of gpo changes (and the
45 original setting value).
47 The log is organized like so:
52 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
54 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
55 <gp_ext name="System Access">
56 <attribute name="minPwdAge">-864000000000</attribute>
57 <attribute name="maxPwdAge">-36288000000000</attribute>
58 <attribute name="minPwdLength">7</attribute>
59 <attribute name="pwdProperties">1</attribute>
61 <gp_ext name="Kerberos Policy">
62 <attribute name="ticket_lifetime">1d</attribute>
63 <attribute name="renew_lifetime" />
64 <attribute name="clockskew">300</attribute>
70 Each guid value contains a list of extensions, which contain a list of
71 attributes. The guid value represents a GPO. The attributes are the values
72 of those settings prior to the application of the GPO.
73 The list of guids is enclosed within a user name, which represents the user
74 the settings were applied to. This user may be the samaccountname of the
75 local computer, which implies that these are machine policies.
76 The applylog keeps track of the order in which the GPOs were applied, so
77 that they can be rolled back in reverse, returning the machine to the state
78 prior to policy application.
80 def __init__(self, user, gpostore, db_log=None):
81 ''' Initialize the gp_log
82 param user - the username (or machine name) that policies are
84 param gpostore - the GPOStorage obj which references the tdb which
86 param db_log - (optional) a string to initialize the gp_log
88 self._state = GPOSTATE.APPLY
89 self.gpostore = gpostore
92 self.gpdb = etree.fromstring(db_log)
94 self.gpdb = etree.Element('gp')
96 user_obj = self.gpdb.find('user[@name="%s"]' % user)
98 user_obj = etree.SubElement(self.gpdb, 'user')
99 user_obj.attrib['name'] = user
101 def state(self, value):
102 ''' Policy application state
103 param value - APPLY, ENFORCE, or UNAPPLY
105 The behavior of the gp_log depends on whether we are applying policy,
106 enforcing policy, or unapplying policy. During an apply, old settings
107 are recorded in the log. During an enforce, settings are being applied
108 but the gp_log does not change. During an unapply, additions to the log
109 should be ignored (since function calls to apply settings are actually
110 reverting policy), but removals from the log are allowed.
112 # If we're enforcing, but we've unapplied, apply instead
113 if value == GPOSTATE.ENFORCE:
114 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
115 apply_log = user_obj.find('applylog')
116 if apply_log is None or len(apply_log) == 0:
117 self._state = GPOSTATE.APPLY
123 def set_guid(self, guid):
124 ''' Log to a different GPO guid
125 param guid - guid value of the GPO from which we're applying
129 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
130 obj = user_obj.find('guid[@value="%s"]' % guid)
132 obj = etree.SubElement(user_obj, 'guid')
133 obj.attrib['value'] = guid
134 if self._state == GPOSTATE.APPLY:
135 apply_log = user_obj.find('applylog')
136 if apply_log is None:
137 apply_log = etree.SubElement(user_obj, 'applylog')
138 item = etree.SubElement(apply_log, 'guid')
139 item.attrib['count'] = '%d' % (len(apply_log)-1)
140 item.attrib['value'] = guid
142 def apply_log_pop(self):
143 ''' Pop a GPO guid from the applylog
144 return - last applied GPO guid
146 Removes the GPO guid last added to the list, which is the most recently
149 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
150 apply_log = user_obj.find('applylog')
151 if apply_log is not None:
152 ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
154 apply_log.remove(ret)
155 return ret.attrib['value']
156 if len(apply_log) == 0 and apply_log in user_obj:
157 user_obj.remove(apply_log)
160 def store(self, gp_ext_name, attribute, old_val):
161 ''' Store an attribute in the gp_log
162 param gp_ext_name - Name of the extension applying policy
163 param attribute - The attribute being modified
164 param old_val - The value of the attribute prior to policy
167 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
169 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
170 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
171 assert guid_obj is not None, "gpo guid was not set"
172 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
174 ext = etree.SubElement(guid_obj, 'gp_ext')
175 ext.attrib['name'] = gp_ext_name
176 attr = ext.find('attribute[@name="%s"]' % attribute)
178 attr = etree.SubElement(ext, 'attribute')
179 attr.attrib['name'] = attribute
182 def retrieve(self, gp_ext_name, attribute):
183 ''' Retrieve a stored attribute from the gp_log
184 param gp_ext_name - Name of the extension which applied policy
185 param attribute - The attribute being retrieved
186 return - The value of the attribute prior to policy
189 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
190 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
191 assert guid_obj is not None, "gpo guid was not set"
192 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
194 attr = ext.find('attribute[@name="%s"]' % attribute)
199 def list(self, gp_extensions):
200 ''' Return a list of attributes, their previous values, and functions
202 param gp_extensions - list of extension objects, for retrieving attr to
204 return - list of (attr, value, apply_func) tuples for
207 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
208 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
209 assert guid_obj is not None, "gpo guid was not set"
212 for gp_ext in gp_extensions:
213 data_maps.update(gp_ext.apply_map())
214 exts = guid_obj.findall('gp_ext')
217 attrs = ext.findall('attribute')
220 if attr.attrib['name'] in data_maps[ext.attrib['name']]:
221 func = data_maps[ext.attrib['name']]\
222 [attr.attrib['name']][-1]
224 for dmap in data_maps[ext.attrib['name']].keys():
225 if data_maps[ext.attrib['name']][dmap][0] == \
227 func = data_maps[ext.attrib['name']][dmap][-1]
229 ret.append((attr.attrib['name'], attr.text, func))
232 def delete(self, gp_ext_name, attribute):
233 ''' Remove an attribute from the gp_log
234 param gp_ext_name - name of extension from which to remove the
236 param attribute - attribute to remove
238 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
239 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
240 assert guid_obj is not None, "gpo guid was not set"
241 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
243 attr = ext.find('attribute[@name="%s"]' % attribute)
250 ''' Write gp_log changes to disk '''
251 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
254 def __init__(self, log_file):
255 if os.path.isfile(log_file):
256 self.log = tdb.open(log_file)
258 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
261 self.log.transaction_start()
263 def get_int(self, key):
265 return int(self.log.get(key))
270 return self.log.get(key)
272 def get_gplog(self, user):
273 return gp_log(user, self, self.log.get(user))
275 def store(self, key, val):
276 self.log.store(key, val)
279 self.log.transaction_cancel()
281 def delete(self, key):
285 self.log.transaction_commit()
290 class gp_ext(object):
291 __metaclass__ = ABCMeta
293 def __init__(self, logger):
297 def list(self, rootpath):
305 def read(self, policy):
308 def parse(self, afile, ldb, conn, gp_db, lp):
313 # Fixing the bug where only some Linux Boxes capitalize MACHINE
315 blist = afile.split('/')
316 idx = afile.lower().split('/').index('machine')
319 blist[idx].capitalize(),
322 bfile = '/'.join(blist[:idx]) + '/' + case + '/' + \
323 '/'.join(blist[idx+1:])
325 return self.read(conn.loadfile(bfile.replace('/', '\\')))
326 except NTSTATUSError:
330 return self.read(conn.loadfile(afile.replace('/', '\\')))
331 except Exception as e:
332 self.logger.error(str(e))
339 class gp_ext_setter():
340 __metaclass__ = ABCMeta
342 def __init__(self, logger, ldb, gp_db, lp, attribute, val):
345 self.attribute = attribute
353 def update_samba(self):
354 (upd_sam, value) = self.mapper().get(self.attribute)
365 class gp_inf_ext(gp_ext):
367 def list(self, rootpath):
374 def read(self, policy):
376 inftable = self.apply_map()
378 current_section = None
380 # So here we would declare a boolean,
381 # that would get changed to TRUE.
383 # If at any point in time a GPO was applied,
384 # then we return that boolean at the end.
386 inf_conf = ConfigParser()
387 inf_conf.optionxform=str
389 inf_conf.readfp(StringIO(policy))
391 inf_conf.readfp(StringIO(policy.decode('utf-16')))
393 for section in inf_conf.sections():
394 current_section = inftable.get(section)
395 if not current_section:
397 for key, value in inf_conf.items(section):
398 if current_section.get(key):
399 (att, setter) = current_section.get(key)
400 value = value.encode('ascii', 'ignore')
402 setter(self.logger, self.ldb, self.gp_db, self.lp, att,
403 value).update_samba()
411 ''' Fetch the hostname of a writable DC '''
412 def get_dc_hostname(creds, lp):
413 net = Net(creds=creds, lp=lp)
414 cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
416 return cldap_ret.pdc_dns_name
418 ''' Fetch a list of GUIDs for applicable GPOs '''
419 def get_gpo_list(dc_hostname, creds, lp):
421 ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
423 gpos = ads.get_gpo_list(creds.get_username())
426 def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
427 gp_db = store.get_gplog(creds.get_username())
428 dc_hostname = get_dc_hostname(creds, lp)
430 conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds)
432 logger.error('Error connecting to \'%s\' using SMB' % dc_hostname)
434 gpos = get_gpo_list(dc_hostname, creds, lp)
438 if guid == 'Local Policy':
440 path = os.path.join(lp.get('realm').lower(), 'Policies', guid)
441 local_path = os.path.join(lp.get("path", "sysvol"), path)
442 version = int(gpo.gpo_get_sysvol_gpt_version(local_path)[1])
443 if version != store.get_int(guid):
444 logger.info('GPO %s has changed' % guid)
445 gp_db.state(GPOSTATE.APPLY)
447 gp_db.state(GPOSTATE.ENFORCE)
450 for ext in gp_extensions:
452 ext.parse(ext.list(path), test_ldb, conn, gp_db, lp)
453 except Exception as e:
454 logger.error('Failed to parse gpo %s for extension %s' % \
456 logger.error('Message was: ' + str(e))
459 store.store(guid, '%i' % version)
462 def unapply_log(gp_db):
464 item = gp_db.apply_log_pop()
470 def unapply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
471 gp_db = store.get_gplog(creds.get_username())
472 gp_db.state(GPOSTATE.UNAPPLY)
473 for gpo_guid in unapply_log(gp_db):
474 gp_db.set_guid(gpo_guid)
475 unapply_attributes = gp_db.list(gp_extensions)
476 for attr in unapply_attributes:
477 attr_obj = attr[-1](logger, test_ldb, gp_db, lp, attr[0], attr[1])
478 attr_obj.mapper()[attr[0]][0](attr[1]) # Set the old value
479 gp_db.delete(str(attr_obj), attr[0])