samba-gpupdate: Check sysvol download paths in case-insensitive way
[Samba.git] / python / samba / gpclass.py
blob7d3841ba8da035fcd01879818fe0e9539f7bae34
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/>.
18 import sys
19 import os, shutil
20 import errno
21 import tdb
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
29 import re
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
36 from uuid import UUID
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
43 try:
44 from enum import Enum
45 GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
46 except ImportError:
47 class GPOSTATE:
48 APPLY = 1
49 ENFORCE = 2
50 UNAPPLY = 3
53 class gp_log:
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:
60 <gp>
61 <user name="KDC-1$">
62 <applylog>
63 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
64 </applylog>
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>
71 </gp_ext>
72 <gp_ext name="Kerberos Policy">
73 <attribute name="ticket_lifetime">1d</attribute>
74 <attribute name="renew_lifetime" />
75 <attribute name="clockskew">300</attribute>
76 </gp_ext>
77 </guid>
78 </user>
79 </gp>
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.
90 '''
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
94 being applied to
95 param gpostore - the GPOStorage obj which references the tdb which
96 contains gp_logs
97 param db_log - (optional) a string to initialize the gp_log
98 '''
99 self._state = GPOSTATE.APPLY
100 self.gpostore = gpostore
101 self.username = user
102 if db_log:
103 self.gpdb = etree.fromstring(db_log)
104 else:
105 self.gpdb = etree.Element('gp')
106 self.user = user
107 user_obj = self.gpdb.find('user[@name="%s"]' % user)
108 if user_obj is None:
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
129 else:
130 self._state = value
131 else:
132 self._state = value
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
137 policy
139 self.guid = guid
140 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
141 obj = user_obj.find('guid[@value="%s"]' % guid)
142 if obj is None:
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)
150 if prev is None:
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
160 application
162 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
163 return None
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)
168 if ext is None:
169 ext = etree.SubElement(guid_obj, 'gp_ext')
170 ext.attrib['name'] = gp_ext_name
171 attr = ext.find('attribute[@name="%s"]' % attribute)
172 if attr is None:
173 attr = etree.SubElement(ext, 'attribute')
174 attr.attrib['name'] = attribute
175 attr.text = old_val
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
182 application
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)
188 if ext is not None:
189 attr = ext.find('attribute[@name="%s"]' % attribute)
190 if attr is not None:
191 return attr.text
192 return None
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
197 to the system.
199 guids = []
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'))
206 for g in guid_objs]
207 guids_by_count.sort(reverse=True)
208 guids.extend(guid for count, guid in guids_by_count)
209 return guids
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.
218 ret = []
219 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
220 for guid in guids:
221 guid_settings = user_obj.find('guid[@value="%s"]' % guid)
222 exts = guid_settings.findall('gp_ext')
223 settings = {}
224 for ext in exts:
225 attr_dict = {}
226 attrs = ext.findall('attribute')
227 for attr in attrs:
228 attr_dict[attr.attrib['name']] = attr.text
229 settings[ext.attrib['name']] = attr_dict
230 ret.append((guid, settings))
231 return ret
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
236 attribute
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)
243 if ext is not None:
244 attr = ext.find('attribute[@name="%s"]' % attribute)
245 if attr is not None:
246 ext.remove(attr)
247 if len(ext) == 0:
248 guid_obj.remove(ext)
250 def commit(self):
251 ''' Write gp_log changes to disk '''
252 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
255 class GPOStorage:
256 def __init__(self, log_file):
257 if os.path.isfile(log_file):
258 self.log = tdb.open(log_file)
259 else:
260 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT | os.O_RDWR)
262 def start(self):
263 self.log.transaction_start()
265 def get_int(self, key):
266 try:
267 return int(self.log.get(get_bytes(key)))
268 except TypeError:
269 return None
271 def get(self, 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))
280 def cancel(self):
281 self.log.transaction_cancel()
283 def delete(self, key):
284 self.log.delete(get_bytes(key))
286 def commit(self):
287 self.log.transaction_commit()
289 def __del__(self):
290 self.log.close()
293 class gp_ext(object):
294 __metaclass__ = ABCMeta
296 def __init__(self, logger, lp, creds, store):
297 self.logger = logger
298 self.lp = lp
299 self.creds = creds
300 self.gp_db = store.get_gplog(creds.get_username())
302 @abstractmethod
303 def process_group_policy(self, deleted_gpo_list, changed_gpo_list):
304 pass
306 @abstractmethod
307 def read(self, policy):
308 pass
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)
315 return None
317 @abstractmethod
318 def __str__(self):
319 pass
321 @abstractmethod
322 def rsop(self, gpo):
323 return {}
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
331 try:
332 inf_conf.readfp(StringIO(policy.decode()))
333 except UnicodeDecodeError:
334 inf_conf.readfp(StringIO(policy.decode('utf-16')))
335 return inf_conf
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()
347 try:
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 |
359 nbt.NBT_SERVER_DS))
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):
367 gpos = []
368 ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
369 if ads.connect():
370 gpos = ads.get_gpo_list(creds.get_username())
371 return gpos
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)
377 try:
378 os.makedirs(local_dir, mode=0o755)
379 except OSError as e:
380 if e.errno != errno.EEXIST:
381 raise
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']))
385 else:
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))
390 f.close()
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:]
399 if '..' not in dirs:
400 return os.path.join(*dirs)
401 raise OSError(path)
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')
416 for gpo in gpos:
417 if not gpo.file_sys_path:
418 continue
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)
440 try:
441 check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
442 except:
443 logger.error('Failed downloading gpt cache from \'%s\' using SMB'
444 % dc_hostname)
445 return
447 if force:
448 changed_gpos = gpos
449 gp_db.state(GPOSTATE.ENFORCE)
450 else:
451 changed_gpos = []
452 for gpo_obj in gpos:
453 if not gpo_obj.file_sys_path:
454 continue
455 guid = gpo_obj.name
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)
463 store.start()
464 for ext in gp_extensions:
465 try:
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))
471 continue
472 for gpo_obj in gpos:
473 if not gpo_obj.file_sys_path:
474 continue
475 guid = gpo_obj.name
476 path = check_safe_path(gpo_obj.file_sys_path).upper()
477 version = gpo_version(lp, path)
478 store.store(guid, '%i' % version)
479 store.commit()
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())
487 store.start()
488 for ext in gp_extensions:
489 try:
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))
495 continue
496 store.commit()
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)
507 else:
508 return vals
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]
518 for gpo in gpos:
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]
528 else:
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):
542 lp = LoadParm()
543 if smb_conf is not None:
544 lp.load(smb_conf)
545 else:
546 lp.load_default()
547 ext_conf = lp.state_path('gpext.conf')
548 parser = ConfigParser(interpolation=None)
549 parser.read(ext_conf)
550 return lp, parser
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:
556 parser.write(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:
563 return False
564 try:
565 UUID(guid, version=4)
566 except ValueError:
567 return False
568 return True
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):
575 return False
576 if not check_guid(guid):
577 return False
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)
589 return True
592 def list_gp_extensions(smb_conf=None):
593 _, parser = parse_gpext_conf(smb_conf)
594 results = {}
595 for guid in parser.sections():
596 results[guid] = {}
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'))
603 return results
606 def unregister_gp_extension(guid, smb_conf=None):
607 if not check_guid(guid):
608 return False
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)
616 return True