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/>.
22 sys
.path
.insert(0, "bin/python")
23 from samba
import NTSTATUSError
24 from ConfigParser
import ConfigParser
25 from samba
.compat
import StringIO
26 from abc
import ABCMeta
, abstractmethod
27 import xml
.etree
.ElementTree
as etree
29 from samba
.net
import Net
30 from samba
.dcerpc
import nbt
32 import samba
.gpo
as gpo
33 from samba
.param
import LoadParm
35 from tempfile
import NamedTemporaryFile
39 GPOSTATE
= Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
48 ''' Log settings overwritten by gpo apply
49 The gp_log is an xml file that stores a history of gpo changes (and the
50 original setting value).
52 The log is organized like so:
57 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
59 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
60 <gp_ext name="System Access">
61 <attribute name="minPwdAge">-864000000000</attribute>
62 <attribute name="maxPwdAge">-36288000000000</attribute>
63 <attribute name="minPwdLength">7</attribute>
64 <attribute name="pwdProperties">1</attribute>
66 <gp_ext name="Kerberos Policy">
67 <attribute name="ticket_lifetime">1d</attribute>
68 <attribute name="renew_lifetime" />
69 <attribute name="clockskew">300</attribute>
75 Each guid value contains a list of extensions, which contain a list of
76 attributes. The guid value represents a GPO. The attributes are the values
77 of those settings prior to the application of the GPO.
78 The list of guids is enclosed within a user name, which represents the user
79 the settings were applied to. This user may be the samaccountname of the
80 local computer, which implies that these are machine policies.
81 The applylog keeps track of the order in which the GPOs were applied, so
82 that they can be rolled back in reverse, returning the machine to the state
83 prior to policy application.
85 def __init__(self
, user
, gpostore
, db_log
=None):
86 ''' Initialize the gp_log
87 param user - the username (or machine name) that policies are
89 param gpostore - the GPOStorage obj which references the tdb which
91 param db_log - (optional) a string to initialize the gp_log
93 self
._state
= GPOSTATE
.APPLY
94 self
.gpostore
= gpostore
97 self
.gpdb
= etree
.fromstring(db_log
)
99 self
.gpdb
= etree
.Element('gp')
101 user_obj
= self
.gpdb
.find('user[@name="%s"]' % user
)
103 user_obj
= etree
.SubElement(self
.gpdb
, 'user')
104 user_obj
.attrib
['name'] = user
106 def state(self
, value
):
107 ''' Policy application state
108 param value - APPLY, ENFORCE, or UNAPPLY
110 The behavior of the gp_log depends on whether we are applying policy,
111 enforcing policy, or unapplying policy. During an apply, old settings
112 are recorded in the log. During an enforce, settings are being applied
113 but the gp_log does not change. During an unapply, additions to the log
114 should be ignored (since function calls to apply settings are actually
115 reverting policy), but removals from the log are allowed.
117 # If we're enforcing, but we've unapplied, apply instead
118 if value
== GPOSTATE
.ENFORCE
:
119 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
120 apply_log
= user_obj
.find('applylog')
121 if apply_log
is None or len(apply_log
) == 0:
122 self
._state
= GPOSTATE
.APPLY
128 def set_guid(self
, guid
):
129 ''' Log to a different GPO guid
130 param guid - guid value of the GPO from which we're applying
134 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
135 obj
= user_obj
.find('guid[@value="%s"]' % guid
)
137 obj
= etree
.SubElement(user_obj
, 'guid')
138 obj
.attrib
['value'] = guid
139 if self
._state
== GPOSTATE
.APPLY
:
140 apply_log
= user_obj
.find('applylog')
141 if apply_log
is None:
142 apply_log
= etree
.SubElement(user_obj
, 'applylog')
143 prev
= apply_log
.find('guid[@value="%s"]' % guid
)
145 item
= etree
.SubElement(apply_log
, 'guid')
146 item
.attrib
['count'] = '%d' % (len(apply_log
) - 1)
147 item
.attrib
['value'] = guid
149 def apply_log_pop(self
):
150 ''' Pop a GPO guid from the applylog
151 return - last applied GPO guid
153 Removes the GPO guid last added to the list, which is the most recently
156 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
157 apply_log
= user_obj
.find('applylog')
158 if apply_log
is not None:
159 ret
= apply_log
.find('guid[@count="%d"]' % (len(apply_log
) - 1))
161 apply_log
.remove(ret
)
162 return ret
.attrib
['value']
163 if len(apply_log
) == 0 and apply_log
in user_obj
:
164 user_obj
.remove(apply_log
)
167 def store(self
, gp_ext_name
, attribute
, old_val
):
168 ''' Store an attribute in the gp_log
169 param gp_ext_name - Name of the extension applying policy
170 param attribute - The attribute being modified
171 param old_val - The value of the attribute prior to policy
174 if self
._state
== GPOSTATE
.UNAPPLY
or self
._state
== GPOSTATE
.ENFORCE
:
176 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
177 guid_obj
= user_obj
.find('guid[@value="%s"]' % self
.guid
)
178 assert guid_obj
is not None, "gpo guid was not set"
179 ext
= guid_obj
.find('gp_ext[@name="%s"]' % gp_ext_name
)
181 ext
= etree
.SubElement(guid_obj
, 'gp_ext')
182 ext
.attrib
['name'] = gp_ext_name
183 attr
= ext
.find('attribute[@name="%s"]' % attribute
)
185 attr
= etree
.SubElement(ext
, 'attribute')
186 attr
.attrib
['name'] = attribute
189 def retrieve(self
, gp_ext_name
, attribute
):
190 ''' Retrieve a stored attribute from the gp_log
191 param gp_ext_name - Name of the extension which applied policy
192 param attribute - The attribute being retrieved
193 return - The value of the attribute prior to policy
196 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
197 guid_obj
= user_obj
.find('guid[@value="%s"]' % self
.guid
)
198 assert guid_obj
is not None, "gpo guid was not set"
199 ext
= guid_obj
.find('gp_ext[@name="%s"]' % gp_ext_name
)
201 attr
= ext
.find('attribute[@name="%s"]' % attribute
)
206 def list(self
, gp_extensions
):
207 ''' Return a list of attributes, their previous values, and functions
209 param gp_extensions - list of extension objects, for retrieving attr to
211 return - list of (attr, value, apply_func) tuples for
214 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
215 guid_obj
= user_obj
.find('guid[@value="%s"]' % self
.guid
)
216 assert guid_obj
is not None, "gpo guid was not set"
219 for gp_ext
in gp_extensions
:
220 data_maps
.update(gp_ext
.apply_map())
221 exts
= guid_obj
.findall('gp_ext')
224 attrs
= ext
.findall('attribute')
227 if attr
.attrib
['name'] in data_maps
[ext
.attrib
['name']]:
228 func
= data_maps
[ext
.attrib
['name']][attr
.attrib
['name']][-1]
230 for dmap
in data_maps
[ext
.attrib
['name']].keys():
231 if data_maps
[ext
.attrib
['name']][dmap
][0] == \
233 func
= data_maps
[ext
.attrib
['name']][dmap
][-1]
235 ret
.append((attr
.attrib
['name'], attr
.text
, func
))
238 def delete(self
, gp_ext_name
, attribute
):
239 ''' Remove an attribute from the gp_log
240 param gp_ext_name - name of extension from which to remove the
242 param attribute - attribute to remove
244 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
245 guid_obj
= user_obj
.find('guid[@value="%s"]' % self
.guid
)
246 assert guid_obj
is not None, "gpo guid was not set"
247 ext
= guid_obj
.find('gp_ext[@name="%s"]' % gp_ext_name
)
249 attr
= ext
.find('attribute[@name="%s"]' % attribute
)
256 ''' Write gp_log changes to disk '''
257 self
.gpostore
.store(self
.username
, etree
.tostring(self
.gpdb
, 'utf-8'))
261 def __init__(self
, log_file
):
262 if os
.path
.isfile(log_file
):
263 self
.log
= tdb
.open(log_file
)
265 self
.log
= tdb
.Tdb(log_file
, 0, tdb
.DEFAULT
, os
.O_CREAT |os
.O_RDWR
)
268 self
.log
.transaction_start()
270 def get_int(self
, key
):
272 return int(self
.log
.get(key
))
277 return self
.log
.get(key
)
279 def get_gplog(self
, user
):
280 return gp_log(user
, self
, self
.log
.get(user
))
282 def store(self
, key
, val
):
283 self
.log
.store(key
, val
)
286 self
.log
.transaction_cancel()
288 def delete(self
, key
):
292 self
.log
.transaction_commit()
298 class gp_ext(object):
299 __metaclass__
= ABCMeta
301 def __init__(self
, logger
):
305 def list(self
, rootpath
):
313 def read(self
, policy
):
316 def parse(self
, afile
, ldb
, gp_db
, lp
):
321 local_path
= self
.lp
.cache_path('gpo_cache')
322 data_file
= os
.path
.join(local_path
, check_safe_path(afile
).upper())
323 if os
.path
.exists(data_file
):
324 return self
.read(open(data_file
, 'r').read())
332 class gp_ext_setter():
333 __metaclass__
= ABCMeta
335 def __init__(self
, logger
, ldb
, gp_db
, lp
, attribute
, val
):
338 self
.attribute
= attribute
346 def update_samba(self
):
347 (upd_sam
, value
) = self
.mapper().get(self
.attribute
)
359 class gp_inf_ext(gp_ext
):
361 def list(self
, rootpath
):
368 def read(self
, policy
):
370 inftable
= self
.apply_map()
372 current_section
= None
374 # So here we would declare a boolean,
375 # that would get changed to TRUE.
377 # If at any point in time a GPO was applied,
378 # then we return that boolean at the end.
380 inf_conf
= ConfigParser()
381 inf_conf
.optionxform
= str
383 inf_conf
.readfp(StringIO(policy
))
385 inf_conf
.readfp(StringIO(policy
.decode('utf-16')))
387 for section
in inf_conf
.sections():
388 current_section
= inftable
.get(section
)
389 if not current_section
:
391 for key
, value
in inf_conf
.items(section
):
392 if current_section
.get(key
):
393 (att
, setter
) = current_section
.get(key
)
394 value
= value
.encode('ascii', 'ignore')
396 setter(self
.logger
, self
.ldb
, self
.gp_db
, self
.lp
, att
,
397 value
).update_samba()
406 ''' Fetch the hostname of a writable DC '''
409 def get_dc_hostname(creds
, lp
):
410 net
= Net(creds
=creds
, lp
=lp
)
411 cldap_ret
= net
.finddc(domain
=lp
.get('realm'), flags
=(nbt
.NBT_SERVER_LDAP |
413 return cldap_ret
.pdc_dns_name
416 ''' Fetch a list of GUIDs for applicable GPOs '''
419 def get_gpo_list(dc_hostname
, creds
, lp
):
421 ads
= gpo
.ADS_STRUCT(dc_hostname
, lp
, creds
)
423 gpos
= ads
.get_gpo_list(creds
.get_username())
427 def cache_gpo_dir(conn
, cache
, sub_dir
):
428 loc_sub_dir
= sub_dir
.upper()
429 local_dir
= os
.path
.join(cache
, loc_sub_dir
)
431 os
.makedirs(local_dir
, mode
=0o755)
433 if e
.errno
!= errno
.EEXIST
:
435 for fdata
in conn
.list(sub_dir
):
436 if fdata
['attrib'] & smb
.FILE_ATTRIBUTE_DIRECTORY
:
437 cache_gpo_dir(conn
, cache
, os
.path
.join(sub_dir
, fdata
['name']))
439 local_name
= fdata
['name'].upper()
440 f
= NamedTemporaryFile(delete
=False, dir=local_dir
)
441 fname
= os
.path
.join(sub_dir
, fdata
['name']).replace('/', '\\')
442 f
.write(conn
.loadfile(fname
))
444 os
.rename(f
.name
, os
.path
.join(local_dir
, local_name
))
447 def check_safe_path(path
):
448 dirs
= re
.split('/|\\\\', path
)
450 dirs
= dirs
[dirs
.index('sysvol') + 1:]
452 return os
.path
.join(*dirs
)
456 def check_refresh_gpo_list(dc_hostname
, lp
, creds
, gpos
):
457 conn
= smb
.SMB(dc_hostname
, 'sysvol', lp
=lp
, creds
=creds
, sign
=True)
458 cache_path
= lp
.cache_path('gpo_cache')
460 if not gpo
.file_sys_path
:
462 cache_gpo_dir(conn
, cache_path
, check_safe_path(gpo
.file_sys_path
))
465 def gpo_version(lp
, path
):
466 # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
467 # read from the gpo client cache.
468 gpt_path
= lp
.cache_path(os
.path
.join('gpo_cache', path
))
469 return int(gpo
.gpo_get_sysvol_gpt_version(gpt_path
)[1])
472 def apply_gp(lp
, creds
, test_ldb
, logger
, store
, gp_extensions
):
473 gp_db
= store
.get_gplog(creds
.get_username())
474 dc_hostname
= get_dc_hostname(creds
, lp
)
475 gpos
= get_gpo_list(dc_hostname
, creds
, lp
)
477 check_refresh_gpo_list(dc_hostname
, lp
, creds
, gpos
)
479 logger
.error('Failed downloading gpt cache from \'%s\' using SMB'
485 if guid
== 'Local Policy':
487 path
= os
.path
.join(lp
.get('realm'), 'Policies', guid
).upper()
488 version
= gpo_version(lp
, path
)
489 if version
!= store
.get_int(guid
):
490 logger
.info('GPO %s has changed' % guid
)
491 gp_db
.state(GPOSTATE
.APPLY
)
493 gp_db
.state(GPOSTATE
.ENFORCE
)
496 for ext
in gp_extensions
:
498 ext
.parse(ext
.list(path
), test_ldb
, gp_db
, lp
)
499 except Exception as e
:
500 logger
.error('Failed to parse gpo %s for extension %s' %
502 logger
.error('Message was: ' + str(e
))
505 store
.store(guid
, '%i' % version
)
509 def unapply_log(gp_db
):
511 item
= gp_db
.apply_log_pop()
518 def unapply_gp(lp
, creds
, test_ldb
, logger
, store
, gp_extensions
):
519 gp_db
= store
.get_gplog(creds
.get_username())
520 gp_db
.state(GPOSTATE
.UNAPPLY
)
521 for gpo_guid
in unapply_log(gp_db
):
522 gp_db
.set_guid(gpo_guid
)
523 unapply_attributes
= gp_db
.list(gp_extensions
)
524 for attr
in unapply_attributes
:
525 attr_obj
= attr
[-1](logger
, test_ldb
, gp_db
, lp
, attr
[0], attr
[1])
526 attr_obj
.mapper()[attr
[0]][0](attr
[1]) # Set the old value
527 gp_db
.delete(str(attr_obj
), attr
[0])
531 def parse_gpext_conf(smb_conf
):
533 if smb_conf
is not None:
537 ext_conf
= lp
.state_path('gpext.conf')
538 parser
= ConfigParser()
539 parser
.read(ext_conf
)
543 def atomic_write_conf(lp
, parser
):
544 ext_conf
= lp
.state_path('gpext.conf')
545 with
NamedTemporaryFile(delete
=False, dir=os
.path
.dirname(ext_conf
)) as f
:
547 os
.rename(f
.name
, ext_conf
)
550 def check_guid(guid
):
551 # Check for valid guid with curly braces
552 if guid
[0] != '{' or guid
[-1] != '}' or len(guid
) != 38:
555 UUID(guid
, version
=4)
561 def register_gp_extension(guid
, name
, path
,
562 smb_conf
=None, machine
=True, user
=True):
563 # Check that the module exists
564 if not os
.path
.exists(path
):
566 if not check_guid(guid
):
569 lp
, parser
= parse_gpext_conf(smb_conf
)
570 if guid
not in parser
.sections():
571 parser
.add_section(guid
)
572 parser
.set(guid
, 'DllName', path
)
573 parser
.set(guid
, 'ProcessGroupPolicy', name
)
574 parser
.set(guid
, 'NoMachinePolicy', 0 if machine
else 1)
575 parser
.set(guid
, 'NoUserPolicy', 0 if user
else 1)
577 atomic_write_conf(lp
, parser
)
582 def list_gp_extensions(smb_conf
=None):
583 _
, parser
= parse_gpext_conf(smb_conf
)
585 for guid
in parser
.sections():
587 results
[guid
]['DllName'] = parser
.get(guid
, 'DllName')
588 results
[guid
]['ProcessGroupPolicy'] = \
589 parser
.get(guid
, 'ProcessGroupPolicy')
590 results
[guid
]['MachinePolicy'] = \
591 not int(parser
.get(guid
, 'NoMachinePolicy'))
592 results
[guid
]['UserPolicy'] = not int(parser
.get(guid
, 'NoUserPolicy'))
596 def unregister_gp_extension(guid
, smb_conf
=None):
597 if not check_guid(guid
):
600 lp
, parser
= parse_gpext_conf(smb_conf
)
601 if guid
in parser
.sections():
602 parser
.remove_section(guid
)
604 atomic_write_conf(lp
, parser
)