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 samba
.compat
import ConfigParser
25 from samba
.compat
import StringIO
26 from samba
.compat
import get_bytes
27 from abc
import ABCMeta
, abstractmethod
28 import xml
.etree
.ElementTree
as etree
30 from samba
.net
import Net
31 from samba
.dcerpc
import nbt
32 from samba
.samba3
import libsmb_samba_internal
as libsmb
33 from samba
.samba3
import param
as s3param
34 import samba
.gpo
as gpo
35 from samba
.param
import LoadParm
37 from tempfile
import NamedTemporaryFile
41 GPOSTATE
= Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
50 ''' Log settings overwritten by gpo apply
51 The gp_log is an xml file that stores a history of gpo changes (and the
52 original setting value).
54 The log is organized like so:
59 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
61 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
62 <gp_ext name="System Access">
63 <attribute name="minPwdAge">-864000000000</attribute>
64 <attribute name="maxPwdAge">-36288000000000</attribute>
65 <attribute name="minPwdLength">7</attribute>
66 <attribute name="pwdProperties">1</attribute>
68 <gp_ext name="Kerberos Policy">
69 <attribute name="ticket_lifetime">1d</attribute>
70 <attribute name="renew_lifetime" />
71 <attribute name="clockskew">300</attribute>
77 Each guid value contains a list of extensions, which contain a list of
78 attributes. The guid value represents a GPO. The attributes are the values
79 of those settings prior to the application of the GPO.
80 The list of guids is enclosed within a user name, which represents the user
81 the settings were applied to. This user may be the samaccountname of the
82 local computer, which implies that these are machine policies.
83 The applylog keeps track of the order in which the GPOs were applied, so
84 that they can be rolled back in reverse, returning the machine to the state
85 prior to policy application.
87 def __init__(self
, user
, gpostore
, db_log
=None):
88 ''' Initialize the gp_log
89 param user - the username (or machine name) that policies are
91 param gpostore - the GPOStorage obj which references the tdb which
93 param db_log - (optional) a string to initialize the gp_log
95 self
._state
= GPOSTATE
.APPLY
96 self
.gpostore
= gpostore
99 self
.gpdb
= etree
.fromstring(db_log
)
101 self
.gpdb
= etree
.Element('gp')
103 user_obj
= self
.gpdb
.find('user[@name="%s"]' % user
)
105 user_obj
= etree
.SubElement(self
.gpdb
, 'user')
106 user_obj
.attrib
['name'] = user
108 def state(self
, value
):
109 ''' Policy application state
110 param value - APPLY, ENFORCE, or UNAPPLY
112 The behavior of the gp_log depends on whether we are applying policy,
113 enforcing policy, or unapplying policy. During an apply, old settings
114 are recorded in the log. During an enforce, settings are being applied
115 but the gp_log does not change. During an unapply, additions to the log
116 should be ignored (since function calls to apply settings are actually
117 reverting policy), but removals from the log are allowed.
119 # If we're enforcing, but we've unapplied, apply instead
120 if value
== GPOSTATE
.ENFORCE
:
121 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
122 apply_log
= user_obj
.find('applylog')
123 if apply_log
is None or len(apply_log
) == 0:
124 self
._state
= GPOSTATE
.APPLY
130 def set_guid(self
, guid
):
131 ''' Log to a different GPO guid
132 param guid - guid value of the GPO from which we're applying
136 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
137 obj
= user_obj
.find('guid[@value="%s"]' % guid
)
139 obj
= etree
.SubElement(user_obj
, 'guid')
140 obj
.attrib
['value'] = guid
141 if self
._state
== GPOSTATE
.APPLY
:
142 apply_log
= user_obj
.find('applylog')
143 if apply_log
is None:
144 apply_log
= etree
.SubElement(user_obj
, 'applylog')
145 prev
= apply_log
.find('guid[@value="%s"]' % guid
)
147 item
= etree
.SubElement(apply_log
, 'guid')
148 item
.attrib
['count'] = '%d' % (len(apply_log
) - 1)
149 item
.attrib
['value'] = guid
151 def store(self
, gp_ext_name
, attribute
, old_val
):
152 ''' Store an attribute in the gp_log
153 param gp_ext_name - Name of the extension applying policy
154 param attribute - The attribute being modified
155 param old_val - The value of the attribute prior to policy
158 if self
._state
== GPOSTATE
.UNAPPLY
or self
._state
== GPOSTATE
.ENFORCE
:
160 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
161 guid_obj
= user_obj
.find('guid[@value="%s"]' % self
.guid
)
162 assert guid_obj
is not None, "gpo guid was not set"
163 ext
= guid_obj
.find('gp_ext[@name="%s"]' % gp_ext_name
)
165 ext
= etree
.SubElement(guid_obj
, 'gp_ext')
166 ext
.attrib
['name'] = gp_ext_name
167 attr
= ext
.find('attribute[@name="%s"]' % attribute
)
169 attr
= etree
.SubElement(ext
, 'attribute')
170 attr
.attrib
['name'] = attribute
173 def retrieve(self
, gp_ext_name
, attribute
):
174 ''' Retrieve a stored attribute from the gp_log
175 param gp_ext_name - Name of the extension which applied policy
176 param attribute - The attribute being retrieved
177 return - The value of the attribute prior to policy
180 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
181 guid_obj
= user_obj
.find('guid[@value="%s"]' % self
.guid
)
182 assert guid_obj
is not None, "gpo guid was not set"
183 ext
= guid_obj
.find('gp_ext[@name="%s"]' % gp_ext_name
)
185 attr
= ext
.find('attribute[@name="%s"]' % attribute
)
190 def get_applied_guids(self
):
191 ''' Return a list of applied ext guids
192 return - List of guids for gpos that have applied settings
196 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
197 if user_obj
is not None:
198 apply_log
= user_obj
.find('applylog')
199 if apply_log
is not None:
200 guid_objs
= apply_log
.findall('guid[@count]')
201 guids_by_count
= [(g
.get('count'), g
.get('value'))
203 guids_by_count
.sort(reverse
=True)
204 guids
.extend(guid
for count
, guid
in guids_by_count
)
207 def get_applied_settings(self
, guids
):
208 ''' Return a list of applied ext guids
209 return - List of tuples containing the guid of a gpo, then
210 a dictionary of policies and their values prior
211 policy application. These are sorted so that the
212 most recently applied settings are removed first.
215 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
217 guid_settings
= user_obj
.find('guid[@value="%s"]' % guid
)
218 exts
= guid_settings
.findall('gp_ext')
222 attrs
= ext
.findall('attribute')
224 attr_dict
[attr
.attrib
['name']] = attr
.text
225 settings
[ext
.attrib
['name']] = attr_dict
226 ret
.append((guid
, settings
))
229 def delete(self
, gp_ext_name
, attribute
):
230 ''' Remove an attribute from the gp_log
231 param gp_ext_name - name of extension from which to remove the
233 param attribute - attribute to remove
235 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
236 guid_obj
= user_obj
.find('guid[@value="%s"]' % self
.guid
)
237 assert guid_obj
is not None, "gpo guid was not set"
238 ext
= guid_obj
.find('gp_ext[@name="%s"]' % gp_ext_name
)
240 attr
= ext
.find('attribute[@name="%s"]' % attribute
)
247 ''' Write gp_log changes to disk '''
248 self
.gpostore
.store(self
.username
, etree
.tostring(self
.gpdb
, 'utf-8'))
252 def __init__(self
, log_file
):
253 if os
.path
.isfile(log_file
):
254 self
.log
= tdb
.open(log_file
)
256 self
.log
= tdb
.Tdb(log_file
, 0, tdb
.DEFAULT
, os
.O_CREAT | os
.O_RDWR
)
259 self
.log
.transaction_start()
261 def get_int(self
, key
):
263 return int(self
.log
.get(get_bytes(key
)))
268 return self
.log
.get(get_bytes(key
))
270 def get_gplog(self
, user
):
271 return gp_log(user
, self
, self
.log
.get(get_bytes(user
)))
273 def store(self
, key
, val
):
274 self
.log
.store(get_bytes(key
), get_bytes(val
))
277 self
.log
.transaction_cancel()
279 def delete(self
, key
):
280 self
.log
.delete(get_bytes(key
))
283 self
.log
.transaction_commit()
289 class gp_ext(object):
290 __metaclass__
= ABCMeta
292 def __init__(self
, logger
, lp
, creds
, store
):
296 self
.gp_db
= store
.get_gplog(creds
.get_username())
299 def process_group_policy(self
, deleted_gpo_list
, changed_gpo_list
):
303 def read(self
, policy
):
306 def parse(self
, afile
):
307 local_path
= self
.lp
.cache_path('gpo_cache')
308 data_file
= os
.path
.join(local_path
, check_safe_path(afile
).upper())
309 if os
.path
.exists(data_file
):
310 return self
.read(open(data_file
, 'r').read())
318 class gp_ext_setter(object):
319 __metaclass__
= ABCMeta
321 def __init__(self
, logger
, gp_db
, lp
, creds
, attribute
, val
):
323 self
.attribute
= attribute
332 def update_samba(self
):
333 (upd_sam
, value
) = self
.mapper().get(self
.attribute
)
341 upd_sam
, _
= self
.mapper().get(self
.attribute
)
349 class gp_inf_ext(gp_ext
):
350 def read(self
, policy
):
351 inf_conf
= ConfigParser()
352 inf_conf
.optionxform
= str
354 inf_conf
.readfp(StringIO(policy
))
356 inf_conf
.readfp(StringIO(policy
.decode('utf-16')))
360 ''' Fetch the hostname of a writable DC '''
363 def get_dc_hostname(creds
, lp
):
364 net
= Net(creds
=creds
, lp
=lp
)
365 cldap_ret
= net
.finddc(domain
=lp
.get('realm'), flags
=(nbt
.NBT_SERVER_LDAP |
367 return cldap_ret
.pdc_dns_name
370 ''' Fetch a list of GUIDs for applicable GPOs '''
373 def get_gpo_list(dc_hostname
, creds
, lp
):
375 ads
= gpo
.ADS_STRUCT(dc_hostname
, lp
, creds
)
377 gpos
= ads
.get_gpo_list(creds
.get_username())
381 def cache_gpo_dir(conn
, cache
, sub_dir
):
382 loc_sub_dir
= sub_dir
.upper()
383 local_dir
= os
.path
.join(cache
, loc_sub_dir
)
385 os
.makedirs(local_dir
, mode
=0o755)
387 if e
.errno
!= errno
.EEXIST
:
389 for fdata
in conn
.list(sub_dir
):
390 if fdata
['attrib'] & libsmb
.FILE_ATTRIBUTE_DIRECTORY
:
391 cache_gpo_dir(conn
, cache
, os
.path
.join(sub_dir
, fdata
['name']))
393 local_name
= fdata
['name'].upper()
394 f
= NamedTemporaryFile(delete
=False, dir=local_dir
)
395 fname
= os
.path
.join(sub_dir
, fdata
['name']).replace('/', '\\')
396 f
.write(conn
.loadfile(fname
))
398 os
.rename(f
.name
, os
.path
.join(local_dir
, local_name
))
401 def check_safe_path(path
):
402 dirs
= re
.split('/|\\\\', path
)
404 dirs
= dirs
[dirs
.index('sysvol') + 1:]
406 return os
.path
.join(*dirs
)
410 def check_refresh_gpo_list(dc_hostname
, lp
, creds
, gpos
):
411 # the SMB bindings rely on having a s3 loadparm
412 s3_lp
= s3param
.get_context()
413 s3_lp
.load(lp
.configfile
)
414 conn
= libsmb
.Conn(dc_hostname
, 'sysvol', lp
=s3_lp
, creds
=creds
, sign
=True)
415 cache_path
= lp
.cache_path('gpo_cache')
417 if not gpo
.file_sys_path
:
419 cache_gpo_dir(conn
, cache_path
, check_safe_path(gpo
.file_sys_path
))
422 def get_deleted_gpos_list(gp_db
, gpos
):
423 applied_gpos
= gp_db
.get_applied_guids()
424 current_guids
= set([p
.name
for p
in gpos
])
425 deleted_gpos
= [guid
for guid
in applied_gpos
if guid
not in current_guids
]
426 return gp_db
.get_applied_settings(deleted_gpos
)
428 def gpo_version(lp
, path
):
429 # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
430 # read from the gpo client cache.
431 gpt_path
= lp
.cache_path(os
.path
.join('gpo_cache', path
))
432 return int(gpo
.gpo_get_sysvol_gpt_version(gpt_path
)[1])
435 def apply_gp(lp
, creds
, logger
, store
, gp_extensions
, force
=False):
436 gp_db
= store
.get_gplog(creds
.get_username())
437 dc_hostname
= get_dc_hostname(creds
, lp
)
438 gpos
= get_gpo_list(dc_hostname
, creds
, lp
)
439 del_gpos
= get_deleted_gpos_list(gp_db
, gpos
)
441 check_refresh_gpo_list(dc_hostname
, lp
, creds
, gpos
)
443 logger
.error('Failed downloading gpt cache from \'%s\' using SMB'
449 gp_db
.state(GPOSTATE
.ENFORCE
)
453 if not gpo_obj
.file_sys_path
:
456 path
= check_safe_path(gpo_obj
.file_sys_path
).upper()
457 version
= gpo_version(lp
, path
)
458 if version
!= store
.get_int(guid
):
459 logger
.info('GPO %s has changed' % guid
)
460 changed_gpos
.append(gpo_obj
)
461 gp_db
.state(GPOSTATE
.APPLY
)
464 for ext
in gp_extensions
:
466 ext
.process_group_policy(del_gpos
, changed_gpos
)
467 except Exception as e
:
468 logger
.error('Failed to apply extension %s' % str(ext
))
469 logger
.error('Message was: ' + str(e
))
472 if not gpo_obj
.file_sys_path
:
475 path
= check_safe_path(gpo_obj
.file_sys_path
).upper()
476 version
= gpo_version(lp
, path
)
477 store
.store(guid
, '%i' % version
)
481 def unapply_gp(lp
, creds
, logger
, store
, gp_extensions
):
482 gp_db
= store
.get_gplog(creds
.get_username())
483 gp_db
.state(GPOSTATE
.UNAPPLY
)
484 # Treat all applied gpos as deleted
485 del_gpos
= gp_db
.get_applied_settings(gp_db
.get_applied_guids())
487 for ext
in gp_extensions
:
489 ext
.process_group_policy(del_gpos
, [])
490 except Exception as e
:
491 logger
.error('Failed to unapply extension %s' % str(ext
))
492 logger
.error('Message was: ' + str(e
))
497 def parse_gpext_conf(smb_conf
):
499 if smb_conf
is not None:
503 ext_conf
= lp
.state_path('gpext.conf')
504 parser
= ConfigParser()
505 parser
.read(ext_conf
)
509 def atomic_write_conf(lp
, parser
):
510 ext_conf
= lp
.state_path('gpext.conf')
511 with
NamedTemporaryFile(mode
="w+", delete
=False, dir=os
.path
.dirname(ext_conf
)) as f
:
513 os
.rename(f
.name
, ext_conf
)
516 def check_guid(guid
):
517 # Check for valid guid with curly braces
518 if guid
[0] != '{' or guid
[-1] != '}' or len(guid
) != 38:
521 UUID(guid
, version
=4)
527 def register_gp_extension(guid
, name
, path
,
528 smb_conf
=None, machine
=True, user
=True):
529 # Check that the module exists
530 if not os
.path
.exists(path
):
532 if not check_guid(guid
):
535 lp
, parser
= parse_gpext_conf(smb_conf
)
536 if guid
not in parser
.sections():
537 parser
.add_section(guid
)
538 parser
.set(guid
, 'DllName', path
)
539 parser
.set(guid
, 'ProcessGroupPolicy', name
)
540 parser
.set(guid
, 'NoMachinePolicy', "0" if machine
else "1")
541 parser
.set(guid
, 'NoUserPolicy', "0" if user
else "1")
543 atomic_write_conf(lp
, parser
)
548 def list_gp_extensions(smb_conf
=None):
549 _
, parser
= parse_gpext_conf(smb_conf
)
551 for guid
in parser
.sections():
553 results
[guid
]['DllName'] = parser
.get(guid
, 'DllName')
554 results
[guid
]['ProcessGroupPolicy'] = \
555 parser
.get(guid
, 'ProcessGroupPolicy')
556 results
[guid
]['MachinePolicy'] = \
557 not int(parser
.get(guid
, 'NoMachinePolicy'))
558 results
[guid
]['UserPolicy'] = not int(parser
.get(guid
, 'NoUserPolicy'))
562 def unregister_gp_extension(guid
, smb_conf
=None):
563 if not check_guid(guid
):
566 lp
, parser
= parse_gpext_conf(smb_conf
)
567 if guid
in parser
.sections():
568 parser
.remove_section(guid
)
570 atomic_write_conf(lp
, parser
)