gpo: Move the file parse function to gp_ext
[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 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     def __init__(self, logger):
290         self.logger = logger
291
292     @abstractmethod
293     def list(self, rootpath):
294         pass
295
296     @abstractmethod
297     def apply_map(self):
298         pass
299
300     @abstractmethod
301     def read(self, policy):
302         pass
303
304     def parse(self, afile, ldb, conn, gp_db, lp):
305         self.ldb = ldb
306         self.gp_db = gp_db
307         self.lp = lp
308
309         # Fixing the bug where only some Linux Boxes capitalize MACHINE
310         try:
311             blist = afile.split('/')
312             idx = afile.lower().split('/').index('machine')
313             for case in [
314                             blist[idx].upper(),
315                             blist[idx].capitalize(),
316                             blist[idx].lower()
317                         ]:
318                 bfile = '/'.join(blist[:idx]) + '/' + case + '/' + \
319                     '/'.join(blist[idx+1:])
320                 try:
321                     return self.read(conn.loadfile(bfile.replace('/', '\\')))
322                 except NTSTATUSError:
323                     continue
324         except ValueError:
325             try:
326                 return self.read(conn.loadfile(afile.replace('/', '\\')))
327             except Exception as e:
328                 self.logger.error(str(e))
329                 return None
330
331     @abstractmethod
332     def __str__(self):
333         pass
334
335 class gp_ext_setter():
336     __metaclass__ = ABCMeta
337
338     def __init__(self, logger, ldb, gp_db, lp, attribute, val):
339         self.logger = logger
340         self.ldb = ldb
341         self.attribute = attribute
342         self.val = val
343         self.lp = lp
344         self.gp_db = gp_db
345
346     def explicit(self):
347         return self.val
348
349     def update_samba(self):
350         (upd_sam, value) = self.mapper().get(self.attribute)
351         upd_sam(value())
352
353     @abstractmethod
354     def mapper(self):
355         pass
356
357     @abstractmethod
358     def __str__(self):
359         pass
360
361 class inf_to_kdc_tdb(gp_ext_setter):
362     def mins_to_hours(self):
363         return '%d' % (int(self.val)/60)
364
365     def days_to_hours(self):
366         return '%d' % (int(self.val)*24)
367
368     def set_kdc_tdb(self, val):
369         old_val = self.gp_db.gpostore.get(self.attribute)
370         self.logger.info('%s was changed from %s to %s' % (self.attribute,
371                                                            old_val, val))
372         if val is not None:
373             self.gp_db.gpostore.store(self.attribute, val)
374             self.gp_db.store(str(self), self.attribute, old_val)
375         else:
376             self.gp_db.gpostore.delete(self.attribute)
377             self.gp_db.delete(str(self), self.attribute)
378
379     def mapper(self):
380         return { 'kdc:user_ticket_lifetime': (self.set_kdc_tdb, self.explicit),
381                  'kdc:service_ticket_lifetime': (self.set_kdc_tdb,
382                                                  self.mins_to_hours),
383                  'kdc:renewal_lifetime': (self.set_kdc_tdb,
384                                           self.days_to_hours),
385                }
386
387     def __str__(self):
388         return 'Kerberos Policy'
389
390 class inf_to_ldb(gp_ext_setter):
391     '''This class takes the .inf file parameter (essentially a GPO file mapped
392     to a GUID), hashmaps it to the Samba parameter, which then uses an ldb
393     object to update the parameter to Samba4. Not registry oriented whatsoever.
394     '''
395
396     def ch_minPwdAge(self, val):
397         old_val = self.ldb.get_minPwdAge()
398         self.logger.info('KDC Minimum Password age was changed from %s to %s' \
399                          % (old_val, val))
400         self.gp_db.store(str(self), self.attribute, old_val)
401         self.ldb.set_minPwdAge(val)
402
403     def ch_maxPwdAge(self, val):
404         old_val = self.ldb.get_maxPwdAge()
405         self.logger.info('KDC Maximum Password age was changed from %s to %s' \
406                          % (old_val, val))
407         self.gp_db.store(str(self), self.attribute, old_val)
408         self.ldb.set_maxPwdAge(val)
409
410     def ch_minPwdLength(self, val):
411         old_val = self.ldb.get_minPwdLength()
412         self.logger.info(
413             'KDC Minimum Password length was changed from %s to %s' \
414              % (old_val, val))
415         self.gp_db.store(str(self), self.attribute, old_val)
416         self.ldb.set_minPwdLength(val)
417
418     def ch_pwdProperties(self, val):
419         old_val = self.ldb.get_pwdProperties()
420         self.logger.info('KDC Password Properties were changed from %s to %s' \
421                          % (old_val, val))
422         self.gp_db.store(str(self), self.attribute, old_val)
423         self.ldb.set_pwdProperties(val)
424
425     def days2rel_nttime(self):
426         seconds = 60
427         minutes = 60
428         hours = 24
429         sam_add = 10000000
430         val = (self.val)
431         val = int(val)
432         return  str(-(val * seconds * minutes * hours * sam_add))
433
434     def mapper(self):
435         '''ldap value : samba setter'''
436         return { "minPwdAge" : (self.ch_minPwdAge, self.days2rel_nttime),
437                  "maxPwdAge" : (self.ch_maxPwdAge, self.days2rel_nttime),
438                  # Could be none, but I like the method assignment in
439                  # update_samba
440                  "minPwdLength" : (self.ch_minPwdLength, self.explicit),
441                  "pwdProperties" : (self.ch_pwdProperties, self.explicit),
442
443                }
444
445     def __str__(self):
446         return 'System Access'
447
448
449 class gp_sec_ext(gp_ext):
450     '''This class does the following two things:
451         1) Identifies the GPO if it has a certain kind of filepath,
452         2) Finally parses it.
453     '''
454
455     count = 0
456
457     def __str__(self):
458         return "Security GPO extension"
459
460     def list(self, rootpath):
461         return os.path.join(rootpath,
462                             "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
463
464     def listmachpol(self, rootpath):
465         return os.path.join(rootpath, "Machine/Registry.pol")
466
467     def listuserpol(self, rootpath):
468         return os.path.join(rootpath, "User/Registry.pol")
469
470     def apply_map(self):
471         return {"System Access": {"MinimumPasswordAge": ("minPwdAge",
472                                                          inf_to_ldb),
473                                   "MaximumPasswordAge": ("maxPwdAge",
474                                                          inf_to_ldb),
475                                   "MinimumPasswordLength": ("minPwdLength",
476                                                             inf_to_ldb),
477                                   "PasswordComplexity": ("pwdProperties",
478                                                          inf_to_ldb),
479                                  },
480                 "Kerberos Policy": {"MaxTicketAge": (
481                                         "kdc:user_ticket_lifetime",
482                                         inf_to_kdc_tdb
483                                     ),
484                                     "MaxServiceAge": (
485                                         "kdc:service_ticket_lifetime",
486                                         inf_to_kdc_tdb
487                                     ),
488                                     "MaxRenewAge": (
489                                         "kdc:renewal_lifetime",
490                                         inf_to_kdc_tdb
491                                     ),
492                                    }
493                }
494
495     def read(self, policy):
496         ret = False
497         inftable = self.apply_map()
498
499         current_section = None
500
501         # So here we would declare a boolean,
502         # that would get changed to TRUE.
503         #
504         # If at any point in time a GPO was applied,
505         # then we return that boolean at the end.
506
507         inf_conf = ConfigParser()
508         inf_conf.optionxform=str
509         try:
510             inf_conf.readfp(StringIO(policy))
511         except:
512             inf_conf.readfp(StringIO(policy.decode('utf-16')))
513
514         for section in inf_conf.sections():
515             current_section = inftable.get(section)
516             if not current_section:
517                 continue
518             for key, value in inf_conf.items(section):
519                 if current_section.get(key):
520                     (att, setter) = current_section.get(key)
521                     value = value.encode('ascii', 'ignore')
522                     ret = True
523                     setter(self.logger, self.ldb, self.gp_db, self.lp, att,
524                            value).update_samba()
525                     self.gp_db.commit()
526         return ret
527