ctdb-build: Use wafsamba's INSTALL_DIR()
[Samba.git] / python / samba / gpclass.py
blob3cf1f10dbe39874f27608289861d6a680433ca4b
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 errno
21 import tdb
22 sys.path.insert(0, "bin/python")
23 from samba import NTSTATUSError
24 from ConfigParser import ConfigParser
25 from samba.compat import StringIO
26 from abc import ABCMeta, abstractmethod
27 import xml.etree.ElementTree as etree
28 import re
29 from samba.net import Net
30 from samba.dcerpc import nbt
31 from samba import smb
32 import samba.gpo as gpo
33 from samba.param import LoadParm
34 from uuid import UUID
35 from tempfile import NamedTemporaryFile
37 try:
38 from enum import Enum
39 GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY')
40 except ImportError:
41 class GPOSTATE:
42 APPLY = 1
43 ENFORCE = 2
44 UNAPPLY = 3
47 class gp_log:
48 ''' Log settings overwritten by gpo apply
49 The gp_log is an xml file that stores a history of gpo changes (and the
50 original setting value).
52 The log is organized like so:
54 <gp>
55 <user name="KDC-1$">
56 <applylog>
57 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" />
58 </applylog>
59 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}">
60 <gp_ext name="System Access">
61 <attribute name="minPwdAge">-864000000000</attribute>
62 <attribute name="maxPwdAge">-36288000000000</attribute>
63 <attribute name="minPwdLength">7</attribute>
64 <attribute name="pwdProperties">1</attribute>
65 </gp_ext>
66 <gp_ext name="Kerberos Policy">
67 <attribute name="ticket_lifetime">1d</attribute>
68 <attribute name="renew_lifetime" />
69 <attribute name="clockskew">300</attribute>
70 </gp_ext>
71 </guid>
72 </user>
73 </gp>
75 Each guid value contains a list of extensions, which contain a list of
76 attributes. The guid value represents a GPO. The attributes are the values
77 of those settings prior to the application of the GPO.
78 The list of guids is enclosed within a user name, which represents the user
79 the settings were applied to. This user may be the samaccountname of the
80 local computer, which implies that these are machine policies.
81 The applylog keeps track of the order in which the GPOs were applied, so
82 that they can be rolled back in reverse, returning the machine to the state
83 prior to policy application.
84 '''
85 def __init__(self, user, gpostore, db_log=None):
86 ''' Initialize the gp_log
87 param user - the username (or machine name) that policies are
88 being applied to
89 param gpostore - the GPOStorage obj which references the tdb which
90 contains gp_logs
91 param db_log - (optional) a string to initialize the gp_log
92 '''
93 self._state = GPOSTATE.APPLY
94 self.gpostore = gpostore
95 self.username = user
96 if db_log:
97 self.gpdb = etree.fromstring(db_log)
98 else:
99 self.gpdb = etree.Element('gp')
100 self.user = user
101 user_obj = self.gpdb.find('user[@name="%s"]' % user)
102 if user_obj is None:
103 user_obj = etree.SubElement(self.gpdb, 'user')
104 user_obj.attrib['name'] = user
106 def state(self, value):
107 ''' Policy application state
108 param value - APPLY, ENFORCE, or UNAPPLY
110 The behavior of the gp_log depends on whether we are applying policy,
111 enforcing policy, or unapplying policy. During an apply, old settings
112 are recorded in the log. During an enforce, settings are being applied
113 but the gp_log does not change. During an unapply, additions to the log
114 should be ignored (since function calls to apply settings are actually
115 reverting policy), but removals from the log are allowed.
117 # If we're enforcing, but we've unapplied, apply instead
118 if value == GPOSTATE.ENFORCE:
119 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
120 apply_log = user_obj.find('applylog')
121 if apply_log is None or len(apply_log) == 0:
122 self._state = GPOSTATE.APPLY
123 else:
124 self._state = value
125 else:
126 self._state = value
128 def set_guid(self, guid):
129 ''' Log to a different GPO guid
130 param guid - guid value of the GPO from which we're applying
131 policy
133 self.guid = guid
134 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
135 obj = user_obj.find('guid[@value="%s"]' % guid)
136 if obj is None:
137 obj = etree.SubElement(user_obj, 'guid')
138 obj.attrib['value'] = guid
139 if self._state == GPOSTATE.APPLY:
140 apply_log = user_obj.find('applylog')
141 if apply_log is None:
142 apply_log = etree.SubElement(user_obj, 'applylog')
143 prev = apply_log.find('guid[@value="%s"]' % guid)
144 if prev is None:
145 item = etree.SubElement(apply_log, 'guid')
146 item.attrib['count'] = '%d' % (len(apply_log) - 1)
147 item.attrib['value'] = guid
149 def apply_log_pop(self):
150 ''' Pop a GPO guid from the applylog
151 return - last applied GPO guid
153 Removes the GPO guid last added to the list, which is the most recently
154 applied GPO.
156 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
157 apply_log = user_obj.find('applylog')
158 if apply_log is not None:
159 ret = apply_log.find('guid[@count="%d"]' % (len(apply_log) - 1))
160 if ret is not None:
161 apply_log.remove(ret)
162 return ret.attrib['value']
163 if len(apply_log) == 0 and apply_log in user_obj:
164 user_obj.remove(apply_log)
165 return None
167 def store(self, gp_ext_name, attribute, old_val):
168 ''' Store an attribute in the gp_log
169 param gp_ext_name - Name of the extension applying policy
170 param attribute - The attribute being modified
171 param old_val - The value of the attribute prior to policy
172 application
174 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE:
175 return None
176 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
177 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
178 assert guid_obj is not None, "gpo guid was not set"
179 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
180 if ext is None:
181 ext = etree.SubElement(guid_obj, 'gp_ext')
182 ext.attrib['name'] = gp_ext_name
183 attr = ext.find('attribute[@name="%s"]' % attribute)
184 if attr is None:
185 attr = etree.SubElement(ext, 'attribute')
186 attr.attrib['name'] = attribute
187 attr.text = old_val
189 def retrieve(self, gp_ext_name, attribute):
190 ''' Retrieve a stored attribute from the gp_log
191 param gp_ext_name - Name of the extension which applied policy
192 param attribute - The attribute being retrieved
193 return - The value of the attribute prior to policy
194 application
196 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
197 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
198 assert guid_obj is not None, "gpo guid was not set"
199 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
200 if ext is not None:
201 attr = ext.find('attribute[@name="%s"]' % attribute)
202 if attr is not None:
203 return attr.text
204 return None
206 def list(self, gp_extensions):
207 ''' Return a list of attributes, their previous values, and functions
208 to set them
209 param gp_extensions - list of extension objects, for retrieving attr to
210 func mappings
211 return - list of (attr, value, apply_func) tuples for
212 unapplying policy
214 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
215 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
216 assert guid_obj is not None, "gpo guid was not set"
217 ret = []
218 data_maps = {}
219 for gp_ext in gp_extensions:
220 data_maps.update(gp_ext.apply_map())
221 exts = guid_obj.findall('gp_ext')
222 if exts is not None:
223 for ext in exts:
224 attrs = ext.findall('attribute')
225 for attr in attrs:
226 func = None
227 if attr.attrib['name'] in data_maps[ext.attrib['name']]:
228 func = data_maps[ext.attrib['name']][attr.attrib['name']][-1]
229 else:
230 for dmap in data_maps[ext.attrib['name']].keys():
231 if data_maps[ext.attrib['name']][dmap][0] == \
232 attr.attrib['name']:
233 func = data_maps[ext.attrib['name']][dmap][-1]
234 break
235 ret.append((attr.attrib['name'], attr.text, func))
236 return ret
238 def delete(self, gp_ext_name, attribute):
239 ''' Remove an attribute from the gp_log
240 param gp_ext_name - name of extension from which to remove the
241 attribute
242 param attribute - attribute to remove
244 user_obj = self.gpdb.find('user[@name="%s"]' % self.user)
245 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid)
246 assert guid_obj is not None, "gpo guid was not set"
247 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name)
248 if ext is not None:
249 attr = ext.find('attribute[@name="%s"]' % attribute)
250 if attr is not None:
251 ext.remove(attr)
252 if len(ext) == 0:
253 guid_obj.remove(ext)
255 def commit(self):
256 ''' Write gp_log changes to disk '''
257 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8'))
260 class GPOStorage:
261 def __init__(self, log_file):
262 if os.path.isfile(log_file):
263 self.log = tdb.open(log_file)
264 else:
265 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT |os.O_RDWR)
267 def start(self):
268 self.log.transaction_start()
270 def get_int(self, key):
271 try:
272 return int(self.log.get(key))
273 except TypeError:
274 return None
276 def get(self, key):
277 return self.log.get(key)
279 def get_gplog(self, user):
280 return gp_log(user, self, self.log.get(user))
282 def store(self, key, val):
283 self.log.store(key, val)
285 def cancel(self):
286 self.log.transaction_cancel()
288 def delete(self, key):
289 self.log.delete(key)
291 def commit(self):
292 self.log.transaction_commit()
294 def __del__(self):
295 self.log.close()
298 class gp_ext(object):
299 __metaclass__ = ABCMeta
301 def __init__(self, logger):
302 self.logger = logger
304 @abstractmethod
305 def list(self, rootpath):
306 pass
308 @abstractmethod
309 def apply_map(self):
310 pass
312 @abstractmethod
313 def read(self, policy):
314 pass
316 def parse(self, afile, ldb, gp_db, lp):
317 self.ldb = ldb
318 self.gp_db = gp_db
319 self.lp = lp
321 local_path = self.lp.cache_path('gpo_cache')
322 data_file = os.path.join(local_path, check_safe_path(afile).upper())
323 if os.path.exists(data_file):
324 return self.read(open(data_file, 'r').read())
325 return None
327 @abstractmethod
328 def __str__(self):
329 pass
332 class gp_ext_setter():
333 __metaclass__ = ABCMeta
335 def __init__(self, logger, ldb, gp_db, lp, attribute, val):
336 self.logger = logger
337 self.ldb = ldb
338 self.attribute = attribute
339 self.val = val
340 self.lp = lp
341 self.gp_db = gp_db
343 def explicit(self):
344 return self.val
346 def update_samba(self):
347 (upd_sam, value) = self.mapper().get(self.attribute)
348 upd_sam(value())
350 @abstractmethod
351 def mapper(self):
352 pass
354 @abstractmethod
355 def __str__(self):
356 pass
359 class gp_inf_ext(gp_ext):
360 @abstractmethod
361 def list(self, rootpath):
362 pass
364 @abstractmethod
365 def apply_map(self):
366 pass
368 def read(self, policy):
369 ret = False
370 inftable = self.apply_map()
372 current_section = None
374 # So here we would declare a boolean,
375 # that would get changed to TRUE.
377 # If at any point in time a GPO was applied,
378 # then we return that boolean at the end.
380 inf_conf = ConfigParser()
381 inf_conf.optionxform = str
382 try:
383 inf_conf.readfp(StringIO(policy))
384 except:
385 inf_conf.readfp(StringIO(policy.decode('utf-16')))
387 for section in inf_conf.sections():
388 current_section = inftable.get(section)
389 if not current_section:
390 continue
391 for key, value in inf_conf.items(section):
392 if current_section.get(key):
393 (att, setter) = current_section.get(key)
394 value = value.encode('ascii', 'ignore')
395 ret = True
396 setter(self.logger, self.ldb, self.gp_db, self.lp, att,
397 value).update_samba()
398 self.gp_db.commit()
399 return ret
401 @abstractmethod
402 def __str__(self):
403 pass
406 ''' Fetch the hostname of a writable DC '''
409 def get_dc_hostname(creds, lp):
410 net = Net(creds=creds, lp=lp)
411 cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP |
412 nbt.NBT_SERVER_DS))
413 return cldap_ret.pdc_dns_name
416 ''' Fetch a list of GUIDs for applicable GPOs '''
419 def get_gpo_list(dc_hostname, creds, lp):
420 gpos = []
421 ads = gpo.ADS_STRUCT(dc_hostname, lp, creds)
422 if ads.connect():
423 gpos = ads.get_gpo_list(creds.get_username())
424 return gpos
427 def cache_gpo_dir(conn, cache, sub_dir):
428 loc_sub_dir = sub_dir.upper()
429 local_dir = os.path.join(cache, loc_sub_dir)
430 try:
431 os.makedirs(local_dir, mode=0o755)
432 except OSError as e:
433 if e.errno != errno.EEXIST:
434 raise
435 for fdata in conn.list(sub_dir):
436 if fdata['attrib'] & smb.FILE_ATTRIBUTE_DIRECTORY:
437 cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name']))
438 else:
439 local_name = fdata['name'].upper()
440 f = NamedTemporaryFile(delete=False, dir=local_dir)
441 fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\')
442 f.write(conn.loadfile(fname))
443 f.close()
444 os.rename(f.name, os.path.join(local_dir, local_name))
447 def check_safe_path(path):
448 dirs = re.split('/|\\\\', path)
449 if 'sysvol' in path:
450 dirs = dirs[dirs.index('sysvol') + 1:]
451 if '..' not in dirs:
452 return os.path.join(*dirs)
453 raise OSError(path)
456 def check_refresh_gpo_list(dc_hostname, lp, creds, gpos):
457 conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds, sign=True)
458 cache_path = lp.cache_path('gpo_cache')
459 for gpo in gpos:
460 if not gpo.file_sys_path:
461 continue
462 cache_gpo_dir(conn, cache_path, check_safe_path(gpo.file_sys_path))
465 def gpo_version(lp, path):
466 # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file,
467 # read from the gpo client cache.
468 gpt_path = lp.cache_path(os.path.join('gpo_cache', path))
469 return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1])
472 def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
473 gp_db = store.get_gplog(creds.get_username())
474 dc_hostname = get_dc_hostname(creds, lp)
475 gpos = get_gpo_list(dc_hostname, creds, lp)
476 try:
477 check_refresh_gpo_list(dc_hostname, lp, creds, gpos)
478 except:
479 logger.error('Failed downloading gpt cache from \'%s\' using SMB'
480 % dc_hostname)
481 return
483 for gpo_obj in gpos:
484 guid = gpo_obj.name
485 if guid == 'Local Policy':
486 continue
487 path = os.path.join(lp.get('realm'), 'Policies', guid).upper()
488 version = gpo_version(lp, path)
489 if version != store.get_int(guid):
490 logger.info('GPO %s has changed' % guid)
491 gp_db.state(GPOSTATE.APPLY)
492 else:
493 gp_db.state(GPOSTATE.ENFORCE)
494 gp_db.set_guid(guid)
495 store.start()
496 for ext in gp_extensions:
497 try:
498 ext.parse(ext.list(path), test_ldb, gp_db, lp)
499 except Exception as e:
500 logger.error('Failed to parse gpo %s for extension %s' %
501 (guid, str(ext)))
502 logger.error('Message was: ' + str(e))
503 store.cancel()
504 continue
505 store.store(guid, '%i' % version)
506 store.commit()
509 def unapply_log(gp_db):
510 while True:
511 item = gp_db.apply_log_pop()
512 if item:
513 yield item
514 else:
515 break
518 def unapply_gp(lp, creds, test_ldb, logger, store, gp_extensions):
519 gp_db = store.get_gplog(creds.get_username())
520 gp_db.state(GPOSTATE.UNAPPLY)
521 for gpo_guid in unapply_log(gp_db):
522 gp_db.set_guid(gpo_guid)
523 unapply_attributes = gp_db.list(gp_extensions)
524 for attr in unapply_attributes:
525 attr_obj = attr[-1](logger, test_ldb, gp_db, lp, attr[0], attr[1])
526 attr_obj.mapper()[attr[0]][0](attr[1]) # Set the old value
527 gp_db.delete(str(attr_obj), attr[0])
528 gp_db.commit()
531 def parse_gpext_conf(smb_conf):
532 lp = LoadParm()
533 if smb_conf is not None:
534 lp.load(smb_conf)
535 else:
536 lp.load_default()
537 ext_conf = lp.state_path('gpext.conf')
538 parser = ConfigParser()
539 parser.read(ext_conf)
540 return lp, parser
543 def atomic_write_conf(lp, parser):
544 ext_conf = lp.state_path('gpext.conf')
545 with NamedTemporaryFile(delete=False, dir=os.path.dirname(ext_conf)) as f:
546 parser.write(f)
547 os.rename(f.name, ext_conf)
550 def check_guid(guid):
551 # Check for valid guid with curly braces
552 if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38:
553 return False
554 try:
555 UUID(guid, version=4)
556 except ValueError:
557 return False
558 return True
561 def register_gp_extension(guid, name, path,
562 smb_conf=None, machine=True, user=True):
563 # Check that the module exists
564 if not os.path.exists(path):
565 return False
566 if not check_guid(guid):
567 return False
569 lp, parser = parse_gpext_conf(smb_conf)
570 if guid not in parser.sections():
571 parser.add_section(guid)
572 parser.set(guid, 'DllName', path)
573 parser.set(guid, 'ProcessGroupPolicy', name)
574 parser.set(guid, 'NoMachinePolicy', 0 if machine else 1)
575 parser.set(guid, 'NoUserPolicy', 0 if user else 1)
577 atomic_write_conf(lp, parser)
579 return True
582 def list_gp_extensions(smb_conf=None):
583 _, parser = parse_gpext_conf(smb_conf)
584 results = {}
585 for guid in parser.sections():
586 results[guid] = {}
587 results[guid]['DllName'] = parser.get(guid, 'DllName')
588 results[guid]['ProcessGroupPolicy'] = \
589 parser.get(guid, 'ProcessGroupPolicy')
590 results[guid]['MachinePolicy'] = \
591 not int(parser.get(guid, 'NoMachinePolicy'))
592 results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy'))
593 return results
596 def unregister_gp_extension(guid, smb_conf=None):
597 if not check_guid(guid):
598 return False
600 lp, parser = parse_gpext_conf(smb_conf)
601 if guid in parser.sections():
602 parser.remove_section(guid)
604 atomic_write_conf(lp, parser)
606 return True