gpo: Add the winbind call to gpupdate
[kai/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 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
29 try:
30     from enum import Enum
31     GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
32 except ImportError:
33     class GPOSTATE:
34         APPLY = 1
35         ENFORCE = 2
36         UNAPPLY = 3
37
38 class gp_log:
39     ''' Log settings overwritten by gpo apply
40     The gp_log is an xml file that stores a history of gpo changes (and the
41     original setting value).
42
43     The log is organized like so:
44
45 <gp>
46     <user name="KDC-1$">
47         <applylog>
48             <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
49         </applylog>
50         <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
51             <gp_ext name="System Access">
52                 <attribute name="minPwdAge">-864000000000</attribute>
53                 <attribute name="maxPwdAge">-36288000000000</attribute>
54                 <attribute name="minPwdLength">7</attribute>
55                 <attribute name="pwdProperties">1</attribute>
56             </gp_ext>
57             <gp_ext name="Kerberos Policy">
58                 <attribute name="ticket_lifetime">1d</attribute>
59                 <attribute name="renew_lifetime" />
60                 <attribute name="clockskew">300</attribute>
61             </gp_ext>
62         </guid>
63     </user>
64 </gp>
65
66     Each guid value contains a list of extensions, which contain a list of
67     attributes. The guid value represents a GPO. The attributes are the values
68     of those settings prior to the application of the GPO.
69     The list of guids is enclosed within a user name, which represents the user
70     the settings were applied to. This user may be the samaccountname of the
71     local computer, which implies that these are machine policies.
72     The applylog keeps track of the order in which the GPOs were applied, so
73     that they can be rolled back in reverse, returning the machine to the state
74     prior to policy application.
75     '''
76     def __init__(self, user, gpostore, db_log=None):
77         ''' Initialize the gp_log
78         param user          - the username (or machine name) that policies are
79                               being applied to
80         param gpostore      - the GPOStorage obj which references the tdb which
81                               contains gp_logs
82         param db_log        - (optional) a string to initialize the gp_log
83         '''
84         self._state = GPOSTATE.APPLY
85         self.gpostore = gpostore
86         self.username = user
87         if db_log:
88             self.gpdb = etree.fromstring(db_log)
89         else:
90             self.gpdb = etree.Element('gp')
91         self.user = user
92         user_obj = self.gpdb.find('user[@name="%s"]' % user)
93         if user_obj is None:
94             user_obj = etree.SubElement(self.gpdb, 'user')
95             user_obj.attrib['name'] = user
96
97     def state(self, value):
98         ''' Policy application state
99         param value         - APPLY, ENFORCE, or UNAPPLY
100
101         The behavior of the gp_log depends on whether we are applying policy,
102         enforcing policy, or unapplying policy. During an apply, old settings
103         are recorded in the log. During an enforce, settings are being applied
104         but the gp_log does not change. During an unapply, additions to the log
105         should be ignored (since function calls to apply settings are actually
106         reverting policy), but removals from the log are allowed.
107         '''
108         # If we're enforcing, but we've unapplied, apply instead
109         if value == GPOSTATE.ENFORCE:
110             user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
111             apply_log = user_obj.find('applylog')
112             if apply_log is None or len(apply_log) == 0:
113                 self._state = GPOSTATE.APPLY
114             else:
115                 self._state = value
116         else:
117             self._state = value
118
119     def set_guid(self, guid):
120         ''' Log to a different GPO guid
121         param guid          - guid value of the GPO from which we're applying
122                               policy
123         '''
124         self.guid = guid
125         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
126         obj = user_obj.find('guid[@value="%s"]' % guid)
127         if obj is None:
128             obj = etree.SubElement(user_obj, 'guid')
129             obj.attrib['value'] = guid
130         if self._state == GPOSTATE.APPLY:
131             apply_log = user_obj.find('applylog')
132             if apply_log is None:
133                 apply_log = etree.SubElement(user_obj, 'applylog')
134             item = etree.SubElement(apply_log, 'guid')
135             item.attrib['count'] = '%d' % (len(apply_log)-1)
136             item.attrib['value'] = guid
137
138     def apply_log_pop(self):
139         ''' Pop a GPO guid from the applylog
140         return              - last applied GPO guid
141
142         Removes the GPO guid last added to the list, which is the most recently
143         applied GPO.
144         '''
145         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
146         apply_log = user_obj.find('applylog')
147         if apply_log is not None:
148             ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
149             if ret is not None:
150                 apply_log.remove(ret)
151                 return ret.attrib['value']
152             if len(apply_log) == 0 and apply_log in user_obj:
153                 user_obj.remove(apply_log)
154         return None
155
156     def store(self, gp_ext_name, attribute, old_val):
157         ''' Store an attribute in the gp_log
158         param gp_ext_name   - Name of the extension applying policy
159         param attribute     - The attribute being modified
160         param old_val       - The value of the attribute prior to policy
161                               application
162         '''
163         if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
164             return None
165         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
166         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
167         assert guid_obj is not None, "gpo guid was not set"
168         ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
169         if ext is None:
170             ext = etree.SubElement(guid_obj, 'gp_ext')
171             ext.attrib['name'] = gp_ext_name
172         attr = ext.find('attribute[@name="%s"]' % attribute)
173         if attr is None:
174             attr = etree.SubElement(ext, 'attribute')
175             attr.attrib['name'] = attribute
176             attr.text = old_val
177
178     def retrieve(self, gp_ext_name, attribute):
179         ''' Retrieve a stored attribute from the gp_log
180         param gp_ext_name   - Name of the extension which applied policy
181         param attribute     - The attribute being retrieved
182         return              - The value of the attribute prior to policy
183                               application
184         '''
185         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
186         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
187         assert guid_obj is not None, "gpo guid was not set"
188         ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
189         if ext is not None:
190             attr = ext.find('attribute[@name="%s"]' % attribute)
191             if attr is not None:
192                 return attr.text
193         return None
194
195     def list(self, gp_extensions):
196         ''' Return a list of attributes, their previous values, and functions
197             to set them
198         param gp_extensions - list of extension objects, for retrieving attr to
199                               func mappings
200         return              - list of (attr, value, apply_func) tuples for
201                               unapplying policy
202         '''
203         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
204         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
205         assert guid_obj is not None, "gpo guid was not set"
206         ret = []
207         data_maps = {}
208         for gp_ext in gp_extensions:
209             data_maps.update(gp_ext.apply_map())
210         exts = guid_obj.findall('gp_ext')
211         if exts is not None:
212             for ext in exts:
213                 attrs = ext.findall('attribute')
214                 for attr in attrs:
215                     func = None
216                     if attr.attrib['name'] in data_maps[ext.attrib['name']]:
217                         func = data_maps[ext.attrib['name']]\
218                                [attr.attrib['name']][-1]
219                     else:
220                         for dmap in data_maps[ext.attrib['name']].keys():
221                             if data_maps[ext.attrib['name']][dmap][0] == \
222                                attr.attrib['name']:
223                                 func = data_maps[ext.attrib['name']][dmap][-1]
224                                 break
225                     ret.append((attr.attrib['name'], attr.text, func))
226         return ret
227
228     def delete(self, gp_ext_name, attribute):
229         ''' Remove an attribute from the gp_log
230         param gp_ext_name   - name of extension from which to remove the
231                               attribute
232         param attribute     - attribute to remove
233         '''
234         user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
235         guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
236         assert guid_obj is not None, "gpo guid was not set"
237         ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
238         if ext is not None:
239             attr = ext.find('attribute[@name="%s"]' % attribute)
240             if attr is not None:
241                 ext.remove(attr)
242                 if len(ext) == 0:
243                     guid_obj.remove(ext)
244
245     def commit(self):
246         ''' Write gp_log changes to disk '''
247         self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
248
249 class GPOStorage:
250     def __init__(self, log_file):
251         if os.path.isfile(log_file):
252             self.log = tdb.open(log_file)
253         else:
254             self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
255
256     def start(self):
257         self.log.transaction_start()
258
259     def get_int(self, key):
260         try:
261             return int(self.log.get(key))
262         except TypeError:
263             return None
264
265     def get(self, key):
266         return self.log.get(key)
267
268     def get_gplog(self, user):
269         return gp_log(user, self, self.log.get(user))
270
271     def store(self, key, val):
272         self.log.store(key, val)
273
274     def cancel(self):
275         self.log.transaction_cancel()
276
277     def delete(self, key):
278         self.log.delete(key)
279
280     def commit(self):
281         self.log.transaction_commit()
282
283     def __del__(self):
284         self.log.close()
285
286 class gp_ext(object):
287     __metaclass__ = ABCMeta
288
289     @abstractmethod
290     def list(self, rootpath):
291         pass
292
293     @abstractmethod
294     def apply_map(self):
295         pass
296
297     @abstractmethod
298     def parse(self, afile, ldb, conn, gp_db, lp):
299         pass
300
301     @abstractmethod
302     def __str__(self):
303         pass
304
305 class inf_to():
306     __metaclass__ = ABCMeta
307
308     def __init__(self, logger, ldb, gp_db, lp, attribute, val):
309         self.logger = logger
310         self.ldb = ldb
311         self.attribute = attribute
312         self.val = val
313         self.lp = lp
314         self.gp_db = gp_db
315
316     def explicit(self):
317         return self.val
318
319     def update_samba(self):
320         (upd_sam, value) = self.mapper().get(self.attribute)
321         upd_sam(value())
322
323     @abstractmethod
324     def mapper(self):
325         pass
326
327     @abstractmethod
328     def __str__(self):
329         pass
330
331 class inf_to_kdc_tdb(inf_to):
332     def mins_to_hours(self):
333         return '%d' % (int(self.val)/60)
334
335     def days_to_hours(self):
336         return '%d' % (int(self.val)*24)
337
338     def set_kdc_tdb(self, val):
339         old_val = self.gp_db.gpostore.get(self.attribute)
340         self.logger.info('%s was changed from %s to %s' % (self.attribute,
341                                                            old_val, val))
342         if val is not None:
343             self.gp_db.gpostore.store(self.attribute, val)
344             self.gp_db.store(str(self), self.attribute, old_val)
345         else:
346             self.gp_db.gpostore.delete(self.attribute)
347             self.gp_db.delete(str(self), self.attribute)
348
349     def mapper(self):
350         return { 'kdc:user_ticket_lifetime': (self.set_kdc_tdb, self.explicit),
351                  'kdc:service_ticket_lifetime': (self.set_kdc_tdb,
352                                                  self.mins_to_hours),
353                  'kdc:renewal_lifetime': (self.set_kdc_tdb,
354                                           self.days_to_hours),
355                }
356
357     def __str__(self):
358         return 'Kerberos Policy'
359
360 class inf_to_ldb(inf_to):
361     '''This class takes the .inf file parameter (essentially a GPO file mapped
362     to a GUID), hashmaps it to the Samba parameter, which then uses an ldb
363     object to update the parameter to Samba4. Not registry oriented whatsoever.
364     '''
365
366     def ch_minPwdAge(self, val):
367         old_val = self.ldb.get_minPwdAge()
368         self.logger.info('KDC Minimum Password age was changed from %s to %s' \
369                          % (old_val, val))
370         self.gp_db.store(str(self), self.attribute, old_val)
371         self.ldb.set_minPwdAge(val)
372
373     def ch_maxPwdAge(self, val):
374         old_val = self.ldb.get_maxPwdAge()
375         self.logger.info('KDC Maximum Password age was changed from %s to %s' \
376                          % (old_val, val))
377         self.gp_db.store(str(self), self.attribute, old_val)
378         self.ldb.set_maxPwdAge(val)
379
380     def ch_minPwdLength(self, val):
381         old_val = self.ldb.get_minPwdLength()
382         self.logger.info(
383             'KDC Minimum Password length was changed from %s to %s' \
384              % (old_val, val))
385         self.gp_db.store(str(self), self.attribute, old_val)
386         self.ldb.set_minPwdLength(val)
387
388     def ch_pwdProperties(self, val):
389         old_val = self.ldb.get_pwdProperties()
390         self.logger.info('KDC Password Properties were changed from %s to %s' \
391                          % (old_val, val))
392         self.gp_db.store(str(self), self.attribute, old_val)
393         self.ldb.set_pwdProperties(val)
394
395     def days2rel_nttime(self):
396         seconds = 60
397         minutes = 60
398         hours = 24
399         sam_add = 10000000
400         val = (self.val)
401         val = int(val)
402         return  str(-(val * seconds * minutes * hours * sam_add))
403
404     def mapper(self):
405         '''ldap value : samba setter'''
406         return { "minPwdAge" : (self.ch_minPwdAge, self.days2rel_nttime),
407                  "maxPwdAge" : (self.ch_maxPwdAge, self.days2rel_nttime),
408                  # Could be none, but I like the method assignment in
409                  # update_samba
410                  "minPwdLength" : (self.ch_minPwdLength, self.explicit),
411                  "pwdProperties" : (self.ch_pwdProperties, self.explicit),
412
413                }
414
415     def __str__(self):
416         return 'System Access'
417
418
419 class gp_sec_ext(gp_ext):
420     '''This class does the following two things:
421         1) Identifies the GPO if it has a certain kind of filepath,
422         2) Finally parses it.
423     '''
424
425     count = 0
426
427     def __init__(self, logger):
428         self.logger = logger
429
430     def __str__(self):
431         return "Security GPO extension"
432
433     def list(self, rootpath):
434         return os.path.join(rootpath,
435                             "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
436
437     def listmachpol(self, rootpath):
438         return os.path.join(rootpath, "Machine/Registry.pol")
439
440     def listuserpol(self, rootpath):
441         return os.path.join(rootpath, "User/Registry.pol")
442
443     def apply_map(self):
444         return {"System Access": {"MinimumPasswordAge": ("minPwdAge",
445                                                          inf_to_ldb),
446                                   "MaximumPasswordAge": ("maxPwdAge",
447                                                          inf_to_ldb),
448                                   "MinimumPasswordLength": ("minPwdLength",
449                                                             inf_to_ldb),
450                                   "PasswordComplexity": ("pwdProperties",
451                                                          inf_to_ldb),
452                                  },
453                 "Kerberos Policy": {"MaxTicketAge": (
454                                         "kdc:user_ticket_lifetime",
455                                         inf_to_kdc_tdb
456                                     ),
457                                     "MaxServiceAge": (
458                                         "kdc:service_ticket_lifetime",
459                                         inf_to_kdc_tdb
460                                     ),
461                                     "MaxRenewAge": (
462                                         "kdc:renewal_lifetime",
463                                         inf_to_kdc_tdb
464                                     ),
465                                    }
466                }
467
468     def read_inf(self, path, conn):
469         ret = False
470         inftable = self.apply_map()
471
472         policy = conn.loadfile(path.replace('/', '\\'))
473         current_section = None
474
475         # So here we would declare a boolean,
476         # that would get changed to TRUE.
477         #
478         # If at any point in time a GPO was applied,
479         # then we return that boolean at the end.
480
481         inf_conf = ConfigParser()
482         inf_conf.optionxform=str
483         try:
484             inf_conf.readfp(StringIO(policy))
485         except:
486             inf_conf.readfp(StringIO(policy.decode('utf-16')))
487
488         for section in inf_conf.sections():
489             current_section = inftable.get(section)
490             if not current_section:
491                 continue
492             for key, value in inf_conf.items(section):
493                 if current_section.get(key):
494                     (att, setter) = current_section.get(key)
495                     value = value.encode('ascii', 'ignore')
496                     ret = True
497                     setter(self.logger, self.ldb, self.gp_db, self.lp, att,
498                            value).update_samba()
499                     self.gp_db.commit()
500         return ret
501
502     def parse(self, afile, ldb, conn, gp_db, lp):
503         self.ldb = ldb
504         self.gp_db = gp_db
505         self.lp = lp
506
507         # Fixing the bug where only some Linux Boxes capitalize MACHINE
508         if afile.endswith('inf'):
509             try:
510                 blist = afile.split('/')
511                 idx = afile.lower().split('/').index('machine')
512                 for case in [blist[idx].upper(), blist[idx].capitalize(),
513                              blist[idx].lower()]:
514                     bfile = '/'.join(blist[:idx]) + '/' + case + '/' + \
515                             '/'.join(blist[idx+1:])
516                     try:
517                         return self.read_inf(bfile, conn)
518                     except NTSTATUSError:
519                         continue
520             except ValueError:
521                 try:
522                     return self.read_inf(afile, conn)
523                 except:
524                     return None
525