diff options
author | Luke Morrison <luc785@hotmail.com> | 2014-01-31 13:27:05 +1300 |
---|---|---|
committer | Garming Sam <garming@samba.org> | 2017-11-20 21:41:14 +0100 |
commit | 5194cd4e8d9d0308775042eeba544a5ea0a927a0 (patch) | |
tree | 8abc1401b5b40e447a937e26ce0d3235bfed202a | |
parent | 148b7ae707f31e221fef79e80ccda2663d5526ee (diff) | |
download | samba-5194cd4e8d9d0308775042eeba544a5ea0a927a0.tar.gz |
gpo: Initial commit for GPO work
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@catalyst.net.nz>
Signed-off-by: Luke Morrison <luke@hubtrek.com>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
-rw-r--r-- | python/samba/gpclass.py | 315 | ||||
-rw-r--r-- | python/samba/samdb.py | 18 | ||||
-rwxr-xr-x | source4/scripting/bin/samba_gpoupdate | 235 |
3 files changed, 568 insertions, 0 deletions
diff --git a/python/samba/gpclass.py b/python/samba/gpclass.py new file mode 100644 index 00000000000..304d670e432 --- /dev/null +++ b/python/samba/gpclass.py @@ -0,0 +1,315 @@ +# Reads important GPO parameters and updates Samba +# Copyright (C) Luke Morrison <luc785@.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 +from samba import NTSTATUSError + +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") + return path + + def listmachpol(self, rootpath): + path = "%s%s" % (rootpath, "Machine/Registry.pol") + return path + + def listuserpol(self, rootpath): + path = "%s%s" % (rootpath, "User/Registry.pol") + 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), + } + } + + def read_inf(self, path, conn, attr_log): + ret = False + inftable = self.populate_inf() + + policy = conn.loadfile(path).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. + # + # If at any point in time a GPO was applied, + # then we return that boolean at the end. + + for line in policy.splitlines(): + 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') + ret = True + setter(self.ldb, self.dn, att, value).update_samba() + return ret + + def parse(self, afile, ldb, conn, attr_log): + self.ldb = ldb + self.dn = ldb.get_default_basedn() + + # Fixing the bug where only some Linux Boxes capitalize MACHINE + if afile.endswith('inf'): + try: + blist = afile.split('/') + idx = afile.lower().split('/').index('machine') + 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) + except NTSTATUSError: + continue + except ValueError: + try: + return self.read_inf(afile, conn, attr_log) + except: + return None + + +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 + + +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/python/samba/samdb.py b/python/samba/samdb.py index 6fe680d30d8..46456290c9e 100644 --- a/python/samba/samdb.py +++ b/python/samba/samdb.py @@ -831,6 +831,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 00000000000..ba83dcf7e91 --- /dev/null +++ b/source4/scripting/bin/samba_gpoupdate @@ -0,0 +1,235 @@ +#!/usr/bin/env python +# Copyright Luke Morrison <luc785@.hotmail.com> July 2013 +# Co-Edited by Matthieu Pattou July 2013 from original August 2013 +# Edited by Garming Sam Feb. 2014 +# Edited by Luke Morrison April 2014 + +# 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 * +from samba.net import Net +from samba.dcerpc import nbt +from samba import smb + + +# 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): + '''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) + else: + temp = ext.parse(thefile, ldb, conn, attr_log) + 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 + + +def GetBackLog(sys_log): + """Reads BackLog and makes thread aware of which GPO are unchanged or empty + :param String sys_log: path to backLog + :return Dictionary previous_scanned_version: {Unedited GPO: Version Number} + *NOTE on Version below + """ + previous_scanned_version = {} + if os.path.isfile(sys_log): + previous_scanned_version = scan_log(sys_log) + return previous_scanned_version + else: + return None + +# 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() + +# 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"), 'syslog.txt') +attr_log = '%s/%s' % (lp.get("path", "sysvol"), 'attrlog.txt') +BackLoggedGPO = GetBackLog(sys_log) + + +BackLog = open(sys_log, "w") + + +# 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 + +for guid_eval in hierarchy_gpos: + guid = guid_eval[0] + gp_extensions = [gp_sec_ext()] + local_path = '%s/Policies' % lp.get("realm").lower() + '/' + guid + '/' + version = gpo.gpo_get_sysvol_gpt_version(lp.get("path", "sysvol") + '/' + local_path)[1] + gpolist = gp_path_list(local_path) + if(version != BackLoggedGPO.get(guid)): + 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: + change_backlog = gpo_parser(gpolist, test_ldb, conn, attr_log) + + BackLog.write('%s %i\n' % (guid, version)) |