gpo: Make the gpclass more easily extensible
[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 sys.path.insert(0, "bin/python")
21 import samba.gpo as gpo
22 import optparse
23 import ldb
24 from samba.auth import system_session
25 import samba.getopt as options
26 from samba.samdb import SamDB
27 from samba.netcmd import gpo as gpo_user
28 import codecs
29 from samba import NTSTATUSError
30 from ConfigParser import ConfigParser
31 from StringIO import StringIO
32 from abc import ABCMeta, abstractmethod
33
34 class gp_ext(object):
35     __metaclass__ = ABCMeta
36
37     @abstractmethod
38     def list(self, rootpath):
39         pass
40
41     @abstractmethod
42     def parse(self, afile, ldb, conn, attr_log, lp):
43         pass
44
45     @abstractmethod
46     def __str__(self):
47         pass
48
49
50 class inf_to():
51     __metaclass__ = ABCMeta
52
53     def __init__(self, logger, ldb, dn, lp, attribute, val):
54         self.logger = logger
55         self.ldb = ldb
56         self.dn = dn
57         self.attribute = attribute
58         self.val = val
59         self.lp = lp
60
61     def explicit(self):
62         return self.val
63
64     def update_samba(self):
65         (upd_sam, value) = self.mapper().get(self.attribute)
66         upd_sam(value())
67
68     @abstractmethod
69     def mapper(self):
70         pass
71
72 class inf_to_ldb(inf_to):
73     '''This class takes the .inf file parameter (essentially a GPO file mapped to a GUID),
74     hashmaps it to the Samba parameter, which then uses an ldb object to update the
75     parameter to Samba4. Not registry oriented whatsoever.
76     '''
77
78     def ch_minPwdAge(self, val):
79         self.logger.info('KDC Minimum Password age was changed from %s to %s' % (self.ldb.get_minPwdAge(), val))
80         self.ldb.set_minPwdAge(val)
81
82     def ch_maxPwdAge(self, val):
83         self.logger.info('KDC Maximum Password age was changed from %s to %s' % (self.ldb.get_maxPwdAge(), val))
84         self.ldb.set_maxPwdAge(val)
85
86     def ch_minPwdLength(self, val):
87         self.logger.info('KDC Minimum Password length was changed from %s to %s' % (self.ldb.get_minPwdLength(), val))
88         self.ldb.set_minPwdLength(val)
89
90     def ch_pwdProperties(self, val):
91         self.logger.info('KDC Password Properties were changed from %s to %s' % (self.ldb.get_pwdProperties(), val))
92         self.ldb.set_pwdProperties(val)
93
94     def nttime2unix(self):
95         seconds = 60
96         minutes = 60
97         hours = 24
98         sam_add = 10000000
99         val = (self.val)
100         val = int(val)
101         return  str(-(val * seconds * minutes * hours * sam_add))
102
103     def mapper(self):
104         '''ldap value : samba setter'''
105         return { "minPwdAge" : (self.ch_minPwdAge, self.nttime2unix),
106                  "maxPwdAge" : (self.ch_maxPwdAge, self.nttime2unix),
107                  # Could be none, but I like the method assignment in update_samba
108                  "minPwdLength" : (self.ch_minPwdLength, self.explicit),
109                  "pwdProperties" : (self.ch_pwdProperties, self.explicit),
110
111                }
112
113
114 class gp_sec_ext(gp_ext):
115     '''This class does the following two things:
116         1) Identifies the GPO if it has a certain kind of filepath,
117         2) Finally parses it.
118     '''
119
120     count = 0
121
122     def __init__(self, logger):
123         self.logger = logger
124
125     def __str__(self):
126         return "Security GPO extension"
127
128     def list(self, rootpath):
129         path = "%s%s" % (rootpath, "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
130         return path
131
132     def listmachpol(self, rootpath):
133         path = "%s%s" % (rootpath, "Machine/Registry.pol")
134         return path
135
136     def listuserpol(self, rootpath):
137         path = "%s%s" % (rootpath, "User/Registry.pol")
138         return path
139
140     def populate_inf(self):
141         return {"System Access": {"MinimumPasswordAge": ("minPwdAge", inf_to_ldb),
142                                   "MaximumPasswordAge": ("maxPwdAge", inf_to_ldb),
143                                   "MinimumPasswordLength": ("minPwdLength", inf_to_ldb),
144                                   "PasswordComplexity": ("pwdProperties", inf_to_ldb),
145                                  }
146                }
147
148     def read_inf(self, path, conn, attr_log):
149         ret = False
150         inftable = self.populate_inf()
151
152         policy = conn.loadfile(path.replace('/', '\\')).decode('utf-16')
153         current_section = None
154         LOG = open(attr_log, "a")
155         LOG.write(str(path.split('/')[2]) + '\n')
156
157         # So here we would declare a boolean,
158         # that would get changed to TRUE.
159         #
160         # If at any point in time a GPO was applied,
161         # then we return that boolean at the end.
162
163         inf_conf = ConfigParser()
164         inf_conf.optionxform=str
165         inf_conf.readfp(StringIO(policy))
166
167         for section in inf_conf.sections():
168             current_section = inftable.get(section)
169             if not current_section:
170                 continue
171             for key, value in inf_conf.items(section):
172                 if current_section.get(key):
173                     (att, setter) = current_section.get(key)
174                     value = value.encode('ascii', 'ignore')
175                     ret = True
176                     setter(self.logger, self.ldb, self.dn, self.lp, att, value).update_samba()
177         return ret
178
179     def parse(self, afile, ldb, conn, attr_log, lp):
180         self.ldb = ldb
181         self.lp = lp
182         self.dn = ldb.get_default_basedn()
183
184         # Fixing the bug where only some Linux Boxes capitalize MACHINE
185         if afile.endswith('inf'):
186             try:
187                 blist = afile.split('/')
188                 idx = afile.lower().split('/').index('machine')
189                 for case in [blist[idx].upper(), blist[idx].capitalize(), blist[idx].lower()]:
190                     bfile = '/'.join(blist[:idx]) + '/' + case + '/' + '/'.join(blist[idx+1:])
191                     try:
192                         return self.read_inf(bfile, conn, attr_log)
193                     except NTSTATUSError:
194                         continue
195             except ValueError:
196                 try:
197                     return self.read_inf(afile, conn, attr_log)
198                 except:
199                     return None
200
201
202 def scan_log(sysvol_tdb):
203     data = {}
204     for key in sysvol_tdb.iterkeys():
205         data[key] = sysvol_tdb.get(key)
206     return data
207
208
209 def Reset_Defaults(test_ldb):
210     test_ldb.set_minPwdAge(str(-25920000000000))
211     test_ldb.set_maxPwdAge(str(-38016000000000))
212     test_ldb.set_minPwdLength(str(7))
213     test_ldb.set_pwdProperties(str(1))
214
215
216 def check_deleted(guid_list, backloggpo):
217     if backloggpo is None:
218         return False
219     for guid in backloggpo:
220         if guid not in guid_list:
221             return True
222     return False
223
224
225 # The hierarchy is as per MS http://msdn.microsoft.com/en-us/library/windows/desktop/aa374155%28v=vs.85%29.aspx
226 #
227 # It does not care about local GPO, because GPO and snap-ins are not made in Linux yet.
228 # It follows the linking order and children GPO are last written format.
229 #
230 # Also, couple further testing with call scripts entitled informant and informant2.
231 # They explicitly show the returned hierarchically sorted list.
232
233
234 def container_indexes(GUID_LIST):
235     '''So the original list will need to be seperated into containers.
236     Returns indexed list of when the container changes after hierarchy
237     '''
238     count = 0
239     container_indexes = []
240     while count < (len(GUID_LIST)-1):
241         if GUID_LIST[count][2] != GUID_LIST[count+1][2]:
242             container_indexes.append(count+1)
243         count += 1
244     container_indexes.append(len(GUID_LIST))
245     return container_indexes
246
247
248 def sort_linked(SAMDB, guid_list, start, end):
249     '''So GPO in same level need to have link level.
250     This takes a container and sorts it.
251
252     TODO:  Small small problem, it is backwards
253     '''
254     containers = gpo_user.get_gpo_containers(SAMDB, guid_list[start][0])
255     for right_container in containers:
256         if right_container.get('dn') == guid_list[start][2]:
257             break
258     gplink = str(right_container.get('gPLink'))
259     gplink_split = gplink.split('[')
260     linked_order = []
261     ret_list = []
262     for ldap_guid in gplink_split:
263         linked_order.append(str(ldap_guid[10:48]))
264     count = len(linked_order) - 1
265     while count > 0:
266         ret_list.append([linked_order[count], guid_list[start][1], guid_list[start][2]])
267         count -= 1
268     return ret_list
269
270
271 def establish_hierarchy(SamDB, GUID_LIST, DC_OU, global_dn):
272     '''Takes a list of GUID from gpo, and sorts them based on OU, and realm.
273     See http://msdn.microsoft.com/en-us/library/windows/desktop/aa374155%28v=vs.85%29.aspx
274     '''
275     final_list = []
276     count_unapplied_GPO = 0
277     for GUID in GUID_LIST:
278
279         container_iteration = 0
280         # Assume first it is not applied
281         applied = False
282         # Realm only written on last call, if the GPO is linked to multiple places
283         gpo_realm = False
284
285         # A very important call. This gets all of the linked information.
286         GPO_CONTAINERS = gpo_user.get_gpo_containers(SamDB, GUID)
287         for GPO_CONTAINER in GPO_CONTAINERS:
288
289             container_iteration += 1
290
291             if DC_OU == str(GPO_CONTAINER.get('dn')):
292                 applied = True
293                 insert_gpo = [GUID, applied, str(GPO_CONTAINER.get('dn'))]
294                 final_list.append(insert_gpo)
295                 break
296
297             if global_dn == str(GPO_CONTAINER.get('dn')) and (len(GPO_CONTAINERS) == 1):
298                 gpo_realm = True
299                 applied = True
300
301
302             if global_dn == str(GPO_CONTAINER.get('dn')) and (len(GPO_CONTAINERS) > 1):
303                 gpo_realm = True
304                 applied = True
305
306
307             if container_iteration == len(GPO_CONTAINERS):
308                 if gpo_realm == False:
309                     insert_dud = [GUID, applied, str(GPO_CONTAINER.get('dn'))]
310                     final_list.insert(0, insert_dud)
311                     count_unapplied_GPO += 1
312                 else:
313                     REALM_GPO = [GUID, applied, str(GPO_CONTAINER.get('dn'))]
314                     final_list.insert(count_unapplied_GPO, REALM_GPO)
315
316     # After GPO are sorted into containers, let's sort the containers themselves.
317     # But first we can get the GPO that we don't care about, out of the way.
318     indexed_places = container_indexes(final_list)
319     count = 0
320     unapplied_gpo = []
321     # Sorted by container
322     sorted_gpo_list = []
323
324     # Unapplied GPO live at start of list, append them to final list
325     while final_list[0][1] == False:
326         unapplied_gpo.append(final_list[count])
327         count += 1
328     count = 0
329     sorted_gpo_list += unapplied_gpo
330
331     # A single container call gets the linked order for all GPO in container.
332     # So we need one call per container - > index of the Original list
333     indexed_places.insert(0, 0)
334     while count < (len(indexed_places)-1):
335         sorted_gpo_list += (sort_linked(SamDB, final_list, indexed_places[count], indexed_places[count+1]))
336         count += 1
337     return sorted_gpo_list