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 import samba.gpo as gpo
25 from samba.auth import system_session
26 import samba.getopt as options
27 from samba.samdb import SamDB
28 from samba.netcmd import gpo as gpo_user
30 from samba import NTSTATUSError
31 from ConfigParser import ConfigParser
32 from StringIO import StringIO
33 from abc import ABCMeta, abstractmethod
34 import xml.etree.ElementTree as etree
38 GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
46 ''' Log settings overwritten by gpo apply
47 The gp_log is an xml file that stores a history of gpo changes (and the original setting value).
49 The log is organized like so:
54 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
56 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
57 <gp_ext name="System Access">
58 <attribute name="minPwdAge">-864000000000</attribute>
59 <attribute name="maxPwdAge">-36288000000000</attribute>
60 <attribute name="minPwdLength">7</attribute>
61 <attribute name="pwdProperties">1</attribute>
63 <gp_ext name="Kerberos Policy">
64 <attribute name="ticket_lifetime">1d</attribute>
65 <attribute name="renew_lifetime" />
66 <attribute name="clockskew">300</attribute>
72 Each guid value contains a list of extensions, which contain a list of attributes. The guid value
73 represents a GPO. The attributes are the values of those settings prior to the application of
75 The list of guids is enclosed within a user name, which represents the user the settings were
76 applied to. This user may be the samaccountname of the local computer, which implies that these
78 The applylog keeps track of the order in which the GPOs were applied, so that they can be rolled
79 back in reverse, returning the machine to the state prior to policy application.
81 def __init__(self, user, gpostore, db_log=None):
82 ''' Initialize the gp_log
83 param user - the username (or machine name) that policies are being applied to
84 param gpostore - the GPOStorage obj which references the tdb which contains gp_logs
85 param db_log - (optional) a string to initialize the gp_log
87 self._state = GPOSTATE.APPLY
88 self.gpostore = gpostore
91 self.gpdb = etree.fromstring(db_log)
93 self.gpdb = etree.Element('gp')
94 self.user = self.gpdb.find('user[@name="%s"]' % user)
96 self.user = etree.SubElement(self.gpdb, 'user')
97 self.user.attrib['name'] = user
99 def state(self, value):
100 ''' Policy application state
101 param value - APPLY, ENFORCE, or UNAPPLY
103 The behavior of the gp_log depends on whether we are applying policy, enforcing policy,
104 or unapplying policy. During an apply, old settings are recorded in the log. During an
105 enforce, settings are being applied but the gp_log does not change. During an unapply,
106 additions to the log should be ignored (since function calls to apply settings are actually
107 reverting policy), but removals from the log are allowed.
109 # If we're enforcing, but we've unapplied, apply instead
110 if value == GPOSTATE.ENFORCE:
111 apply_log = self.user.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 policy
123 self.guid = self.user.find('guid[@value="%s"]' % guid)
124 if self.guid is None:
125 self.guid = etree.SubElement(self.user, 'guid')
126 self.guid.attrib['value'] = guid
127 if self._state == GPOSTATE.APPLY:
128 apply_log = self.user.find('applylog')
129 if apply_log is None:
130 apply_log = etree.SubElement(self.user, 'applylog')
131 item = etree.SubElement(apply_log, 'guid')
132 item.attrib['count'] = '%d' % (len(apply_log)-1)
133 item.attrib['value'] = guid
135 def apply_log_pop(self):
136 ''' Pop a GPO guid from the applylog
137 return - last applied GPO guid
139 Removes the GPO guid last added to the list, which is the most recently applied GPO.
141 apply_log = self.user.find('applylog')
142 if apply_log is not None:
143 ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
145 apply_log.remove(ret)
146 return ret.attrib['value']
147 if len(apply_log) == 0 and apply_log in self.user:
148 self.user.remove(apply_log)
151 def store(self, gp_ext_name, attribute, old_val):
152 ''' Store an attribute in the gp_log
153 param gp_ext_name - Name of the extension applying policy
154 param attribute - The attribute being modified
155 param old_val - The value of the attribute prior to policy application
157 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
159 assert self.guid is not None, "gpo guid was not set"
160 ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
162 ext = etree.SubElement(self.guid, 'gp_ext')
163 ext.attrib['name'] = gp_ext_name
164 attr = ext.find('attribute[@name="%s"]' % attribute)
166 attr = etree.SubElement(ext, 'attribute')
167 attr.attrib['name'] = attribute
170 def retrieve(self, gp_ext_name, attribute):
171 ''' Retrieve a stored attribute from the gp_log
172 param gp_ext_name - Name of the extension which applied policy
173 param attribute - The attribute being retrieved
174 return - The value of the attribute prior to policy application
176 assert self.guid is not None, "gpo guid was not set"
177 ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
179 attr = ext.find('attribute[@name="%s"]' % attribute)
184 def list(self, gp_extensions):
185 ''' Return a list of attributes, their previous values, and functions to set them
186 param gp_extensions - list of extension objects, for retrieving attr to func mappings
187 return - list of (attr, value, apply_func) tuples for unapplying policy
189 assert self.guid is not None, "gpo guid was not set"
192 for gp_ext in gp_extensions:
193 data_maps.update(gp_ext.apply_map())
194 exts = self.guid.findall('gp_ext')
197 ext_map = {val[0]: val[1] for (key, val) in data_maps[ext.attrib['name']].items()}
198 attrs = ext.findall('attribute')
200 ret.append((attr.attrib['name'], attr.text, ext_map[attr.attrib['name']]))
203 def delete(self, gp_ext_name, attribute):
204 ''' Remove an attribute from the gp_log
205 param gp_ext_name - name of extension from which to remove the attribute
206 param attribute - attribute to remove
208 assert self.guid is not None, "gpo guid was not set"
209 ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
211 attr = ext.find('attribute[@name="%s"]' % attribute)
215 self.guid.remove(ext)
218 ''' Write gp_log changes to disk '''
219 if len(self.guid) == 0 and self.guid in self.user:
220 self.user.remove(self.guid)
221 if len(self.user) == 0 and self.user in self.gpdb:
222 self.gpdb.remove(self.user)
223 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
226 def __init__(self, log_file):
227 if os.path.isfile(log_file):
228 self.log = tdb.open(log_file)
230 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
233 self.log.transaction_start()
235 def get_int(self, key):
237 return int(self.log.get(key))
242 return self.log.get(key)
244 def get_gplog(self, user):
245 return gp_log(user, self, self.log.get(user))
247 def store(self, key, val):
248 self.log.store(key, val)
251 self.log.transaction_cancel()
253 def delete(self, key):
257 self.log.transaction_commit()
262 class gp_ext(object):
263 __metaclass__ = ABCMeta
266 def list(self, rootpath):
274 def parse(self, afile, ldb, conn, gp_db, lp):
282 __metaclass__ = ABCMeta
284 def __init__(self, logger, ldb, gp_db, lp, attribute, val):
287 self.attribute = attribute
295 def update_samba(self):
296 (upd_sam, value) = self.mapper().get(self.attribute)
307 class inf_to_ldb(inf_to):
308 '''This class takes the .inf file parameter (essentially a GPO file mapped to a GUID),
309 hashmaps it to the Samba parameter, which then uses an ldb object to update the
310 parameter to Samba4. Not registry oriented whatsoever.
313 def ch_minPwdAge(self, val):
314 old_val = self.ldb.get_minPwdAge()
315 self.logger.info('KDC Minimum Password age was changed from %s to %s' % (old_val, val))
316 self.gp_db.store(str(self), self.attribute, old_val)
317 self.ldb.set_minPwdAge(val)
319 def ch_maxPwdAge(self, val):
320 old_val = self.ldb.get_maxPwdAge()
321 self.logger.info('KDC Maximum Password age was changed from %s to %s' % (old_val, val))
322 self.gp_db.store(str(self), self.attribute, old_val)
323 self.ldb.set_maxPwdAge(val)
325 def ch_minPwdLength(self, val):
326 old_val = self.ldb.get_minPwdLength()
327 self.logger.info('KDC Minimum Password length was changed from %s to %s' % (old_val, val))
328 self.gp_db.store(str(self), self.attribute, old_val)
329 self.ldb.set_minPwdLength(val)
331 def ch_pwdProperties(self, val):
332 old_val = self.ldb.get_pwdProperties()
333 self.logger.info('KDC Password Properties were changed from %s to %s' % (old_val, val))
334 self.gp_db.store(str(self), self.attribute, old_val)
335 self.ldb.set_pwdProperties(val)
337 def nttime2unix(self):
344 return str(-(val * seconds * minutes * hours * sam_add))
347 '''ldap value : samba setter'''
348 return { "minPwdAge" : (self.ch_minPwdAge, self.nttime2unix),
349 "maxPwdAge" : (self.ch_maxPwdAge, self.nttime2unix),
350 # Could be none, but I like the method assignment in update_samba
351 "minPwdLength" : (self.ch_minPwdLength, self.explicit),
352 "pwdProperties" : (self.ch_pwdProperties, self.explicit),
357 return 'System Access'
360 class gp_sec_ext(gp_ext):
361 '''This class does the following two things:
362 1) Identifies the GPO if it has a certain kind of filepath,
363 2) Finally parses it.
368 def __init__(self, logger):
372 return "Security GPO extension"
374 def list(self, rootpath):
375 return os.path.join(rootpath, "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
377 def listmachpol(self, rootpath):
378 return os.path.join(rootpath, "Machine/Registry.pol")
380 def listuserpol(self, rootpath):
381 return os.path.join(rootpath, "User/Registry.pol")
384 return {"System Access": {"MinimumPasswordAge": ("minPwdAge", inf_to_ldb),
385 "MaximumPasswordAge": ("maxPwdAge", inf_to_ldb),
386 "MinimumPasswordLength": ("minPwdLength", inf_to_ldb),
387 "PasswordComplexity": ("pwdProperties", inf_to_ldb),
391 def read_inf(self, path, conn):
393 inftable = self.apply_map()
395 policy = conn.loadfile(path.replace('/', '\\'))
396 current_section = None
398 # So here we would declare a boolean,
399 # that would get changed to TRUE.
401 # If at any point in time a GPO was applied,
402 # then we return that boolean at the end.
404 inf_conf = ConfigParser()
405 inf_conf.optionxform=str
407 inf_conf.readfp(StringIO(policy))
409 inf_conf.readfp(StringIO(policy.decode('utf-16')))
411 for section in inf_conf.sections():
412 current_section = inftable.get(section)
413 if not current_section:
415 for key, value in inf_conf.items(section):
416 if current_section.get(key):
417 (att, setter) = current_section.get(key)
418 value = value.encode('ascii', 'ignore')
420 setter(self.logger, self.ldb, self.gp_db, self.lp, att, value).update_samba()
424 def parse(self, afile, ldb, conn, gp_db, lp):
429 # Fixing the bug where only some Linux Boxes capitalize MACHINE
430 if afile.endswith('inf'):
432 blist = afile.split('/')
433 idx = afile.lower().split('/').index('machine')
434 for case in [blist[idx].upper(), blist[idx].capitalize(), blist[idx].lower()]:
435 bfile = '/'.join(blist[:idx]) + '/' + case + '/' + '/'.join(blist[idx+1:])
437 return self.read_inf(bfile, conn)
438 except NTSTATUSError:
442 return self.read_inf(afile, conn)