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 read(self, policy):
304 def parse(self, afile, ldb, conn, gp_db, lp):
309 # Fixing the bug where only some Linux Boxes capitalize MACHINE
311 blist = afile.split('/')
312 idx = afile.lower().split('/').index('machine')
315 blist[idx].capitalize(),
318 bfile = '/'.join(blist[:idx]) + '/' + case + '/' + \
319 '/'.join(blist[idx+1:])
321 return self.read(conn.loadfile(bfile.replace('/', '\\')))
322 except NTSTATUSError:
326 return self.read(conn.loadfile(afile.replace('/', '\\')))
327 except Exception as e:
328 self.logger.error(str(e))
335 class gp_ext_setter():
336 __metaclass__ = ABCMeta
338 def __init__(self, logger, ldb, gp_db, lp, attribute, val):
341 self.attribute = attribute
349 def update_samba(self):
350 (upd_sam, value) = self.mapper().get(self.attribute)
361 class inf_to_kdc_tdb(gp_ext_setter):
362 def mins_to_hours(self):
363 return '%d' % (int(self.val)/60)
365 def days_to_hours(self):
366 return '%d' % (int(self.val)*24)
368 def set_kdc_tdb(self, val):
369 old_val = self.gp_db.gpostore.get(self.attribute)
370 self.logger.info('%s was changed from %s to %s' % (self.attribute,
373 self.gp_db.gpostore.store(self.attribute, val)
374 self.gp_db.store(str(self), self.attribute, old_val)
376 self.gp_db.gpostore.delete(self.attribute)
377 self.gp_db.delete(str(self), self.attribute)
380 return { 'kdc:user_ticket_lifetime': (self.set_kdc_tdb, self.explicit),
381 'kdc:service_ticket_lifetime': (self.set_kdc_tdb,
383 'kdc:renewal_lifetime': (self.set_kdc_tdb,
388 return 'Kerberos Policy'
390 class inf_to_ldb(gp_ext_setter):
391 '''This class takes the .inf file parameter (essentially a GPO file mapped
392 to a GUID), hashmaps it to the Samba parameter, which then uses an ldb
393 object to update the parameter to Samba4. Not registry oriented whatsoever.
396 def ch_minPwdAge(self, val):
397 old_val = self.ldb.get_minPwdAge()
398 self.logger.info('KDC Minimum Password age was changed from %s to %s' \
400 self.gp_db.store(str(self), self.attribute, old_val)
401 self.ldb.set_minPwdAge(val)
403 def ch_maxPwdAge(self, val):
404 old_val = self.ldb.get_maxPwdAge()
405 self.logger.info('KDC Maximum Password age was changed from %s to %s' \
407 self.gp_db.store(str(self), self.attribute, old_val)
408 self.ldb.set_maxPwdAge(val)
410 def ch_minPwdLength(self, val):
411 old_val = self.ldb.get_minPwdLength()
413 'KDC Minimum Password length was changed from %s to %s' \
415 self.gp_db.store(str(self), self.attribute, old_val)
416 self.ldb.set_minPwdLength(val)
418 def ch_pwdProperties(self, val):
419 old_val = self.ldb.get_pwdProperties()
420 self.logger.info('KDC Password Properties were changed from %s to %s' \
422 self.gp_db.store(str(self), self.attribute, old_val)
423 self.ldb.set_pwdProperties(val)
425 def days2rel_nttime(self):
432 return str(-(val * seconds * minutes * hours * sam_add))
435 '''ldap value : samba setter'''
436 return { "minPwdAge" : (self.ch_minPwdAge, self.days2rel_nttime),
437 "maxPwdAge" : (self.ch_maxPwdAge, self.days2rel_nttime),
438 # Could be none, but I like the method assignment in
440 "minPwdLength" : (self.ch_minPwdLength, self.explicit),
441 "pwdProperties" : (self.ch_pwdProperties, self.explicit),
446 return 'System Access'
449 class gp_inf_ext(gp_ext):
451 def list(self, rootpath):
458 def read(self, policy):
460 inftable = self.apply_map()
462 current_section = None
464 # So here we would declare a boolean,
465 # that would get changed to TRUE.
467 # If at any point in time a GPO was applied,
468 # then we return that boolean at the end.
470 inf_conf = ConfigParser()
471 inf_conf.optionxform=str
473 inf_conf.readfp(StringIO(policy))
475 inf_conf.readfp(StringIO(policy.decode('utf-16')))
477 for section in inf_conf.sections():
478 current_section = inftable.get(section)
479 if not current_section:
481 for key, value in inf_conf.items(section):
482 if current_section.get(key):
483 (att, setter) = current_section.get(key)
484 value = value.encode('ascii', 'ignore')
486 setter(self.logger, self.ldb, self.gp_db, self.lp, att,
487 value).update_samba()
495 class gp_sec_ext(gp_inf_ext):
496 '''This class does the following two things:
497 1) Identifies the GPO if it has a certain kind of filepath,
498 2) Finally parses it.
504 return "Security GPO extension"
506 def list(self, rootpath):
507 return os.path.join(rootpath,
508 "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
510 def listmachpol(self, rootpath):
511 return os.path.join(rootpath, "Machine/Registry.pol")
513 def listuserpol(self, rootpath):
514 return os.path.join(rootpath, "User/Registry.pol")
517 return {"System Access": {"MinimumPasswordAge": ("minPwdAge",
519 "MaximumPasswordAge": ("maxPwdAge",
521 "MinimumPasswordLength": ("minPwdLength",
523 "PasswordComplexity": ("pwdProperties",
526 "Kerberos Policy": {"MaxTicketAge": (
527 "kdc:user_ticket_lifetime",
531 "kdc:service_ticket_lifetime",
535 "kdc:renewal_lifetime",