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