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