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 18import sys 19import os 20import errno 21import tdb 22sys.path.insert(0, "bin/python") 23from samba import NTSTATUSError 24from samba.compat import ConfigParser 25from samba.compat import StringIO 26from samba.compat import get_bytes 27from abc import ABCMeta, abstractmethod 28import xml.etree.ElementTree as etree 29import re 30from samba.net import Net 31from samba.dcerpc import nbt 32from samba.samba3 import libsmb_samba_internal as libsmb 33from samba.samba3 import param as s3param 34import samba.gpo as gpo 35from samba.param import LoadParm 36from uuid import UUID 37from tempfile import NamedTemporaryFile 38 39try: 40 from enum import Enum 41 GPOSTATE = Enum('GPOSTATE', 'APPLY ENFORCE UNAPPLY') 42except ImportError: 43 class GPOSTATE: 44 APPLY = 1 45 ENFORCE = 2 46 UNAPPLY = 3 47 48 49class gp_log: 50 ''' Log settings overwritten by gpo apply 51 The gp_log is an xml file that stores a history of gpo changes (and the 52 original setting value). 53 54 The log is organized like so: 55 56<gp> 57 <user name="KDC-1$"> 58 <applylog> 59 <guid count="0" value="{31B2F340-016D-11D2-945F-00C04FB984F9}" /> 60 </applylog> 61 <guid value="{31B2F340-016D-11D2-945F-00C04FB984F9}"> 62 <gp_ext name="System Access"> 63 <attribute name="minPwdAge">-864000000000</attribute> 64 <attribute name="maxPwdAge">-36288000000000</attribute> 65 <attribute name="minPwdLength">7</attribute> 66 <attribute name="pwdProperties">1</attribute> 67 </gp_ext> 68 <gp_ext name="Kerberos Policy"> 69 <attribute name="ticket_lifetime">1d</attribute> 70 <attribute name="renew_lifetime" /> 71 <attribute name="clockskew">300</attribute> 72 </gp_ext> 73 </guid> 74 </user> 75</gp> 76 77 Each guid value contains a list of extensions, which contain a list of 78 attributes. The guid value represents a GPO. The attributes are the values 79 of those settings prior to the application of the GPO. 80 The list of guids is enclosed within a user name, which represents the user 81 the settings were applied to. This user may be the samaccountname of the 82 local computer, which implies that these are machine policies. 83 The applylog keeps track of the order in which the GPOs were applied, so 84 that they can be rolled back in reverse, returning the machine to the state 85 prior to policy application. 86 ''' 87 def __init__(self, user, gpostore, db_log=None): 88 ''' Initialize the gp_log 89 param user - the username (or machine name) that policies are 90 being applied to 91 param gpostore - the GPOStorage obj which references the tdb which 92 contains gp_logs 93 param db_log - (optional) a string to initialize the gp_log 94 ''' 95 self._state = GPOSTATE.APPLY 96 self.gpostore = gpostore 97 self.username = user 98 if db_log: 99 self.gpdb = etree.fromstring(db_log) 100 else: 101 self.gpdb = etree.Element('gp') 102 self.user = user 103 user_obj = self.gpdb.find('user[@name="%s"]' % user) 104 if user_obj is None: 105 user_obj = etree.SubElement(self.gpdb, 'user') 106 user_obj.attrib['name'] = user 107 108 def state(self, value): 109 ''' Policy application state 110 param value - APPLY, ENFORCE, or UNAPPLY 111 112 The behavior of the gp_log depends on whether we are applying policy, 113 enforcing policy, or unapplying policy. During an apply, old settings 114 are recorded in the log. During an enforce, settings are being applied 115 but the gp_log does not change. During an unapply, additions to the log 116 should be ignored (since function calls to apply settings are actually 117 reverting policy), but removals from the log are allowed. 118 ''' 119 # If we're enforcing, but we've unapplied, apply instead 120 if value == GPOSTATE.ENFORCE: 121 user_obj = self.gpdb.find('user[@name="%s"]' % self.user) 122 apply_log = user_obj.find('applylog') 123 if apply_log is None or len(apply_log) == 0: 124 self._state = GPOSTATE.APPLY 125 else: 126 self._state = value 127 else: 128 self._state = value 129 130 def set_guid(self, guid): 131 ''' Log to a different GPO guid 132 param guid - guid value of the GPO from which we're applying 133 policy 134 ''' 135 self.guid = guid 136 user_obj = self.gpdb.find('user[@name="%s"]' % self.user) 137 obj = user_obj.find('guid[@value="%s"]' % guid) 138 if obj is None: 139 obj = etree.SubElement(user_obj, 'guid') 140 obj.attrib['value'] = guid 141 if self._state == GPOSTATE.APPLY: 142 apply_log = user_obj.find('applylog') 143 if apply_log is None: 144 apply_log = etree.SubElement(user_obj, 'applylog') 145 prev = apply_log.find('guid[@value="%s"]' % guid) 146 if prev is None: 147 item = etree.SubElement(apply_log, 'guid') 148 item.attrib['count'] = '%d' % (len(apply_log) - 1) 149 item.attrib['value'] = guid 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 156 application 157 ''' 158 if self._state == GPOSTATE.UNAPPLY or self._state == GPOSTATE.ENFORCE: 159 return None 160 user_obj = self.gpdb.find('user[@name="%s"]' % self.user) 161 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid) 162 assert guid_obj is not None, "gpo guid was not set" 163 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name) 164 if ext is None: 165 ext = etree.SubElement(guid_obj, 'gp_ext') 166 ext.attrib['name'] = gp_ext_name 167 attr = ext.find('attribute[@name="%s"]' % attribute) 168 if attr is None: 169 attr = etree.SubElement(ext, 'attribute') 170 attr.attrib['name'] = attribute 171 attr.text = old_val 172 173 def retrieve(self, gp_ext_name, attribute): 174 ''' Retrieve a stored attribute from the gp_log 175 param gp_ext_name - Name of the extension which applied policy 176 param attribute - The attribute being retrieved 177 return - The value of the attribute prior to policy 178 application 179 ''' 180 user_obj = self.gpdb.find('user[@name="%s"]' % self.user) 181 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid) 182 assert guid_obj is not None, "gpo guid was not set" 183 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name) 184 if ext is not None: 185 attr = ext.find('attribute[@name="%s"]' % attribute) 186 if attr is not None: 187 return attr.text 188 return None 189 190 def get_applied_guids(self): 191 ''' Return a list of applied ext guids 192 return - List of guids for gpos that have applied settings 193 to the system. 194 ''' 195 guids = [] 196 user_obj = self.gpdb.find('user[@name="%s"]' % self.user) 197 if user_obj is not None: 198 apply_log = user_obj.find('applylog') 199 if apply_log is not None: 200 guid_objs = apply_log.findall('guid[@count]') 201 guids_by_count = [(g.get('count'), g.get('value')) 202 for g in guid_objs] 203 guids_by_count.sort(reverse=True) 204 guids.extend(guid for count, guid in guids_by_count) 205 return guids 206 207 def get_applied_settings(self, guids): 208 ''' Return a list of applied ext guids 209 return - List of tuples containing the guid of a gpo, then 210 a dictionary of policies and their values prior 211 policy application. These are sorted so that the 212 most recently applied settings are removed first. 213 ''' 214 ret = [] 215 user_obj = self.gpdb.find('user[@name="%s"]' % self.user) 216 for guid in guids: 217 guid_settings = user_obj.find('guid[@value="%s"]' % guid) 218 exts = guid_settings.findall('gp_ext') 219 settings = {} 220 for ext in exts: 221 attr_dict = {} 222 attrs = ext.findall('attribute') 223 for attr in attrs: 224 attr_dict[attr.attrib['name']] = attr.text 225 settings[ext.attrib['name']] = attr_dict 226 ret.append((guid, settings)) 227 return ret 228 229 def delete(self, gp_ext_name, attribute): 230 ''' Remove an attribute from the gp_log 231 param gp_ext_name - name of extension from which to remove the 232 attribute 233 param attribute - attribute to remove 234 ''' 235 user_obj = self.gpdb.find('user[@name="%s"]' % self.user) 236 guid_obj = user_obj.find('guid[@value="%s"]' % self.guid) 237 assert guid_obj is not None, "gpo guid was not set" 238 ext = guid_obj.find('gp_ext[@name="%s"]' % gp_ext_name) 239 if ext is not None: 240 attr = ext.find('attribute[@name="%s"]' % attribute) 241 if attr is not None: 242 ext.remove(attr) 243 if len(ext) == 0: 244 guid_obj.remove(ext) 245 246 def commit(self): 247 ''' Write gp_log changes to disk ''' 248 self.gpostore.store(self.username, etree.tostring(self.gpdb, 'utf-8')) 249 250 251class GPOStorage: 252 def __init__(self, log_file): 253 if os.path.isfile(log_file): 254 self.log = tdb.open(log_file) 255 else: 256 self.log = tdb.Tdb(log_file, 0, tdb.DEFAULT, os.O_CREAT | os.O_RDWR) 257 258 def start(self): 259 self.log.transaction_start() 260 261 def get_int(self, key): 262 try: 263 return int(self.log.get(get_bytes(key))) 264 except TypeError: 265 return None 266 267 def get(self, key): 268 return self.log.get(get_bytes(key)) 269 270 def get_gplog(self, user): 271 return gp_log(user, self, self.log.get(get_bytes(user))) 272 273 def store(self, key, val): 274 self.log.store(get_bytes(key), get_bytes(val)) 275 276 def cancel(self): 277 self.log.transaction_cancel() 278 279 def delete(self, key): 280 self.log.delete(get_bytes(key)) 281 282 def commit(self): 283 self.log.transaction_commit() 284 285 def __del__(self): 286 self.log.close() 287 288 289class gp_ext(object): 290 __metaclass__ = ABCMeta 291 292 def __init__(self, logger, lp, creds, store): 293 self.logger = logger 294 self.lp = lp 295 self.creds = creds 296 self.gp_db = store.get_gplog(creds.get_username()) 297 298 @abstractmethod 299 def process_group_policy(self, deleted_gpo_list, changed_gpo_list): 300 pass 301 302 @abstractmethod 303 def read(self, policy): 304 pass 305 306 def parse(self, afile): 307 local_path = self.lp.cache_path('gpo_cache') 308 data_file = os.path.join(local_path, check_safe_path(afile).upper()) 309 if os.path.exists(data_file): 310 return self.read(open(data_file, 'r').read()) 311 return None 312 313 @abstractmethod 314 def __str__(self): 315 pass 316 317 318class gp_ext_setter(object): 319 __metaclass__ = ABCMeta 320 321 def __init__(self, logger, gp_db, lp, creds, attribute, val): 322 self.logger = logger 323 self.attribute = attribute 324 self.val = val 325 self.lp = lp 326 self.creds = creds 327 self.gp_db = gp_db 328 329 def explicit(self): 330 return self.val 331 332 def update_samba(self): 333 (upd_sam, value) = self.mapper().get(self.attribute) 334 upd_sam(value()) 335 336 @abstractmethod 337 def mapper(self): 338 pass 339 340 def delete(self): 341 upd_sam, _ = self.mapper().get(self.attribute) 342 upd_sam(self.val) 343 344 @abstractmethod 345 def __str__(self): 346 pass 347 348 349class gp_inf_ext(gp_ext): 350 def read(self, policy): 351 inf_conf = ConfigParser() 352 inf_conf.optionxform = str 353 try: 354 inf_conf.readfp(StringIO(policy)) 355 except: 356 inf_conf.readfp(StringIO(policy.decode('utf-16'))) 357 return inf_conf 358 359 360''' Fetch the hostname of a writable DC ''' 361 362 363def get_dc_hostname(creds, lp): 364 net = Net(creds=creds, lp=lp) 365 cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP | 366 nbt.NBT_SERVER_DS)) 367 return cldap_ret.pdc_dns_name 368 369 370''' Fetch a list of GUIDs for applicable GPOs ''' 371 372 373def get_gpo_list(dc_hostname, creds, lp): 374 gpos = [] 375 ads = gpo.ADS_STRUCT(dc_hostname, lp, creds) 376 if ads.connect(): 377 gpos = ads.get_gpo_list(creds.get_username()) 378 return gpos 379 380 381def cache_gpo_dir(conn, cache, sub_dir): 382 loc_sub_dir = sub_dir.upper() 383 local_dir = os.path.join(cache, loc_sub_dir) 384 try: 385 os.makedirs(local_dir, mode=0o755) 386 except OSError as e: 387 if e.errno != errno.EEXIST: 388 raise 389 for fdata in conn.list(sub_dir): 390 if fdata['attrib'] & libsmb.FILE_ATTRIBUTE_DIRECTORY: 391 cache_gpo_dir(conn, cache, os.path.join(sub_dir, fdata['name'])) 392 else: 393 local_name = fdata['name'].upper() 394 f = NamedTemporaryFile(delete=False, dir=local_dir) 395 fname = os.path.join(sub_dir, fdata['name']).replace('/', '\\') 396 f.write(conn.loadfile(fname)) 397 f.close() 398 os.rename(f.name, os.path.join(local_dir, local_name)) 399 400 401def check_safe_path(path): 402 dirs = re.split('/|\\\\', path) 403 if 'sysvol' in path: 404 dirs = dirs[dirs.index('sysvol') + 1:] 405 if '..' not in dirs: 406 return os.path.join(*dirs) 407 raise OSError(path) 408 409 410def check_refresh_gpo_list(dc_hostname, lp, creds, gpos): 411 # the SMB bindings rely on having a s3 loadparm 412 s3_lp = s3param.get_context() 413 s3_lp.load(lp.configfile) 414 conn = libsmb.Conn(dc_hostname, 'sysvol', lp=s3_lp, creds=creds, sign=True) 415 cache_path = lp.cache_path('gpo_cache') 416 for gpo in gpos: 417 if not gpo.file_sys_path: 418 continue 419 cache_gpo_dir(conn, cache_path, check_safe_path(gpo.file_sys_path)) 420 421 422def get_deleted_gpos_list(gp_db, gpos): 423 applied_gpos = gp_db.get_applied_guids() 424 current_guids = set([p.name for p in gpos]) 425 deleted_gpos = [guid for guid in applied_gpos if guid not in current_guids] 426 return gp_db.get_applied_settings(deleted_gpos) 427 428def gpo_version(lp, path): 429 # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file, 430 # read from the gpo client cache. 431 gpt_path = lp.cache_path(os.path.join('gpo_cache', path)) 432 return int(gpo.gpo_get_sysvol_gpt_version(gpt_path)[1]) 433 434 435def apply_gp(lp, creds, logger, store, gp_extensions, force=False): 436 gp_db = store.get_gplog(creds.get_username()) 437 dc_hostname = get_dc_hostname(creds, lp) 438 gpos = get_gpo_list(dc_hostname, creds, lp) 439 del_gpos = get_deleted_gpos_list(gp_db, gpos) 440 try: 441 check_refresh_gpo_list(dc_hostname, lp, creds, gpos) 442 except: 443 logger.error('Failed downloading gpt cache from \'%s\' using SMB' 444 % dc_hostname) 445 return 446 447 if force: 448 changed_gpos = gpos 449 gp_db.state(GPOSTATE.ENFORCE) 450 else: 451 changed_gpos = [] 452 for gpo_obj in gpos: 453 if not gpo_obj.file_sys_path: 454 continue 455 guid = gpo_obj.name 456 path = check_safe_path(gpo_obj.file_sys_path).upper() 457 version = gpo_version(lp, path) 458 if version != store.get_int(guid): 459 logger.info('GPO %s has changed' % guid) 460 changed_gpos.append(gpo_obj) 461 gp_db.state(GPOSTATE.APPLY) 462 463 store.start() 464 for ext in gp_extensions: 465 try: 466 ext.process_group_policy(del_gpos, changed_gpos) 467 except Exception as e: 468 logger.error('Failed to apply extension %s' % str(ext)) 469 logger.error('Message was: ' + str(e)) 470 continue 471 for gpo_obj in gpos: 472 if not gpo_obj.file_sys_path: 473 continue 474 guid = gpo_obj.name 475 path = check_safe_path(gpo_obj.file_sys_path).upper() 476 version = gpo_version(lp, path) 477 store.store(guid, '%i' % version) 478 store.commit() 479 480 481def unapply_gp(lp, creds, logger, store, gp_extensions): 482 gp_db = store.get_gplog(creds.get_username()) 483 gp_db.state(GPOSTATE.UNAPPLY) 484 # Treat all applied gpos as deleted 485 del_gpos = gp_db.get_applied_settings(gp_db.get_applied_guids()) 486 store.start() 487 for ext in gp_extensions: 488 try: 489 ext.process_group_policy(del_gpos, []) 490 except Exception as e: 491 logger.error('Failed to unapply extension %s' % str(ext)) 492 logger.error('Message was: ' + str(e)) 493 continue 494 store.commit() 495 496 497def parse_gpext_conf(smb_conf): 498 lp = LoadParm() 499 if smb_conf is not None: 500 lp.load(smb_conf) 501 else: 502 lp.load_default() 503 ext_conf = lp.state_path('gpext.conf') 504 parser = ConfigParser() 505 parser.read(ext_conf) 506 return lp, parser 507 508 509def atomic_write_conf(lp, parser): 510 ext_conf = lp.state_path('gpext.conf') 511 with NamedTemporaryFile(mode="w+", delete=False, dir=os.path.dirname(ext_conf)) as f: 512 parser.write(f) 513 os.rename(f.name, ext_conf) 514 515 516def check_guid(guid): 517 # Check for valid guid with curly braces 518 if guid[0] != '{' or guid[-1] != '}' or len(guid) != 38: 519 return False 520 try: 521 UUID(guid, version=4) 522 except ValueError: 523 return False 524 return True 525 526 527def register_gp_extension(guid, name, path, 528 smb_conf=None, machine=True, user=True): 529 # Check that the module exists 530 if not os.path.exists(path): 531 return False 532 if not check_guid(guid): 533 return False 534 535 lp, parser = parse_gpext_conf(smb_conf) 536 if guid not in parser.sections(): 537 parser.add_section(guid) 538 parser.set(guid, 'DllName', path) 539 parser.set(guid, 'ProcessGroupPolicy', name) 540 parser.set(guid, 'NoMachinePolicy', "0" if machine else "1") 541 parser.set(guid, 'NoUserPolicy', "0" if user else "1") 542 543 atomic_write_conf(lp, parser) 544 545 return True 546 547 548def list_gp_extensions(smb_conf=None): 549 _, parser = parse_gpext_conf(smb_conf) 550 results = {} 551 for guid in parser.sections(): 552 results[guid] = {} 553 results[guid]['DllName'] = parser.get(guid, 'DllName') 554 results[guid]['ProcessGroupPolicy'] = \ 555 parser.get(guid, 'ProcessGroupPolicy') 556 results[guid]['MachinePolicy'] = \ 557 not int(parser.get(guid, 'NoMachinePolicy')) 558 results[guid]['UserPolicy'] = not int(parser.get(guid, 'NoUserPolicy')) 559 return results 560 561 562def unregister_gp_extension(guid, smb_conf=None): 563 if not check_guid(guid): 564 return False 565 566 lp, parser = parse_gpext_conf(smb_conf) 567 if guid in parser.sections(): 568 parser.remove_section(guid) 569 570 atomic_write_conf(lp, parser) 571 572 return True 573