gpo: Fix the empty apply log
[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
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                 ext_map = {val[0]: val[1] for (key, val) in \
221                     data_maps[ext.attrib['name']].items()}
222                 attrs = ext.findall('attribute')
223                 for attr in attrs:
224                     ret.append((attr.attrib['name'], attr.text,
225                                 ext_map[attr.attrib['name']]))
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