gpo: Always enforce policy, even if unchanged
[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_ldb(inf_to):
308     '''This class takes the .inf file parameter (essentially a GPO file mapped to a GUID),
309     hashmaps it to the Samba parameter, which then uses an ldb object to update the
310     parameter to Samba4. Not registry oriented whatsoever.
311     '''
312
313     def ch_minPwdAge(self, val):
314         old_val = self.ldb.get_minPwdAge()
315         self.logger.info('KDC Minimum Password age was changed from %s to %s' % (old_val, val))
316         self.gp_db.store(str(self), self.attribute, old_val)
317         self.ldb.set_minPwdAge(val)
318
319     def ch_maxPwdAge(self, val):
320         old_val = self.ldb.get_maxPwdAge()
321         self.logger.info('KDC Maximum Password age was changed from %s to %s' % (old_val, val))
322         self.gp_db.store(str(self), self.attribute, old_val)
323         self.ldb.set_maxPwdAge(val)
324
325     def ch_minPwdLength(self, val):
326         old_val = self.ldb.get_minPwdLength()
327         self.logger.info('KDC Minimum Password length was changed from %s to %s' % (old_val, val))
328         self.gp_db.store(str(self), self.attribute, old_val)
329         self.ldb.set_minPwdLength(val)
330
331     def ch_pwdProperties(self, val):
332         old_val = self.ldb.get_pwdProperties()
333         self.logger.info('KDC Password Properties were changed from %s to %s' % (old_val, val))
334         self.gp_db.store(str(self), self.attribute, old_val)
335         self.ldb.set_pwdProperties(val)
336
337     def nttime2unix(self):
338         seconds = 60
339         minutes = 60
340         hours = 24
341         sam_add = 10000000
342         val = (self.val)
343         val = int(val)
344         return  str(-(val * seconds * minutes * hours * sam_add))
345
346     def mapper(self):
347         '''ldap value : samba setter'''
348         return { "minPwdAge" : (self.ch_minPwdAge, self.nttime2unix),
349                  "maxPwdAge" : (self.ch_maxPwdAge, self.nttime2unix),
350                  # Could be none, but I like the method assignment in update_samba
351                  "minPwdLength" : (self.ch_minPwdLength, self.explicit),
352                  "pwdProperties" : (self.ch_pwdProperties, self.explicit),
353
354                }
355
356     def __str__(self):
357         return 'System Access'
358
359
360 class gp_sec_ext(gp_ext):
361     '''This class does the following two things:
362         1) Identifies the GPO if it has a certain kind of filepath,
363         2) Finally parses it.
364     '''
365
366     count = 0
367
368     def __init__(self, logger):
369         self.logger = logger
370
371     def __str__(self):
372         return "Security GPO extension"
373
374     def list(self, rootpath):
375         return os.path.join(rootpath, "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
376
377     def listmachpol(self, rootpath):
378         return os.path.join(rootpath, "Machine/Registry.pol")
379
380     def listuserpol(self, rootpath):
381         return os.path.join(rootpath, "User/Registry.pol")
382
383     def apply_map(self):
384         return {"System Access": {"MinimumPasswordAge": ("minPwdAge", inf_to_ldb),
385                                   "MaximumPasswordAge": ("maxPwdAge", inf_to_ldb),
386                                   "MinimumPasswordLength": ("minPwdLength", inf_to_ldb),
387                                   "PasswordComplexity": ("pwdProperties", inf_to_ldb),
388                                  }
389                }
390
391     def read_inf(self, path, conn):
392         ret = False
393         inftable = self.apply_map()
394
395         policy = conn.loadfile(path.replace('/', '\\'))
396         current_section = None
397
398         # So here we would declare a boolean,
399         # that would get changed to TRUE.
400         #
401         # If at any point in time a GPO was applied,
402         # then we return that boolean at the end.
403
404         inf_conf = ConfigParser()
405         inf_conf.optionxform=str
406         try:
407             inf_conf.readfp(StringIO(policy))
408         except:
409             inf_conf.readfp(StringIO(policy.decode('utf-16')))
410
411         for section in inf_conf.sections():
412             current_section = inftable.get(section)
413             if not current_section:
414                 continue
415             for key, value in inf_conf.items(section):
416                 if current_section.get(key):
417                     (att, setter) = current_section.get(key)
418                     value = value.encode('ascii', 'ignore')
419                     ret = True
420                     setter(self.logger, self.ldb, self.gp_db, self.lp, att, value).update_samba()
421                     self.gp_db.commit()
422         return ret
423
424     def parse(self, afile, ldb, conn, gp_db, lp):
425         self.ldb = ldb
426         self.gp_db = gp_db
427         self.lp = lp
428
429         # Fixing the bug where only some Linux Boxes capitalize MACHINE
430         if afile.endswith('inf'):
431             try:
432                 blist = afile.split('/')
433                 idx = afile.lower().split('/').index('machine')
434                 for case in [blist[idx].upper(), blist[idx].capitalize(), blist[idx].lower()]:
435                     bfile = '/'.join(blist[:idx]) + '/' + case + '/' + '/'.join(blist[idx+1:])
436                     try:
437                         return self.read_inf(bfile, conn)
438                     except NTSTATUSError:
439                         continue
440             except ValueError:
441                 try:
442                     return self.read_inf(afile, conn)
443                 except:
444                     return None
445