5a0ca9fdec7556241f568166371dc7e818af6bc3
[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 = self.gpdb.find('user[@name="%s"]' % user)
99         if self.user is None:
100             self.user = etree.SubElement(self.gpdb, 'user')
101             self.user.attrib['name'] = user
102
103     def state(self, value):
104         ''' Policy application state
105         param value         - APPLY, ENFORCE, or UNAPPLY
106
107         The behavior of the gp_log depends on whether we are applying policy,
108         enforcing policy, or unapplying policy. During an apply, old settings
109         are recorded in the log. During an enforce, settings are being applied
110         but the gp_log does not change. During an unapply, additions to the log
111         should be ignored (since function calls to apply settings are actually
112         reverting policy), but removals from the log are allowed.
113         '''
114         # If we're enforcing, but we've unapplied, apply instead
115         if value == GPOSTATE.ENFORCE:
116             apply_log = self.user.find('applylog')
117             if apply_log is None or len(apply_log) == 0:
118                 self._state = GPOSTATE.APPLY
119             else:
120                 self._state = value
121         else:
122             self._state = value
123
124     def set_guid(self, guid):
125         ''' Log to a different GPO guid
126         param guid          - guid value of the GPO from which we're applying
127                               policy
128         '''
129         self.guid = self.user.find('guid[@value="%s"]' % guid)
130         if self.guid is None:
131             self.guid = etree.SubElement(self.user, 'guid')
132             self.guid.attrib['value'] = guid
133         if self._state == GPOSTATE.APPLY:
134             apply_log = self.user.find('applylog')
135             if apply_log is None:
136                 apply_log = etree.SubElement(self.user, 'applylog')
137             item = etree.SubElement(apply_log, 'guid')
138             item.attrib['count'] = '%d' % (len(apply_log)-1)
139             item.attrib['value'] = guid
140
141     def apply_log_pop(self):
142         ''' Pop a GPO guid from the applylog
143         return              - last applied GPO guid
144
145         Removes the GPO guid last added to the list, which is the most recently
146         applied GPO.
147         '''
148         apply_log = self.user.find('applylog')
149         if apply_log is not None:
150             ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
151             if ret is not None:
152                 apply_log.remove(ret)
153                 return ret.attrib['value']
154             if len(apply_log) == 0 and apply_log in self.user:
155                 self.user.remove(apply_log)
156         return None
157
158     def store(self, gp_ext_name, attribute, old_val):
159         ''' Store an attribute in the gp_log
160         param gp_ext_name   - Name of the extension applying policy
161         param attribute     - The attribute being modified
162         param old_val       - The value of the attribute prior to policy
163                               application
164         '''
165         if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
166             return None
167         assert self.guid is not None, "gpo guid was not set"
168         ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
169         if ext is None:
170             ext = etree.SubElement(self.guid, '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         assert self.guid is not None, "gpo guid was not set"
186         ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
187         if ext is not None:
188             attr = ext.find('attribute[@name="%s"]' % attribute)
189             if attr is not None:
190                 return attr.text
191         return None
192
193     def list(self, gp_extensions):
194         ''' Return a list of attributes, their previous values, and functions
195             to set them
196         param gp_extensions - list of extension objects, for retrieving attr to
197                               func mappings
198         return              - list of (attr, value, apply_func) tuples for
199                               unapplying policy
200         '''
201         assert self.guid is not None, "gpo guid was not set"
202         ret = []
203         data_maps = {}
204         for gp_ext in gp_extensions:
205             data_maps.update(gp_ext.apply_map())
206         exts = self.guid.findall('gp_ext')
207         if exts is not None:
208             for ext in exts:
209                 ext_map = {val[0]: val[1] for (key, val) in \
210                     data_maps[ext.attrib['name']].items()}
211                 attrs = ext.findall('attribute')
212                 for attr in attrs:
213                     ret.append((attr.attrib['name'], attr.text,
214                                 ext_map[attr.attrib['name']]))
215         return ret
216
217     def delete(self, gp_ext_name, attribute):
218         ''' Remove an attribute from the gp_log
219         param gp_ext_name   - name of extension from which to remove the
220                               attribute
221         param attribute     - attribute to remove
222         '''
223         assert self.guid is not None, "gpo guid was not set"
224         ext = self.guid.find('gp_ext[@name="%s"]' % gp_ext_name)
225         if ext is not None:
226             attr = ext.find('attribute[@name="%s"]' % attribute)
227             if attr is not None:
228                 ext.remove(attr)
229                 if len(ext) == 0:
230                     self.guid.remove(ext)
231
232     def commit(self):
233         ''' Write gp_log changes to disk '''
234         if len(self.guid) == 0 and self.guid in self.user:
235             self.user.remove(self.guid)
236         if len(self.user) == 0 and self.user in self.gpdb:
237             self.gpdb.remove(self.user)
238         self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
239
240 class GPOStorage:
241     def __init__(self, log_file):
242         if os.path.isfile(log_file):
243             self.log = tdb.open(log_file)
244         else:
245             self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR)
246
247     def start(self):
248         self.log.transaction_start()
249
250     def get_int(self, key):
251         try:
252             return int(self.log.get(key))
253         except TypeError:
254             return None
255
256     def get(self, key):
257         return self.log.get(key)
258
259     def get_gplog(self, user):
260         return gp_log(user, self, self.log.get(user))
261
262     def store(self, key, val):
263         self.log.store(key, val)
264
265     def cancel(self):
266         self.log.transaction_cancel()
267
268     def delete(self, key):
269         self.log.delete(key)
270
271     def commit(self):
272         self.log.transaction_commit()
273
274     def __del__(self):
275         self.log.close()
276
277 class gp_ext(object):
278     __metaclass__ = ABCMeta
279
280     @abstractmethod
281     def list(self, rootpath):
282         pass
283
284     @abstractmethod
285     def apply_map(self):
286         pass
287
288     @abstractmethod
289     def parse(self, afile, ldb, conn, gp_db, lp):
290         pass
291
292     @abstractmethod
293     def __str__(self):
294         pass
295
296 class inf_to():
297     __metaclass__ = ABCMeta
298
299     def __init__(self, logger, ldb, gp_db, lp, attribute, val):
300         self.logger = logger
301         self.ldb = ldb
302         self.attribute = attribute
303         self.val = val
304         self.lp = lp
305         self.gp_db = gp_db
306
307     def explicit(self):
308         return self.val
309
310     def update_samba(self):
311         (upd_sam, value) = self.mapper().get(self.attribute)
312         upd_sam(value())
313
314     @abstractmethod
315     def mapper(self):
316         pass
317
318     @abstractmethod
319     def __str__(self):
320         pass
321
322 class inf_to_kdc_tdb(inf_to):
323     def mins_to_hours(self):
324         return '%d' % (int(self.val)/60)
325
326     def days_to_hours(self):
327         return '%d' % (int(self.val)*24)
328
329     def set_kdc_tdb(self, val):
330         old_val = self.gp_db.gpostore.get(self.attribute)
331         self.logger.info('%s was changed from %s to %s' % (self.attribute,
332                                                            old_val, val))
333         if val is not None:
334             self.gp_db.gpostore.store(self.attribute, val)
335             self.gp_db.store(str(self), self.attribute, old_val)
336         else:
337             self.gp_db.gpostore.delete(self.attribute)
338             self.gp_db.delete(str(self), self.attribute)
339
340     def mapper(self):
341         return { 'kdc:user_ticket_lifetime': (self.set_kdc_tdb, self.explicit),
342                  'kdc:service_ticket_lifetime': (self.set_kdc_tdb,
343                                                  self.mins_to_hours),
344                  'kdc:renewal_lifetime': (self.set_kdc_tdb,
345                                           self.days_to_hours),
346                }
347
348     def __str__(self):
349         return 'Kerberos Policy'
350
351 class inf_to_ldb(inf_to):
352     '''This class takes the .inf file parameter (essentially a GPO file mapped
353     to a GUID), hashmaps it to the Samba parameter, which then uses an ldb
354     object to update the parameter to Samba4. Not registry oriented whatsoever.
355     '''
356
357     def ch_minPwdAge(self, val):
358         old_val = self.ldb.get_minPwdAge()
359         self.logger.info('KDC Minimum Password age was changed from %s to %s' \
360                          % (old_val, val))
361         self.gp_db.store(str(self), self.attribute, old_val)
362         self.ldb.set_minPwdAge(val)
363
364     def ch_maxPwdAge(self, val):
365         old_val = self.ldb.get_maxPwdAge()
366         self.logger.info('KDC Maximum Password age was changed from %s to %s' \
367                          % (old_val, val))
368         self.gp_db.store(str(self), self.attribute, old_val)
369         self.ldb.set_maxPwdAge(val)
370
371     def ch_minPwdLength(self, val):
372         old_val = self.ldb.get_minPwdLength()
373         self.logger.info(
374             'KDC Minimum Password length was changed from %s to %s' \
375              % (old_val, val))
376         self.gp_db.store(str(self), self.attribute, old_val)
377         self.ldb.set_minPwdLength(val)
378
379     def ch_pwdProperties(self, val):
380         old_val = self.ldb.get_pwdProperties()
381         self.logger.info('KDC Password Properties were changed from %s to %s' \
382                          % (old_val, val))
383         self.gp_db.store(str(self), self.attribute, old_val)
384         self.ldb.set_pwdProperties(val)
385
386     def days2rel_nttime(self):
387         seconds = 60
388         minutes = 60
389         hours = 24
390         sam_add = 10000000
391         val = (self.val)
392         val = int(val)
393         return  str(-(val * seconds * minutes * hours * sam_add))
394
395     def mapper(self):
396         '''ldap value : samba setter'''
397         return { "minPwdAge" : (self.ch_minPwdAge, self.days2rel_nttime),
398                  "maxPwdAge" : (self.ch_maxPwdAge, self.days2rel_nttime),
399                  # Could be none, but I like the method assignment in
400                  # update_samba
401                  "minPwdLength" : (self.ch_minPwdLength, self.explicit),
402                  "pwdProperties" : (self.ch_pwdProperties, self.explicit),
403
404                }
405
406     def __str__(self):
407         return 'System Access'
408
409
410 class gp_sec_ext(gp_ext):
411     '''This class does the following two things:
412         1) Identifies the GPO if it has a certain kind of filepath,
413         2) Finally parses it.
414     '''
415
416     count = 0
417
418     def __init__(self, logger):
419         self.logger = logger
420
421     def __str__(self):
422         return "Security GPO extension"
423
424     def list(self, rootpath):
425         return os.path.join(rootpath,
426                             "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
427
428     def listmachpol(self, rootpath):
429         return os.path.join(rootpath, "Machine/Registry.pol")
430
431     def listuserpol(self, rootpath):
432         return os.path.join(rootpath, "User/Registry.pol")
433
434     def apply_map(self):
435         return {"System Access": {"MinimumPasswordAge": ("minPwdAge",
436                                                          inf_to_ldb),
437                                   "MaximumPasswordAge": ("maxPwdAge",
438                                                          inf_to_ldb),
439                                   "MinimumPasswordLength": ("minPwdLength",
440                                                             inf_to_ldb),
441                                   "PasswordComplexity": ("pwdProperties",
442                                                          inf_to_ldb),
443                                  },
444                 "Kerberos Policy": {"MaxTicketAge": (
445                                         "kdc:user_ticket_lifetime",
446                                         inf_to_kdc_tdb
447                                     ),
448                                     "MaxServiceAge": (
449                                         "kdc:service_ticket_lifetime",
450                                         inf_to_kdc_tdb
451                                     ),
452                                     "MaxRenewAge": (
453                                         "kdc:renewal_lifetime",
454                                         inf_to_kdc_tdb
455                                     ),
456                                    }
457                }
458
459     def read_inf(self, path, conn):
460         ret = False
461         inftable = self.apply_map()
462
463         policy = conn.loadfile(path.replace('/', '\\'))
464         current_section = None
465
466         # So here we would declare a boolean,
467         # that would get changed to TRUE.
468         #
469         # If at any point in time a GPO was applied,
470         # then we return that boolean at the end.
471
472         inf_conf = ConfigParser()
473         inf_conf.optionxform=str
474         try:
475             inf_conf.readfp(StringIO(policy))
476         except:
477             inf_conf.readfp(StringIO(policy.decode('utf-16')))
478
479         for section in inf_conf.sections():
480             current_section = inftable.get(section)
481             if not current_section:
482                 continue
483             for key, value in inf_conf.items(section):
484                 if current_section.get(key):
485                     (att, setter) = current_section.get(key)
486                     value = value.encode('ascii', 'ignore')
487                     ret = True
488                     setter(self.logger, self.ldb, self.gp_db, self.lp, att,
489                            value).update_samba()
490                     self.gp_db.commit()
491         return ret
492
493     def parse(self, afile, ldb, conn, gp_db, lp):
494         self.ldb = ldb
495         self.gp_db = gp_db
496         self.lp = lp
497
498         # Fixing the bug where only some Linux Boxes capitalize MACHINE
499         if afile.endswith('inf'):
500             try:
501                 blist = afile.split('/')
502                 idx = afile.lower().split('/').index('machine')
503                 for case in [blist[idx].upper(), blist[idx].capitalize(),
504                              blist[idx].lower()]:
505                     bfile = '/'.join(blist[:idx]) + '/' + case + '/' + \
506                             '/'.join(blist[idx+1:])
507                     try:
508                         return self.read_inf(bfile, conn)
509                     except NTSTATUSError:
510                         continue
511             except ValueError:
512                 try:
513                     return self.read_inf(afile, conn)
514                 except:
515                     return None
516