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