winbindd: initialize type = SID_NAME_UNKNOWN in wb_lookupsids_single_done()
[Samba.git] / python / samba / gpclass.py
blob33c9001cb6d0e86778507d64ea34069cb9a57da0
1 # Reads important GPO parameters and updates Samba
2 # Copyright (C) Luke Morrison <luc785@.hotmail.com> 2013
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.
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.
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/>.
18 import sys
19 import os
20 import tdb
21 sys.path.insert(0, "bin/python")
22 from samba import NTSTATUSError
23 from ConfigParser import ConfigParser
24 from StringIO import StringIO
25 from abc import ABCMeta, abstractmethod
26 import xml.etree.ElementTree as etree
27 import re
29 try:
30 from enum import Enum
31 GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
32 except ImportError:
33 class GPOSTATE:
34 APPLY = 1
35 ENFORCE = 2
36 UNAPPLY = 3
38 class gp_log:
39 ''' Log settings overwritten by gpo apply
40 The gp_log is an xml file that stores a history of gpo changes (and the
41 original setting value).
43 The log is organized like so:
45 <gp>
46 <user name="KDC-1$">
47 <applylog>
48 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
49 </applylog>
50 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
51 <gp_ext name="System Access">
52 <attribute name="minPwdAge">-864000000000</attribute>
53 <attribute name="maxPwdAge">-36288000000000</attribute>
54 <attribute name="minPwdLength">7</attribute>
55 <attribute name="pwdProperties">1</attribute>
56 </gp_ext>
57 <gp_ext name="Kerberos Policy">
58 <attribute name="ticket_lifetime">1d</attribute>
59 <attribute name="renew_lifetime" />
60 <attribute name="clockskew">300</attribute>
61 </gp_ext>
62 </guid>
63 </user>
64 </gp>
66 Each guid value contains a list of extensions, which contain a list of
67 attributes. The guid value represents a GPO. The attributes are the values
68 of those settings prior to the application of the GPO.
69 The list of guids is enclosed within a user name, which represents the user
70 the settings were applied to. This user may be the samaccountname of the
71 local computer, which implies that these are machine policies.
72 The applylog keeps track of the order in which the GPOs were applied, so
73 that they can be rolled back in reverse, returning the machine to the state
74 prior to policy application.
75 '''
76 def __init__(self, user, gpostore, db_log=None):
77 ''' Initialize the gp_log
78 param user - the username (or machine name) that policies are
79 being applied to
80 param gpostore - the GPOStorage obj which references the tdb which
81 contains gp_logs
82 param db_log - (optional) a string to initialize the gp_log
83 '''
84 self._state = GPOSTATE.APPLY
85 self.gpostore = gpostore
86 self.username = user
87 if db_log:
88 self.gpdb = etree.fromstring(db_log)
89 else:
90 self.gpdb = etree.Element('gp')
91 self.user = user
92 user_obj = self.gpdb.find('user[@name="%s"]' % user)
93 if user_obj is None:
94 user_obj = etree.SubElement(self.gpdb, 'user')
95 user_obj.attrib['name'] = user
97 def state(self, value):
98 ''' Policy application state
99 param value - APPLY, ENFORCE, or UNAPPLY
101 The behavior of the gp_log depends on whether we are applying policy,
102 enforcing policy, or unapplying policy. During an apply, old settings
103 are recorded in the log. During an enforce, settings are being applied
104 but the gp_log does not change. During an unapply, additions to the log
105 should be ignored (since function calls to apply settings are actually
106 reverting policy), but removals from the log are allowed.
108 # If we're enforcing, but we've unapplied, apply instead
109 if value == GPOSTATE.ENFORCE:
110 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
111 apply_log = user_obj.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
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
122 policy
124 self.guid = guid
125 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
126 obj = user_obj.find('guid[@value="%s"]' % guid)
127 if obj is None:
128 obj = etree.SubElement(user_obj, 'guid')
129 obj.attrib['value'] = guid
130 if self._state == GPOSTATE.APPLY:
131 apply_log = user_obj.find('applylog')
132 if apply_log is None:
133 apply_log = etree.SubElement(user_obj, 'applylog')
134 item = etree.SubElement(apply_log, 'guid')
135 item.attrib['count'] = '%d' % (len(apply_log)-1)
136 item.attrib['value'] = guid
138 def apply_log_pop(self):
139 ''' Pop a GPO guid from the applylog
140 return - last applied GPO guid
142 Removes the GPO guid last added to the list, which is the most recently
143 applied GPO.
145 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
146 apply_log = user_obj.find('applylog')
147 if apply_log is not None:
148 ret = apply_log.find('guid[@count="%d"]' % (len(apply_log)-1))
149 if ret is not None:
150 apply_log.remove(ret)
151 return ret.attrib['value']
152 if len(apply_log) == 0 and apply_log in user_obj:
153 user_obj.remove(apply_log)
154 return None
156 def store(self, gp_ext_name, attribute, old_val):
157 ''' Store an attribute in the gp_log
158 param gp_ext_name - Name of the extension applying policy
159 param attribute - The attribute being modified
160 param old_val - The value of the attribute prior to policy
161 application
163 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
164 return None
165 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
166 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
167 assert guid_obj is not None, "gpo guid was not set"
168 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
169 if ext is None:
170 ext = etree.SubElement(guid_obj, '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
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
185 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
186 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
187 assert guid_obj is not None, "gpo guid was not set"
188 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
189 if ext is not None:
190 attr = ext.find('attribute[@name="%s"]' % attribute)
191 if attr is not None:
192 return attr.text
193 return None
195 def list(self, gp_extensions):
196 ''' Return a list of attributes, their previous values, and functions
197 to set them
198 param gp_extensions - list of extension objects, for retrieving attr to
199 func mappings
200 return - list of (attr, value, apply_func) tuples for
201 unapplying policy
203 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
204 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
205 assert guid_obj is not None, "gpo guid was not set"
206 ret = []
207 data_maps = {}
208 for gp_ext in gp_extensions:
209 data_maps.update(gp_ext.apply_map())
210 exts = guid_obj.findall('gp_ext')
211 if exts is not None:
212 for ext in exts:
213 attrs = ext.findall('attribute')
214 for attr in attrs:
215 func = None
216 if attr.attrib['name'] in data_maps[ext.attrib['name']]:
217 func = data_maps[ext.attrib['name']]\
218 [attr.attrib['name']][-1]
219 else:
220 for dmap in data_maps[ext.attrib['name']].keys():
221 if data_maps[ext.attrib['name']][dmap][0] == \
222 attr.attrib['name']:
223 func = data_maps[ext.attrib['name']][dmap][-1]
224 break
225 ret.append((attr.attrib['name'], attr.text, func))
226 return ret
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
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)
245 def commit(self):
246 ''' Write gp_log changes to disk '''
247 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
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)
256 def start(self):
257 self.log.transaction_start()
259 def get_int(self, key):
260 try:
261 return int(self.log.get(key))
262 except TypeError:
263 return None
265 def get(self, key):
266 return self.log.get(key)
268 def get_gplog(self, user):
269 return gp_log(user, self, self.log.get(user))
271 def store(self, key, val):
272 self.log.store(key, val)
274 def cancel(self):
275 self.log.transaction_cancel()
277 def delete(self, key):
278 self.log.delete(key)
280 def commit(self):
281 self.log.transaction_commit()
283 def __del__(self):
284 self.log.close()
286 class gp_ext(object):
287 __metaclass__ = ABCMeta
289 @abstractmethod
290 def list(self, rootpath):
291 pass
293 @abstractmethod
294 def apply_map(self):
295 pass
297 @abstractmethod
298 def parse(self, afile, ldb, conn, gp_db, lp):
299 pass
301 @abstractmethod
302 def __str__(self):
303 pass
305 class inf_to():
306 __metaclass__ = ABCMeta
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
316 def explicit(self):
317 return self.val
319 def update_samba(self):
320 (upd_sam, value) = self.mapper().get(self.attribute)
321 upd_sam(value())
323 @abstractmethod
324 def mapper(self):
325 pass
327 @abstractmethod
328 def __str__(self):
329 pass
331 class inf_to_kdc_tdb(inf_to):
332 def mins_to_hours(self):
333 return '%d' % (int(self.val)/60)
335 def days_to_hours(self):
336 return '%d' % (int(self.val)*24)
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)
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),
357 def __str__(self):
358 return 'Kerberos Policy'
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.
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)
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)
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)
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)
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))
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),
415 def __str__(self):
416 return 'System Access'
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.
425 count = 0
427 def __init__(self, logger):
428 self.logger = logger
430 def __str__(self):
431 return "Security GPO extension"
433 def list(self, rootpath):
434 return os.path.join(rootpath,
435 "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
437 def listmachpol(self, rootpath):
438 return os.path.join(rootpath, "Machine/Registry.pol")
440 def listuserpol(self, rootpath):
441 return os.path.join(rootpath, "User/Registry.pol")
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),
453 "Kerberos Policy": {"MaxTicketAge": (
454 "kdc:user_ticket_lifetime",
455 inf_to_kdc_tdb
457 "MaxServiceAge": (
458 "kdc:service_ticket_lifetime",
459 inf_to_kdc_tdb
461 "MaxRenewAge": (
462 "kdc:renewal_lifetime",
463 inf_to_kdc_tdb
468 def read_inf(self, path, conn):
469 ret = False
470 inftable = self.apply_map()
472 policy = conn.loadfile(path.replace('/', '\\'))
473 current_section = None
475 # So here we would declare a boolean,
476 # that would get changed to TRUE.
478 # If at any point in time a GPO was applied,
479 # then we return that boolean at the end.
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')))
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
502 def parse(self, afile, ldb, conn, gp_db, lp):
503 self.ldb = ldb
504 self.gp_db = gp_db
505 self.lp = lp
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