gpo: avoid quadratic behaviour in guid retrieval
[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 ConfigParser 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 apply_log_pop(self):
150         ''' Pop a GPO guid from the applylog
151         return              - last applied GPO guid
152
153         Removes the GPO guid last added to the list, which is the most recently
154         applied GPO.
155         '''
156         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
157         apply_log = user_obj.find('applylog')
158         if apply_log is not None:
159             ret = apply_log.find('guid[@count="%d"]' % (len(apply_log) - 1))
160             if ret is not None:
161                 apply_log.remove(ret)
162                 return ret.attrib['value']
163             if len(apply_log) == 0 and apply_log in user_obj:
164                 user_obj.remove(apply_log)
165         return None
166
167     def store(self, gp_ext_name, attribute, old_val):
168         ''' Store an attribute in the gp_log
169         param gp_ext_name   - Name of the extension applying policy
170         param attribute     - The attribute being modified
171         param old_val       - The value of the attribute prior to policy
172                               application
173         '''
174         if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
175             return None
176         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
177         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
178         assert guid_obj is not None, "gpo guid was not set"
179         ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
180         if ext is None:
181             ext = etree.SubElement(guid_obj, 'gp_ext')
182             ext.attrib['name'] = gp_ext_name
183         attr = ext.find('attribute[@name="%s"]' % attribute)
184         if attr is None:
185             attr = etree.SubElement(ext, 'attribute')
186             attr.attrib['name'] = attribute
187             attr.text = old_val
188
189     def retrieve(self, gp_ext_name, attribute):
190         ''' Retrieve a stored attribute from the gp_log
191         param gp_ext_name   - Name of the extension which applied policy
192         param attribute     - The attribute being retrieved
193         return              - The value of the attribute prior to policy
194                               application
195         '''
196         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
197         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
198         assert guid_obj is not None, "gpo guid was not set"
199         ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
200         if ext is not None:
201             attr = ext.find('attribute[@name="%s"]' % attribute)
202             if attr is not None:
203                 return attr.text
204         return None
205
206     def list(self, gp_extensions):
207         ''' Return a list of attributes, their previous values, and functions
208             to set them
209         param gp_extensions - list of extension objects, for retrieving attr to
210                               func mappings
211         return              - list of (attr, value, apply_func) tuples for
212                               unapplying policy
213         '''
214         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
215         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
216         assert guid_obj is not None, "gpo guid was not set"
217         ret = []
218         data_maps = {}
219         for gp_ext in gp_extensions:
220             data_maps.update(gp_ext.apply_map())
221         exts = guid_obj.findall('gp_ext')
222         if exts is not None:
223             for ext in exts:
224                 attrs = ext.findall('attribute')
225                 for attr in attrs:
226                     func = None
227                     if attr.attrib['name'] in data_maps[ext.attrib['name']]:
228                         func = data_maps[ext.attrib['name']][attr.attrib['name']][-1]
229                     else:
230                         for dmap in data_maps[ext.attrib['name']].keys():
231                             if data_maps[ext.attrib['name']][dmap][0] == \
232                                attr.attrib['name']:
233                                 func = data_maps[ext.attrib['name']][dmap][-1]
234                                 break
235                     ret.append((attr.attrib['name'], attr.text, func))
236         return ret
237
238     def get_applied_guids(self):
239         ''' Return a list of applied ext guids
240         return              - List of guids for gpos that have applied settings
241                               to the system.
242         '''
243         guids = []
244         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
245         if user_obj is not None:
246             apply_log = user_obj.find('applylog')
247             if apply_log is not None:
248                 guid_objs = apply_log.findall('guid[@count]')
249                 guids_by_count = [(g.get('count'), g.get('value'))
250                                   for g in guid_objs]
251                 guids_by_count.sort(reverse=True)
252                 guids.extend(guid for count, guid in guids_by_count)
253         return guids
254
255     def get_applied_settings(self, guids):
256         ''' Return a list of applied ext guids
257         return              - List of tuples containing the guid of a gpo, then
258                               a dictionary of policies and their values prior
259                               policy application. These are sorted so that the
260                               most recently applied settings are removed first.
261         '''
262         ret = []
263         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
264         for guid in guids:
265             guid_settings = user_obj.find('guid[@value="%s"]' % guid)
266             exts = guid_settings.findall('gp_ext')
267             settings = {}
268             for ext in exts:
269                 attr_dict = {}
270                 attrs = ext.findall('attribute')
271                 for attr in attrs:
272                     attr_dict[attr.attrib['name']] = attr.text
273                 settings[ext.attrib['name']] = attr_dict
274             ret.append((guid, settings))
275         return ret
276
277     def delete(self, gp_ext_name, attribute):
278         ''' Remove an attribute from the gp_log
279         param gp_ext_name   - name of extension from which to remove the
280                               attribute
281         param attribute     - attribute to remove
282         '''
283         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
284         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
285         assert guid_obj is not None, "gpo guid was not set"
286         ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
287         if ext is not None:
288             attr = ext.find('attribute[@name="%s"]' % attribute)
289             if attr is not None:
290                 ext.remove(attr)
291                 if len(ext) == 0:
292                     guid_obj.remove(ext)
293
294     def commit(self):
295         ''' Write gp_log changes to disk '''
296         self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
297
298
299 class GPOStorage:
300     def __init__(self, log_file):
301         if os.path.isfile(log_file):
302             self.log = tdb.open(log_file)
303         else:
304             self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT |os.O_RDWR)
305
306     def start(self):
307         self.log.transaction_start()
308
309     def get_int(self, key):
310         try:
311             return int(self.log.get(key))
312         except TypeError:
313             return None
314
315     def get(self, key):
316         return self.log.get(key)
317
318     def get_gplog(self, user):
319         return gp_log(user, self, self.log.get(user))
320
321     def store(self, key, val):
322         self.log.store(key, val)
323
324     def cancel(self):
325         self.log.transaction_cancel()
326
327     def delete(self, key):
328         self.log.delete(key)
329
330     def commit(self):
331         self.log.transaction_commit()
332
333     def __del__(self):
334         self.log.close()
335
336
337 class gp_ext(object):
338     __metaclass__ = ABCMeta
339
340     def __init__(self, logger, lp, creds, store):
341         self.logger = logger
342         self.lp = lp
343         self.creds = creds
344         self.gp_db = store.get_gplog(creds.get_username())
345
346     @abstractmethod
347     def process_group_policy(self, deleted_gpo_list, changed_gpo_list):
348         pass
349
350     @abstractmethod
351     def read(self, policy):
352         pass
353
354     def parse(self, afile):
355         local_path = self.lp.cache_path('gpo_cache')
356         data_file = os.path.join(local_path, check_safe_path(afile).upper())
357         if os.path.exists(data_file):
358             return self.read(open(data_file, 'r').read())
359         return None
360
361     @abstractmethod
362     def __str__(self):
363         pass
364
365
366 class gp_ext_setter(object):
367     __metaclass__ = ABCMeta
368
369     def __init__(self, logger, gp_db, lp, creds, attribute, val):
370         self.logger = logger
371         self.attribute = attribute
372         self.val = val
373         self.lp = lp
374         self.creds = creds
375         self.gp_db = gp_db
376
377     def explicit(self):
378         return self.val
379
380     def update_samba(self):
381         (upd_sam, value) = self.mapper().get(self.attribute)
382         upd_sam(value())
383
384     @abstractmethod
385     def mapper(self):
386         pass
387
388     @abstractmethod
389     def __str__(self):
390         pass
391
392
393 class gp_inf_ext(gp_ext):
394     def read(self, policy):
395         inf_conf = ConfigParser()
396         inf_conf.optionxform = str
397         try:
398             inf_conf.readfp(StringIO(policy))
399         except:
400             inf_conf.readfp(StringIO(policy.decode('utf-16')))
401         return inf_conf
402
403
404 ''' Fetch the hostname of a writable DC '''
405
406
407 def get_dc_hostname(creds, lp):
408     net = Net(creds=creds, lp=lp)
409     cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
410                                                           nbt.NBT_SERVER_DS))
411     return cldap_ret.pdc_dns_name
412
413
414 ''' Fetch a list of GUIDs for applicable GPOs '''
415
416
417 def get_gpo_list(dc_hostname, creds, lp):
418     gpos = []
419     ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
420     if ads.connect():
421         gpos = ads.get_gpo_list(creds.get_username())
422     return gpos
423
424
425 def cache_gpo_dir(conn, cache, sub_dir):
426     loc_sub_dir = sub_dir.upper()
427     local_dir = os.path.join(cache, loc_sub_dir)
428     try:
429         os.makedirs(local_dir, mode=0o755)
430     except OSError as e:
431         if e.errno != errno.EEXIST:
432             raise
433     for fdata in conn.list(sub_dir):
434         if fdata['attrib'] & smb.FILE_ATTRIBUTE_DIRECTORY:
435             cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
436         else:
437             local_name = fdata['name'].upper()
438             f = NamedTemporaryFile(delete=False, dir=local_dir)
439             fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
440             f.write(conn.loadfile(fname))
441             f.close()
442             os.rename(f.name, os.path.join(local_dir, local_name))
443
444
445 def check_safe_path(path):
446     dirs = re.split('/|\\\\', path)
447     if 'sysvol' in path:
448         dirs = dirs[dirs.index('sysvol') + 1:]
449     if '..' not in dirs:
450         return os.path.join(*dirs)
451     raise OSError(path)
452
453
454 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
455     conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds, sign=True)
456     cache_path = lp.cache_path('gpo_cache')
457     for gpo in gpos:
458         if not gpo.file_sys_path:
459             continue
460         cache_gpo_dir(conn, cache_path, check_safe_path(gpo.file_sys_path))
461
462
463 def gpo_version(lp, path):
464     # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
465     # read from the gpo client cache.
466     gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
467     return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
468
469
470 def apply_gp(lp, creds, logger, store, gp_extensions):
471     gp_db = store.get_gplog(creds.get_username())
472     dc_hostname = get_dc_hostname(creds, lp)
473     gpos = get_gpo_list(dc_hostname, creds, lp)
474     try:
475         check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
476     except:
477         logger.error('Failed downloading gpt cache from \'%s\' using SMB'
478                      % dc_hostname)
479         return
480
481     changed_gpos = []
482     for gpo_obj in gpos:
483         if not gpo_obj.file_sys_path:
484             continue
485         guid = gpo_obj.name
486         path = check_safe_path(gpo_obj.file_sys_path).upper()
487         version = gpo_version(lp, path)
488         if version != store.get_int(guid):
489             logger.info('GPO %s has changed' % guid)
490             changed_gpos.append(gpo_obj)
491
492     store.start()
493     for ext in gp_extensions:
494         try:
495             ext.process_group_policy([], changed_gpos)
496         except Exception as e:
497             logger.error('Failed to apply extension  %s' % str(ext))
498             logger.error('Message was: ' + str(e))
499             continue
500     for gpo_obj in gpos:
501         if not gpo_obj.file_sys_path:
502             continue
503         guid = gpo_obj.name
504         path = check_safe_path(gpo_obj.file_sys_path).upper()
505         version = gpo_version(lp, path)
506         store.store(guid, '%i' % version)
507     store.commit()
508
509
510 def unapply_log(gp_db):
511     while True:
512         item = gp_db.apply_log_pop()
513         if item:
514             yield item
515         else:
516             break
517
518
519 def unapply_gp(lp, creds, logger, store, gp_extensions):
520     gp_db = store.get_gplog(creds.get_username())
521     gp_db.state(GPOSTATE.UNAPPLY)
522     for gpo_guid in unapply_log(gp_db):
523         gp_db.set_guid(gpo_guid)
524         unapply_attributes = gp_db.list(gp_extensions)
525         for attr in unapply_attributes:
526             attr_obj = attr[-1](logger, gp_db, lp, attr[0], attr[1])
527             attr_obj.mapper()[attr[0]][0](attr[1])  # Set the old value
528             gp_db.delete(str(attr_obj), attr[0])
529         gp_db.commit()
530
531
532 def parse_gpext_conf(smb_conf):
533     lp = LoadParm()
534     if smb_conf is not None:
535         lp.load(smb_conf)
536     else:
537         lp.load_default()
538     ext_conf = lp.state_path('gpext.conf')
539     parser = ConfigParser()
540     parser.read(ext_conf)
541     return lp, parser
542
543
544 def atomic_write_conf(lp, parser):
545     ext_conf = lp.state_path('gpext.conf')
546     with NamedTemporaryFile(delete=False, dir=os.path.dirname(ext_conf)) as f:
547         parser.write(f)
548         os.rename(f.name, ext_conf)
549
550
551 def check_guid(guid):
552     # Check for valid guid with curly braces
553     if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
554         return False
555     try:
556         UUID(guid, version=4)
557     except ValueError:
558         return False
559     return True
560
561
562 def register_gp_extension(guid, name, path,
563                           smb_conf=None, machine=True, user=True):
564     # Check that the module exists
565     if not os.path.exists(path):
566         return False
567     if not check_guid(guid):
568         return False
569
570     lp, parser = parse_gpext_conf(smb_conf)
571     if guid not in parser.sections():
572         parser.add_section(guid)
573     parser.set(guid, 'DllName', path)
574     parser.set(guid, 'ProcessGroupPolicy', name)
575     parser.set(guid, 'NoMachinePolicy', 0 if machine else 1)
576     parser.set(guid, 'NoUserPolicy', 0 if user else 1)
577
578     atomic_write_conf(lp, parser)
579
580     return True
581
582
583 def list_gp_extensions(smb_conf=None):
584     _, parser = parse_gpext_conf(smb_conf)
585     results = {}
586     for guid in parser.sections():
587         results[guid] = {}
588         results[guid]['DllName'] = parser.get(guid, 'DllName')
589         results[guid]['ProcessGroupPolicy'] = \
590             parser.get(guid, 'ProcessGroupPolicy')
591         results[guid]['MachinePolicy'] = \
592             not int(parser.get(guid, 'NoMachinePolicy'))
593         results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
594     return results
595
596
597 def unregister_gp_extension(guid, smb_conf=None):
598     if not check_guid(guid):
599         return False
600
601     lp, parser = parse_gpext_conf(smb_conf)
602     if guid in parser.sections():
603         parser.remove_section(guid)
604
605     atomic_write_conf(lp, parser)
606
607     return True