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')
99 user_obj = self.gpdb.find('user[@name="%s"]' % user)
101 user_obj = etree.SubElement(self.gpdb, 'user')
102 user_obj.attrib['name'] = user
104 def state(self, value):
105 ''' Policy application state
106 param value - APPLY, ENFORCE, or UNAPPLY
108 The behavior of the gp_log depends on whether we are applying policy,
109 enforcing policy, or unapplying policy. During an apply, old settings
110 are recorded in the log. During an enforce, settings are being applied
111 but the gp_log does not change. During an unapply, additions to the log
112 should be ignored (since function calls to apply settings are actually
113 reverting policy), but removals from the log are allowed.
115 # If we're enforcing, but we've unapplied, apply instead
116 if value == GPOSTATE.ENFORCE:
117 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
118 apply_log = user_obj.find('applylog')
119 if apply_log is None or len(apply_log) == 0:
120 self._state = GPOSTATE.APPLY
126 def set_guid(self, guid):
127 ''' Log to a different GPO guid
128 param guid - guid value of the GPO from which we're applying
132 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
133 obj = user_obj.find('guid[@value="%s"]' % guid)
135 obj = etree.SubElement(user_obj, 'guid')
136 obj.attrib['value'] = guid
137 if self._state == GPOSTATE.APPLY:
138 apply_log = user_obj.find('applylog')
139 if apply_log is None:
140 apply_log = etree.SubElement(user_obj, 'applylog')
141 item = etree.SubElement(apply_log, 'guid')
142 item.attrib['count'] = '%d' % (len(apply_log)-1)
143 item.attrib['value'] = guid
145 def apply_log_pop(self):
146 ''' Pop a GPO guid from the applylog
147 return - last applied GPO guid
149 Removes the GPO guid last added to the list, which is the most recently
152 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
153 apply_log = user_obj.find('applylog')
154 if apply_log is not None:
155 ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
157 apply_log.remove(ret)
158 return ret.attrib['value']
159 if len(apply_log) == 0 and apply_log in user_obj:
160 user_obj.remove(apply_log)
163 def store(self, gp_ext_name, attribute, old_val):
164 ''' Store an attribute in the gp_log
165 param gp_ext_name - Name of the extension applying policy
166 param attribute - The attribute being modified
167 param old_val - The value of the attribute prior to policy
170 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
172 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
173 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
174 assert guid_obj is not None, "gpo guid was not set"
175 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
177 ext = etree.SubElement(guid_obj, 'gp_ext')
178 ext.attrib['name'] = gp_ext_name
179 attr = ext.find('attribute[@name="%s"]' % attribute)
181 attr = etree.SubElement(ext, 'attribute')
182 attr.attrib['name'] = attribute
185 def retrieve(self, gp_ext_name, attribute):
186 ''' Retrieve a stored attribute from the gp_log
187 param gp_ext_name - Name of the extension which applied policy
188 param attribute - The attribute being retrieved
189 return - The value of the attribute prior to policy
192 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
193 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
194 assert guid_obj is not None, "gpo guid was not set"
195 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
197 attr = ext.find('attribute[@name="%s"]' % attribute)
202 def list(self, gp_extensions):
203 ''' Return a list of attributes, their previous values, and functions
205 param gp_extensions - list of extension objects, for retrieving attr to
207 return - list of (attr, value, apply_func) tuples for
210 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
211 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
212 assert guid_obj is not None, "gpo guid was not set"
215 for gp_ext in gp_extensions:
216 data_maps.update(gp_ext.apply_map())
217 exts = guid_obj.findall('gp_ext')
220 attrs = ext.findall('attribute')
223 if attr.attrib['name'] in data_maps[ext.attrib['name']]:
224 func = data_maps[ext.attrib['name']]\
225 [attr.attrib['name']][-1]
227 for dmap in data_maps[ext.attrib['name']].keys():
228 if data_maps[ext.attrib['name']][dmap][0] == \
230 func = data_maps[ext.attrib['name']][dmap][-1]
232 ret.append((attr.attrib['name'], attr.text, func))
235 def delete(self, gp_ext_name, attribute):
236 ''' Remove an attribute from the gp_log
237 param gp_ext_name - name of extension from which to remove the
239 param attribute - attribute to remove
241 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
242 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
243 assert guid_obj is not None, "gpo guid was not set"
244 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
246 attr = ext.find('attribute[@name="%s"]' % attribute)
253 ''' Write gp_log changes to disk '''
254 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
257 def __init__(self, log_file):
258 if os.path.isfile(log_file):
259 self.log = tdb.open(log_file)
261 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
264 self.log.transaction_start()
266 def get_int(self, key):
268 return int(self.log.get(key))
273 return self.log.get(key)
275 def get_gplog(self, user):
276 return gp_log(user, self, self.log.get(user))
278 def store(self, key, val):
279 self.log.store(key, val)
282 self.log.transaction_cancel()
284 def delete(self, key):
288 self.log.transaction_commit()
293 class gp_ext(object):
294 __metaclass__ = ABCMeta
297 def list(self, rootpath):
305 def parse(self, afile, ldb, conn, gp_db, lp):
313 __metaclass__ = ABCMeta
315 def __init__(self, logger, ldb, gp_db, lp, attribute, val):
318 self.attribute = attribute
326 def update_samba(self):
327 (upd_sam, value) = self.mapper().get(self.attribute)
338 class inf_to_kdc_tdb(inf_to):
339 def mins_to_hours(self):
340 return '%d' % (int(self.val)/60)
342 def days_to_hours(self):
343 return '%d' % (int(self.val)*24)
345 def set_kdc_tdb(self, val):
346 old_val = self.gp_db.gpostore.get(self.attribute)
347 self.logger.info('%s was changed from %s to %s' % (self.attribute,
350 self.gp_db.gpostore.store(self.attribute, val)
351 self.gp_db.store(str(self), self.attribute, old_val)
353 self.gp_db.gpostore.delete(self.attribute)
354 self.gp_db.delete(str(self), self.attribute)
357 return { 'kdc:user_ticket_lifetime': (self.set_kdc_tdb, self.explicit),
358 'kdc:service_ticket_lifetime': (self.set_kdc_tdb,
360 'kdc:renewal_lifetime': (self.set_kdc_tdb,
365 return 'Kerberos Policy'
367 class inf_to_ldb(inf_to):
368 '''This class takes the .inf file parameter (essentially a GPO file mapped
369 to a GUID), hashmaps it to the Samba parameter, which then uses an ldb
370 object to update the parameter to Samba4. Not registry oriented whatsoever.
373 def ch_minPwdAge(self, val):
374 old_val = self.ldb.get_minPwdAge()
375 self.logger.info('KDC Minimum Password age was changed from %s to %s' \
377 self.gp_db.store(str(self), self.attribute, old_val)
378 self.ldb.set_minPwdAge(val)
380 def ch_maxPwdAge(self, val):
381 old_val = self.ldb.get_maxPwdAge()
382 self.logger.info('KDC Maximum Password age was changed from %s to %s' \
384 self.gp_db.store(str(self), self.attribute, old_val)
385 self.ldb.set_maxPwdAge(val)
387 def ch_minPwdLength(self, val):
388 old_val = self.ldb.get_minPwdLength()
390 'KDC Minimum Password length was changed from %s to %s' \
392 self.gp_db.store(str(self), self.attribute, old_val)
393 self.ldb.set_minPwdLength(val)
395 def ch_pwdProperties(self, val):
396 old_val = self.ldb.get_pwdProperties()
397 self.logger.info('KDC Password Properties were changed from %s to %s' \
399 self.gp_db.store(str(self), self.attribute, old_val)
400 self.ldb.set_pwdProperties(val)
402 def days2rel_nttime(self):
409 return str(-(val * seconds * minutes * hours * sam_add))
412 '''ldap value : samba setter'''
413 return { "minPwdAge" : (self.ch_minPwdAge, self.days2rel_nttime),
414 "maxPwdAge" : (self.ch_maxPwdAge, self.days2rel_nttime),
415 # Could be none, but I like the method assignment in
417 "minPwdLength" : (self.ch_minPwdLength, self.explicit),
418 "pwdProperties" : (self.ch_pwdProperties, self.explicit),
423 return 'System Access'
426 class gp_sec_ext(gp_ext):
427 '''This class does the following two things:
428 1) Identifies the GPO if it has a certain kind of filepath,
429 2) Finally parses it.
434 def __init__(self, logger):
438 return "Security GPO extension"
440 def list(self, rootpath):
441 return os.path.join(rootpath,
442 "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
444 def listmachpol(self, rootpath):
445 return os.path.join(rootpath, "Machine/Registry.pol")
447 def listuserpol(self, rootpath):
448 return os.path.join(rootpath, "User/Registry.pol")
451 return {"System Access": {"MinimumPasswordAge": ("minPwdAge",
453 "MaximumPasswordAge": ("maxPwdAge",
455 "MinimumPasswordLength": ("minPwdLength",
457 "PasswordComplexity": ("pwdProperties",
460 "Kerberos Policy": {"MaxTicketAge": (
461 "kdc:user_ticket_lifetime",
465 "kdc:service_ticket_lifetime",
469 "kdc:renewal_lifetime",
475 def read_inf(self, path, conn):
477 inftable = self.apply_map()
479 policy = conn.loadfile(path.replace('/', '\\'))
480 current_section = None
482 # So here we would declare a boolean,
483 # that would get changed to TRUE.
485 # If at any point in time a GPO was applied,
486 # then we return that boolean at the end.
488 inf_conf = ConfigParser()
489 inf_conf.optionxform=str
491 inf_conf.readfp(StringIO(policy))
493 inf_conf.readfp(StringIO(policy.decode('utf-16')))
495 for section in inf_conf.sections():
496 current_section = inftable.get(section)
497 if not current_section:
499 for key, value in inf_conf.items(section):
500 if current_section.get(key):
501 (att, setter) = current_section.get(key)
502 value = value.encode('ascii', 'ignore')
504 setter(self.logger, self.ldb, self.gp_db, self.lp, att,
505 value).update_samba()
509 def parse(self, afile, ldb, conn, gp_db, lp):
514 # Fixing the bug where only some Linux Boxes capitalize MACHINE
515 if afile.endswith('inf'):
517 blist = afile.split('/')
518 idx = afile.lower().split('/').index('machine')
519 for case in [blist[idx].upper(), blist[idx].capitalize(),
521 bfile = '/'.join(blist[:idx]) + '/' + case + '/' + \
522 '/'.join(blist[idx+1:])
524 return self.read_inf(bfile, conn)
525 except NTSTATUSError:
529 return self.read_inf(afile, conn)