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