traffic: reinitialise RNG in each child
[sfrench/samba-autobuild/.git] / python / samba / gpclass.py
1 # Reads important GPO parameters and updates Samba
2 # Copyright (C) Luke Morrison <luc785@.hotmail.com> 2013
3 #
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.
8 #
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.
13 #
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/>.
16
17
18 import sys
19 import os
20 import errno
21 import tdb
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
29 import re
30 from samba.net import Net
31 from samba.dcerpc import nbt
32 from samba import smb
33 import samba.gpo as gpo
34 from samba.param import LoadParm
35 from uuid import UUID
36 from tempfile import NamedTemporaryFile
37
38 try:
39     from enum import Enum
40     GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
41 except ImportError:
42     class GPOSTATE:
43         APPLY = 1
44         ENFORCE = 2
45         UNAPPLY = 3
46
47
48 class gp_log:
49     ''' Log settings overwritten by gpo apply
50     The gp_log is an xml file that stores a history of gpo changes (and the
51     original setting value).
52
53     The log is organized like so:
54
55 <gp>
56     <user name="KDC-1$">
57         <applylog>
58             <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
59         </applylog>
60         <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
61             <gp_ext name="System Access">
62                 <attribute name="minPwdAge">-864000000000</attribute>
63                 <attribute name="maxPwdAge">-36288000000000</attribute>
64                 <attribute name="minPwdLength">7</attribute>
65                 <attribute name="pwdProperties">1</attribute>
66             </gp_ext>
67             <gp_ext name="Kerberos Policy">
68                 <attribute name="ticket_lifetime">1d</attribute>
69                 <attribute name="renew_lifetime" />
70                 <attribute name="clockskew">300</attribute>
71             </gp_ext>
72         </guid>
73     </user>
74 </gp>
75
76     Each guid value contains a list of extensions, which contain a list of
77     attributes. The guid value represents a GPO. The attributes are the values
78     of those settings prior to the application of the GPO.
79     The list of guids is enclosed within a user name, which represents the user
80     the settings were applied to. This user may be the samaccountname of the
81     local computer, which implies that these are machine policies.
82     The applylog keeps track of the order in which the GPOs were applied, so
83     that they can be rolled back in reverse, returning the machine to the state
84     prior to policy application.
85     '''
86     def __init__(self, user, gpostore, db_log=None):
87         ''' Initialize the gp_log
88         param user          - the username (or machine name) that policies are
89                               being applied to
90         param gpostore      - the GPOStorage obj which references the tdb which
91                               contains gp_logs
92         param db_log        - (optional) a string to initialize the gp_log
93         '''
94         self._state = GPOSTATE.APPLY
95         self.gpostore = gpostore
96         self.username = user
97         if db_log:
98             self.gpdb = etree.fromstring(db_log)
99         else:
100             self.gpdb = etree.Element('gp')
101         self.user = user
102         user_obj = self.gpdb.find('user[@name="%s"]' % user)
103         if user_obj is None:
104             user_obj = etree.SubElement(self.gpdb, 'user')
105             user_obj.attrib['name'] = user
106
107     def state(self, value):
108         ''' Policy application state
109         param value         - APPLY, ENFORCE, or UNAPPLY
110
111         The behavior of the gp_log depends on whether we are applying policy,
112         enforcing policy, or unapplying policy. During an apply, old settings
113         are recorded in the log. During an enforce, settings are being applied
114         but the gp_log does not change. During an unapply, additions to the log
115         should be ignored (since function calls to apply settings are actually
116         reverting policy), but removals from the log are allowed.
117         '''
118         # If we're enforcing, but we've unapplied, apply instead
119         if value == GPOSTATE.ENFORCE:
120             user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
121             apply_log = user_obj.find('applylog')
122             if apply_log is None or len(apply_log) == 0:
123                 self._state = GPOSTATE.APPLY
124             else:
125                 self._state = value
126         else:
127             self._state = value
128
129     def set_guid(self, guid):
130         ''' Log to a different GPO guid
131         param guid          - guid value of the GPO from which we're applying
132                               policy
133         '''
134         self.guid = guid
135         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
136         obj = user_obj.find('guid[@value="%s"]' % guid)
137         if obj is None:
138             obj = etree.SubElement(user_obj, 'guid')
139             obj.attrib['value'] = guid
140         if self._state == GPOSTATE.APPLY:
141             apply_log = user_obj.find('applylog')
142             if apply_log is None:
143                 apply_log = etree.SubElement(user_obj, 'applylog')
144             prev = apply_log.find('guid[@value="%s"]' % guid)
145             if prev is None:
146                 item = etree.SubElement(apply_log, 'guid')
147                 item.attrib['count'] = '%d' % (len(apply_log) - 1)
148                 item.attrib['value'] = guid
149
150     def store(self, gp_ext_name, attribute, old_val):
151         ''' Store an attribute in the gp_log
152         param gp_ext_name   - Name of the extension applying policy
153         param attribute     - The attribute being modified
154         param old_val       - The value of the attribute prior to policy
155                               application
156         '''
157         if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
158             return None
159         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
160         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
161         assert guid_obj is not None, "gpo guid was not set"
162         ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
163         if ext is None:
164             ext = etree.SubElement(guid_obj, 'gp_ext')
165             ext.attrib['name'] = gp_ext_name
166         attr = ext.find('attribute[@name="%s"]' % attribute)
167         if attr is None:
168             attr = etree.SubElement(ext, 'attribute')
169             attr.attrib['name'] = attribute
170             attr.text = old_val
171
172     def retrieve(self, gp_ext_name, attribute):
173         ''' Retrieve a stored attribute from the gp_log
174         param gp_ext_name   - Name of the extension which applied policy
175         param attribute     - The attribute being retrieved
176         return              - The value of the attribute prior to policy
177                               application
178         '''
179         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
180         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
181         assert guid_obj is not None, "gpo guid was not set"
182         ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
183         if ext is not None:
184             attr = ext.find('attribute[@name="%s"]' % attribute)
185             if attr is not None:
186                 return attr.text
187         return None
188
189     def get_applied_guids(self):
190         ''' Return a list of applied ext guids
191         return              - List of guids for gpos that have applied settings
192                               to the system.
193         '''
194         guids = []
195         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
196         if user_obj is not None:
197             apply_log = user_obj.find('applylog')
198             if apply_log is not None:
199                 guid_objs = apply_log.findall('guid[@count]')
200                 guids_by_count = [(g.get('count'), g.get('value'))
201                                   for g in guid_objs]
202                 guids_by_count.sort(reverse=True)
203                 guids.extend(guid for count, guid in guids_by_count)
204         return guids
205
206     def get_applied_settings(self, guids):
207         ''' Return a list of applied ext guids
208         return              - List of tuples containing the guid of a gpo, then
209                               a dictionary of policies and their values prior
210                               policy application. These are sorted so that the
211                               most recently applied settings are removed first.
212         '''
213         ret = []
214         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
215         for guid in guids:
216             guid_settings = user_obj.find('guid[@value="%s"]' % guid)
217             exts = guid_settings.findall('gp_ext')
218             settings = {}
219             for ext in exts:
220                 attr_dict = {}
221                 attrs = ext.findall('attribute')
222                 for attr in attrs:
223                     attr_dict[attr.attrib['name']] = attr.text
224                 settings[ext.attrib['name']] = attr_dict
225             ret.append((guid, settings))
226         return ret
227
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
231                               attribute
232         param attribute     - attribute to remove
233         '''
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)
238         if ext is not None:
239             attr = ext.find('attribute[@name="%s"]' % attribute)
240             if attr is not None:
241                 ext.remove(attr)
242                 if len(ext) == 0:
243                     guid_obj.remove(ext)
244
245     def commit(self):
246         ''' Write gp_log changes to disk '''
247         self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
248
249
250 class GPOStorage:
251     def __init__(self, log_file):
252         if os.path.isfile(log_file):
253             self.log = tdb.open(log_file)
254         else:
255             self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT | os.O_RDWR)
256
257     def start(self):
258         self.log.transaction_start()
259
260     def get_int(self, key):
261         try:
262             return int(self.log.get(get_bytes(key)))
263         except TypeError:
264             return None
265
266     def get(self, key):
267         return self.log.get(get_bytes(key))
268
269     def get_gplog(self, user):
270         return gp_log(user, self, self.log.get(get_bytes(user)))
271
272     def store(self, key, val):
273         self.log.store(get_bytes(key), get_bytes(val))
274
275     def cancel(self):
276         self.log.transaction_cancel()
277
278     def delete(self, key):
279         self.log.delete(get_bytes(key))
280
281     def commit(self):
282         self.log.transaction_commit()
283
284     def __del__(self):
285         self.log.close()
286
287
288 class gp_ext(object):
289     __metaclass__ = ABCMeta
290
291     def __init__(self, logger, lp, creds, store):
292         self.logger = logger
293         self.lp = lp
294         self.creds = creds
295         self.gp_db = store.get_gplog(creds.get_username())
296
297     @abstractmethod
298     def process_group_policy(self, deleted_gpo_list, changed_gpo_list):
299         pass
300
301     @abstractmethod
302     def read(self, policy):
303         pass
304
305     def parse(self, afile):
306         local_path = self.lp.cache_path('gpo_cache')
307         data_file = os.path.join(local_path, check_safe_path(afile).upper())
308         if os.path.exists(data_file):
309             return self.read(open(data_file, 'r').read())
310         return None
311
312     @abstractmethod
313     def __str__(self):
314         pass
315
316
317 class gp_ext_setter(object):
318     __metaclass__ = ABCMeta
319
320     def __init__(self, logger, gp_db, lp, creds, attribute, val):
321         self.logger = logger
322         self.attribute = attribute
323         self.val = val
324         self.lp = lp
325         self.creds = creds
326         self.gp_db = gp_db
327
328     def explicit(self):
329         return self.val
330
331     def update_samba(self):
332         (upd_sam, value) = self.mapper().get(self.attribute)
333         upd_sam(value())
334
335     @abstractmethod
336     def mapper(self):
337         pass
338
339     def delete(self):
340         upd_sam, _ = self.mapper().get(self.attribute)
341         upd_sam(self.val)
342
343     @abstractmethod
344     def __str__(self):
345         pass
346
347
348 class gp_inf_ext(gp_ext):
349     def read(self, policy):
350         inf_conf = ConfigParser()
351         inf_conf.optionxform = str
352         try:
353             inf_conf.readfp(StringIO(policy))
354         except:
355             inf_conf.readfp(StringIO(policy.decode('utf-16')))
356         return inf_conf
357
358
359 ''' Fetch the hostname of a writable DC '''
360
361
362 def get_dc_hostname(creds, lp):
363     net = Net(creds=creds, lp=lp)
364     cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
365                                                           nbt.NBT_SERVER_DS))
366     return cldap_ret.pdc_dns_name
367
368
369 ''' Fetch a list of GUIDs for applicable GPOs '''
370
371
372 def get_gpo_list(dc_hostname, creds, lp):
373     gpos = []
374     ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
375     if ads.connect():
376         gpos = ads.get_gpo_list(creds.get_username())
377     return gpos
378
379
380 def cache_gpo_dir(conn, cache, sub_dir):
381     loc_sub_dir = sub_dir.upper()
382     local_dir = os.path.join(cache, loc_sub_dir)
383     try:
384         os.makedirs(local_dir, mode=0o755)
385     except OSError as e:
386         if e.errno != errno.EEXIST:
387             raise
388     for fdata in conn.list(sub_dir):
389         if fdata['attrib'] & smb.FILE_ATTRIBUTE_DIRECTORY:
390             cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
391         else:
392             local_name = fdata['name'].upper()
393             f = NamedTemporaryFile(delete=False, dir=local_dir)
394             fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
395             f.write(conn.loadfile(fname))
396             f.close()
397             os.rename(f.name, os.path.join(local_dir, local_name))
398
399
400 def check_safe_path(path):
401     dirs = re.split('/|\\\\', path)
402     if 'sysvol' in path:
403         dirs = dirs[dirs.index('sysvol') + 1:]
404     if '..' not in dirs:
405         return os.path.join(*dirs)
406     raise OSError(path)
407
408
409 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
410     conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds, sign=True)
411     cache_path = lp.cache_path('gpo_cache')
412     for gpo in gpos:
413         if not gpo.file_sys_path:
414             continue
415         cache_gpo_dir(conn, cache_path, check_safe_path(gpo.file_sys_path))
416
417
418 def get_deleted_gpos_list(gp_db, gpos):
419     applied_gpos = gp_db.get_applied_guids()
420     current_guids = set([p.name for p in gpos])
421     deleted_gpos = [guid for guid in applied_gpos if guid not in current_guids]
422     return gp_db.get_applied_settings(deleted_gpos)
423
424 def gpo_version(lp, path):
425     # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
426     # read from the gpo client cache.
427     gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
428     return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
429
430
431 def apply_gp(lp, creds, logger, store, gp_extensions, force=False):
432     gp_db = store.get_gplog(creds.get_username())
433     dc_hostname = get_dc_hostname(creds, lp)
434     gpos = get_gpo_list(dc_hostname, creds, lp)
435     del_gpos = get_deleted_gpos_list(gp_db, gpos)
436     try:
437         check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
438     except:
439         logger.error('Failed downloading gpt cache from \'%s\' using SMB'
440                      % dc_hostname)
441         return
442
443     if force:
444         changed_gpos = gpos
445         gp_db.state(GPOSTATE.ENFORCE)
446     else:
447         changed_gpos = []
448         for gpo_obj in gpos:
449             if not gpo_obj.file_sys_path:
450                 continue
451             guid = gpo_obj.name
452             path = check_safe_path(gpo_obj.file_sys_path).upper()
453             version = gpo_version(lp, path)
454             if version != store.get_int(guid):
455                 logger.info('GPO %s has changed' % guid)
456                 changed_gpos.append(gpo_obj)
457         gp_db.state(GPOSTATE.APPLY)
458
459     store.start()
460     for ext in gp_extensions:
461         try:
462             ext.process_group_policy(del_gpos, changed_gpos)
463         except Exception as e:
464             logger.error('Failed to apply extension  %s' % str(ext))
465             logger.error('Message was: ' + str(e))
466             continue
467     for gpo_obj in gpos:
468         if not gpo_obj.file_sys_path:
469             continue
470         guid = gpo_obj.name
471         path = check_safe_path(gpo_obj.file_sys_path).upper()
472         version = gpo_version(lp, path)
473         store.store(guid, '%i' % version)
474     store.commit()
475
476
477 def unapply_gp(lp, creds, logger, store, gp_extensions):
478     gp_db = store.get_gplog(creds.get_username())
479     gp_db.state(GPOSTATE.UNAPPLY)
480     # Treat all applied gpos as deleted
481     del_gpos = gp_db.get_applied_settings(gp_db.get_applied_guids())
482     store.start()
483     for ext in gp_extensions:
484         try:
485             ext.process_group_policy(del_gpos, [])
486         except Exception as e:
487             logger.error('Failed to unapply extension  %s' % str(ext))
488             logger.error('Message was: ' + str(e))
489             continue
490     store.commit()
491
492
493 def parse_gpext_conf(smb_conf):
494     lp = LoadParm()
495     if smb_conf is not None:
496         lp.load(smb_conf)
497     else:
498         lp.load_default()
499     ext_conf = lp.state_path('gpext.conf')
500     parser = ConfigParser()
501     parser.read(ext_conf)
502     return lp, parser
503
504
505 def atomic_write_conf(lp, parser):
506     ext_conf = lp.state_path('gpext.conf')
507     with NamedTemporaryFile(mode="w+", delete=False, dir=os.path.dirname(ext_conf)) as f:
508         parser.write(f)
509         os.rename(f.name, ext_conf)
510
511
512 def check_guid(guid):
513     # Check for valid guid with curly braces
514     if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
515         return False
516     try:
517         UUID(guid, version=4)
518     except ValueError:
519         return False
520     return True
521
522
523 def register_gp_extension(guid, name, path,
524                           smb_conf=None, machine=True, user=True):
525     # Check that the module exists
526     if not os.path.exists(path):
527         return False
528     if not check_guid(guid):
529         return False
530
531     lp, parser = parse_gpext_conf(smb_conf)
532     if guid not in parser.sections():
533         parser.add_section(guid)
534     parser.set(guid, 'DllName', path)
535     parser.set(guid, 'ProcessGroupPolicy', name)
536     parser.set(guid, 'NoMachinePolicy', "0" if machine else "1")
537     parser.set(guid, 'NoUserPolicy', "0" if user else "1")
538
539     atomic_write_conf(lp, parser)
540
541     return True
542
543
544 def list_gp_extensions(smb_conf=None):
545     _, parser = parse_gpext_conf(smb_conf)
546     results = {}
547     for guid in parser.sections():
548         results[guid] = {}
549         results[guid]['DllName'] = parser.get(guid, 'DllName')
550         results[guid]['ProcessGroupPolicy'] = \
551             parser.get(guid, 'ProcessGroupPolicy')
552         results[guid]['MachinePolicy'] = \
553             not int(parser.get(guid, 'NoMachinePolicy'))
554         results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
555     return results
556
557
558 def unregister_gp_extension(guid, smb_conf=None):
559     if not check_guid(guid):
560         return False
561
562     lp, parser = parse_gpext_conf(smb_conf)
563     if guid in parser.sections():
564         parser.remove_section(guid)
565
566     atomic_write_conf(lp, parser)
567
568     return True