From de9cee2262637e854f7e06ef3bd48a43f5f31798 Mon Sep 17 00:00:00 2001 From: David Mulder Date: Thu, 25 May 2017 07:27:27 -0600 Subject: [PATCH] gpoupdate: Rewrite samba_gpoupdate Use new python bindings and remove obsoleted code Signed-off-by: David Mulder Reviewed-by: Garming Sam Reviewed-by: Andrew Bartlett --- python/samba/gpclass.py | 189 +++------------- source4/scripting/bin/samba_gpoupdate | 301 ++++++++------------------ 2 files changed, 122 insertions(+), 368 deletions(-) diff --git a/python/samba/gpclass.py b/python/samba/gpclass.py index 3b8738e330..c3f7512a00 100644 --- a/python/samba/gpclass.py +++ b/python/samba/gpclass.py @@ -17,6 +17,7 @@ import sys import os +import tdb sys.path.insert(0, "bin/python") import samba.gpo as gpo import optparse @@ -31,6 +32,30 @@ from ConfigParser import ConfigParser from StringIO import StringIO from abc import ABCMeta, abstractmethod +class Backlog: + def __init__(self, sysvol_log): + if os.path.isfile(sysvol_log): + self.backlog = tdb.open(sysvol_log) + else: + self.backlog = tdb.Tdb(sysvol_log, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR) + self.backlog.transaction_start() + + def version(self, guid): + try: + old_version = int(self.backlog.get(guid)) + except TypeError: + old_version = -1 + return old_version + + def store(self, guid, version): + self.backlog.store(guid, '%i' % version) + + def commit(self): + self.backlog.transaction_commit() + + def __del__(self): + self.backlog.close() + class gp_ext(object): __metaclass__ = ABCMeta @@ -39,7 +64,7 @@ class gp_ext(object): pass @abstractmethod - def parse(self, afile, ldb, conn, attr_log, lp): + def parse(self, afile, ldb, conn, lp): pass @abstractmethod @@ -50,10 +75,9 @@ class gp_ext(object): class inf_to(): __metaclass__ = ABCMeta - def __init__(self, logger, ldb, dn, lp, attribute, val): + def __init__(self, logger, ldb, lp, attribute, val): self.logger = logger self.ldb = ldb - self.dn = dn self.attribute = attribute self.val = val self.lp = lp @@ -126,16 +150,13 @@ class gp_sec_ext(gp_ext): return "Security GPO extension" def list(self, rootpath): - path = "%s%s" % (rootpath, "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf") - return path + return os.path.join(rootpath, "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf") def listmachpol(self, rootpath): - path = "%s%s" % (rootpath, "Machine/Registry.pol") - return path + return os.path.join(rootpath, "Machine/Registry.pol") def listuserpol(self, rootpath): - path = "%s%s" % (rootpath, "User/Registry.pol") - return path + return os.path.join(rootpath, "User/Registry.pol") def populate_inf(self): return {"System Access": {"MinimumPasswordAge": ("minPwdAge", inf_to_ldb), @@ -145,14 +166,12 @@ class gp_sec_ext(gp_ext): } } - def read_inf(self, path, conn, attr_log): + def read_inf(self, path, conn): ret = False inftable = self.populate_inf() policy = conn.loadfile(path.replace('/', '\\')).decode('utf-16') current_section = None - LOG = open(attr_log, "a") - LOG.write(str(path.split('/')[2]) + '\n') # So here we would declare a boolean, # that would get changed to TRUE. @@ -173,13 +192,12 @@ class gp_sec_ext(gp_ext): (att, setter) = current_section.get(key) value = value.encode('ascii', 'ignore') ret = True - setter(self.logger, self.ldb, self.dn, self.lp, att, value).update_samba() + setter(self.logger, self.ldb, self.lp, att, value).update_samba() return ret - def parse(self, afile, ldb, conn, attr_log, lp): + def parse(self, afile, ldb, conn, lp): self.ldb = ldb self.lp = lp - self.dn = ldb.get_default_basedn() # Fixing the bug where only some Linux Boxes capitalize MACHINE if afile.endswith('inf'): @@ -189,149 +207,12 @@ class gp_sec_ext(gp_ext): for case in [blist[idx].upper(), blist[idx].capitalize(), blist[idx].lower()]: bfile = '/'.join(blist[:idx]) + '/' + case + '/' + '/'.join(blist[idx+1:]) try: - return self.read_inf(bfile, conn, attr_log) + return self.read_inf(bfile, conn) except NTSTATUSError: continue except ValueError: try: - return self.read_inf(afile, conn, attr_log) + return self.read_inf(afile, conn) except: return None - -def scan_log(sysvol_tdb): - data = {} - for key in sysvol_tdb.iterkeys(): - data[key] = sysvol_tdb.get(key) - return data - - -def Reset_Defaults(test_ldb): - test_ldb.set_minPwdAge(str(-25920000000000)) - test_ldb.set_maxPwdAge(str(-38016000000000)) - test_ldb.set_minPwdLength(str(7)) - test_ldb.set_pwdProperties(str(1)) - - -def check_deleted(guid_list, backloggpo): - if backloggpo is None: - return False - for guid in backloggpo: - if guid not in guid_list: - return True - return False - - -# The hierarchy is as per MS http://msdn.microsoft.com/en-us/library/windows/desktop/aa374155%28v=vs.85%29.aspx -# -# It does not care about local GPO, because GPO and snap-ins are not made in Linux yet. -# It follows the linking order and children GPO are last written format. -# -# Also, couple further testing with call scripts entitled informant and informant2. -# They explicitly show the returned hierarchically sorted list. - - -def container_indexes(GUID_LIST): - '''So the original list will need to be seperated into containers. - Returns indexed list of when the container changes after hierarchy - ''' - count = 0 - container_indexes = [] - while count < (len(GUID_LIST)-1): - if GUID_LIST[count][2] != GUID_LIST[count+1][2]: - container_indexes.append(count+1) - count += 1 - container_indexes.append(len(GUID_LIST)) - return container_indexes - - -def sort_linked(SAMDB, guid_list, start, end): - '''So GPO in same level need to have link level. - This takes a container and sorts it. - - TODO: Small small problem, it is backwards - ''' - containers = gpo_user.get_gpo_containers(SAMDB, guid_list[start][0]) - for right_container in containers: - if right_container.get('dn') == guid_list[start][2]: - break - gplink = str(right_container.get('gPLink')) - gplink_split = gplink.split('[') - linked_order = [] - ret_list = [] - for ldap_guid in gplink_split: - linked_order.append(str(ldap_guid[10:48])) - count = len(linked_order) - 1 - while count > 0: - ret_list.append([linked_order[count], guid_list[start][1], guid_list[start][2]]) - count -= 1 - return ret_list - - -def establish_hierarchy(SamDB, GUID_LIST, DC_OU, global_dn): - '''Takes a list of GUID from gpo, and sorts them based on OU, and realm. - See http://msdn.microsoft.com/en-us/library/windows/desktop/aa374155%28v=vs.85%29.aspx - ''' - final_list = [] - count_unapplied_GPO = 0 - for GUID in GUID_LIST: - - container_iteration = 0 - # Assume first it is not applied - applied = False - # Realm only written on last call, if the GPO is linked to multiple places - gpo_realm = False - - # A very important call. This gets all of the linked information. - GPO_CONTAINERS = gpo_user.get_gpo_containers(SamDB, GUID) - for GPO_CONTAINER in GPO_CONTAINERS: - - container_iteration += 1 - - if DC_OU == str(GPO_CONTAINER.get('dn')): - applied = True - insert_gpo = [GUID, applied, str(GPO_CONTAINER.get('dn'))] - final_list.append(insert_gpo) - break - - if global_dn == str(GPO_CONTAINER.get('dn')) and (len(GPO_CONTAINERS) == 1): - gpo_realm = True - applied = True - - - if global_dn == str(GPO_CONTAINER.get('dn')) and (len(GPO_CONTAINERS) > 1): - gpo_realm = True - applied = True - - - if container_iteration == len(GPO_CONTAINERS): - if gpo_realm == False: - insert_dud = [GUID, applied, str(GPO_CONTAINER.get('dn'))] - final_list.insert(0, insert_dud) - count_unapplied_GPO += 1 - else: - REALM_GPO = [GUID, applied, str(GPO_CONTAINER.get('dn'))] - final_list.insert(count_unapplied_GPO, REALM_GPO) - - # After GPO are sorted into containers, let's sort the containers themselves. - # But first we can get the GPO that we don't care about, out of the way. - indexed_places = container_indexes(final_list) - count = 0 - unapplied_gpo = [] - # Sorted by container - sorted_gpo_list = [] - - # Unapplied GPO live at start of list, append them to final list - while final_list[0][1] == False: - unapplied_gpo.append(final_list[count]) - count += 1 - count = 0 - sorted_gpo_list += unapplied_gpo - - # A single container call gets the linked order for all GPO in container. - # So we need one call per container - > index of the Original list - indexed_places.insert(0, 0) - while count < (len(indexed_places)-1): - sorted_gpo_list += (sort_linked(SamDB, final_list, indexed_places[count], indexed_places[count+1])) - count += 1 - return sorted_gpo_list diff --git a/source4/scripting/bin/samba_gpoupdate b/source4/scripting/bin/samba_gpoupdate index 3a6ed99d7b..bba5398571 100755 --- a/source4/scripting/bin/samba_gpoupdate +++ b/source4/scripting/bin/samba_gpoupdate @@ -3,6 +3,7 @@ # Co-Edited by Matthieu Pattou July 2013 from original August 2013 # Edited by Garming Sam Feb. 2014 # Edited by Luke Morrison April 2014 +# Edited by David Mulder May 2017 # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -22,15 +23,10 @@ and sorts them by container. Then, it applies the ones that haven't been applied, have changed, or is in the right container''' import os -import fcntl import sys -import tempfile -import subprocess -import tdb sys.path.insert(0, "bin/python") -import samba import optparse from samba import getopt as options from samba.gpclass import * @@ -39,215 +35,92 @@ from samba.dcerpc import nbt from samba import smb import logging - -# Finds all GPO Files ending in inf -def gp_path_list(path): - - GPO_LIST = [] - for ext in gp_extensions: - GPO_LIST.append((ext, ext.list(path))) - return GPO_LIST - - -def gpo_parser(GPO_LIST, ldb, conn, attr_log, lp): - '''The API method to parse the GPO - :param GPO_LIST: - :param ldb: Live instance of an LDB object AKA Samba - :param conn: Live instance of a CIFS connection - :param attr_log: backlog path for GPO and attribute to be written - no return except a newly updated Samba - ''' - - ret = False - for entry in GPO_LIST: - (ext, thefile) = entry - if ret == False: - ret = ext.parse(thefile, ldb, conn, attr_log, lp) - else: - temp = ext.parse(thefile, ldb, conn, attr_log, lp) - return ret - - -class GPOServiceSetup: - def __init__(self): - """Initialize all components necessary to return instances of - a Samba lp context (smb.conf) and Samba LDB context - """ - - self.parser = optparse.OptionParser("samba_gpoupdate [options]") - self.sambaopts = options.SambaOptions(self.parser) - self.credopts = None - self.opts = None - self.args = None - self.lp = None - self.smbconf = None - self.creds = None - self.url = None - - # Setters or Initializers - def init_parser(self): - '''Get the command line options''' - self.parser.add_option_group(self.sambaopts) - self.parser.add_option_group(options.VersionOptions(self.parser)) - self.init_credopts() - self.parser.add_option("-H", dest="url", help="URL for the samdb") - self.parser.add_option_group(self.credopts) - - def init_argsopts(self): - '''Set the options and the arguments''' - (opts, args) = self.parser.parse_args() - - self.opts = opts - self.args = args - - def init_credopts(self): - '''Set Credential operations''' - self.credopts = options.CredentialsOptions(self.parser) - - def init_lp(self): - '''Set the loadparm context''' - self.lp = self.sambaopts.get_loadparm() - self.smbconf = self.lp.configfile - if (not self.opts.url): - self.url = self.lp.samdb_url() - else: - self.url = self.opts.url - - def init_session(self): - '''Initialize the session''' - self.creds = self.credopts.get_credentials(self.lp, - fallback_machine=True) - self.session = system_session() - - def InitializeService(self): - '''Inializer for the thread''' - self.init_parser() - self.init_argsopts() - self.init_lp() - self.init_session() - - # Getters - def Get_LDB(self): - '''Return a live instance of Samba''' - SambaDB = SamDB(self.url, session_info=self.session, - credentials=self.creds, lp=self.lp) - return SambaDB - - def Get_lp_Content(self): - '''Return an instance of a local lp context''' - return self.lp - - def Get_Creds(self): - '''Return an instance of a local creds''' - return self.creds - - -# Set up the GPO service -GPOService = GPOServiceSetup() -GPOService.InitializeService() - -# Get the Samba Instance -test_ldb = GPOService.Get_LDB() - -# Get The lp context -lp = GPOService.Get_lp_Content() - -# Set up logging -logger = logging.getLogger('samba_gpoupdate') -logger.addHandler(logging.StreamHandler(sys.stdout)) -logger.setLevel(logging.CRITICAL) -log_level = lp.log_level() -if log_level == 1: - logger.setLevel(logging.ERROR) -elif log_level == 2: - logger.setLevel(logging.WARNING) -elif log_level == 3: - logger.setLevel(logging.INFO) -elif log_level >= 4: - logger.setLevel(logging.DEBUG) - -# Get the CREDS -creds = GPOService.Get_Creds() - -# Read the readable backLog into a hashmap -# then open writable backLog in same location -BackLoggedGPO = None -sys_log = '%s/%s' % (lp.get("path", "sysvol"), 'gpo.tdb') -attr_log = '%s/%s' % (lp.get("path", "sysvol"), 'attrlog.txt') - - -if os.path.isfile(sys_log): - BackLog = tdb.open(sys_log) -else: - BackLog = tdb.Tdb(sys_log, 0, tdb.DEFAULT, os.O_CREAT|os.O_RDWR) -BackLoggedGPO = scan_log(BackLog) - - -# We need to know writable DC to setup SMB connection -net = Net(creds=creds, lp=lp) -cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP | - nbt.NBT_SERVER_DS)) -dc_hostname = cldap_ret.pdc_dns_name - -try: - conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds) -except Exception, e: - raise Exception("Error connecting to '%s' using SMB" % dc_hostname, e) - -# Get the dn of the domain, and the dn of readable/writable DC -global_dn = test_ldb.domain_dn() -DC_OU = "OU=Domain Controllers" + ',' + global_dn - -# Set up a List of the GUID for all GPO's -guid_list = [x['name'] for x in conn.list('%s/Policies' % lp.get("realm").lower())] -SYSV_PATH = '%s/%s/%s' % (lp.get("path", "sysvol"), lp.get("realm"), 'Policies') - -hierarchy_gpos = establish_hierarchy(test_ldb, guid_list, DC_OU, global_dn) -change_backlog = False - -# Take a local list of all current GPO list and run it against previous GPO's -# to see if something has changed. If so reset default and re-apply GPO. -Applicable_GPO = [] -for i in hierarchy_gpos: - Applicable_GPO += i - -# Flag gets set when -GPO_Changed = False -GPO_Deleted = check_deleted(Applicable_GPO, BackLoggedGPO) -if (GPO_Deleted): - # Null the backlog - BackLoggedGPO = {} - # Reset defaults then overwrite them - Reset_Defaults(test_ldb) - GPO_Changed = False - -BackLog.transaction_start() -for guid_eval in hierarchy_gpos: - guid = guid_eval[0] - gp_extensions = [gp_sec_ext(logger)] - local_path = '%s/Policies' % lp.get("realm").lower() + '/' + guid + '/' - version = int(gpo.gpo_get_sysvol_gpt_version(lp.get("path", "sysvol") + '/' + local_path)[1]) +''' Fetch the hostname of a writable DC ''' +def get_dc_hostname(): + net = Net(creds=creds, lp=lp) + cldap_ret = net.finddc(domain=lp.get('realm'), flags=(nbt.NBT_SERVER_LDAP | + nbt.NBT_SERVER_DS)) + return cldap_ret.pdc_dns_name + +''' Fetch a list of GUIDs for applicable GPOs ''' +def get_gpo_list(dc_hostname, creds, lp): + gpos = [] + ads = gpo.ADS_STRUCT(dc_hostname, lp, creds) + if ads.connect(): + gpos = ads.get_gpo_list(creds.get_username()) + return gpos + +if __name__ == "__main__": + parser = optparse.OptionParser('samba_gpoupdate [options]') + sambaopts = options.SambaOptions(parser) + + # Get the command line options + parser.add_option_group(sambaopts) + parser.add_option_group(options.VersionOptions(parser)) + credopts = options.CredentialsOptions(parser) + parser.add_option('-H', '--url', dest='url', help='URL for the samdb') + parser.add_option_group(credopts) + + # Set the options and the arguments + (opts, args) = parser.parse_args() + + # Set the loadparm context + lp = sambaopts.get_loadparm() + if not opts.url: + url = lp.samdb_url() + else: + url = opts.url + + # Initialize the session + creds = credopts.get_credentials(lp, fallback_machine=True) + session = system_session() + + # Set up logging + logger = logging.getLogger('samba_gpoupdate') + logger.addHandler(logging.StreamHandler(sys.stdout)) + logger.setLevel(logging.CRITICAL) + log_level = lp.log_level() + if log_level == 1: + logger.setLevel(logging.ERROR) + elif log_level == 2: + logger.setLevel(logging.WARNING) + elif log_level == 3: + logger.setLevel(logging.INFO) + elif log_level >= 4: + logger.setLevel(logging.DEBUG) + + '''Return a live instance of Samba''' + test_ldb = SamDB(url, session_info=session, credentials=creds, lp=lp) + + # Read the readable backLog into a hashmap + # then open writable backLog in same location + sysvol_log = os.path.join(lp.get('cache directory'), 'gpo.tdb') + + backlog = Backlog(sysvol_log) + + dc_hostname = get_dc_hostname() try: - old_version = int(BackLoggedGPO.get(guid)) + conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds) except: - old_version = -1 - gpolist = gp_path_list(local_path) - if version != old_version: - GPO_Changed = True - # If the GPO has a dn that is applicable to Samba - if guid_eval[1]: - # If it has a GPO file that could apply to Samba - if gpolist[0][1]: - # If it we have not read it before and is not empty - # Rewrite entire logfile here - if (version != 0) and GPO_Changed == True: - logger.info('GPO %s has changed' % guid) - try: - change_backlog = gpo_parser(gpolist, test_ldb, conn, attr_log, lp) - except: - logger.error('Failed to parse gpo %s' % guid) - continue - BackLog.store(guid, '%i' % version) -BackLog.transaction_commit() -BackLog.close() + logger.error('Error connecting to \'%s\' using SMB' % dc_hostname) + raise + gpos = get_gpo_list(dc_hostname, creds, lp) + + for gpo_obj in gpos: + guid = gpo_obj.name + if guid == 'Local Policy': + continue + gp_extensions = [gp_sec_ext(logger)] + local_path = os.path.join(lp.get('realm').lower(), 'Policies', guid) + version = int(gpo.gpo_get_sysvol_gpt_version(os.path.join(lp.get("path", "sysvol"), local_path))[1]) + if version != backlog.version(guid): + logger.info('GPO %s has changed' % guid) + try: + for ext in gp_extensions: + ext.parse(ext.list(local_path), test_ldb, conn, lp) + except: + logger.error('Failed to parse gpo %s' % guid) + continue + backlog.store(guid, version) + backlog.commit() -- 2.34.1