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