gpoupdate: Rewrite samba_gpoupdate
authorDavid Mulder <dmulder@suse.com>
Thu, 25 May 2017 13:27:27 +0000 (07:27 -0600)
committerGarming Sam <garming@samba.org>
Mon, 20 Nov 2017 20:41:15 +0000 (21:41 +0100)
Use new python bindings and remove obsoleted code

Signed-off-by: David Mulder <dmulder@suse.com>
Reviewed-by: Garming Sam <garming@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
python/samba/gpclass.py
source4/scripting/bin/samba_gpoupdate

index 3b8738e33062957c2a170a59c8778049630b4341..c3f7512a00d07e220cebb8609a4ec34ab888fe75 100644 (file)
@@ -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
index 3a6ed99d7b8125a243edaf1eb01edd03797e8eab..bba5398571a9b335fca21a17ada489141515129c 100755 (executable)
@@ -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()