22f57c0dd95a47da6b318c3e6b55b8ad3fc5b228
[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 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
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 delete(self, gp_ext_name, attribute):
239         ''' Remove an attribute from the gp_log
240         param gp_ext_name   - name of extension from which to remove the
241                               attribute
242         param attribute     - attribute to remove
243         '''
244         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
245         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
246         assert guid_obj is not None, "gpo guid was not set"
247         ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
248         if ext is not None:
249             attr = ext.find('attribute[@name="%s"]' % attribute)
250             if attr is not None:
251                 ext.remove(attr)
252                 if len(ext) == 0:
253                     guid_obj.remove(ext)
254
255     def commit(self):
256         ''' Write gp_log changes to disk '''
257         self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
258
259
260 class GPOStorage:
261     def __init__(self, log_file):
262         if os.path.isfile(log_file):
263             self.log = tdb.open(log_file)
264         else:
265             self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT |os.O_RDWR)
266
267     def start(self):
268         self.log.transaction_start()
269
270     def get_int(self, key):
271         try:
272             return int(self.log.get(key))
273         except TypeError:
274             return None
275
276     def get(self, key):
277         return self.log.get(key)
278
279     def get_gplog(self, user):
280         return gp_log(user, self, self.log.get(user))
281
282     def store(self, key, val):
283         self.log.store(key, val)
284
285     def cancel(self):
286         self.log.transaction_cancel()
287
288     def delete(self, key):
289         self.log.delete(key)
290
291     def commit(self):
292         self.log.transaction_commit()
293
294     def __del__(self):
295         self.log.close()
296
297
298 class gp_ext(object):
299     __metaclass__ = ABCMeta
300
301     def __init__(self, logger):
302         self.logger = logger
303
304     @abstractmethod
305     def list(self, rootpath):
306         pass
307
308     @abstractmethod
309     def apply_map(self):
310         pass
311
312     @abstractmethod
313     def read(self, policy):
314         pass
315
316     def parse(self, afile, ldb, gp_db, lp):
317         self.ldb = ldb
318         self.gp_db = gp_db
319         self.lp = lp
320
321         local_path = self.lp.cache_path('gpo_cache')
322         data_file = os.path.join(local_path, check_safe_path(afile).upper())
323         if os.path.exists(data_file):
324             return self.read(open(data_file, 'r').read())
325         return None
326
327     @abstractmethod
328     def __str__(self):
329         pass
330
331
332 class gp_ext_setter():
333     __metaclass__ = ABCMeta
334
335     def __init__(self, logger, ldb, gp_db, lp, attribute, val):
336         self.logger = logger
337         self.ldb = ldb
338         self.attribute = attribute
339         self.val = val
340         self.lp = lp
341         self.gp_db = gp_db
342
343     def explicit(self):
344         return self.val
345
346     def update_samba(self):
347         (upd_sam, value) = self.mapper().get(self.attribute)
348         upd_sam(value())
349
350     @abstractmethod
351     def mapper(self):
352         pass
353
354     @abstractmethod
355     def __str__(self):
356         pass
357
358
359 class gp_inf_ext(gp_ext):
360     @abstractmethod
361     def list(self, rootpath):
362         pass
363
364     @abstractmethod
365     def apply_map(self):
366         pass
367
368     def read(self, policy):
369         ret = False
370         inftable = self.apply_map()
371
372         current_section = None
373
374         # So here we would declare a boolean,
375         # that would get changed to TRUE.
376         #
377         # If at any point in time a GPO was applied,
378         # then we return that boolean at the end.
379
380         inf_conf = ConfigParser()
381         inf_conf.optionxform = str
382         try:
383             inf_conf.readfp(StringIO(policy))
384         except:
385             inf_conf.readfp(StringIO(policy.decode('utf-16')))
386
387         for section in inf_conf.sections():
388             current_section = inftable.get(section)
389             if not current_section:
390                 continue
391             for key, value in inf_conf.items(section):
392                 if current_section.get(key):
393                     (att, setter) = current_section.get(key)
394                     value = value.encode('ascii', 'ignore')
395                     ret = True
396                     setter(self.logger, self.ldb, self.gp_db, self.lp, att,
397                            value).update_samba()
398                     self.gp_db.commit()
399         return ret
400
401     @abstractmethod
402     def __str__(self):
403         pass
404
405
406 ''' Fetch the hostname of a writable DC '''
407
408
409 def get_dc_hostname(creds, lp):
410     net = Net(creds=creds, lp=lp)
411     cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
412                                                           nbt.NBT_SERVER_DS))
413     return cldap_ret.pdc_dns_name
414
415
416 ''' Fetch a list of GUIDs for applicable GPOs '''
417
418
419 def get_gpo_list(dc_hostname, creds, lp):
420     gpos = []
421     ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
422     if ads.connect():
423         gpos = ads.get_gpo_list(creds.get_username())
424     return gpos
425
426
427 def cache_gpo_dir(conn, cache, sub_dir):
428     loc_sub_dir = sub_dir.upper()
429     local_dir = os.path.join(cache, loc_sub_dir)
430     try:
431         os.makedirs(local_dir, mode=0o755)
432     except OSError as e:
433         if e.errno != errno.EEXIST:
434             raise
435     for fdata in conn.list(sub_dir):
436         if fdata['attrib'] & smb.FILE_ATTRIBUTE_DIRECTORY:
437             cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
438         else:
439             local_name = fdata['name'].upper()
440             f = NamedTemporaryFile(delete=False, dir=local_dir)
441             fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
442             f.write(conn.loadfile(fname))
443             f.close()
444             os.rename(f.name, os.path.join(local_dir, local_name))
445
446
447 def check_safe_path(path):
448     dirs = re.split('/|\\\\', path)
449     if 'sysvol' in path:
450         dirs = dirs[dirs.index('sysvol') + 1:]
451     if not '..' in dirs:
452         return os.path.join(*dirs)
453     raise OSError(path)
454
455
456 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
457     conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds, sign=True)
458     cache_path = lp.cache_path('gpo_cache')
459     for gpo in gpos:
460         if not gpo.file_sys_path:
461             continue
462         cache_gpo_dir(conn, cache_path, check_safe_path(gpo.file_sys_path))
463
464
465 def gpo_version(lp, path):
466     # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
467     # read from the gpo client cache.
468     gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
469     return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
470
471
472 def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
473     gp_db = store.get_gplog(creds.get_username())
474     dc_hostname = get_dc_hostname(creds, lp)
475     gpos = get_gpo_list(dc_hostname, creds, lp)
476     try:
477         check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
478     except:
479         logger.error('Failed downloading gpt cache from \'%s\' using SMB'
480                      % dc_hostname)
481         return
482
483     for gpo_obj in gpos:
484         guid = gpo_obj.name
485         if guid == 'Local Policy':
486             continue
487         path = os.path.join(lp.get('realm'), 'Policies', guid).upper()
488         version = gpo_version(lp, path)
489         if version != store.get_int(guid):
490             logger.info('GPO %s has changed' % guid)
491             gp_db.state(GPOSTATE.APPLY)
492         else:
493             gp_db.state(GPOSTATE.ENFORCE)
494         gp_db.set_guid(guid)
495         store.start()
496         for ext in gp_extensions:
497             try:
498                 ext.parse(ext.list(path), test_ldb, gp_db, lp)
499             except Exception as e:
500                 logger.error('Failed to parse gpo %s for extension %s' %
501                              (guid, str(ext)))
502                 logger.error('Message was: ' + str(e))
503                 store.cancel()
504                 continue
505         store.store(guid, '%i' % version)
506         store.commit()
507
508
509 def unapply_log(gp_db):
510     while True:
511         item = gp_db.apply_log_pop()
512         if item:
513             yield item
514         else:
515             break
516
517
518 def unapply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
519     gp_db = store.get_gplog(creds.get_username())
520     gp_db.state(GPOSTATE.UNAPPLY)
521     for gpo_guid in unapply_log(gp_db):
522         gp_db.set_guid(gpo_guid)
523         unapply_attributes = gp_db.list(gp_extensions)
524         for attr in unapply_attributes:
525             attr_obj = attr[-1](logger, test_ldb, gp_db, lp, attr[0], attr[1])
526             attr_obj.mapper()[attr[0]][0](attr[1])  # Set the old value
527             gp_db.delete(str(attr_obj), attr[0])
528         gp_db.commit()
529
530
531 def parse_gpext_conf(smb_conf):
532     lp = LoadParm()
533     if smb_conf is not None:
534         lp.load(smb_conf)
535     else:
536         lp.load_default()
537     ext_conf = lp.state_path('gpext.conf')
538     parser = ConfigParser()
539     parser.read(ext_conf)
540     return lp, parser
541
542
543 def atomic_write_conf(lp, parser):
544     ext_conf = lp.state_path('gpext.conf')
545     with NamedTemporaryFile(delete=False, dir=os.path.dirname(ext_conf)) as f:
546         parser.write(f)
547         os.rename(f.name, ext_conf)
548
549
550 def check_guid(guid):
551     # Check for valid guid with curly braces
552     if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
553         return False
554     try:
555         UUID(guid, version=4)
556     except ValueError:
557         return False
558     return True
559
560
561 def register_gp_extension(guid, name, path,
562                           smb_conf=None, machine=True, user=True):
563     # Check that the module exists
564     if not os.path.exists(path):
565         return False
566     if not check_guid(guid):
567         return False
568
569     lp, parser = parse_gpext_conf(smb_conf)
570     if not guid in parser.sections():
571         parser.add_section(guid)
572     parser.set(guid, 'DllName', path)
573     parser.set(guid, 'ProcessGroupPolicy', name)
574     parser.set(guid, 'NoMachinePolicy', 0 if machine else 1)
575     parser.set(guid, 'NoUserPolicy', 0 if user else 1)
576
577     atomic_write_conf(lp, parser)
578
579     return True
580
581
582 def list_gp_extensions(smb_conf=None):
583     _, parser = parse_gpext_conf(smb_conf)
584     results = {}
585     for guid in parser.sections():
586         results[guid] = {}
587         results[guid]['DllName'] = parser.get(guid, 'DllName')
588         results[guid]['ProcessGroupPolicy'] = \
589             parser.get(guid, 'ProcessGroupPolicy')
590         results[guid]['MachinePolicy'] = \
591             not int(parser.get(guid, 'NoMachinePolicy'))
592         results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
593     return results
594
595
596 def unregister_gp_extension(guid, smb_conf=None):
597     if not check_guid(guid):
598         return False
599
600     lp, parser = parse_gpext_conf(smb_conf)
601     if guid in parser.sections():
602         parser.remove_section(guid)
603
604     atomic_write_conf(lp, parser)
605
606     return True