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