[PATCH 03/23] gpo: Initial commit for GPO work

David Mulder dmulder at suse.com
Fri Mar 10 16:09:10 UTC 2017


From: Luke Morrison <luc785 at hotmail.com>

Enclosed is my Summer of Code 2013 patch to have vital password GPO always applied to the Samba4 Domain Controller using a GPO update service.

To try it out "make -j" your samba with the patch, apply a security password GPO and see the difference in ~20 seconds. It also takes GPO hierarchy into account.

Split from "Initial commit for GPO work done by Luke Morrison" by David Mulder

Signed-off-by: Garming Sam <garming at catalyst.net.nz>
Signed-off-by: Luke Morrison <luke at hubtrek.com>
---
 python/samba/gpclass.py               | 362 ++++++++++++++++++++++++++++++++++
 python/samba/samdb.py                 |  18 ++
 source4/scripting/bin/samba_gpoupdate | 125 ++++++++++++
 3 files changed, 505 insertions(+)
 create mode 100755 python/samba/gpclass.py
 create mode 100755 source4/scripting/bin/samba_gpoupdate

diff --git a/python/samba/gpclass.py b/python/samba/gpclass.py
new file mode 100755
index 0000000..c596c94
--- /dev/null
+++ b/python/samba/gpclass.py
@@ -0,0 +1,362 @@
+#!/usr/bin/env python
+#
+# Reads important GPO parameters and updates Samba
+# Copyright (C) Luke Morrison <luc785 at .hotmail.com> 2013
+#
+# 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
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import sys
+import os
+sys.path.insert(0, "bin/python")
+import samba.gpo as gpo
+import optparse
+import ldb
+from samba.auth import system_session
+import samba.getopt as options
+from samba.samdb import SamDB
+from samba.netcmd import gpo as gpo_user
+import codecs
+
+class gp_ext(object):
+    def list(self, rootpath):
+        return None
+
+    def __str__(self):
+        return "default_gp_ext"
+
+
+class inf_to_ldb(object):
+    '''This class takes the .inf file parameter (essentially a GPO file mapped to a GUID),
+    hashmaps it to the Samba parameter, which then uses an ldb object to update the
+    parameter to Samba4. Not registry oriented whatsoever.
+    '''
+
+    def __init__(self, ldb, dn, attribute, val):
+        self.ldb = ldb
+        self.dn = dn
+        self.attribute = attribute
+        self.val = val
+
+    def ch_minPwdAge(self, val):
+        self.ldb.set_minPwdAge(val)
+
+    def ch_maxPwdAge(self, val):
+        self.ldb.set_maxPwdAge(val)
+
+    def ch_minPwdLength(self, val):
+        self.ldb.set_minPwdLength(val)
+
+    def ch_pwdProperties(self, val):
+        self.ldb.set_pwdProperties(val)
+
+    def explicit(self):
+        return self.val
+
+    def nttime2unix(self):
+        seconds = 60
+        minutes = 60
+        hours = 24
+        sam_add = 10000000
+        val = (self.val)
+        val = int(val)
+        return  str(-(val * seconds * minutes * hours * sam_add))
+
+    def mapper(self):
+        '''ldap value : samba setter'''
+        return { "minPwdAge" : (self.ch_minPwdAge, self.nttime2unix),
+                 "maxPwdAge" : (self.ch_maxPwdAge, self.nttime2unix),
+                 # Could be none, but I like the method assignment in update_samba
+                 "minPwdLength" : (self.ch_minPwdLength, self.explicit),
+                 "pwdProperties" : (self.ch_pwdProperties, self.explicit),
+
+               }
+
+    def update_samba(self):
+        (upd_sam, value) = self.mapper().get(self.attribute)
+        upd_sam(value())     # or val = value() then update(val)
+
+
+class gp_sec_ext(gp_ext):
+    '''This class does the following two things:
+        1) Identifies the GPO if it has a certain kind of filepath,
+        2) Finally parses it.
+    '''
+
+    count = 0
+
+    def __str__(self):
+        return "Security GPO extension"
+
+    def list(self, rootpath):
+        path = "%s/%s" % (rootpath, "/Machine/Microsoft/Windows NT/SecEdit/GptTmpl.inf")
+        if os.path.exists(path):
+                return path
+
+    def listmachpol(self, rootpath):
+        path = "%s/%s" % (rootpath, "Machine/Registry.pol")
+        if os.path.exists(path):
+            return path
+
+    def listuserpol(self, rootpath):
+        path = "%s/%s" % (rootpath, "User/Registry.pol")
+        if os.path.exists(path):
+            return path
+
+    def populate_inf(self):
+        return {"System Access": {"MinimumPasswordAge": ("minPwdAge", inf_to_ldb),
+                                  "MaximumPasswordAge": ("maxPwdAge", inf_to_ldb),
+                                  "MinimumPasswordLength": ("minPwdLength", inf_to_ldb),
+                                  "PasswordComplexity": ("pwdProperties", inf_to_ldb),
+                                 }
+               }
+#FIXME. EACH gpo should have a parser, and a creater. Essentially a gpo is just a file. Possibly a method and class to link it to organization unit (if that already does not exist) so that GPO's can be created arithmetically, possibly with a hashtable for certain GPO, then linked if desired. Also could store a backup folder of gpo's and then configure them without necessarily deploying it.
+
+    def read_inf(self, path):
+        inftable = self.populate_inf()
+        '''The inf file to be mapped'''
+        policy = codecs.open(path, encoding='utf-16')
+        if not policy:
+            return None
+        current_section = None
+        for line in policy.readlines():
+            line = line.strip()
+            if line[0] == '[':
+                section = line[1: -1]
+                current_section = inftable.get(section.encode('ascii', 'ignore'))
+
+            else:
+                # We must be in a section
+                if not current_section:
+                    continue
+                (key, value) = line.split("=")
+                key = key.strip()
+                if current_section.get(key):
+                    (att, setter) = current_section.get(key)
+                    value = value.encode('ascii', 'ignore')
+                    setter(self.ldb, self.dn, att, value).update_samba()
+    #FIXME read registry files (.pol). Can they ever apply? Define read_registry():
+
+    def parse(self, afile, ldb):
+        self.ldb = ldb
+        self.dn = ldb.get_default_basedn()
+        if afile.endswith('inf'):
+            self.read_inf(afile)
+
+class samba4_gpo_hierarchy(object):
+
+    def __init__(self, SamDB, sysvol_guid_list, DC_OU, GLOBAL_DN):
+        """
+        :param SamDB: An instance of the live samba database
+        :param sysvol_guid_list: The complete list of all GPO GUID's listed in sysvol folder
+        :param DC_OU: The respective distinguished name of the Domain Controller
+        :param GLOBAL_DN: The Domain DN that Samba is a part of
+        """
+        self.SamDB = SamDB
+        self.GUID_L = sysvol_guid_list
+        self.DC_OU = DC_OU
+        self.GL_DN = GLOBAL_DN
+        self.sorted_containers = []
+        self.sorted_full = []
+        self.indexed_places = []
+        self.unapplied_gpo = 0
+
+    def update_unapplied_gpo(self):
+        self.update_unapplied_gpo += 1
+
+    '''Returns list of int indexes to where the dn changes'''
+    def container_indexes(self):
+        count = 0
+        container_indexes = []
+        while count < (len(self.GUID_L)-1):
+            if self.sorted_containers[count][2] != self.sorted_containers[count+1][2]:
+                container_indexes.append(count+1)
+            count += 1
+        container_indexes.append(len(self.sorted_containers))
+        return container_indexes
+
+
+    def establish_hierarchy(self):
+        final_list = []
+        count_unapplied_GPO = 0
+        for GUID in self.GUID_L:
+            container_iteration = 0
+            applied = False # Assume first it is not applied
+            gpo_realm = False # Realm only written on last call, if the GPO is linked to multiple places
+            '''Get all of the linked information'''
+            GPO_CONTAINERS = gpo_user.get_gpo_containers(self.SamDB, GUID)
+            for GPO_CONTAINER in GPO_CONTAINERS:
+
+                container_iteration +=1
+
+                if self.DC_OU == str(GPO_CONTAINER.get('dn')):
+                    applied = True
+                    insert_gpo = [GUID, applied, str(GPO_CONTAINER.get('dn'))]
+                    self.sorted_containers.append(insert_gpo)
+                    break
+
+                if self.GL_DN == str(GPO_CONTAINER.get('dn')) and (len(GPO_CONTAINERS) == 1):
+                    gpo_realm = True
+                    applied = True
+
+                if self.GL_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'))]
+                        self.sorted_containers.insert(0, insert_dud)
+                        self.count_unapplied_GPO()
+                    else :
+                        REALM_GPO = [GUID, applied, str(GPO_CONTAINER.get('dn'))]
+                        self.sorted_containers.insert(count_unapplied_GPO, REALM_GPO)
+
+        '''After GPO are sorted into containers, sort the containers themselves. But first append non-applicable GPO.'''
+        self.indexed_places = self.container_indexes()
+        count = 0
+        unapplied_gpo = []
+        self.sorted_full = []
+        '''Append all empties to final from first change of container'''
+        while count < self.indexed_places[0]:
+            unapplied_gpo.append(self.sorted_containers[count])
+            count += 1
+
+        count = 0
+        self.sorted_full += unapplied_gpo
+        while count < (len(self.indexed_places)-1): # Already accounted for one in empties
+            self.sorted_full += (sort_linked(self.SamDB, self.sorted_containers, self.indexed_places[count], self.indexed_places[count + 1]))
+            count += 1
+
+
+def scan_log(sysvol_path):
+    a = open(sysvol_path, "r")
+    data = {}
+    for line in a.readlines():
+        line = line.strip()
+        (guid, version) = line.split(" ")
+        data[guid] = int(version)
+    return data
+
+# 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], True, 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 = []
+    '''Since the unapplied GPO are put at the front of the list, just once again append them to the linked container sorted list'''
+    while count < indexed_places[0]:
+        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
+    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/python/samba/samdb.py b/python/samba/samdb.py
index eabe363..c82209d 100644
--- a/python/samba/samdb.py
+++ b/python/samba/samdb.py
@@ -828,6 +828,24 @@ accountExpires: %u
         else:
             return res[0]["minPwdAge"][0]
 
+    def set_maxPwdAge(self, value):
+        m = ldb.Message()
+        m.dn = ldb.Dn(self, self.domain_dn())
+        m["maxPwdAge"] = ldb.MessageElement(value, ldb.FLAG_MOD_REPLACE, "maxPwdAge")
+        self.modify(m)
+
+
+    def get_maxPwdAge(self):
+        res = self.search(self.domain_dn(), scope=ldb.SCOPE_BASE, attrs=["maxPwdAge"])
+        if len(res) == 0:
+            return None
+        elif not "maxPwdAge" in res[0]:
+            return None
+        else:
+            return res[0]["maxPwdAge"][0]
+
+
+
     def set_minPwdLength(self, value):
         m = ldb.Message()
         m.dn = ldb.Dn(self, self.domain_dn())
diff --git a/source4/scripting/bin/samba_gpoupdate b/source4/scripting/bin/samba_gpoupdate
new file mode 100755
index 0000000..618552b
--- /dev/null
+++ b/source4/scripting/bin/samba_gpoupdate
@@ -0,0 +1,125 @@
+#!/usr/bin/env python
+# Copyright Luke Morrison <luc785 at .hotmail.com> 2013
+
+# 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
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+'''This script reads a log file of previous GPO, gets all GPO from sysvol
+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
+
+sys.path.insert(0, "bin/python")
+
+import samba
+import optparse
+from samba import getopt as options
+from samba.gpclass import *
+
+# 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
+
+# Reads the GPOs and sends them to their proper handlers
+def gpo_parser(GPO_LIST, ldb):
+    for entry in GPO_LIST:
+        (ext, thefile) = entry
+        ext.parse(thefile, ldb)
+
+
+parser = optparse.OptionParser("testsearchdn [options]")
+
+sambaopts = options.SambaOptions(parser)
+
+parser.add_option_group(sambaopts)
+parser.add_option_group(options.VersionOptions(parser))
+
+credopts = options.CredentialsOptions(parser)
+
+parser.add_option("-H", dest = "url", help="URL for the samdb")
+
+parser.add_option_group(credopts)
+
+opts, args = parser.parse_args()
+lp = sambaopts.get_loadparm()
+
+smbconf = lp.configfile
+creds = credopts.get_credentials(lp)
+
+session = system_session()
+
+if not opts.url:
+    url = lp.samdb_url()
+else:
+    url = opts.url
+
+#########################
+#Inialize Samba Database#
+#########################
+
+test_ldb = SamDB(url, session_info=session,
+                 credentials=creds,lp=lp)
+
+schemadn = test_ldb.get_schema_basedn()
+
+basedn = test_ldb.get_default_basedn()
+
+'''Will need sysvol to write a basic GUID version dynamic log file'''
+path = '%s/%s/%s' % (lp.get("path", "sysvol"), lp.get("realm"), 'Policies')
+sys_log = '%s/%s' % (lp.get("path", "sysvol"), 'syslog.txt')
+
+'''Returns dict from previous logfile, then scraps the logfile '''
+previous_scanned_version = {'a' : 4}
+if os.path.isfile(sys_log):
+    previous_scanned_version = scan_log(sys_log)
+sys_log = open(sys_log, "w")
+
+'''Establishes the hierarchy TODO - insert the link fom Microsoft and vouch why we dont care about site or local'''
+specific_ou = "OU=Domain Controllers"
+'''TODO Definitely get DC from Samba'''
+global_dn = test_ldb.domain_dn()
+print 'The global DN for this domain is ' + global_dn
+DC_OU = specific_ou + ',' + global_dn
+guid_list = os.listdir(path)
+
+hierarchy_gpos = samba4_gpo_hierarchy(test_ldb, guid_list, DC_OU, global_dn)
+hierarchy_gpos.establish_hierarchy()
+
+
+for guid_eval in hierarchy_gpos.sorted_full:
+    guid = guid_eval[0]
+    gp_extensions = [gp_sec_ext()]
+    local_path = path + '/' + guid + '/'
+    version = gpo.gpo_get_sysvol_gpt_version(local_path)[1]
+
+    gpolist = gp_path_list(local_path)
+    print local_path
+
+    '''If an important GPO parse it. Will not parse if it has not changed, is empty, or is not in the right container'''
+    if guid_eval[1]:
+        if gpolist[0][1]:
+            if (version != previous_scanned_version.get(guid)) and (version != 0):
+                print ('GPO %s has changed' % guid)
+                gpo_parser(gpolist, test_ldb)
+
+    sys_log.write('%s %i\n' % (guid,version))
-- 
2.10.2




More information about the samba-technical mailing list