summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Morrison <luc785@hotmail.com>2014-01-31 13:27:05 +1300
committerGarming Sam <garming@samba.org>2017-11-20 21:41:14 +0100
commit5194cd4e8d9d0308775042eeba544a5ea0a927a0 (patch)
tree8abc1401b5b40e447a937e26ce0d3235bfed202a
parent148b7ae707f31e221fef79e80ccda2663d5526ee (diff)
downloadsamba-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.py315
-rw-r--r--python/samba/samdb.py18
-rwxr-xr-xsource4/scripting/bin/samba_gpoupdate235
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))