From 89cef25f9343fad2d11b161bd89281f9633e1d08 Mon Sep 17 00:00:00 2001 From: David Mulder Date: Mon, 12 Mar 2018 09:44:38 -0600 Subject: [PATCH 1/4] gpo: Separate logic from implementation in gpclass This patch does not change functionality, it only rearranges sources to make it possible to add new gpo extensions. The previous logic has the gp_sec_ext tightly coupled with the parent gp_ext class. This patch decouples that relationship. Group Policy extensions can now be added as a single file which will import parent functionality from the samba.gpclass module. The gp_sec_ext has been modified this way. Signed-off-by: David Mulder --- python/samba/gp_sec_ext.py | 136 +++++++++++++ python/samba/gpclass.py | 277 +++++++++++--------------- source4/scripting/bin/samba_gpoupdate | 79 +------- 3 files changed, 255 insertions(+), 237 deletions(-) create mode 100644 python/samba/gp_sec_ext.py diff --git a/python/samba/gp_sec_ext.py b/python/samba/gp_sec_ext.py new file mode 100644 index 000000000000..b50f1fb3f3e0 --- /dev/null +++ b/python/samba/gp_sec_ext.py @@ -0,0 +1,136 @@ +import os.path +from gpclass import file_to, gp_inf_ext + +class inf_to_kdc_tdb(file_to): + def mins_to_hours(self): + return '%d' % (int(self.val)/60) + + def days_to_hours(self): + return '%d' % (int(self.val)*24) + + def set_kdc_tdb(self, val): + old_val = self.gp_db.gpostore.get(self.attribute) + self.logger.info('%s was changed from %s to %s' % (self.attribute, + old_val, val)) + if val is not None: + self.gp_db.gpostore.store(self.attribute, val) + self.gp_db.store(str(self), self.attribute, old_val) + else: + self.gp_db.gpostore.delete(self.attribute) + self.gp_db.delete(str(self), self.attribute) + + def mapper(self): + return { 'kdc:user_ticket_lifetime': (self.set_kdc_tdb, self.explicit), + 'kdc:service_ticket_lifetime': (self.set_kdc_tdb, + self.mins_to_hours), + 'kdc:renewal_lifetime': (self.set_kdc_tdb, + self.days_to_hours), + } + + def __str__(self): + return 'Kerberos Policy' + +class inf_to_ldb(file_to): + '''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 ch_minPwdAge(self, val): + old_val = self.ldb.get_minPwdAge() + self.logger.info('KDC Minimum Password age was changed from %s to %s' \ + % (old_val, val)) + self.gp_db.store(str(self), self.attribute, old_val) + self.ldb.set_minPwdAge(val) + + def ch_maxPwdAge(self, val): + old_val = self.ldb.get_maxPwdAge() + self.logger.info('KDC Maximum Password age was changed from %s to %s' \ + % (old_val, val)) + self.gp_db.store(str(self), self.attribute, old_val) + self.ldb.set_maxPwdAge(val) + + def ch_minPwdLength(self, val): + old_val = self.ldb.get_minPwdLength() + self.logger.info( + 'KDC Minimum Password length was changed from %s to %s' \ + % (old_val, val)) + self.gp_db.store(str(self), self.attribute, old_val) + self.ldb.set_minPwdLength(val) + + def ch_pwdProperties(self, val): + old_val = self.ldb.get_pwdProperties() + self.logger.info('KDC Password Properties were changed from %s to %s' \ + % (old_val, val)) + self.gp_db.store(str(self), self.attribute, old_val) + self.ldb.set_pwdProperties(val) + + def days2rel_nttime(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.days2rel_nttime), + "maxPwdAge" : (self.ch_maxPwdAge, self.days2rel_nttime), + # 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 __str__(self): + return 'System Access' + +class gp_sec_ext(gp_inf_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): + return os.path.join(rootpath, + "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf") + + def listmachpol(self, rootpath): + return os.path.join(rootpath, "Machine/Registry.pol") + + def listuserpol(self, rootpath): + return os.path.join(rootpath, "User/Registry.pol") + + def apply_map(self): + return {"System Access": {"MinimumPasswordAge": ("minPwdAge", + inf_to_ldb), + "MaximumPasswordAge": ("maxPwdAge", + inf_to_ldb), + "MinimumPasswordLength": ("minPwdLength", + inf_to_ldb), + "PasswordComplexity": ("pwdProperties", + inf_to_ldb), + }, + "Kerberos Policy": {"MaxTicketAge": ( + "kdc:user_ticket_lifetime", + inf_to_kdc_tdb + ), + "MaxServiceAge": ( + "kdc:service_ticket_lifetime", + inf_to_kdc_tdb + ), + "MaxRenewAge": ( + "kdc:renewal_lifetime", + inf_to_kdc_tdb + ), + } + } + diff --git a/python/samba/gpclass.py b/python/samba/gpclass.py index 33c9001cb6d0..6888a18999a6 100644 --- a/python/samba/gpclass.py +++ b/python/samba/gpclass.py @@ -25,6 +25,11 @@ from abc import ABCMeta, abstractmethod import xml.etree.ElementTree as etree import re +from samba.net import Net +from samba.dcerpc import nbt +from samba import smb +import samba.gpo as gpo +import chardet try: from enum import Enum @@ -286,6 +291,9 @@ def __del__(self): class gp_ext(object): __metaclass__ = ABCMeta + def __init__(self, logger): + self.logger = logger + @abstractmethod def list(self, rootpath): pass @@ -295,14 +303,41 @@ def apply_map(self): pass @abstractmethod - def parse(self, afile, ldb, conn, gp_db, lp): + def read(self, policy): pass + def parse(self, afile, ldb, conn, gp_db, lp): + self.ldb = ldb + self.gp_db = gp_db + self.lp = lp + + # Fixing the bug where only some Linux Boxes capitalize MACHINE + 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(conn.loadfile(bfile.replace('/', '\\'))) + except NTSTATUSError: + continue + except ValueError: + try: + return self.read(conn.loadfile(afile.replace('/', '\\'))) + except Exception as e: + self.logger.error(str(e)) + return None + @abstractmethod def __str__(self): pass -class inf_to(): +class file_to(): __metaclass__ = ABCMeta def __init__(self, logger, ldb, gp_db, lp, attribute, val): @@ -317,7 +352,7 @@ def explicit(self): return self.val def update_samba(self): - (upd_sam, value) = self.mapper().get(self.attribute) + (upd_sam, value) = self.mapper()[self.attribute] upd_sam(value()) @abstractmethod @@ -328,148 +363,19 @@ def mapper(self): def __str__(self): pass -class inf_to_kdc_tdb(inf_to): - def mins_to_hours(self): - return '%d' % (int(self.val)/60) - - def days_to_hours(self): - return '%d' % (int(self.val)*24) - - def set_kdc_tdb(self, val): - old_val = self.gp_db.gpostore.get(self.attribute) - self.logger.info('%s was changed from %s to %s' % (self.attribute, - old_val, val)) - if val is not None: - self.gp_db.gpostore.store(self.attribute, val) - self.gp_db.store(str(self), self.attribute, old_val) - else: - self.gp_db.gpostore.delete(self.attribute) - self.gp_db.delete(str(self), self.attribute) - - def mapper(self): - return { 'kdc:user_ticket_lifetime': (self.set_kdc_tdb, self.explicit), - 'kdc:service_ticket_lifetime': (self.set_kdc_tdb, - self.mins_to_hours), - 'kdc:renewal_lifetime': (self.set_kdc_tdb, - self.days_to_hours), - } - - def __str__(self): - return 'Kerberos Policy' - -class inf_to_ldb(inf_to): - '''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 ch_minPwdAge(self, val): - old_val = self.ldb.get_minPwdAge() - self.logger.info('KDC Minimum Password age was changed from %s to %s' \ - % (old_val, val)) - self.gp_db.store(str(self), self.attribute, old_val) - self.ldb.set_minPwdAge(val) - - def ch_maxPwdAge(self, val): - old_val = self.ldb.get_maxPwdAge() - self.logger.info('KDC Maximum Password age was changed from %s to %s' \ - % (old_val, val)) - self.gp_db.store(str(self), self.attribute, old_val) - self.ldb.set_maxPwdAge(val) - - def ch_minPwdLength(self, val): - old_val = self.ldb.get_minPwdLength() - self.logger.info( - 'KDC Minimum Password length was changed from %s to %s' \ - % (old_val, val)) - self.gp_db.store(str(self), self.attribute, old_val) - self.ldb.set_minPwdLength(val) - - def ch_pwdProperties(self, val): - old_val = self.ldb.get_pwdProperties() - self.logger.info('KDC Password Properties were changed from %s to %s' \ - % (old_val, val)) - self.gp_db.store(str(self), self.attribute, old_val) - self.ldb.set_pwdProperties(val) - - def days2rel_nttime(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.days2rel_nttime), - "maxPwdAge" : (self.ch_maxPwdAge, self.days2rel_nttime), - # 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 __str__(self): - return 'System Access' - - -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 __init__(self, logger): - self.logger = logger - - def __str__(self): - return "Security GPO extension" - +class gp_inf_ext(gp_ext): + @abstractmethod def list(self, rootpath): - return os.path.join(rootpath, - "MACHINE/Microsoft/Windows NT/SecEdit/GptTmpl.inf") - - def listmachpol(self, rootpath): - return os.path.join(rootpath, "Machine/Registry.pol") - - def listuserpol(self, rootpath): - return os.path.join(rootpath, "User/Registry.pol") + pass + @abstractmethod def apply_map(self): - return {"System Access": {"MinimumPasswordAge": ("minPwdAge", - inf_to_ldb), - "MaximumPasswordAge": ("maxPwdAge", - inf_to_ldb), - "MinimumPasswordLength": ("minPwdLength", - inf_to_ldb), - "PasswordComplexity": ("pwdProperties", - inf_to_ldb), - }, - "Kerberos Policy": {"MaxTicketAge": ( - "kdc:user_ticket_lifetime", - inf_to_kdc_tdb - ), - "MaxServiceAge": ( - "kdc:service_ticket_lifetime", - inf_to_kdc_tdb - ), - "MaxRenewAge": ( - "kdc:renewal_lifetime", - inf_to_kdc_tdb - ), - } - } - - def read_inf(self, path, conn): + pass + + def read(self, policy): ret = False inftable = self.apply_map() - policy = conn.loadfile(path.replace('/', '\\')) current_section = None # So here we would declare a boolean, @@ -499,27 +405,78 @@ def read_inf(self, path, conn): self.gp_db.commit() return ret - def parse(self, afile, ldb, conn, gp_db, lp): - self.ldb = ldb - self.gp_db = gp_db - self.lp = lp + @abstractmethod + def __str__(self): + pass - # Fixing the bug where only some Linux Boxes capitalize MACHINE - if afile.endswith('inf'): +''' Fetch the hostname of a writable DC ''' +def get_dc_hostname(creds, lp): + 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 + +def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions): + gp_db = store.get_gplog(creds.get_username()) + dc_hostname = get_dc_hostname(creds, lp) + try: + conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds) + except: + 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 + path = os.path.join(lp.get('realm').lower(), 'Policies', guid) + local_path = os.path.join(lp.get("path", "sysvol"), path) + version = int(gpo.gpo_get_sysvol_gpt_version(local_path)[1]) + if version != store.get_int(guid): + logger.info('GPO %s has changed' % guid) + gp_db.state(GPOSTATE.APPLY) + else: + gp_db.state(GPOSTATE.ENFORCE) + gp_db.set_guid(guid) + store.start() + for ext in gp_extensions: 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) - except NTSTATUSError: - continue - except ValueError: - try: - return self.read_inf(afile, conn) - except: - return None + ext.parse(ext.list(path), test_ldb, conn, gp_db, lp) + except Exception as e: + logger.error('Failed to parse gpo %s for extension %s' % \ + (guid, str(ext))) + logger.error('Message was: ' + str(e)) + store.cancel() + continue + store.store(guid, '%i' % version) + store.commit() + +def unapply_log(gp_db): + while True: + item = gp_db.apply_log_pop() + if item: + yield item + else: + break + +def unapply_gp(lp, creds, test_ldb, logger, store, gp_extensions): + gp_db = store.get_gplog(creds.get_username()) + gp_db.state(GPOSTATE.UNAPPLY) + for gpo_guid in unapply_log(gp_db): + gp_db.set_guid(gpo_guid) + unapply_attributes = gp_db.list(gp_extensions) + for attr in unapply_attributes: + attr_obj = attr[-1](logger, test_ldb, gp_db, lp, attr[0], attr[1]) + attr_obj.mapper()[attr[0]][0](attr[1]) # Set the old value + gp_db.delete(str(attr_obj), attr[0]) + gp_db.commit() diff --git a/source4/scripting/bin/samba_gpoupdate b/source4/scripting/bin/samba_gpoupdate index 26e0984413ed..89b3ed776162 100755 --- a/source4/scripting/bin/samba_gpoupdate +++ b/source4/scripting/bin/samba_gpoupdate @@ -34,84 +34,9 @@ try: from samba.samdb import SamDB except: SamDB = None -from samba.gpclass import * -from samba.net import Net -from samba.dcerpc import nbt -from samba import smb -import samba.gpo as gpo +from samba.gpclass import apply_gp, unapply_gp, GPOStorage +from samba.gp_sec_ext import gp_sec_ext import logging -import chardet - -''' Fetch the hostname of a writable DC ''' -def get_dc_hostname(creds, lp): - 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 - -def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions): - gp_db = store.get_gplog(creds.get_username()) - dc_hostname = get_dc_hostname(creds, lp) - try: - conn = smb.SMB(dc_hostname, 'sysvol', lp=lp, creds=creds) - except: - 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 - path = os.path.join(lp.get('realm').lower(), 'Policies', guid) - local_path = os.path.join(lp.get("path", "sysvol"), path) - version = int(gpo.gpo_get_sysvol_gpt_version(local_path)[1]) - if version != store.get_int(guid): - logger.info('GPO %s has changed' % guid) - gp_db.state(GPOSTATE.APPLY) - else: - gp_db.state(GPOSTATE.ENFORCE) - gp_db.set_guid(guid) - store.start() - for ext in gp_extensions: - try: - ext.parse(ext.list(path), test_ldb, conn, gp_db, lp) - except Exception as e: - logger.error('Failed to parse gpo %s for extension %s' % \ - (guid, str(ext))) - logger.error('Message was: ' + str(e)) - store.cancel() - continue - store.store(guid, '%i' % version) - store.commit() - -def unapply_log(gp_db): - while True: - item = gp_db.apply_log_pop() - if item: - yield item - else: - break - -def unapply_gp(lp, creds, test_ldb, logger, store, gp_extensions): - gp_db = store.get_gplog(creds.get_username()) - gp_db.state(GPOSTATE.UNAPPLY) - for gpo_guid in unapply_log(gp_db): - gp_db.set_guid(gpo_guid) - unapply_attributes = gp_db.list(gp_extensions) - for attr in unapply_attributes: - attr_obj = attr[-1](logger, test_ldb, gp_db, lp, attr[0], attr[1]) - attr_obj.mapper()[attr[0]][0](attr[1]) # Set the old value - gp_db.delete(str(attr_obj), attr[0]) - gp_db.commit() if __name__ == "__main__": parser = optparse.OptionParser('samba_gpoupdate [options]') From 47e58fcf5b97dcb7c1004420af11231b1220cc99 Mon Sep 17 00:00:00 2001 From: David Mulder Date: Mon, 8 Jan 2018 07:17:29 -0700 Subject: [PATCH 2/4] gpo: Read GPO versions locally, not from sysvol This patch does not change current functionality for the kdc. Non-kdc clients cannot read directly from the sysvol, so we need to store the GPT.INI file locally to read each gpo version. Signed-off-by: David Mulder --- python/samba/gpclass.py | 20 ++++++++++++++++++-- source4/param/pyparam.c | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/python/samba/gpclass.py b/python/samba/gpclass.py index 6888a18999a6..2678069090ac 100644 --- a/python/samba/gpclass.py +++ b/python/samba/gpclass.py @@ -424,6 +424,23 @@ def get_gpo_list(dc_hostname, creds, lp): gpos = ads.get_gpo_list(creds.get_username()) return gpos +def gpo_version(lp, conn, path): + # gpo.gpo_get_sysvol_gpt_version() reads the GPT.INI from a local file. + # If we don't have a sysvol path locally (if we're not a kdc), then + # store the file locally here so it can be read on a client. + sysvol = lp.get("path", "sysvol") + if sysvol: + local_path = os.path.join(sysvol, path, 'GPT.INI') + else: + gpt_path = lp.cache_path(os.path.join('gpt', path)) + local_path = os.path.join(gpt_path, 'GPT.INI') + if not os.path.exists(os.path.dirname(local_path)): + os.makedirs(os.path.dirname(local_path), 0o700) + data = conn.loadfile(os.path.join(path, 'GPT.INI').replace('/', '\\')) + encoding = chardet.detect(data) + open(local_path, 'w').write(data.decode(encoding['encoding'])) + return int(gpo.gpo_get_sysvol_gpt_version(os.path.dirname(local_path))[1]) + def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions): gp_db = store.get_gplog(creds.get_username()) dc_hostname = get_dc_hostname(creds, lp) @@ -439,8 +456,7 @@ def apply_gp(lp, creds, test_ldb, logger, store, gp_extensions): if guid == 'Local Policy': continue path = os.path.join(lp.get('realm').lower(), 'Policies', guid) - local_path = os.path.join(lp.get("path", "sysvol"), path) - version = int(gpo.gpo_get_sysvol_gpt_version(local_path)[1]) + version = gpo_version(lp, conn, path) if version != store.get_int(guid): logger.info('GPO %s has changed' % guid) gp_db.state(GPOSTATE.APPLY) diff --git a/source4/param/pyparam.c b/source4/param/pyparam.c index f16c2c0b227b..1d99ada09da7 100644 --- a/source4/param/pyparam.c +++ b/source4/param/pyparam.c @@ -358,6 +358,22 @@ static PyObject *py_samdb_url(PyObject *self, PyObject *unused) return PyStr_FromFormat("tdb://%s/sam.ldb", lpcfg_private_dir(lp_ctx)); } +static PyObject *py_cache_path(PyObject *self, PyObject *args) +{ + struct loadparm_context *lp_ctx = PyLoadparmContext_AsLoadparmContext(self); + char *name, *path; + PyObject *ret; + + if (!PyArg_ParseTuple(args, "s", &name)) { + return NULL; + } + + path = lpcfg_cache_path(NULL, lp_ctx, name); + ret = PyStr_FromString(path); + talloc_free(path); + + return ret; +} static PyMethodDef py_lp_ctx_methods[] = { { "load", py_lp_ctx_load, METH_VARARGS, @@ -394,6 +410,9 @@ static PyMethodDef py_lp_ctx_methods[] = { { "samdb_url", py_samdb_url, METH_NOARGS, "S.samdb_url() -> string\n" "Returns the current URL for sam.ldb." }, + { "cache_path", py_cache_path, METH_VARARGS, + "S.cache_path(name) -> string\n" + "Returns a path in the Samba cache directory." }, { NULL } }; From 075a75737e54d810fda8bc8adf4ebd456377e378 Mon Sep 17 00:00:00 2001 From: David Mulder Date: Mon, 8 Jan 2018 07:39:49 -0700 Subject: [PATCH 3/4] gpo: provide a means for disabling gpo exts Signed-off-by: David Mulder --- python/samba/gp_sec_ext.py | 11 ++ source4/scripting/bin/samba_gpoupdate | 2 +- source4/torture/gpo/apply.c | 258 +++++++++++++++++++++----- 3 files changed, 224 insertions(+), 47 deletions(-) diff --git a/python/samba/gp_sec_ext.py b/python/samba/gp_sec_ext.py index b50f1fb3f3e0..ed38625be771 100644 --- a/python/samba/gp_sec_ext.py +++ b/python/samba/gp_sec_ext.py @@ -134,3 +134,14 @@ def apply_map(self): } } + @classmethod + def enabled(cls): + lp = LoadParm() + lp.load_default() + if lp.get('server role') == 'active directory domain controller': + disabled_file = \ + os.path.splitext(os.path.abspath(__file__))[0] + '.py.disabled' + if not os.path.exists(disabled_file): + return True + return False + diff --git a/source4/scripting/bin/samba_gpoupdate b/source4/scripting/bin/samba_gpoupdate index 89b3ed776162..dd0cddaea4cd 100755 --- a/source4/scripting/bin/samba_gpoupdate +++ b/source4/scripting/bin/samba_gpoupdate @@ -86,7 +86,7 @@ if __name__ == "__main__": gp_extensions = [] if opts.machine: - if lp.get('server role') == 'active directory domain controller': + if gp_sec_ext.enabled(): gp_extensions.append(gp_sec_ext(logger)) else: pass # User extensions diff --git a/source4/torture/gpo/apply.c b/source4/torture/gpo/apply.c index 88da0b1e5fc9..dcfdd7a5dbe8 100644 --- a/source4/torture/gpo/apply.c +++ b/source4/torture/gpo/apply.c @@ -27,13 +27,16 @@ #include "lib/ldb/include/ldb.h" #include "torture/gpo/proto.h" #include +#include struct torture_suite *gpo_apply_suite(TALLOC_CTX *ctx) { struct torture_suite *suite = torture_suite_create(ctx, "apply"); - torture_suite_add_simple_test(suite, "gpo_param_from_gpo", + torture_suite_add_simple_test(suite, "gpo_system_access_policies", torture_gpo_system_access_policies); + torture_suite_add_simple_test(suite, "gpo_disable_policies", + torture_gpo_disable_policies); suite->description = talloc_strdup(suite, "Group Policy apply tests"); @@ -78,10 +81,85 @@ PasswordComplexity = %d\n\ #define GPTINI "addom.samba.example.com/Policies/"\ "{31B2F340-016D-11D2-945F-00C04FB984F9}/GPT.INI" +static void increment_gpt_ini(TALLOC_CTX *ctx, const char *gpt_file) +{ + FILE *fp = NULL; + int vers = 0; + + /* Update the version in the GPT.INI */ + if ( (fp = fopen(gpt_file, "r")) ) { + char line[256]; + while (fgets(line, 256, fp)) { + if (strncasecmp(line, "Version=", 8) == 0) { + vers = atoi(line+8); + break; + } + } + fclose(fp); + } + if ( (fp = fopen(gpt_file, "w")) ) { + char *data = talloc_asprintf(ctx, + "[General]\nVersion=%d\n", + ++vers); + fputs(data, fp); + fclose(fp); + } +} + +static bool exec_gpo_update_command(struct torture_context *tctx) +{ + int ret = 0; + const char **gpo_update_cmd; + + /* Get the gpo update command */ + gpo_update_cmd = lpcfg_gpo_update_command(tctx->lp_ctx); + torture_assert(tctx, gpo_update_cmd && gpo_update_cmd[0], + "Failed to fetch the gpo update command"); + + /* Run the gpo update command */ + ret = exec_wait(discard_const_p(char *, gpo_update_cmd)); + torture_assert(tctx, ret == 0, + "Failed to execute the gpo update command"); + + return true; +} + +static bool exec_gpo_unapply_command(struct torture_context *tctx) +{ + TALLOC_CTX *ctx = talloc_new(tctx); + char **gpo_unapply_cmd; + const char **gpo_update_cmd; + int gpo_update_len = 0; + const char **itr; + int ret = 0, i; + + /* Get the gpo update command */ + gpo_update_cmd = lpcfg_gpo_update_command(tctx->lp_ctx); + torture_assert(tctx, gpo_update_cmd && gpo_update_cmd[0], + "Failed to fetch the gpo update command"); + + for (itr = gpo_update_cmd; *itr != NULL; itr++) { + gpo_update_len++; + } + gpo_unapply_cmd = talloc_array(ctx, char*, gpo_update_len+2); + for (i = 0; i < gpo_update_len; i++) { + gpo_unapply_cmd[i] = talloc_strdup(gpo_unapply_cmd, + gpo_update_cmd[i]); + } + gpo_unapply_cmd[i] = talloc_asprintf(gpo_unapply_cmd, "--unapply"); + gpo_unapply_cmd[i+1] = NULL; + ret = exec_wait(gpo_unapply_cmd); + torture_assert(tctx, ret == 0, + "Failed to execute the gpo unapply command"); + + talloc_free(ctx); + return true; +} + bool torture_gpo_system_access_policies(struct torture_context *tctx) { TALLOC_CTX *ctx = talloc_new(tctx); - int ret, vers = 0, i; + int ret, i; const char *sysvol_path = NULL, *gpo_dir = NULL; const char *gpo_file = NULL, *gpt_file = NULL; struct ldb_context *samdb = NULL; @@ -94,15 +172,11 @@ bool torture_gpo_system_access_policies(struct torture_context *tctx) NULL }; FILE *fp = NULL; - const char **gpo_update_cmd; - char **gpo_unapply_cmd; int minpwdcases[] = { 0, 1, 998 }; int maxpwdcases[] = { 0, 1, 999 }; int pwdlencases[] = { 0, 1, 14 }; int pwdpropcases[] = { 0, 1, 1 }; struct ldb_message *old_message = NULL; - const char **itr; - int gpo_update_len = 0; sysvol_path = lpcfg_path(lpcfg_service(tctx->lp_ctx, "sysvol"), lpcfg_default_service(tctx->lp_ctx), tctx); @@ -113,11 +187,6 @@ bool torture_gpo_system_access_policies(struct torture_context *tctx) mkdir_p(gpo_dir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); gpo_file = talloc_asprintf(ctx, "%s/%s", gpo_dir, GPOFILE); - /* Get the gpo update command */ - gpo_update_cmd = lpcfg_gpo_update_command(tctx->lp_ctx); - torture_assert(tctx, gpo_update_cmd && gpo_update_cmd[0], - "Failed to fetch the gpo update command"); - /* Open and read the samba db and store the initial password settings */ samdb = samdb_connect(ctx, tctx->ev, tctx->lp_ctx, system_session(tctx->lp_ctx), 0); @@ -139,30 +208,10 @@ bool torture_gpo_system_access_policies(struct torture_context *tctx) fclose(fp); } - /* Update the version in the GPT.INI */ gpt_file = talloc_asprintf(ctx, "%s/%s", sysvol_path, GPTINI); - if ( (fp = fopen(gpt_file, "r")) ) { - char line[256]; - while (fgets(line, 256, fp)) { - if (strncasecmp(line, "Version=", 8) == 0) { - vers = atoi(line+8); - break; - } - } - fclose(fp); - } - if ( (fp = fopen(gpt_file, "w")) ) { - char *data = talloc_asprintf(ctx, - "[General]\nVersion=%d\n", - ++vers); - fputs(data, fp); - fclose(fp); - } + increment_gpt_ini(ctx, gpt_file); - /* Run the gpo update command */ - ret = exec_wait(discard_const_p(char *, gpo_update_cmd)); - torture_assert(tctx, ret == 0, - "Failed to execute the gpo update command"); + exec_gpo_update_command(tctx); ret = ldb_search(samdb, ctx, &result, ldb_get_default_basedn(samdb), @@ -204,19 +253,8 @@ bool torture_gpo_system_access_policies(struct torture_context *tctx) } /* Unapply the settings and verify they are removed */ - for (itr = gpo_update_cmd; *itr != NULL; itr++) { - gpo_update_len++; - } - gpo_unapply_cmd = talloc_array(ctx, char*, gpo_update_len+2); - for (i = 0; i < gpo_update_len; i++) { - gpo_unapply_cmd[i] = talloc_strdup(gpo_unapply_cmd, - gpo_update_cmd[i]); - } - gpo_unapply_cmd[i] = talloc_asprintf(gpo_unapply_cmd, "--unapply"); - gpo_unapply_cmd[i+1] = NULL; - ret = exec_wait(gpo_unapply_cmd); - torture_assert(tctx, ret == 0, - "Failed to execute the gpo unapply command"); + exec_gpo_unapply_command(tctx); + ret = ldb_search(samdb, ctx, &result, ldb_get_default_basedn(samdb), LDB_SCOPE_BASE, attrs, NULL); torture_assert(tctx, ret == LDB_SUCCESS && result->count == 1, @@ -265,3 +303,131 @@ bool torture_gpo_system_access_policies(struct torture_context *tctx) talloc_free(ctx); return true; } + +bool torture_gpo_disable_policies(struct torture_context *tctx) +{ + TALLOC_CTX *ctx = talloc_new(tctx); + int ret, i; + const char *sysvol_path = NULL, *gpo_dir = NULL; + const char *gpo_file = NULL, *gpt_file = NULL; + struct ldb_context *samdb = NULL; + struct ldb_result *result; + const char *attrs[] = { + "minPwdAge", + "maxPwdAge", + "minPwdLength", + "pwdProperties", + NULL + }; + FILE *fp = NULL; + int minpwdcases[] = { 0, 1, 998 }; + int maxpwdcases[] = { 0, 1, 999 }; + int pwdlencases[] = { 0, 1, 14 }; + int pwdpropcases[] = { 0, 1, 1 }; + struct ldb_message *old_message = NULL; + const char *disable_file = "bin/python/samba/gp_sec_ext.py.disabled"; + + sysvol_path = lpcfg_path(lpcfg_service(tctx->lp_ctx, "sysvol"), + lpcfg_default_service(tctx->lp_ctx), tctx); + torture_assert(tctx, sysvol_path, "Failed to fetch the sysvol path"); + + /* Ensure the sysvol path exists */ + gpo_dir = talloc_asprintf(ctx, "%s/%s", sysvol_path, GPODIR); + mkdir_p(gpo_dir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); + gpo_file = talloc_asprintf(ctx, "%s/%s", gpo_dir, GPOFILE); + + /* Open and read the samba db and store the initial password settings */ + samdb = samdb_connect(ctx, tctx->ev, tctx->lp_ctx, + system_session(tctx->lp_ctx), 0); + torture_assert(tctx, samdb, "Failed to connect to the samdb"); + + ret = ldb_search(samdb, ctx, &result, ldb_get_default_basedn(samdb), + LDB_SCOPE_BASE, attrs, NULL); + torture_assert(tctx, ret == LDB_SUCCESS && result->count == 1, + "Searching the samdb failed"); + + old_message = result->msgs[0]; + + /* Disable the policy */ + open(disable_file, O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK, 0666); + + for (i = 0; i < 3; i++) { + /* Write out the sysvol */ + if ( (fp = fopen(gpo_file, "w")) ) { + fputs(talloc_asprintf(ctx, GPTTMPL, minpwdcases[i], + maxpwdcases[i], pwdlencases[i], + pwdpropcases[i]), fp); + fclose(fp); + } + + gpt_file = talloc_asprintf(ctx, "%s/%s", sysvol_path, GPTINI); + increment_gpt_ini(ctx, gpt_file); + + exec_gpo_update_command(tctx); + + ret = ldb_search(samdb, ctx, &result, + ldb_get_default_basedn(samdb), + LDB_SCOPE_BASE, attrs, NULL); + torture_assert(tctx, ret == LDB_SUCCESS && result->count == 1, + "Searching the samdb failed"); + /* minPwdAge */ + torture_assert_int_equal(tctx, unix2nttime( + ldb_msg_find_attr_as_string( + result->msgs[0], + attrs[0], + "") + ), + unix2nttime(ldb_msg_find_attr_as_string(old_message, + attrs[0], + "") + ), + "The minPwdAge should not have been applied"); + /* maxPwdAge */ + torture_assert_int_equal(tctx, unix2nttime( + ldb_msg_find_attr_as_string( + result->msgs[0], + attrs[1], + "") + ), + unix2nttime(ldb_msg_find_attr_as_string(old_message, + attrs[1], + "") + ), + "The maxPwdAge should not have been applied"); + /* minPwdLength */ + torture_assert_int_equal(tctx, + ldb_msg_find_attr_as_int( + result->msgs[0], + attrs[2], + -1 + ), + ldb_msg_find_attr_as_int( + old_message, + attrs[2], + -2 + ), + "The minPwdLength should not have been applied"); + /* pwdProperties */ + torture_assert_int_equal(tctx, + ldb_msg_find_attr_as_int( + result->msgs[0], + attrs[3], + -1 + ), + ldb_msg_find_attr_as_int( + old_message, + attrs[3], + -2 + ), + "The pwdProperties should not have been applied"); + } + + /* Unapply the settings and verify they are removed */ + exec_gpo_unapply_command(tctx); + + /* Re-enable the policy */ + remove(disable_file); + + talloc_free(ctx); + return true; +} From bb0e6ad1e170570c80eba2ee2ac223d7a21783a2 Mon Sep 17 00:00:00 2001 From: David Mulder Date: Mon, 15 Jan 2018 13:38:59 -0700 Subject: [PATCH 4/4] gpo: make gpo extensible Make gpo easily extensible by automatically importing extensions based on their location, ex. Python scripts in /var/lib/samba/gp_exts/machine/ will apply machine policies (which apply to both client machines and kdc). Extensions must inherit from the class gp_ext(), and these child classes are automatically detected. Signed-off-by: David Mulder --- buildtools/wafsamba/wafsamba.py | 8 +- python/gp_exts/__init__.py | 35 ++++++ python/gp_exts/gp_example_ex.py | 113 ++++++++++++++++++ python/gp_exts/machine/__init__.py | 4 + .../{samba => gp_exts/machine}/gp_sec_ext.py | 3 +- python/wscript | 6 + source4/scripting/bin/samba_gpoupdate | 8 +- source4/torture/gpo/apply.c | 2 +- 8 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 python/gp_exts/__init__.py create mode 100644 python/gp_exts/gp_example_ex.py create mode 100644 python/gp_exts/machine/__init__.py rename python/{samba => gp_exts/machine}/gp_sec_ext.py (98%) diff --git a/buildtools/wafsamba/wafsamba.py b/buildtools/wafsamba/wafsamba.py index 1e331e5fcb38..6f4a675cc7bb 100644 --- a/buildtools/wafsamba/wafsamba.py +++ b/buildtools/wafsamba/wafsamba.py @@ -779,6 +779,9 @@ def copy_and_fix_python_path(task): else: replacement_shebang = "#!/usr/bin/env %s\n" % task.env["PYTHON"] + gp_pattern='sys.path.insert(0, "bin/py_gp")' + gp_replacement="""sys.path.insert(0, "%s")""" % task.env["STATEDIR"] + installed_location=task.outputs[0].bldpath(task.env) source_file = open(task.inputs[0].srcpath(task.env)) installed_file = open(installed_location, 'w') @@ -790,6 +793,8 @@ def copy_and_fix_python_path(task): newline = replacement_shebang elif pattern in line: newline = line.replace(pattern, replacement) + elif gp_pattern and gp_pattern in line: + newline = line.replace(gp_pattern, gp_replacement) installed_file.write(newline) lineno = lineno + 1 installed_file.close() @@ -840,7 +845,8 @@ def install_file(bld, destdir, file, chmod=MODE_644, flat=False, inst_file = file + '.inst' bld.SAMBA_GENERATOR('python_%s' % destname, rule=copy_and_fix_python_path, - dep_vars=["PYTHON","PYTHON_SPECIFIED","PYTHONDIR","PYTHONARCHDIR"], + dep_vars=["PYTHON","PYTHON_SPECIFIED", + "PYTHONDIR","PYTHONARCHDIR","STATEDIR"], source=file, target=inst_file) file = inst_file diff --git a/python/gp_exts/__init__.py b/python/gp_exts/__init__.py new file mode 100644 index 000000000000..857ce55c9b4e --- /dev/null +++ b/python/gp_exts/__init__.py @@ -0,0 +1,35 @@ +# Get a list of modules names +def list_modules(filename): + from os import listdir + from os.path import dirname, abspath, splitext + module_names = [] + for f in listdir(dirname(abspath(filename))): + split = splitext(f) + if not '__init__' in f and (split[-1] == '.py' or split[-1] == '.pyc'): + module_names.append(split[0]) + return list(set(module_names)) + +# Find the top base class of a class +# doesn't work with multiple base classes +def get_base(cls): + base = None + bases = cls.__bases__ + while len(bases) == 1 and bases[-1].__name__ != 'object': + base = bases[0] + bases = base.__bases__ + return base + +def get_gp_exts_from_module(parent): + import inspect + parent_gp_exts = [] + for mod_name in parent.modules: + mod = getattr(parent, mod_name) + clses = inspect.getmembers(mod, inspect.isclass) + for cls in clses: + base = get_base(cls[-1]) + if base and base.__name__ == 'gp_ext' and cls[-1].__module__ == mod.__name__: + parent_gp_exts.append(cls[-1]) + return parent_gp_exts + +from machine import * +machine_gp_exts = get_gp_exts_from_module(machine) diff --git a/python/gp_exts/gp_example_ex.py b/python/gp_exts/gp_example_ex.py new file mode 100644 index 000000000000..e0479f3b7137 --- /dev/null +++ b/python/gp_exts/gp_example_ex.py @@ -0,0 +1,113 @@ +from samba.gpclass import gp_ext, file_to + +# When placed in one of the respective machine or user sub-directoriers, +# a python file defining a class inheriting from gp_ext() will be automatically +# imported and loaded by the samba_gpoupdate script and applied on the gpupdate +# interval. + +class example_setter(file_to): + '''An example setter class, which must inherit from file_to + The mapper() and __str__() functions are mandatory, the rest of the + implementation is arbitrary. + ''' + + def set_int(self, val): + example = open('/etc/example.conf', 'w') + example.write('%s = %d\n' % (self.attr, val)) + example.close() + + def set_str(self, val): + example = open('/etc/example.conf', 'w') + example.write('%s = %s\n' % (self.attr, val)) + example.close() + + def to_int(self): + return int(self.val) + + def mapper(self): + ''' + Maps local setting names to an apply function, and a value converter. + The self.explicit converter causes the original value to be used, + without conversion. + ''' + return { "LinuxSettingInt" : (self.set_int, self.to_int), + "LinuxSettingStr" : (self.set_str, self.explicit), + } + + def __str__(self): + ''' + The name of the setter, as seen in the apply_map() keys. + ''' + return "Example" + +class gp_example_ex(gp_ext): + '''An example group policy extension, which must inhert from gp_ext + A group policy extension reads policies from the sysvol, and applies them + as settings to the local machine. + ''' + + def __str__(self): + ''' + Must return a unique extension name for identifying the extension + ''' + return "Example extension" + + def read(self, policy): + ''' + Receives the policy file as a string, and must parse and apply the + policy using the setters returned by the apply_map() function. + Alteratively, your class could inherit from gp_inf_ext() if reading a + ini/inf file (common for gpos) and this function is implemented for + you. + ''' + mappings = self.apply_map() + + # Policies are all on seperate lines, space delimited + for line in policy.split('\n'): + (key, value) = line.split() + (att, setter) = mappings['Example'].get(key) + setter(self.logger, + self.ldb, + self.gp_db, + self.lp, + self.creds, + att, + value).update_samba() + # gp_db.commit() saves the unapply log, this is mandatory + self.gp_db.commit() + + def list(self, rootpath): + ''' + This function must return the sysvol path to a policy file to be read + ''' + return os.path.join(rootpath, "MACHINE/SomeGPfile.txt") + + def apply_map(self): + ''' + The apply_map must return a dictionary of dictionaries. The first key + "Example" is the name of the setter class. The inner keys + "MSSettingName1", etc are the gpo value of the setting being applied. + The value tuple contains the name of the local setting, and the + setter class. The setter class is used within the read() function to + convert and apply settings. + ''' + return { "Example" : { "MSSettingName1": + ("LinuxSettingInt", example_setter), + "MSSettingName2": + ("LinuxSettingStr", example_setter), + } + } + + @classmethod + def enabled(cls): + ''' + This function must be included in every extension. + It returns a boolean that determines whether this policy extension + is enabled. Here you should check for the existence of a .disabled + file, and possibly perform other checks (such as whether this is a + kdc, or a client machine, etc). + ''' + disabled_file = \ + os.path.splitext(os.path.abspath(__file__))[0] + '.py.disabled' + return not os.path.exists(disabled_file) + diff --git a/python/gp_exts/machine/__init__.py b/python/gp_exts/machine/__init__.py new file mode 100644 index 000000000000..f88f7798389c --- /dev/null +++ b/python/gp_exts/machine/__init__.py @@ -0,0 +1,4 @@ +from gp_exts import list_modules +modules = list_modules(__file__) +__all__ = modules +del list_modules diff --git a/python/samba/gp_sec_ext.py b/python/gp_exts/machine/gp_sec_ext.py similarity index 98% rename from python/samba/gp_sec_ext.py rename to python/gp_exts/machine/gp_sec_ext.py index ed38625be771..8a249727f3ba 100644 --- a/python/samba/gp_sec_ext.py +++ b/python/gp_exts/machine/gp_sec_ext.py @@ -1,5 +1,6 @@ import os.path -from gpclass import file_to, gp_inf_ext +from samba.gpclass import file_to, gp_inf_ext +from samba.param import LoadParm class inf_to_kdc_tdb(file_to): def mins_to_hours(self): diff --git a/python/wscript b/python/wscript index 211fac4de620..9bfff7fb9916 100644 --- a/python/wscript +++ b/python/wscript @@ -82,3 +82,9 @@ def build(bld): installdir='python') bld.INSTALL_WILDCARD('${PYTHONARCHDIR}', 'samba/**/*.py', flat=False) + for env in bld.gen_python_environments(): + bld.SAMBA_SCRIPT('samba_python_files', + pattern='gp_exts/**/*.py', + installdir='py_gp') + + bld.INSTALL_WILDCARD('${STATEDIR}', 'gp_exts/**/*.py', flat=False) diff --git a/source4/scripting/bin/samba_gpoupdate b/source4/scripting/bin/samba_gpoupdate index dd0cddaea4cd..6f25b4ade3fd 100755 --- a/source4/scripting/bin/samba_gpoupdate +++ b/source4/scripting/bin/samba_gpoupdate @@ -26,7 +26,9 @@ import os import sys sys.path.insert(0, "bin/python") +sys.path.insert(0, "bin/py_gp") +import gp_exts import optparse from samba import getopt as options from samba.auth import system_session @@ -35,7 +37,6 @@ try: except: SamDB = None from samba.gpclass import apply_gp, unapply_gp, GPOStorage -from samba.gp_sec_ext import gp_sec_ext import logging if __name__ == "__main__": @@ -86,8 +87,9 @@ if __name__ == "__main__": gp_extensions = [] if opts.machine: - if gp_sec_ext.enabled(): - gp_extensions.append(gp_sec_ext(logger)) + for ext in gp_exts.machine_gp_exts: + if ext.enabled(): + gp_extensions.append(ext(logger)) else: pass # User extensions diff --git a/source4/torture/gpo/apply.c b/source4/torture/gpo/apply.c index dcfdd7a5dbe8..8dd2be975670 100644 --- a/source4/torture/gpo/apply.c +++ b/source4/torture/gpo/apply.c @@ -325,7 +325,7 @@ bool torture_gpo_disable_policies(struct torture_context *tctx) int pwdlencases[] = { 0, 1, 14 }; int pwdpropcases[] = { 0, 1, 1 }; struct ldb_message *old_message = NULL; - const char *disable_file = "bin/python/samba/gp_sec_ext.py.disabled"; + const char *disable_file = "bin/py_gp/gp_exts/machine/gp_sec_ext.py.disabled"; sysvol_path = lpcfg_path(lpcfg_service(tctx->lp_ctx, "sysvol"), lpcfg_default_service(tctx->lp_ctx), tctx);