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
31 GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
39 ''' Log settings overwritten by gpo apply
40 The gp_log is an xml file that stores a history of gpo changes (and the
41 original setting value).
43 The log is organized like so:
48 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
50 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
51 <gp_ext name="System Access">
52 <attribute name="minPwdAge">-864000000000</attribute>
53 <attribute name="maxPwdAge">-36288000000000</attribute>
54 <attribute name="minPwdLength">7</attribute>
55 <attribute name="pwdProperties">1</attribute>
57 <gp_ext name="Kerberos Policy">
58 <attribute name="ticket_lifetime">1d</attribute>
59 <attribute name="renew_lifetime" />
60 <attribute name="clockskew">300</attribute>
66 Each guid value contains a list of extensions, which contain a list of
67 attributes. The guid value represents a GPO. The attributes are the values
68 of those settings prior to the application of the GPO.
69 The list of guids is enclosed within a user name, which represents the user
70 the settings were applied to. This user may be the samaccountname of the
71 local computer, which implies that these are machine policies.
72 The applylog keeps track of the order in which the GPOs were applied, so
73 that they can be rolled back in reverse, returning the machine to the state
74 prior to policy application.
76 def __init__(self, user, gpostore, db_log=None):
77 ''' Initialize the gp_log
78 param user - the username (or machine name) that policies are
80 param gpostore - the GPOStorage obj which references the tdb which
82 param db_log - (optional) a string to initialize the gp_log
84 self._state = GPOSTATE.APPLY
85 self.gpostore = gpostore
88 self.gpdb = etree.fromstring(db_log)
90 self.gpdb = etree.Element('gp')
92 user_obj = self.gpdb.find('user[@name="%s"]' % user)
94 user_obj = etree.SubElement(self.gpdb, 'user')
95 user_obj.attrib['name'] = user
97 def state(self, value):
98 ''' Policy application state
99 param value - APPLY, ENFORCE, or UNAPPLY
101 The behavior of the gp_log depends on whether we are applying policy,
102 enforcing policy, or unapplying policy. During an apply, old settings
103 are recorded in the log. During an enforce, settings are being applied
104 but the gp_log does not change. During an unapply, additions to the log
105 should be ignored (since function calls to apply settings are actually
106 reverting policy), but removals from the log are allowed.
108 # If we're enforcing, but we've unapplied, apply instead
109 if value == GPOSTATE.ENFORCE:
110 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
111 apply_log = user_obj.find('applylog')
112 if apply_log is None or len(apply_log) == 0:
113 self._state = GPOSTATE.APPLY
119 def set_guid(self, guid):
120 ''' Log to a different GPO guid
121 param guid - guid value of the GPO from which we're applying
125 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
126 obj = user_obj.find('guid[@value="%s"]' % guid)
128 obj = etree.SubElement(user_obj, 'guid')
129 obj.attrib['value'] = guid
130 if self._state == GPOSTATE.APPLY:
131 apply_log = user_obj.find('applylog')
132 if apply_log is None:
133 apply_log = etree.SubElement(user_obj, 'applylog')
134 item = etree.SubElement(apply_log, 'guid')
135 item.attrib['count'] = '%d' % (len(apply_log)-1)
136 item.attrib['value'] = guid
138 def apply_log_pop(self):
139 ''' Pop a GPO guid from the applylog
140 return - last applied GPO guid
142 Removes the GPO guid last added to the list, which is the most recently
145 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
146 apply_log = user_obj.find('applylog')
147 if apply_log is not None:
148 ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
150 apply_log.remove(ret)
151 return ret.attrib['value']
152 if len(apply_log) == 0 and apply_log in user_obj:
153 user_obj.remove(apply_log)
156 def store(self, gp_ext_name, attribute, old_val):
157 ''' Store an attribute in the gp_log
158 param gp_ext_name - Name of the extension applying policy
159 param attribute - The attribute being modified
160 param old_val - The value of the attribute prior to policy
163 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
165 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
166 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
167 assert guid_obj is not None, "gpo guid was not set"
168 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
170 ext = etree.SubElement(guid_obj, 'gp_ext')
171 ext.attrib['name'] = gp_ext_name
172 attr = ext.find('attribute[@name="%s"]' % attribute)
174 attr = etree.SubElement(ext, 'attribute')
175 attr.attrib['name'] = attribute
178 def retrieve(self, gp_ext_name, attribute):
179 ''' Retrieve a stored attribute from the gp_log
180 param gp_ext_name - Name of the extension which applied policy
181 param attribute - The attribute being retrieved
182 return - The value of the attribute prior to policy
185 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
186 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
187 assert guid_obj is not None, "gpo guid was not set"
188 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
190 attr = ext.find('attribute[@name="%s"]' % attribute)
195 def list(self, gp_extensions):
196 ''' Return a list of attributes, their previous values, and functions
198 param gp_extensions - list of extension objects, for retrieving attr to
200 return - list of (attr, value, apply_func) tuples for
203 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
204 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
205 assert guid_obj is not None, "gpo guid was not set"
208 for gp_ext in gp_extensions:
209 data_maps.update(gp_ext.apply_map())
210 exts = guid_obj.findall('gp_ext')
213 attrs = ext.findall('attribute')
216 if attr.attrib['name'] in data_maps[ext.attrib['name']]:
217 func = data_maps[ext.attrib['name']]\
218 [attr.attrib['name']][-1]
220 for dmap in data_maps[ext.attrib['name']].keys():
221 if data_maps[ext.attrib['name']][dmap][0] == \
223 func = data_maps[ext.attrib['name']][dmap][-1]
225 ret.append((attr.attrib['name'], attr.text, func))
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'))
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()
286 class gp_ext(object):
287 __metaclass__ = ABCMeta
289 def __init__(self, logger):
293 def list(self, rootpath):
301 def parse(self, afile, ldb, conn, gp_db, lp):
308 class gp_ext_setter():
309 __metaclass__ = ABCMeta
311 def __init__(self, logger, ldb, gp_db, lp, attribute, val):
314 self.attribute = attribute
322 def update_samba(self):
323 (upd_sam, value) = self.mapper().get(self.attribute)
334 class inf_to_kdc_tdb(gp_ext_setter):
335 def mins_to_hours(self):
336 return '%d' % (int(self.val)/60)
338 def days_to_hours(self):
339 return '%d' % (int(self.val)*24)
341 def set_kdc_tdb(self, val):
342 old_val = self.gp_db.gpostore.get(self.attribute)
343 self.logger.info('%s was changed from %s to %s' % (self.attribute,
346 self.gp_db.gpostore.store(self.attribute, val)
347 self.gp_db.store(str(self), self.attribute, old_val)
349 self.gp_db.gpostore.delete(self.attribute)
350 self.gp_db.delete(str(self), self.attribute)
353 return { 'kdc:user_ticket_lifetime': (self.set_kdc_tdb, self.explicit),
354 'kdc:service_ticket_lifetime': (self.set_kdc_tdb,
356 'kdc:renewal_lifetime': (self.set_kdc_tdb,
361 return 'Kerberos Policy'
363 class inf_to_ldb(gp_ext_setter):
364 '''This class takes the .inf file parameter (essentially a GPO file mapped
365 to a GUID), hashmaps it to the Samba parameter, which then uses an ldb
366 object to update the parameter to Samba4. Not registry oriented whatsoever.
369 def ch_minPwdAge(self, val):
370 old_val = self.ldb.get_minPwdAge()
371 self.logger.info('KDC Minimum Password age was changed from %s to %s' \
373 self.gp_db.store(str(self), self.attribute, old_val)
374 self.ldb.set_minPwdAge(val)
376 def ch_maxPwdAge(self, val):
377 old_val = self.ldb.get_maxPwdAge()
378 self.logger.info('KDC Maximum Password age was changed from %s to %s' \
380 self.gp_db.store(str(self), self.attribute, old_val)
381 self.ldb.set_maxPwdAge(val)
383 def ch_minPwdLength(self, val):
384 old_val = self.ldb.get_minPwdLength()
386 'KDC Minimum Password length was changed from %s to %s' \
388 self.gp_db.store(str(self), self.attribute, old_val)
389 self.ldb.set_minPwdLength(val)
391 def ch_pwdProperties(self, val):
392 old_val = self.ldb.get_pwdProperties()
393 self.logger.info('KDC Password Properties were changed from %s to %s' \
395 self.gp_db.store(str(self), self.attribute, old_val)
396 self.ldb.set_pwdProperties(val)
398 def days2rel_nttime(self):
405 return str(-(val * seconds * minutes * hours * sam_add))
408 '''ldap value : samba setter'''
409 return { "minPwdAge" : (self.ch_minPwdAge, self.days2rel_nttime),
410 "maxPwdAge" : (self.ch_maxPwdAge, self.days2rel_nttime),
411 # Could be none, but I like the method assignment in
413 "minPwdLength" : (self.ch_minPwdLength, self.explicit),
414 "pwdProperties" : (self.ch_pwdProperties, self.explicit),
419 return 'System Access'
422 class gp_sec_ext(gp_ext):
423 '''This class does the following two things:
424 1) Identifies the GPO if it has a certain kind of filepath,
425 2) Finally parses it.
431 return "Security GPO extension"
433 def list(self, rootpath):
434 return os.path.join(rootpath,
435 "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
437 def listmachpol(self, rootpath):
438 return os.path.join(rootpath, "Machine/Registry.pol")
440 def listuserpol(self, rootpath):
441 return os.path.join(rootpath, "User/Registry.pol")
444 return {"System Access": {"MinimumPasswordAge": ("minPwdAge",
446 "MaximumPasswordAge": ("maxPwdAge",
448 "MinimumPasswordLength": ("minPwdLength",
450 "PasswordComplexity": ("pwdProperties",
453 "Kerberos Policy": {"MaxTicketAge": (
454 "kdc:user_ticket_lifetime",
458 "kdc:service_ticket_lifetime",
462 "kdc:renewal_lifetime",
468 def read_inf(self, path, conn):
470 inftable = self.apply_map()
472 policy = conn.loadfile(path.replace('/', '\\'))
473 current_section = None
475 # So here we would declare a boolean,
476 # that would get changed to TRUE.
478 # If at any point in time a GPO was applied,
479 # then we return that boolean at the end.
481 inf_conf = ConfigParser()
482 inf_conf.optionxform=str
484 inf_conf.readfp(StringIO(policy))
486 inf_conf.readfp(StringIO(policy.decode('utf-16')))
488 for section in inf_conf.sections():
489 current_section = inftable.get(section)
490 if not current_section:
492 for key, value in inf_conf.items(section):
493 if current_section.get(key):
494 (att, setter) = current_section.get(key)
495 value = value.encode('ascii', 'ignore')
497 setter(self.logger, self.ldb, self.gp_db, self.lp, att,
498 value).update_samba()
502 def parse(self, afile, ldb, conn, gp_db, lp):
507 # Fixing the bug where only some Linux Boxes capitalize MACHINE
508 if afile.endswith('inf'):
510 blist = afile.split('/')
511 idx = afile.lower().split('/').index('machine')
512 for case in [blist[idx].upper(), blist[idx].capitalize(),
514 bfile = '/'.join(blist[:idx]) + '/' + case + '/' + \
515 '/'.join(blist[idx+1:])
517 return self.read_inf(bfile, conn)
518 except NTSTATUSError:
522 return self.read_inf(afile, conn)