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