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
28 from samba.net import Net
29 from samba.dcerpc import nbt
31 import samba.gpo as gpo
36 GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
44 ''' Log settings overwritten by gpo apply
45 The gp_log is an xml file that stores a history of gpo changes (and the
46 original setting value).
48 The log is organized like so:
53 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
55 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
56 <gp_ext name="System Access">
57 <attribute name="minPwdAge">-864000000000</attribute>
58 <attribute name="maxPwdAge">-36288000000000</attribute>
59 <attribute name="minPwdLength">7</attribute>
60 <attribute name="pwdProperties">1</attribute>
62 <gp_ext name="Kerberos Policy">
63 <attribute name="ticket_lifetime">1d</attribute>
64 <attribute name="renew_lifetime" />
65 <attribute name="clockskew">300</attribute>
71 Each guid value contains a list of extensions, which contain a list of
72 attributes. The guid value represents a GPO. The attributes are the values
73 of those settings prior to the application of the GPO.
74 The list of guids is enclosed within a user name, which represents the user
75 the settings were applied to. This user may be the samaccountname of the
76 local computer, which implies that these are machine policies.
77 The applylog keeps track of the order in which the GPOs were applied, so
78 that they can be rolled back in reverse, returning the machine to the state
79 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
85 param gpostore - the GPOStorage obj which references the tdb which
87 param db_log - (optional) a string to initialize the gp_log
89 self._state = GPOSTATE.APPLY
90 self.gpostore = gpostore
93 self.gpdb = etree.fromstring(db_log)
95 self.gpdb = etree.Element('gp')
97 user_obj = self.gpdb.find('user[@name="%s"]' % user)
99 user_obj = etree.SubElement(self.gpdb, 'user')
100 user_obj.attrib['name'] = user
102 def state(self, value):
103 ''' Policy application state
104 param value - APPLY, ENFORCE, or UNAPPLY
106 The behavior of the gp_log depends on whether we are applying policy,
107 enforcing policy, or unapplying policy. During an apply, old settings
108 are recorded in the log. During an enforce, settings are being applied
109 but the gp_log does not change. During an unapply, additions to the log
110 should be ignored (since function calls to apply settings are actually
111 reverting policy), but removals from the log are allowed.
113 # If we're enforcing, but we've unapplied, apply instead
114 if value == GPOSTATE.ENFORCE:
115 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
116 apply_log = user_obj.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
130 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
131 obj = user_obj.find('guid[@value="%s"]' % guid)
133 obj = etree.SubElement(user_obj, 'guid')
134 obj.attrib['value'] = guid
135 if self._state == GPOSTATE.APPLY:
136 apply_log = user_obj.find('applylog')
137 if apply_log is None:
138 apply_log = etree.SubElement(user_obj, 'applylog')
139 item = etree.SubElement(apply_log, 'guid')
140 item.attrib['count'] = '%d' % (len(apply_log)-1)
141 item.attrib['value'] = guid
143 def apply_log_pop(self):
144 ''' Pop a GPO guid from the applylog
145 return - last applied GPO guid
147 Removes the GPO guid last added to the list, which is the most recently
150 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
151 apply_log = user_obj.find('applylog')
152 if apply_log is not None:
153 ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
155 apply_log.remove(ret)
156 return ret.attrib['value']
157 if len(apply_log) == 0 and apply_log in user_obj:
158 user_obj.remove(apply_log)
161 def store(self, gp_ext_name, attribute, old_val):
162 ''' Store an attribute in the gp_log
163 param gp_ext_name - Name of the extension applying policy
164 param attribute - The attribute being modified
165 param old_val - The value of the attribute prior to policy
168 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
170 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
171 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
172 assert guid_obj is not None, "gpo guid was not set"
173 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
175 ext = etree.SubElement(guid_obj, 'gp_ext')
176 ext.attrib['name'] = gp_ext_name
177 attr = ext.find('attribute[@name="%s"]' % attribute)
179 attr = etree.SubElement(ext, 'attribute')
180 attr.attrib['name'] = attribute
183 def retrieve(self, gp_ext_name, attribute):
184 ''' Retrieve a stored attribute from the gp_log
185 param gp_ext_name - Name of the extension which applied policy
186 param attribute - The attribute being retrieved
187 return - The value of the attribute prior to policy
190 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
191 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
192 assert guid_obj is not None, "gpo guid was not set"
193 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
195 attr = ext.find('attribute[@name="%s"]' % attribute)
200 def list(self, gp_extensions):
201 ''' Return a list of attributes, their previous values, and functions
203 param gp_extensions - list of extension objects, for retrieving attr to
205 return - list of (attr, value, apply_func) tuples for
208 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
209 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
210 assert guid_obj is not None, "gpo guid was not set"
213 for gp_ext in gp_extensions:
214 data_maps.update(gp_ext.apply_map())
215 exts = guid_obj.findall('gp_ext')
218 attrs = ext.findall('attribute')
221 if attr.attrib['name'] in data_maps[ext.attrib['name']]:
222 func = data_maps[ext.attrib['name']]\
223 [attr.attrib['name']][-1]
225 for dmap in data_maps[ext.attrib['name']].keys():
226 if data_maps[ext.attrib['name']][dmap][0] == \
228 func = data_maps[ext.attrib['name']][dmap][-1]
230 ret.append((attr.attrib['name'], attr.text, func))
233 def delete(self, gp_ext_name, attribute):
234 ''' Remove an attribute from the gp_log
235 param gp_ext_name - name of extension from which to remove the
237 param attribute - attribute to remove
239 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
240 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
241 assert guid_obj is not None, "gpo guid was not set"
242 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
244 attr = ext.find('attribute[@name="%s"]' % attribute)
251 ''' Write gp_log changes to disk '''
252 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
255 def __init__(self, log_file):
256 if os.path.isfile(log_file):
257 self.log = tdb.open(log_file)
259 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
262 self.log.transaction_start()
264 def get_int(self, key):
266 return int(self.log.get(key))
271 return self.log.get(key)
273 def get_gplog(self, user):
274 return gp_log(user, self, self.log.get(user))
276 def store(self, key, val):
277 self.log.store(key, val)
280 self.log.transaction_cancel()
282 def delete(self, key):
286 self.log.transaction_commit()
291 class gp_ext(object):
292 __metaclass__ = ABCMeta
294 def __init__(self, logger):
298 def list(self, rootpath):
306 def read(self, policy):
309 def parse(self, afile, ldb, conn, gp_db, lp):
314 # Fixing the bug where only some Linux Boxes capitalize MACHINE
316 blist = afile.split('/')
317 idx = afile.lower().split('/').index('machine')
320 blist[idx].capitalize(),
323 bfile = '/'.join(blist[:idx]) + '/' + case + '/' + \
324 '/'.join(blist[idx+1:])
326 return self.read(conn.loadfile(bfile.replace('/', '\\')))
327 except NTSTATUSError:
331 return self.read(conn.loadfile(afile.replace('/', '\\')))
332 except Exception as e:
333 self.logger.error(str(e))
340 class gp_ext_setter():
341 __metaclass__ = ABCMeta
343 def __init__(self, logger, ldb, gp_db, lp, attribute, val):
346 self.attribute = attribute
354 def update_samba(self):
355 (upd_sam, value) = self.mapper().get(self.attribute)
366 class inf_to_kdc_tdb(gp_ext_setter):
367 def mins_to_hours(self):
368 return '%d' % (int(self.val)/60)
370 def days_to_hours(self):
371 return '%d' % (int(self.val)*24)
373 def set_kdc_tdb(self, val):
374 old_val = self.gp_db.gpostore.get(self.attribute)
375 self.logger.info('%s was changed from %s to %s' % (self.attribute,
378 self.gp_db.gpostore.store(self.attribute, val)
379 self.gp_db.store(str(self), self.attribute, old_val)
381 self.gp_db.gpostore.delete(self.attribute)
382 self.gp_db.delete(str(self), self.attribute)
385 return { 'kdc:user_ticket_lifetime': (self.set_kdc_tdb, self.explicit),
386 'kdc:service_ticket_lifetime': (self.set_kdc_tdb,
388 'kdc:renewal_lifetime': (self.set_kdc_tdb,
393 return 'Kerberos Policy'
395 class inf_to_ldb(gp_ext_setter):
396 '''This class takes the .inf file parameter (essentially a GPO file mapped
397 to a GUID), hashmaps it to the Samba parameter, which then uses an ldb
398 object to update the parameter to Samba4. Not registry oriented whatsoever.
401 def ch_minPwdAge(self, val):
402 old_val = self.ldb.get_minPwdAge()
403 self.logger.info('KDC Minimum Password age was changed from %s to %s' \
405 self.gp_db.store(str(self), self.attribute, old_val)
406 self.ldb.set_minPwdAge(val)
408 def ch_maxPwdAge(self, val):
409 old_val = self.ldb.get_maxPwdAge()
410 self.logger.info('KDC Maximum Password age was changed from %s to %s' \
412 self.gp_db.store(str(self), self.attribute, old_val)
413 self.ldb.set_maxPwdAge(val)
415 def ch_minPwdLength(self, val):
416 old_val = self.ldb.get_minPwdLength()
418 'KDC Minimum Password length was changed from %s to %s' \
420 self.gp_db.store(str(self), self.attribute, old_val)
421 self.ldb.set_minPwdLength(val)
423 def ch_pwdProperties(self, val):
424 old_val = self.ldb.get_pwdProperties()
425 self.logger.info('KDC Password Properties were changed from %s to %s' \
427 self.gp_db.store(str(self), self.attribute, old_val)
428 self.ldb.set_pwdProperties(val)
430 def days2rel_nttime(self):
437 return str(-(val * seconds * minutes * hours * sam_add))
440 '''ldap value : samba setter'''
441 return { "minPwdAge" : (self.ch_minPwdAge, self.days2rel_nttime),
442 "maxPwdAge" : (self.ch_maxPwdAge, self.days2rel_nttime),
443 # Could be none, but I like the method assignment in
445 "minPwdLength" : (self.ch_minPwdLength, self.explicit),
446 "pwdProperties" : (self.ch_pwdProperties, self.explicit),
451 return 'System Access'
454 class gp_inf_ext(gp_ext):
456 def list(self, rootpath):
463 def read(self, policy):
465 inftable = self.apply_map()
467 current_section = None
469 # So here we would declare a boolean,
470 # that would get changed to TRUE.
472 # If at any point in time a GPO was applied,
473 # then we return that boolean at the end.
475 inf_conf = ConfigParser()
476 inf_conf.optionxform=str
478 inf_conf.readfp(StringIO(policy))
480 inf_conf.readfp(StringIO(policy.decode('utf-16')))
482 for section in inf_conf.sections():
483 current_section = inftable.get(section)
484 if not current_section:
486 for key, value in inf_conf.items(section):
487 if current_section.get(key):
488 (att, setter) = current_section.get(key)
489 value = value.encode('ascii', 'ignore')
491 setter(self.logger, self.ldb, self.gp_db, self.lp, att,
492 value).update_samba()
500 class gp_sec_ext(gp_inf_ext):
501 '''This class does the following two things:
502 1) Identifies the GPO if it has a certain kind of filepath,
503 2) Finally parses it.
509 return "Security GPO extension"
511 def list(self, rootpath):
512 return os.path.join(rootpath,
513 "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
515 def listmachpol(self, rootpath):
516 return os.path.join(rootpath, "Machine/Registry.pol")
518 def listuserpol(self, rootpath):
519 return os.path.join(rootpath, "User/Registry.pol")
522 return {"System Access": {"MinimumPasswordAge": ("minPwdAge",
524 "MaximumPasswordAge": ("maxPwdAge",
526 "MinimumPasswordLength": ("minPwdLength",
528 "PasswordComplexity": ("pwdProperties",
531 "Kerberos Policy": {"MaxTicketAge": (
532 "kdc:user_ticket_lifetime",
536 "kdc:service_ticket_lifetime",
540 "kdc:renewal_lifetime",
546 ''' Fetch the hostname of a writable DC '''
547 def get_dc_hostname(creds, lp):
548 net = Net(creds=creds, lp=lp)
549 cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
551 return cldap_ret.pdc_dns_name
553 ''' Fetch a list of GUIDs for applicable GPOs '''
554 def get_gpo_list(dc_hostname, creds, lp):
556 ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
558 gpos = ads.get_gpo_list(creds.get_username())
561 def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
562 gp_db = store.get_gplog(creds.get_username())
563 dc_hostname = get_dc_hostname(creds, lp)
565 conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds)
567 logger.error('Error connecting to \'%s\' using SMB' % dc_hostname)
569 gpos = get_gpo_list(dc_hostname, creds, lp)
573 if guid == 'Local Policy':
575 path = os.path.join(lp.get('realm').lower(), 'Policies', guid)
576 local_path = os.path.join(lp.get("path", "sysvol"), path)
577 version = int(gpo.gpo_get_sysvol_gpt_version(local_path)[1])
578 if version != store.get_int(guid):
579 logger.info('GPO %s has changed' % guid)
580 gp_db.state(GPOSTATE.APPLY)
582 gp_db.state(GPOSTATE.ENFORCE)
585 for ext in gp_extensions:
587 ext.parse(ext.list(path), test_ldb, conn, gp_db, lp)
588 except Exception as e:
589 logger.error('Failed to parse gpo %s for extension %s' % \
591 logger.error('Message was: ' + str(e))
594 store.store(guid, '%i' % version)
597 def unapply_log(gp_db):
599 item = gp_db.apply_log_pop()
605 def unapply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
606 gp_db = store.get_gplog(creds.get_username())
607 gp_db.state(GPOSTATE.UNAPPLY)
608 for gpo_guid in unapply_log(gp_db):
609 gp_db.set_guid(gpo_guid)
610 unapply_attributes = gp_db.list(gp_extensions)
611 for attr in unapply_attributes:
612 attr_obj = attr[-1](logger, test_ldb, gp_db, lp, attr[0], attr[1])
613 attr_obj.mapper()[attr[0]][0](attr[1]) # Set the old value
614 gp_db.delete(str(attr_obj), attr[0])