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
48 original setting value).
50 The log is organized like so:
55 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
57 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
58 <gp_ext name="System Access">
59 <attribute name="minPwdAge">-864000000000</attribute>
60 <attribute name="maxPwdAge">-36288000000000</attribute>
61 <attribute name="minPwdLength">7</attribute>
62 <attribute name="pwdProperties">1</attribute>
64 <gp_ext name="Kerberos Policy">
65 <attribute name="ticket_lifetime">1d</attribute>
66 <attribute name="renew_lifetime" />
67 <attribute name="clockskew">300</attribute>
73 Each guid value contains a list of extensions, which contain a list of
74 attributes. The guid value represents a GPO. The attributes are the values
75 of those settings prior to the application of the GPO.
76 The list of guids is enclosed within a user name, which represents the user
77 the settings were applied to. This user may be the samaccountname of the
78 local computer, which implies that these are machine policies.
79 The applylog keeps track of the order in which the GPOs were applied, so
80 that they can be rolled back in reverse, returning the machine to the state
81 prior to policy application.
83 def __init__(self, user, gpostore, db_log=None):
84 ''' Initialize the gp_log
85 param user - the username (or machine name) that policies are
87 param gpostore - the GPOStorage obj which references the tdb which
89 param db_log - (optional) a string to initialize the gp_log
91 self._state = GPOSTATE.APPLY
92 self.gpostore = gpostore
95 self.gpdb = etree.fromstring(db_log)
97 self.gpdb = etree.Element('gp')
98 self.user = self.gpdb.find('user[@name="%s"]' % user)
100 self.user = etree.SubElement(self.gpdb, 'user')
101 self.user.attrib['name'] = user
103 def state(self, value):
104 ''' Policy application state
105 param value - APPLY, ENFORCE, or UNAPPLY
107 The behavior of the gp_log depends on whether we are applying policy,
108 enforcing policy, or unapplying policy. During an apply, old settings
109 are recorded in the log. During an enforce, settings are being applied
110 but the gp_log does not change. During an unapply, additions to the log
111 should be ignored (since function calls to apply settings are actually
112 reverting policy), but removals from the log are allowed.
114 # If we're enforcing, but we've unapplied, apply instead
115 if value == GPOSTATE.ENFORCE:
116 apply_log = self.user.find('applylog')
117 if apply_log is None or len(apply_log) == 0:
118 self._state = GPOSTATE.APPLY
124 def set_guid(self, guid):
125 ''' Log to a different GPO guid
126 param guid - guid value of the GPO from which we're applying
129 self.guid = self.user.find('guid[@value="%s"]' % guid)
130 if self.guid is None:
131 self.guid = etree.SubElement(self.user, 'guid')
132 self.guid.attrib['value'] = guid
133 if self._state == GPOSTATE.APPLY:
134 apply_log = self.user.find('applylog')
135 if apply_log is None:
136 apply_log = etree.SubElement(self.user, 'applylog')
137 item = etree.SubElement(apply_log, 'guid')
138 item.attrib['count'] = '%d' % (len(apply_log)-1)
139 item.attrib['value'] = guid
141 def apply_log_pop(self):
142 ''' Pop a GPO guid from the applylog
143 return - last applied GPO guid
145 Removes the GPO guid last added to the list, which is the most recently
148 apply_log = self.user.find('applylog')
149 if apply_log is not None:
150 ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
152 apply_log.remove(ret)
153 return ret.attrib['value']
154 if len(apply_log) == 0 and apply_log in self.user:
155 self.user.remove(apply_log)
158 def store(self, gp_ext_name, attribute, old_val):
159 ''' Store an attribute in the gp_log
160 param gp_ext_name - Name of the extension applying policy
161 param attribute - The attribute being modified
162 param old_val - The value of the attribute prior to policy
165 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
167 assert self.guid is not None, "gpo guid was not set"
168 ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
170 ext = etree.SubElement(self.guid, '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 assert self.guid is not None, "gpo guid was not set"
186 ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
188 attr = ext.find('attribute[@name="%s"]' % attribute)
193 def list(self, gp_extensions):
194 ''' Return a list of attributes, their previous values, and functions
196 param gp_extensions - list of extension objects, for retrieving attr to
198 return - list of (attr, value, apply_func) tuples for
201 assert self.guid is not None, "gpo guid was not set"
204 for gp_ext in gp_extensions:
205 data_maps.update(gp_ext.apply_map())
206 exts = self.guid.findall('gp_ext')
209 ext_map = {val[0]: val[1] for (key, val) in \
210 data_maps[ext.attrib['name']].items()}
211 attrs = ext.findall('attribute')
213 ret.append((attr.attrib['name'], attr.text,
214 ext_map[attr.attrib['name']]))
217 def delete(self, gp_ext_name, attribute):
218 ''' Remove an attribute from the gp_log
219 param gp_ext_name - name of extension from which to remove the
221 param attribute - attribute to remove
223 assert self.guid is not None, "gpo guid was not set"
224 ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
226 attr = ext.find('attribute[@name="%s"]' % attribute)
230 self.guid.remove(ext)
233 ''' Write gp_log changes to disk '''
234 if len(self.guid) == 0 and self.guid in self.user:
235 self.user.remove(self.guid)
236 if len(self.user) == 0 and self.user in self.gpdb:
237 self.gpdb.remove(self.user)
238 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
241 def __init__(self, log_file):
242 if os.path.isfile(log_file):
243 self.log = tdb.open(log_file)
245 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
248 self.log.transaction_start()
250 def get_int(self, key):
252 return int(self.log.get(key))
257 return self.log.get(key)
259 def get_gplog(self, user):
260 return gp_log(user, self, self.log.get(user))
262 def store(self, key, val):
263 self.log.store(key, val)
266 self.log.transaction_cancel()
268 def delete(self, key):
272 self.log.transaction_commit()
277 class gp_ext(object):
278 __metaclass__ = ABCMeta
281 def list(self, rootpath):
289 def parse(self, afile, ldb, conn, gp_db, lp):
297 __metaclass__ = ABCMeta
299 def __init__(self, logger, ldb, gp_db, lp, attribute, val):
302 self.attribute = attribute
310 def update_samba(self):
311 (upd_sam, value) = self.mapper().get(self.attribute)
322 class inf_to_kdc_tdb(inf_to):
323 def mins_to_hours(self):
324 return '%d' % (int(self.val)/60)
326 def days_to_hours(self):
327 return '%d' % (int(self.val)*24)
329 def set_kdc_tdb(self, val):
330 old_val = self.gp_db.gpostore.get(self.attribute)
331 self.logger.info('%s was changed from %s to %s' % (self.attribute,
334 self.gp_db.gpostore.store(self.attribute, val)
335 self.gp_db.store(str(self), self.attribute, old_val)
337 self.gp_db.gpostore.delete(self.attribute)
338 self.gp_db.delete(str(self), self.attribute)
341 return { 'kdc:user_ticket_lifetime': (self.set_kdc_tdb, self.explicit),
342 'kdc:service_ticket_lifetime': (self.set_kdc_tdb,
344 'kdc:renewal_lifetime': (self.set_kdc_tdb,
349 return 'Kerberos Policy'
351 class inf_to_ldb(inf_to):
352 '''This class takes the .inf file parameter (essentially a GPO file mapped
353 to a GUID), hashmaps it to the Samba parameter, which then uses an ldb
354 object to update the parameter to Samba4. Not registry oriented whatsoever.
357 def ch_minPwdAge(self, val):
358 old_val = self.ldb.get_minPwdAge()
359 self.logger.info('KDC Minimum Password age was changed from %s to %s' \
361 self.gp_db.store(str(self), self.attribute, old_val)
362 self.ldb.set_minPwdAge(val)
364 def ch_maxPwdAge(self, val):
365 old_val = self.ldb.get_maxPwdAge()
366 self.logger.info('KDC Maximum Password age was changed from %s to %s' \
368 self.gp_db.store(str(self), self.attribute, old_val)
369 self.ldb.set_maxPwdAge(val)
371 def ch_minPwdLength(self, val):
372 old_val = self.ldb.get_minPwdLength()
374 'KDC Minimum Password length was changed from %s to %s' \
376 self.gp_db.store(str(self), self.attribute, old_val)
377 self.ldb.set_minPwdLength(val)
379 def ch_pwdProperties(self, val):
380 old_val = self.ldb.get_pwdProperties()
381 self.logger.info('KDC Password Properties were changed from %s to %s' \
383 self.gp_db.store(str(self), self.attribute, old_val)
384 self.ldb.set_pwdProperties(val)
386 def days2rel_nttime(self):
393 return str(-(val * seconds * minutes * hours * sam_add))
396 '''ldap value : samba setter'''
397 return { "minPwdAge" : (self.ch_minPwdAge, self.days2rel_nttime),
398 "maxPwdAge" : (self.ch_maxPwdAge, self.days2rel_nttime),
399 # Could be none, but I like the method assignment in
401 "minPwdLength" : (self.ch_minPwdLength, self.explicit),
402 "pwdProperties" : (self.ch_pwdProperties, self.explicit),
407 return 'System Access'
410 class gp_sec_ext(gp_ext):
411 '''This class does the following two things:
412 1) Identifies the GPO if it has a certain kind of filepath,
413 2) Finally parses it.
418 def __init__(self, logger):
422 return "Security GPO extension"
424 def list(self, rootpath):
425 return os.path.join(rootpath,
426 "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
428 def listmachpol(self, rootpath):
429 return os.path.join(rootpath, "Machine/Registry.pol")
431 def listuserpol(self, rootpath):
432 return os.path.join(rootpath, "User/Registry.pol")
435 return {"System Access": {"MinimumPasswordAge": ("minPwdAge",
437 "MaximumPasswordAge": ("maxPwdAge",
439 "MinimumPasswordLength": ("minPwdLength",
441 "PasswordComplexity": ("pwdProperties",
444 "Kerberos Policy": {"MaxTicketAge": (
445 "kdc:user_ticket_lifetime",
449 "kdc:service_ticket_lifetime",
453 "kdc:renewal_lifetime",
459 def read_inf(self, path, conn):
461 inftable = self.apply_map()
463 policy = conn.loadfile(path.replace('/', '\\'))
464 current_section = None
466 # So here we would declare a boolean,
467 # that would get changed to TRUE.
469 # If at any point in time a GPO was applied,
470 # then we return that boolean at the end.
472 inf_conf = ConfigParser()
473 inf_conf.optionxform=str
475 inf_conf.readfp(StringIO(policy))
477 inf_conf.readfp(StringIO(policy.decode('utf-16')))
479 for section in inf_conf.sections():
480 current_section = inftable.get(section)
481 if not current_section:
483 for key, value in inf_conf.items(section):
484 if current_section.get(key):
485 (att, setter) = current_section.get(key)
486 value = value.encode('ascii', 'ignore')
488 setter(self.logger, self.ldb, self.gp_db, self.lp, att,
489 value).update_samba()
493 def parse(self, afile, ldb, conn, gp_db, lp):
498 # Fixing the bug where only some Linux Boxes capitalize MACHINE
499 if afile.endswith('inf'):
501 blist = afile.split('/')
502 idx = afile.lower().split('/').index('machine')
503 for case in [blist[idx].upper(), blist[idx].capitalize(),
505 bfile = '/'.join(blist[:idx]) + '/' + case + '/' + \
506 '/'.join(blist[idx+1:])
508 return self.read_inf(bfile, conn)
509 except NTSTATUSError:
513 return self.read_inf(afile, conn)