1
0
mirror of https://github.com/samba-team/samba.git synced 2025-01-11 05:18:09 +03:00

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>
This commit is contained in:
Luke Morrison 2014-01-31 13:27:05 +13:00 committed by Garming Sam
parent 148b7ae707
commit 5194cd4e8d
3 changed files with 568 additions and 0 deletions

315
python/samba/gpclass.py Normal file
View File

@ -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

View File

@ -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())

View File

@ -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))