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 io
import StringIO
26 from samba
.common
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
38 from samba
.dcerpc
import preg
39 from samba
.dcerpc
import misc
40 from samba
.ndr
import ndr_pack
, ndr_unpack
41 from samba
.credentials
import SMB_SIGNING_REQUIRED
45 GPOSTATE
= Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
54 ''' Log settings overwritten by gpo apply
55 The gp_log is an xml file that stores a history of gpo changes (and the
56 original setting value).
58 The log is organized like so:
63 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
65 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
66 <gp_ext name="System Access">
67 <attribute name="minPwdAge">-864000000000</attribute>
68 <attribute name="maxPwdAge">-36288000000000</attribute>
69 <attribute name="minPwdLength">7</attribute>
70 <attribute name="pwdProperties">1</attribute>
72 <gp_ext name="Kerberos Policy">
73 <attribute name="ticket_lifetime">1d</attribute>
74 <attribute name="renew_lifetime" />
75 <attribute name="clockskew">300</attribute>
81 Each guid value contains a list of extensions, which contain a list of
82 attributes. The guid value represents a GPO. The attributes are the values
83 of those settings prior to the application of the GPO.
84 The list of guids is enclosed within a user name, which represents the user
85 the settings were applied to. This user may be the samaccountname of the
86 local computer, which implies that these are machine policies.
87 The applylog keeps track of the order in which the GPOs were applied, so
88 that they can be rolled back in reverse, returning the machine to the state
89 prior to policy application.
91 def __init__(self
, user
, gpostore
, db_log
=None):
92 ''' Initialize the gp_log
93 param user - the username (or machine name) that policies are
95 param gpostore - the GPOStorage obj which references the tdb which
97 param db_log - (optional) a string to initialize the gp_log
99 self
._state
= GPOSTATE
.APPLY
100 self
.gpostore
= gpostore
103 self
.gpdb
= etree
.fromstring(db_log
)
105 self
.gpdb
= etree
.Element('gp')
107 user_obj
= self
.gpdb
.find('user[@name="%s"]' % user
)
109 user_obj
= etree
.SubElement(self
.gpdb
, 'user')
110 user_obj
.attrib
['name'] = user
112 def state(self
, value
):
113 ''' Policy application state
114 param value - APPLY, ENFORCE, or UNAPPLY
116 The behavior of the gp_log depends on whether we are applying policy,
117 enforcing policy, or unapplying policy. During an apply, old settings
118 are recorded in the log. During an enforce, settings are being applied
119 but the gp_log does not change. During an unapply, additions to the log
120 should be ignored (since function calls to apply settings are actually
121 reverting policy), but removals from the log are allowed.
123 # If we're enforcing, but we've unapplied, apply instead
124 if value
== GPOSTATE
.ENFORCE
:
125 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
126 apply_log
= user_obj
.find('applylog')
127 if apply_log
is None or len(apply_log
) == 0:
128 self
._state
= GPOSTATE
.APPLY
134 def set_guid(self
, guid
):
135 ''' Log to a different GPO guid
136 param guid - guid value of the GPO from which we're applying
140 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
141 obj
= user_obj
.find('guid[@value="%s"]' % guid
)
143 obj
= etree
.SubElement(user_obj
, 'guid')
144 obj
.attrib
['value'] = guid
145 if self
._state
== GPOSTATE
.APPLY
:
146 apply_log
= user_obj
.find('applylog')
147 if apply_log
is None:
148 apply_log
= etree
.SubElement(user_obj
, 'applylog')
149 prev
= apply_log
.find('guid[@value="%s"]' % guid
)
151 item
= etree
.SubElement(apply_log
, 'guid')
152 item
.attrib
['count'] = '%d' % (len(apply_log
) - 1)
153 item
.attrib
['value'] = guid
155 def store(self
, gp_ext_name
, attribute
, old_val
):
156 ''' Store an attribute in the gp_log
157 param gp_ext_name - Name of the extension applying policy
158 param attribute - The attribute being modified
159 param old_val - The value of the attribute prior to policy
162 if self
._state
== GPOSTATE
.UNAPPLY
or self
._state
== GPOSTATE
.ENFORCE
:
164 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
165 guid_obj
= user_obj
.find('guid[@value="%s"]' % self
.guid
)
166 assert guid_obj
is not None, "gpo guid was not set"
167 ext
= guid_obj
.find('gp_ext[@name="%s"]' % gp_ext_name
)
169 ext
= etree
.SubElement(guid_obj
, 'gp_ext')
170 ext
.attrib
['name'] = gp_ext_name
171 attr
= ext
.find('attribute[@name="%s"]' % attribute
)
173 attr
= etree
.SubElement(ext
, 'attribute')
174 attr
.attrib
['name'] = attribute
177 def retrieve(self
, gp_ext_name
, attribute
):
178 ''' Retrieve a stored attribute from the gp_log
179 param gp_ext_name - Name of the extension which applied policy
180 param attribute - The attribute being retrieved
181 return - The value of the attribute prior to policy
184 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
185 guid_obj
= user_obj
.find('guid[@value="%s"]' % self
.guid
)
186 assert guid_obj
is not None, "gpo guid was not set"
187 ext
= guid_obj
.find('gp_ext[@name="%s"]' % gp_ext_name
)
189 attr
= ext
.find('attribute[@name="%s"]' % attribute
)
194 def get_applied_guids(self
):
195 ''' Return a list of applied ext guids
196 return - List of guids for gpos that have applied settings
200 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
201 if user_obj
is not None:
202 apply_log
= user_obj
.find('applylog')
203 if apply_log
is not None:
204 guid_objs
= apply_log
.findall('guid[@count]')
205 guids_by_count
= [(g
.get('count'), g
.get('value'))
207 guids_by_count
.sort(reverse
=True)
208 guids
.extend(guid
for count
, guid
in guids_by_count
)
211 def get_applied_settings(self
, guids
):
212 ''' Return a list of applied ext guids
213 return - List of tuples containing the guid of a gpo, then
214 a dictionary of policies and their values prior
215 policy application. These are sorted so that the
216 most recently applied settings are removed first.
219 user_obj
= self
.gpdb
.find('user[@name="%s"]' % self
.user
)
221 guid_settings
= user_obj
.find('guid[@value="%s"]' % guid
)
222 exts
= guid_settings
.findall('gp_ext')
226 attrs
= ext
.findall('attribute')
228 attr_dict
[attr
.attrib
['name']] = attr
.text
229 settings
[ext
.attrib
['name']] = attr_dict
230 ret
.append((guid
, settings
))
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'))
256 def __init__(self
, log_file
):
257 if os
.path
.isfile(log_file
):
258 self
.log
= tdb
.open(log_file
)
260 self
.log
= tdb
.Tdb(log_file
, 0, tdb
.DEFAULT
, os
.O_CREAT | os
.O_RDWR
)
263 self
.log
.transaction_start()
265 def get_int(self
, key
):
267 return int(self
.log
.get(get_bytes(key
)))
272 return self
.log
.get(get_bytes(key
))
274 def get_gplog(self
, user
):
275 return gp_log(user
, self
, self
.log
.get(get_bytes(user
)))
277 def store(self
, key
, val
):
278 self
.log
.store(get_bytes(key
), get_bytes(val
))
281 self
.log
.transaction_cancel()
283 def delete(self
, key
):
284 self
.log
.delete(get_bytes(key
))
287 self
.log
.transaction_commit()
293 class gp_ext(object):
294 __metaclass__
= ABCMeta
296 def __init__(self
, logger
, lp
, creds
, store
):
300 self
.gp_db
= store
.get_gplog(creds
.get_username())
303 def process_group_policy(self
, deleted_gpo_list
, changed_gpo_list
):
307 def read(self
, policy
):
310 def parse(self
, afile
):
311 local_path
= self
.lp
.cache_path('gpo_cache')
312 data_file
= os
.path
.join(local_path
, check_safe_path(afile
).upper())
313 if os
.path
.exists(data_file
):
314 return self
.read(data_file
)
326 class gp_inf_ext(gp_ext
):
327 def read(self
, data_file
):
328 policy
= open(data_file
, 'rb').read()
329 inf_conf
= ConfigParser(interpolation
=None)
330 inf_conf
.optionxform
= str
332 inf_conf
.readfp(StringIO(policy
.decode()))
333 except UnicodeDecodeError:
334 inf_conf
.readfp(StringIO(policy
.decode('utf-16')))
338 class gp_pol_ext(gp_ext
):
339 def read(self
, data_file
):
340 raw
= open(data_file
, 'rb').read()
341 return ndr_unpack(preg
.file, raw
)
344 class gp_xml_ext(gp_ext
):
345 def read(self
, data_file
):
346 raw
= open(data_file
, 'rb').read()
348 return etree
.fromstring(raw
.decode())
349 except UnicodeDecodeError:
350 return etree
.fromstring(raw
.decode('utf-16'))
353 ''' Fetch the hostname of a writable DC '''
356 def get_dc_hostname(creds
, lp
):
357 net
= Net(creds
=creds
, lp
=lp
)
358 cldap_ret
= net
.finddc(domain
=lp
.get('realm'), flags
=(nbt
.NBT_SERVER_LDAP |
360 return cldap_ret
.pdc_dns_name
363 ''' Fetch a list of GUIDs for applicable GPOs '''
366 def get_gpo_list(dc_hostname
, creds
, lp
):
368 ads
= gpo
.ADS_STRUCT(dc_hostname
, lp
, creds
)
370 gpos
= ads
.get_gpo_list(creds
.get_username())
374 def cache_gpo_dir(conn
, cache
, sub_dir
):
375 loc_sub_dir
= sub_dir
.upper()
376 local_dir
= os
.path
.join(cache
, loc_sub_dir
)
378 os
.makedirs(local_dir
, mode
=0o755)
380 if e
.errno
!= errno
.EEXIST
:
382 for fdata
in conn
.list(sub_dir
):
383 if fdata
['attrib'] & libsmb
.FILE_ATTRIBUTE_DIRECTORY
:
384 cache_gpo_dir(conn
, cache
, os
.path
.join(sub_dir
, fdata
['name']))
386 local_name
= fdata
['name'].upper()
387 f
= NamedTemporaryFile(delete
=False, dir=local_dir
)
388 fname
= os
.path
.join(sub_dir
, fdata
['name']).replace('/', '\\')
389 f
.write(conn
.loadfile(fname
))
391 os
.rename(f
.name
, os
.path
.join(local_dir
, local_name
))
394 def check_safe_path(path
):
395 dirs
= re
.split('/|\\\\', path
)
396 if 'sysvol' in path
.lower():
397 ldirs
= re
.split('/|\\\\', path
.lower())
398 dirs
= dirs
[ldirs
.index('sysvol') + 1:]
400 return os
.path
.join(*dirs
)
404 def check_refresh_gpo_list(dc_hostname
, lp
, creds
, gpos
):
405 # the SMB bindings rely on having a s3 loadparm
406 s3_lp
= s3param
.get_context()
407 s3_lp
.load(lp
.configfile
)
409 # Force signing for the connection
410 saved_signing_state
= creds
.get_smb_signing()
411 creds
.set_smb_signing(SMB_SIGNING_REQUIRED
)
412 conn
= libsmb
.Conn(dc_hostname
, 'sysvol', lp
=s3_lp
, creds
=creds
)
413 # Reset signing state
414 creds
.set_smb_signing(saved_signing_state
)
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
= ext(logger
, lp
, creds
, store
)
467 ext
.process_group_policy(del_gpos
, changed_gpos
)
468 except Exception as e
:
469 logger
.error('Failed to apply extension %s' % str(ext
))
470 logger
.error('Message was: ' + str(e
))
473 if not gpo_obj
.file_sys_path
:
476 path
= check_safe_path(gpo_obj
.file_sys_path
).upper()
477 version
= gpo_version(lp
, path
)
478 store
.store(guid
, '%i' % version
)
482 def unapply_gp(lp
, creds
, logger
, store
, gp_extensions
):
483 gp_db
= store
.get_gplog(creds
.get_username())
484 gp_db
.state(GPOSTATE
.UNAPPLY
)
485 # Treat all applied gpos as deleted
486 del_gpos
= gp_db
.get_applied_settings(gp_db
.get_applied_guids())
488 for ext
in gp_extensions
:
490 ext
= ext(logger
, lp
, creds
, store
)
491 ext
.process_group_policy(del_gpos
, [])
492 except Exception as e
:
493 logger
.error('Failed to unapply extension %s' % str(ext
))
494 logger
.error('Message was: ' + str(e
))
499 def __rsop_vals(vals
, level
=4):
500 if type(vals
) == dict:
501 ret
= [' '*level
+ '[ %s ] = %s' % (k
, __rsop_vals(v
, level
+2))
502 for k
, v
in vals
.items()]
503 return '\n'.join(ret
)
504 elif type(vals
) == list:
505 ret
= [' '*level
+ '[ %s ]' % __rsop_vals(v
, level
+2) for v
in vals
]
506 return '\n'.join(ret
)
510 def rsop(lp
, creds
, logger
, store
, gp_extensions
, target
):
511 dc_hostname
= get_dc_hostname(creds
, lp
)
512 gpos
= get_gpo_list(dc_hostname
, creds
, lp
)
513 check_refresh_gpo_list(dc_hostname
, lp
, creds
, gpos
)
515 print('Resultant Set of Policy')
516 print('%s Policy\n' % target
)
517 term_width
= shutil
.get_terminal_size(fallback
=(120, 50))[0]
519 if gpo
.display_name
.strip() == 'Local Policy':
520 continue # We never apply local policy
521 print('GPO: %s' % gpo
.display_name
)
522 print('='*term_width
)
523 for ext
in gp_extensions
:
524 ext
= ext(logger
, lp
, creds
, store
)
525 cse_name_m
= re
.findall("'([\w\.]+)'", str(type(ext
)))
526 if len(cse_name_m
) > 0:
527 cse_name
= cse_name_m
[-1].split('.')[-1]
529 cse_name
= ext
.__module
__.split('.')[-1]
530 print(' CSE: %s' % cse_name
)
531 print(' ' + ('-'*int(term_width
/2)))
532 for section
, settings
in ext
.rsop(gpo
).items():
533 print(' Policy Type: %s' % section
)
534 print(' ' + ('-'*int(term_width
/2)))
535 print(__rsop_vals(settings
))
536 print(' ' + ('-'*int(term_width
/2)))
537 print(' ' + ('-'*int(term_width
/2)))
538 print('%s\n' % ('='*term_width
))
541 def parse_gpext_conf(smb_conf
):
543 if smb_conf
is not None:
547 ext_conf
= lp
.state_path('gpext.conf')
548 parser
= ConfigParser(interpolation
=None)
549 parser
.read(ext_conf
)
553 def atomic_write_conf(lp
, parser
):
554 ext_conf
= lp
.state_path('gpext.conf')
555 with
NamedTemporaryFile(mode
="w+", delete
=False, dir=os
.path
.dirname(ext_conf
)) as f
:
557 os
.rename(f
.name
, ext_conf
)
560 def check_guid(guid
):
561 # Check for valid guid with curly braces
562 if guid
[0] != '{' or guid
[-1] != '}' or len(guid
) != 38:
565 UUID(guid
, version
=4)
571 def register_gp_extension(guid
, name
, path
,
572 smb_conf
=None, machine
=True, user
=True):
573 # Check that the module exists
574 if not os
.path
.exists(path
):
576 if not check_guid(guid
):
579 lp
, parser
= parse_gpext_conf(smb_conf
)
580 if guid
not in parser
.sections():
581 parser
.add_section(guid
)
582 parser
.set(guid
, 'DllName', path
)
583 parser
.set(guid
, 'ProcessGroupPolicy', name
)
584 parser
.set(guid
, 'NoMachinePolicy', "0" if machine
else "1")
585 parser
.set(guid
, 'NoUserPolicy', "0" if user
else "1")
587 atomic_write_conf(lp
, parser
)
592 def list_gp_extensions(smb_conf
=None):
593 _
, parser
= parse_gpext_conf(smb_conf
)
595 for guid
in parser
.sections():
597 results
[guid
]['DllName'] = parser
.get(guid
, 'DllName')
598 results
[guid
]['ProcessGroupPolicy'] = \
599 parser
.get(guid
, 'ProcessGroupPolicy')
600 results
[guid
]['MachinePolicy'] = \
601 not int(parser
.get(guid
, 'NoMachinePolicy'))
602 results
[guid
]['UserPolicy'] = not int(parser
.get(guid
, 'NoUserPolicy'))
606 def unregister_gp_extension(guid
, smb_conf
=None):
607 if not check_guid(guid
):
610 lp
, parser
= parse_gpext_conf(smb_conf
)
611 if guid
in parser
.sections():
612 parser
.remove_section(guid
)
614 atomic_write_conf(lp
, parser
)