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
290 def list(self
, rootpath
):
298 def parse(self
, afile
, ldb
, conn
, gp_db
, lp
):
306 __metaclass__
= ABCMeta
308 def __init__(self
, logger
, ldb
, gp_db
, lp
, attribute
, val
):
311 self
.attribute
= attribute
319 def update_samba(self
):
320 (upd_sam
, value
) = self
.mapper().get(self
.attribute
)
331 class inf_to_kdc_tdb(inf_to
):
332 def mins_to_hours(self
):
333 return '%d' % (int(self
.val
)/60)
335 def days_to_hours(self
):
336 return '%d' % (int(self
.val
)*24)
338 def set_kdc_tdb(self
, val
):
339 old_val
= self
.gp_db
.gpostore
.get(self
.attribute
)
340 self
.logger
.info('%s was changed from %s to %s' % (self
.attribute
,
343 self
.gp_db
.gpostore
.store(self
.attribute
, val
)
344 self
.gp_db
.store(str(self
), self
.attribute
, old_val
)
346 self
.gp_db
.gpostore
.delete(self
.attribute
)
347 self
.gp_db
.delete(str(self
), self
.attribute
)
350 return { 'kdc:user_ticket_lifetime': (self
.set_kdc_tdb
, self
.explicit
),
351 'kdc:service_ticket_lifetime': (self
.set_kdc_tdb
,
353 'kdc:renewal_lifetime': (self
.set_kdc_tdb
,
358 return 'Kerberos Policy'
360 class inf_to_ldb(inf_to
):
361 '''This class takes the .inf file parameter (essentially a GPO file mapped
362 to a GUID), hashmaps it to the Samba parameter, which then uses an ldb
363 object to update the parameter to Samba4. Not registry oriented whatsoever.
366 def ch_minPwdAge(self
, val
):
367 old_val
= self
.ldb
.get_minPwdAge()
368 self
.logger
.info('KDC Minimum Password age was changed from %s to %s' \
370 self
.gp_db
.store(str(self
), self
.attribute
, old_val
)
371 self
.ldb
.set_minPwdAge(val
)
373 def ch_maxPwdAge(self
, val
):
374 old_val
= self
.ldb
.get_maxPwdAge()
375 self
.logger
.info('KDC Maximum Password age was changed from %s to %s' \
377 self
.gp_db
.store(str(self
), self
.attribute
, old_val
)
378 self
.ldb
.set_maxPwdAge(val
)
380 def ch_minPwdLength(self
, val
):
381 old_val
= self
.ldb
.get_minPwdLength()
383 'KDC Minimum Password length was changed from %s to %s' \
385 self
.gp_db
.store(str(self
), self
.attribute
, old_val
)
386 self
.ldb
.set_minPwdLength(val
)
388 def ch_pwdProperties(self
, val
):
389 old_val
= self
.ldb
.get_pwdProperties()
390 self
.logger
.info('KDC Password Properties were changed from %s to %s' \
392 self
.gp_db
.store(str(self
), self
.attribute
, old_val
)
393 self
.ldb
.set_pwdProperties(val
)
395 def days2rel_nttime(self
):
402 return str(-(val
* seconds
* minutes
* hours
* sam_add
))
405 '''ldap value : samba setter'''
406 return { "minPwdAge" : (self
.ch_minPwdAge
, self
.days2rel_nttime
),
407 "maxPwdAge" : (self
.ch_maxPwdAge
, self
.days2rel_nttime
),
408 # Could be none, but I like the method assignment in
410 "minPwdLength" : (self
.ch_minPwdLength
, self
.explicit
),
411 "pwdProperties" : (self
.ch_pwdProperties
, self
.explicit
),
416 return 'System Access'
419 class gp_sec_ext(gp_ext
):
420 '''This class does the following two things:
421 1) Identifies the GPO if it has a certain kind of filepath,
422 2) Finally parses it.
427 def __init__(self
, logger
):
431 return "Security GPO extension"
433 def list(self
, rootpath
):
434 return os
.path
.join(rootpath
,
435 "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
437 def listmachpol(self
, rootpath
):
438 return os
.path
.join(rootpath
, "Machine/Registry.pol")
440 def listuserpol(self
, rootpath
):
441 return os
.path
.join(rootpath
, "User/Registry.pol")
444 return {"System Access": {"MinimumPasswordAge": ("minPwdAge",
446 "MaximumPasswordAge": ("maxPwdAge",
448 "MinimumPasswordLength": ("minPwdLength",
450 "PasswordComplexity": ("pwdProperties",
453 "Kerberos Policy": {"MaxTicketAge": (
454 "kdc:user_ticket_lifetime",
458 "kdc:service_ticket_lifetime",
462 "kdc:renewal_lifetime",
468 def read_inf(self
, path
, conn
):
470 inftable
= self
.apply_map()
472 policy
= conn
.loadfile(path
.replace('/', '\\'))
473 current_section
= None
475 # So here we would declare a boolean,
476 # that would get changed to TRUE.
478 # If at any point in time a GPO was applied,
479 # then we return that boolean at the end.
481 inf_conf
= ConfigParser()
482 inf_conf
.optionxform
=str
484 inf_conf
.readfp(StringIO(policy
))
486 inf_conf
.readfp(StringIO(policy
.decode('utf-16')))
488 for section
in inf_conf
.sections():
489 current_section
= inftable
.get(section
)
490 if not current_section
:
492 for key
, value
in inf_conf
.items(section
):
493 if current_section
.get(key
):
494 (att
, setter
) = current_section
.get(key
)
495 value
= value
.encode('ascii', 'ignore')
497 setter(self
.logger
, self
.ldb
, self
.gp_db
, self
.lp
, att
,
498 value
).update_samba()
502 def parse(self
, afile
, ldb
, conn
, gp_db
, lp
):
507 # Fixing the bug where only some Linux Boxes capitalize MACHINE
508 if afile
.endswith('inf'):
510 blist
= afile
.split('/')
511 idx
= afile
.lower().split('/').index('machine')
512 for case
in [blist
[idx
].upper(), blist
[idx
].capitalize(),
514 bfile
= '/'.join(blist
[:idx
]) + '/' + case
+ '/' + \
515 '/'.join(blist
[idx
+1:])
517 return self
.read_inf(bfile
, conn
)
518 except NTSTATUSError
:
522 return self
.read_inf(afile
, conn
)