8405c8fe59565f2b837141385cff7c975cbfd99b
[metze/samba/wip.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 parse(self, afile, ldb, conn, gp_db, lp):
302         pass
303
304     @abstractmethod
305     def __str__(self):
306         pass
307
308 class gp_ext_setter():
309     __metaclass__ = ABCMeta
310
311     def __init__(self, logger, ldb, gp_db, lp, attribute, val):
312         self.logger = logger
313         self.ldb = ldb
314         self.attribute = attribute
315         self.val = val
316         self.lp = lp
317         self.gp_db = gp_db
318
319     def explicit(self):
320         return self.val
321
322     def update_samba(self):
323         (upd_sam, value) = self.mapper().get(self.attribute)
324         upd_sam(value())
325
326     @abstractmethod
327     def mapper(self):
328         pass
329
330     @abstractmethod
331     def __str__(self):
332         pass
333
334 class inf_to_kdc_tdb(gp_ext_setter):
335     def mins_to_hours(self):
336         return '%d' % (int(self.val)/60)
337
338     def days_to_hours(self):
339         return '%d' % (int(self.val)*24)
340
341     def set_kdc_tdb(self, val):
342         old_val = self.gp_db.gpostore.get(self.attribute)
343         self.logger.info('%s was changed from %s to %s' % (self.attribute,
344                                                            old_val, val))
345         if val is not None:
346             self.gp_db.gpostore.store(self.attribute, val)
347             self.gp_db.store(str(self), self.attribute, old_val)
348         else:
349             self.gp_db.gpostore.delete(self.attribute)
350             self.gp_db.delete(str(self), self.attribute)
351
352     def mapper(self):
353         return { 'kdc:user_ticket_lifetime': (self.set_kdc_tdb, self.explicit),
354                  'kdc:service_ticket_lifetime': (self.set_kdc_tdb,
355                                                  self.mins_to_hours),
356                  'kdc:renewal_lifetime': (self.set_kdc_tdb,
357                                           self.days_to_hours),
358                }
359
360     def __str__(self):
361         return 'Kerberos Policy'
362
363 class inf_to_ldb(gp_ext_setter):
364     '''This class takes the .inf file parameter (essentially a GPO file mapped
365     to a GUID), hashmaps it to the Samba parameter, which then uses an ldb
366     object to update the parameter to Samba4. Not registry oriented whatsoever.
367     '''
368
369     def ch_minPwdAge(self, val):
370         old_val = self.ldb.get_minPwdAge()
371         self.logger.info('KDC Minimum Password age was changed from %s to %s' \
372                          % (old_val, val))
373         self.gp_db.store(str(self), self.attribute, old_val)
374         self.ldb.set_minPwdAge(val)
375
376     def ch_maxPwdAge(self, val):
377         old_val = self.ldb.get_maxPwdAge()
378         self.logger.info('KDC Maximum Password age was changed from %s to %s' \
379                          % (old_val, val))
380         self.gp_db.store(str(self), self.attribute, old_val)
381         self.ldb.set_maxPwdAge(val)
382
383     def ch_minPwdLength(self, val):
384         old_val = self.ldb.get_minPwdLength()
385         self.logger.info(
386             'KDC Minimum Password length was changed from %s to %s' \
387              % (old_val, val))
388         self.gp_db.store(str(self), self.attribute, old_val)
389         self.ldb.set_minPwdLength(val)
390
391     def ch_pwdProperties(self, val):
392         old_val = self.ldb.get_pwdProperties()
393         self.logger.info('KDC Password Properties were changed from %s to %s' \
394                          % (old_val, val))
395         self.gp_db.store(str(self), self.attribute, old_val)
396         self.ldb.set_pwdProperties(val)
397
398     def days2rel_nttime(self):
399         seconds = 60
400         minutes = 60
401         hours = 24
402         sam_add = 10000000
403         val = (self.val)
404         val = int(val)
405         return  str(-(val * seconds * minutes * hours * sam_add))
406
407     def mapper(self):
408         '''ldap value : samba setter'''
409         return { "minPwdAge" : (self.ch_minPwdAge, self.days2rel_nttime),
410                  "maxPwdAge" : (self.ch_maxPwdAge, self.days2rel_nttime),
411                  # Could be none, but I like the method assignment in
412                  # update_samba
413                  "minPwdLength" : (self.ch_minPwdLength, self.explicit),
414                  "pwdProperties" : (self.ch_pwdProperties, self.explicit),
415
416                }
417
418     def __str__(self):
419         return 'System Access'
420
421
422 class gp_sec_ext(gp_ext):
423     '''This class does the following two things:
424         1) Identifies the GPO if it has a certain kind of filepath,
425         2) Finally parses it.
426     '''
427
428     count = 0
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