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