From 6e01fba5f98bc9eae422fcda99e848cc971009e3 Mon Sep 17 00:00:00 2001 From: William Brown Date: Wed, 29 Nov 2017 15:55:12 +0100 Subject: [PATCH 1/2] Ticket 49474 - Improve GSSAPI testing capability Bug Description: GSSAPI is difficult to test correctly. Having support for it as a topology in lib389 will mak ethis easier. Note that these tests require specific dns and hosts settings. Fix Description: Improve the ability to integrate accounts with principles, bind them with GSSAPI, oru ability to return valid uris with krb realms, our saslmap capabilities and create a single master gssapi topology. https://pagure.io/389-ds-base/issue/49474 Author: wibrown Review by: ??? --- dirsrvtests/tests/suites/gssapi/__init__.py | 0 .../tests/suites/gssapi/simple_gssapi_test.py | 140 ++++++++++++++++++++ src/lib389/lib389/__init__.py | 124 +++++++---------- src/lib389/lib389/_constants.py | 2 + src/lib389/lib389/idm/account.py | 30 +++++ src/lib389/lib389/mit_krb5.py | 19 +-- src/lib389/lib389/properties.py | 1 - src/lib389/lib389/saslmap.py | 58 ++++++++ src/lib389/lib389/tests/krb5_create_test.py | 147 --------------------- src/lib389/lib389/topologies.py | 67 +++++++++- 10 files changed, 348 insertions(+), 240 deletions(-) create mode 100644 dirsrvtests/tests/suites/gssapi/__init__.py create mode 100644 dirsrvtests/tests/suites/gssapi/simple_gssapi_test.py create mode 100644 src/lib389/lib389/saslmap.py delete mode 100644 src/lib389/lib389/tests/krb5_create_test.py diff --git a/dirsrvtests/tests/suites/gssapi/__init__.py b/dirsrvtests/tests/suites/gssapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dirsrvtests/tests/suites/gssapi/simple_gssapi_test.py b/dirsrvtests/tests/suites/gssapi/simple_gssapi_test.py new file mode 100644 index 0000000..b12b9c4 --- /dev/null +++ b/dirsrvtests/tests/suites/gssapi/simple_gssapi_test.py @@ -0,0 +1,140 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2017 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + +from lib389.topologies import topology_st_gssapi +from lib389.idm.user import UserAccounts + +from lib389.saslmap import SaslMappings + +from lib389._constants import DEFAULT_SUFFIX + +import ldap +import subprocess +import os +import pytest + +@pytest.fixture(scope='module') +def testuser(topology_st_gssapi): + # Create a user + users = UserAccounts(topology_st_gssapi.standalone, DEFAULT_SUFFIX) + user = users.create(properties={ + 'uid': 'testuser', + 'cn' : 'testuser', + 'sn' : 'user', + 'uidNumber' : '1000', + 'gidNumber' : '2000', + 'homeDirectory' : '/home/testuser' + }) + # Give them a krb princ + user.create_keytab() + return user + +def test_gssapi_bind(topology_st_gssapi, testuser): + """Test that we can bind with GSSAPI + + :id: 894a4c27-3d4c-4ba3-aa33-2910032e3783 + + :setup: standalone gssapi instance + + :steps: + 1. Bind with sasl/gssapi + :expectedresults: + 1. Bind succeeds + + """ + conn = testuser.bind_gssapi() + assert(conn.whoami_s() == "dn: %s" % testuser.dn.lower()) + +def test_invalid_sasl_map(topology_st_gssapi, testuser): + """Test that auth fails when we can not map a user. + + :id: dd4218eb-9237-4611-ba2f-1781391cadd1 + + :setup: standalone gssapi instance + + :steps: + 1. Invalidate a sasl map + 2. Attempt to bind + :expectedresults: + 1. The sasl map is invalid. + 2. The bind fails. + """ + saslmaps = SaslMappings(topology_st_gssapi.standalone) + saslmap = saslmaps.get('suffix map') + saslmap.set('nsSaslMapFilterTemplate', '(invalidattr=\\1)') + + with pytest.raises(ldap.INVALID_CREDENTIALS): + conn = testuser.bind_gssapi() + + saslmap.set('nsSaslMapFilterTemplate', '(uid=\\1)') + +def test_missing_user(topology_st_gssapi): + """Test that binding with no user does not work. + + :id: 109b5ab8-6556-4222-92d6-398476a50d30 + + :setup: standalone gssapi instance + + :steps: + 1. Create a principal with a name that is not mappable + 2. Attempt to bind + :expectedresults: + 1. The principal is created + 2. The bind fails. + """ + # Make a principal and bind with no user. + st = topology_st_gssapi.standalone + st.realm.create_principal("doesnotexist") + st.realm.create_keytab("doesnotexist", "/tmp/doesnotexist.keytab") + # Now try to bind. + subprocess.call(['/usr/bin/kdestroy', '-A']) + os.environ["KRB5_CLIENT_KTNAME"] = "/tmp/doesnotexist.keytab" + + conn = ldap.initialize(st.toLDAPURL()) + sasltok = ldap.sasl.gssapi() + + with pytest.raises(ldap.INVALID_CREDENTIALS): + conn.sasl_interactive_bind_s('', sasltok) + +def test_support_mech(topology_st_gssapi, testuser): + """Test allowed sasl mechs works when GSSAPI is allowed + + :id: 6ec80aca-00c4-4141-b96b-3ae8837fc751 + + :setup: standalone gssapi instance + + :steps: + 1. Add GSSAPI to allowed sasl mechanisms. + 2. Attempt to bind + :expectedresults: + 1. The allowed mechs are changed. + 2. The bind succeeds. + """ + topology_st_gssapi.standalone.config.set('nsslapd-allowed-sasl-mechanisms', 'GSSAPI EXTERNAL ANONYMOUS') + conn = testuser.bind_gssapi() + assert(conn.whoami_s() == "dn: %s" % testuser.dn.lower()) + +def test_rejected_mech(topology_st_gssapi, testuser): + """Test allowed sasl mechs fail when GSSAPI is not allowed. + + :id: 7896c756-6f65-4390-a844-12e2eec19675 + + :setup: standalone gssapi instance + + :steps: + 1. Add GSSAPI to allowed sasl mechanisms. + 2. Attempt to bind + :expectedresults: + 1. The allowed mechs are changed. + 2. The bind fails. + """ + topology_st_gssapi.standalone.config.set('nsslapd-allowed-sasl-mechanisms', 'EXTERNAL ANONYMOUS') + with pytest.raises(ldap.STRONG_AUTH_NOT_SUPPORTED): + conn = testuser.bind_gssapi() + topology_st_gssapi.standalone.config.set('nsslapd-allowed-sasl-mechanisms', 'GSSAPI EXTERNAL ANONYMOUS') + diff --git a/src/lib389/lib389/__init__.py b/src/lib389/lib389/__init__.py index 2c8c22d..4929828 100644 --- a/src/lib389/lib389/__init__.py +++ b/src/lib389/lib389/__init__.py @@ -50,6 +50,8 @@ import subprocess import collections import signal import errno +import pwd +import grp from shutil import copy2 try: # There are too many issues with this on EL7 @@ -386,7 +388,6 @@ class DirSrv(SimpleLDAPObject, object): args_instance[SER_CREATION_SUFFIX] = DEFAULT_SUFFIX args_instance[SER_USER_ID] = None args_instance[SER_GROUP_ID] = None - args_instance[SER_REALM] = None args_instance[SER_INST_SCRIPTS_ENABLED] = None # We allocate a "default" prefix here which allows an un-allocate or @@ -449,7 +450,6 @@ class DirSrv(SimpleLDAPObject, object): self.ldapi_enabled = args.get(SER_LDAPI_ENABLED, 'off') self.ldapi_socket = args.get(SER_LDAPI_SOCKET, None) self.host = None - self.ldapuri = None self.sslport = None self.port = None self.inst_scripts = args.get(SER_INST_SCRIPTS_ENABLED, None) @@ -459,27 +459,26 @@ class DirSrv(SimpleLDAPObject, object): self.isLocal = True if self.verbose: self.log.info("Allocate %s with %s" % (self.__class__, self.ldapi_socket)) - elif args.get(SER_LDAP_URL, None) is not None: - self.ldapuri = args.get(SER_LDAP_URL) - if self.verbose: - self.log.info("Allocate %s with %s" % (self.__class__, self.ldapuri)) - else: - # Settings from args of server attributes - self.strict_hostname = args.get(SER_STRICT_HOSTNAME_CHECKING, False) - if self.strict_hostname is True: - self.host = args.get(SER_HOST, LOCALHOST) - if self.host == LOCALHOST: - DirSrvTools.testLocalhost() - else: - # Make sure our name is in hosts - DirSrvTools.searchHostsFile(self.host, None) + self.ldapuri = args.get(SER_LDAP_URL, None) + if self.verbose: + self.log.info("Allocate %s with %s" % (self.__class__, self.ldapuri)) + + # Settings from args of server attributes + self.strict_hostname = args.get(SER_STRICT_HOSTNAME_CHECKING, False) + if self.strict_hostname is True: + self.host = args.get(SER_HOST, LOCALHOST) + if self.host == LOCALHOST: + DirSrvTools.testLocalhost() else: - self.host = args.get(SER_HOST, LOCALHOST_SHORT) - self.port = args.get(SER_PORT, DEFAULT_PORT) - self.sslport = args.get(SER_SECURE_PORT) - self.isLocal = isLocalHost(self.host) - if self.verbose: - self.log.info("Allocate %s with %s:%s" % (self.__class__, self.host, (self.sslport or self.port))) + # Make sure our name is in hosts + DirSrvTools.searchHostsFile(self.host, None) + else: + self.host = args.get(SER_HOST, LOCALHOST_SHORT) + self.port = args.get(SER_PORT, DEFAULT_PORT) + self.sslport = args.get(SER_SECURE_PORT) + self.isLocal = isLocalHost(self.host) + if self.verbose: + self.log.info("Allocate %s with %s:%s" % (self.__class__, self.host, (self.sslport or self.port))) self.binddn = args.get(SER_ROOT_DN, DN_DM) self.bindpw = args.get(SER_ROOT_PW, PW_DM) @@ -501,9 +500,8 @@ class DirSrv(SimpleLDAPObject, object): # Allocate from the args, or use our env, or use / if args.get(SER_DEPLOYED_DIR, self.prefix) is not None: self.prefix = args.get(SER_DEPLOYED_DIR, self.prefix) - self.realm = args.get(SER_REALM, None) - if self.realm is not None: - self.krb5_realm = MitKrb5(realm=self.realm, debug=self.verbose) + # This will be externally populated in topologies. + self.realm = None # Those variables needs to be revisited (sroot for 64 bits) # self.sroot = os.path.join(self.prefix, "lib/dirsrv") @@ -532,8 +530,8 @@ class DirSrv(SimpleLDAPObject, object): server = DirSrv(verbose=self.verbose) args_instance[SER_HOST] = self.host args_instance[SER_PORT] = self.port - if self.sslport is not None: - args_instance[SER_SECURE_PORT] = self.sslport + args_instance[SER_LDAP_URL] = self.ldapuri + args_instance[SER_SECURE_PORT] = self.sslport args_instance[SER_SERVERID_PROP] = self.serverid args_standalone = args_instance.copy() server.allocate(args_standalone) @@ -801,7 +799,6 @@ class DirSrv(SimpleLDAPObject, object): SER_GROUP_ID (groupid) SER_DEPLOYED_DIR (prefix) SER_BACKUP_INST_DIR (backupdir) - SER_REALM (krb5_realm) @return None @@ -839,19 +836,6 @@ class DirSrv(SimpleLDAPObject, object): prefix=self.prefix) if result != 0: raise Exception('Failed to run setup-ds.pl') - if self.realm is not None: - # This may conflict in some tests, we may need to use /etc/host - # aliases or we may need to use server id - self.krb5_realm.create_principal(principal='ldap/%s' % self.host) - ktab = '%s/ldap.keytab' % (self.ds_paths.config_dir) - self.krb5_realm.create_keytab(principal='ldap/%s' % self.host, keytab=ktab) - with open('%s/dirsrv-%s' % (self.ds_paths.initconfig_dir, self.serverid), 'a') as sfile: - sfile.write("\nKRB5_KTNAME=%s/etc/dirsrv/slapd-%s/" - "ldap.keytab\nexport KRB5_KTNAME\n" % - (self.prefix, self.serverid)) - self.restart() - - # Restart the instance def _createPythonDirsrv(self, version): """ @@ -891,17 +875,7 @@ class DirSrv(SimpleLDAPObject, object): # Go! sds.create_from_args(general, slapd, backends, None) - if self.realm is not None: - # This may conflict in some tests, we may need to use /etc/host - # aliases or we may need to use server id - self.krb5_realm.create_principal(principal='ldap/%s' % self.host) - ktab = '%s/ldap.keytab' % (self.ds_paths.config_dir) - self.krb5_realm.create_keytab(principal='ldap/%s' % self.host, keytab=ktab) - with open('%s/dirsrv-%s' % (self.ds_paths.initconfig_dir, self.serverid), 'a') as sfile: - sfile.write("\nKRB5_KTNAME=%s/etc/dirsrv/slapd-%s/" - "ldap.keytab\nexport KRB5_KTNAME\n" % - (self.prefix, self.serverid)) - self.restart() + def create(self, pyinstall=False, version=INSTALL_LATEST_CONFIG): """ @@ -1072,36 +1046,26 @@ class DirSrv(SimpleLDAPObject, object): # so that we inherit the settings correctly!!!! if self.verbose: self.log.info('open(): Connecting to uri %s' % uri) - if hasattr(ldap, 'PYLDAP_VERSION') and MAJOR >= 3: - super(DirSrv, self).__init__(uri, bytes_mode=False, trace_level=TRACE_LEVEL) - else: - super(DirSrv, self).__init__(uri, trace_level=TRACE_LEVEL) + + super(DirSrv, self).__init__(uri, bytes_mode=False, trace_level=TRACE_LEVEL) if starttls and not uri.startswith('ldaps'): self.start_tls_s() - if saslmethod and saslmethod.lower() == 'gssapi': + if saslmethod and sasltoken is not None: + # Just pass the sasltoken in! + self.sasl_interactive_bind_s("", sasltoken) + elif saslmethod and saslmethod.lower() == 'gssapi': """ Perform kerberos/gssapi authentication """ - try: - sasl_auth = ldap.sasl.gssapi("") - self.sasl_interactive_bind_s("", sasl_auth) - except ldap.LOCAL_ERROR as e: - # No Ticket - ultimately invalid credentials - log.debug("Error: No Ticket (%s)" % str(e)) - raise ldap.INVALID_CREDENTIALS - except ldap.LDAPError as e: - log.debug("SASL/GSSAPI Bind Failed: %s" % str(e)) - raise e + sasl_auth = ldap.sasl.gssapi("") + self.sasl_interactive_bind_s("", sasl_auth) elif saslmethod == 'EXTERNAL': # Do nothing. sasl_auth = ldap.sasl.external() self.sasl_interactive_bind_s("", sasl_auth) - elif saslmethod and sasltoken is not None: - # Just pass the sasltoken in! - self.sasl_interactive_bind_s("", sasltoken) elif saslmethod: # Unknown or unsupported method log.debug('Unsupported SASL method: %s' % saslmethod) @@ -1583,15 +1547,15 @@ class DirSrv(SimpleLDAPObject, object): def toLDAPURL(self): """Return the uri ldap[s]://host:[ssl]port.""" - host = self.host - if self.ldapi_enabled == 'on' and self.ldapi_socket is not None: - return "ldapi://%s" % (ldapurl.ldapUrlEscape(ensure_str(ldapi_socket))) - elif self.ldapuri: + if self.ldapuri: return self.ldapuri - elif self.sslport: - return "ldaps://%s:%d/" % (ensure_str(host), self.sslport) + elif self.ldapi_enabled == 'on' and self.ldapi_socket is not None: + return "ldapi://%s" % (ldapurl.ldapUrlEscape(ensure_str(ldapi_socket))) + elif self.sslport and not self.realm: + # Gssapi can't use SSL so we have to nuke it here. + return "ldaps://%s:%d/" % (ensure_str(self.host), self.sslport) else: - return "ldap://%s:%d/" % (ensure_str(host), self.port) + return "ldap://%s:%d/" % (ensure_str(self.host), self.port) def can_autobind(self): """Check if autobind/LDAPI is enabled.""" @@ -1636,6 +1600,12 @@ class DirSrv(SimpleLDAPObject, object): def get_tmp_dir(self): return self.ds_paths.tmp_dir + def get_user_uid(self): + return pwd.getpwnam(self.ds_paths.user).pw_uid + + def get_group_gid(self): + return grp.getgrnam(self.ds_paths.group).gr_gid + def has_asan(self): return self.ds_paths.asan_enabled diff --git a/src/lib389/lib389/_constants.py b/src/lib389/lib389/_constants.py index 1d458dd..950c520 100644 --- a/src/lib389/lib389/_constants.py +++ b/src/lib389/lib389/_constants.py @@ -102,6 +102,8 @@ DEFAULT_ENV_HEAD = 'dirsrv-' DEFAULT_CHANGELOG_NAME = "changelog5" DEFAULT_CHANGELOG_DB = 'changelogdb' +DEFAULT_REALM = "EXAMPLE.COM" + # CONF_DIR = 'etc/dirsrv' # ENV_SYSCONFIG_DIR = '/etc/sysconfig' ENV_LOCAL_DIR = '.dirsrv' diff --git a/src/lib389/lib389/idm/account.py b/src/lib389/lib389/idm/account.py index 8dcbe6b..470fd86 100644 --- a/src/lib389/lib389/idm/account.py +++ b/src/lib389/lib389/idm/account.py @@ -8,6 +8,8 @@ from lib389._mapped_object import DSLdapObject, DSLdapObjects, _gen_or, _gen_filter, _term_gen +import os +import subprocess class Account(DSLdapObject): """A single instance of Account entry @@ -38,6 +40,34 @@ class Account(DSLdapObject): self.remove('nsAccountLock', None) + def create_keytab(self): + """ + Create a keytab for this account valid to bind with. + """ + assert self._instance.realm is not None + + myuid = self.get_attr_val_utf8('uid') + self._instance.realm.create_principal(myuid) + self._instance.realm.create_keytab(myuid, "/tmp/%s.keytab" % myuid) + + self._keytab = "/tmp/%s.keytab" % myuid + + def bind_gssapi(self): + """ + Bind this account with gssapi credntials (if available) + """ + assert self._instance.realm is not None + # Kill any local ccache. + subprocess.call(['/usr/bin/kdestroy', '-A']) + + # This uses an in memory once off ccache. + os.environ["KRB5_CLIENT_KTNAME"] = self._keytab + + # Because of the way that GSSAPI works, we can't + # use the normal dirsrv open method. + return self._instance.openConnection(saslmethod='gssapi') + + class Accounts(DSLdapObjects): """DSLdapObjects that represents Account entry diff --git a/src/lib389/lib389/mit_krb5.py b/src/lib389/lib389/mit_krb5.py index 64ca24d..fe37387 100644 --- a/src/lib389/lib389/mit_krb5.py +++ b/src/lib389/lib389/mit_krb5.py @@ -108,6 +108,9 @@ class MitKrb5(object): realm = self.realm lrealm = self.realm.lower() cfile.write(""" +[libdefaults] + default_realm = {REALM} + [realms] {REALM} = {{ kdc = {HOST} @@ -130,12 +133,11 @@ class MitKrb5(object): [realms] {REALM} = {{ - #master_key_type = aes256-cts acl_file = {PREFIX}/var/kerberos/krb5kdc/kadm5.acl dict_file = /usr/share/dict/words admin_keytab = {PREFIX}/var/kerberos/krb5kdc/kadm5.keytab # Just use strong enctypes - supported_enctypes = aes256-cts:normal aes128-cts:normal + # supported_enctypes = aes256-cts:normal aes128-cts:normal }} """.format(REALM=self.realm, PREFIX=self.krb_prefix)) @@ -170,7 +172,7 @@ class MitKrb5(object): p = Popen([self.kdb5_util, 'destroy', '-r', self.realm], env=self.krb_env, stdin=PIPE) - p.communicate("yes\n") + p.communicate(b"yes\n") p.wait() assert(p.returncode == 0) # Should we clean up the configurations we made too? @@ -213,14 +215,3 @@ class MitKrb5(object): (keytab, principal, self.realm)]) assert(p.wait() == 0) - -class KrbClient(object): - def __init__(self, principal, keytab, ccache=None): - self.krb_prefix = "" - self.kdestroy = "/usr/bin/kdestroy" - if ccache is not None: - os.environ["KRB5CCNAME"] = ccache - # Destroy the previous cache if any. - subprocess.call(self.kdestroy) - # Gssapi has magic that automatically creates things by env vars - os.environ["KRB5_CLIENT_KTNAME"] = keytab diff --git a/src/lib389/lib389/properties.py b/src/lib389/lib389/properties.py index 2a9e174..e986aea 100644 --- a/src/lib389/lib389/properties.py +++ b/src/lib389/lib389/properties.py @@ -44,7 +44,6 @@ SER_PROPNAME_TO_ATTRNAME = {SER_HOST: 'nsslapd-localhost', # # Those WITHOUT related attribute name # -SER_REALM = 'krb5_realm' SER_SERVERID_PROP = 'server-id' SER_GROUP_ID = 'group-id' SER_DEPLOYED_DIR = 'deployed-dir' diff --git a/src/lib389/lib389/saslmap.py b/src/lib389/lib389/saslmap.py new file mode 100644 index 0000000..f35cdd8 --- /dev/null +++ b/src/lib389/lib389/saslmap.py @@ -0,0 +1,58 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2016 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + +from lib389._mapped_object import DSLdapObject, DSLdapObjects + +class SaslMapping(DSLdapObject): + """A sasl map providing a link from a sasl user and realm + to a valid directory server entry. + + :param instance: An instance + :type instance: lib389.DirSrv + :param dn: Entry DN + :type dn: str + :param batch: Not implemented + :type batch: bool + """ + + def __init__(self, instance, dn=None, batch=False): + super(SaslMapping, self).__init__(instance, dn, batch) + self._rdn_attribute = 'cn' + self._must_attributes = [ + 'cn', + 'nsSaslMapRegexString', + 'nsSaslMapBaseDNTemplate', + 'nsSaslMapFilterTemplate', + ] + self._create_objectclasses = [ + 'top', + 'nsSaslMapping', + ] + self._protected = False + +class SaslMappings(DSLdapObjects): + """DSLdapObjects that represents SaslMappings in the server. + + :param instance: An instance + :type instance: lib389.DirSrv + :param basedn: Base DN for all group entries below + :type basedn: str + :param batch: Not implemented + :type batch: bool + """ + + def __init__(self, instance, batch=False): + super(SaslMappings, self).__init__(instance, batch) + self._objectclasses = [ + 'nsSaslMapping', + ] + self._filterattrs = ['cn'] + self._childobject = SaslMapping + self._basedn = 'cn=mapping,cn=sasl,cn=config' + + diff --git a/src/lib389/lib389/tests/krb5_create_test.py b/src/lib389/lib389/tests/krb5_create_test.py deleted file mode 100644 index 182d51f..0000000 --- a/src/lib389/lib389/tests/krb5_create_test.py +++ /dev/null @@ -1,147 +0,0 @@ - -# --- BEGIN COPYRIGHT BLOCK --- -# Copyright (C) 2015 Red Hat, Inc. -# All rights reserved. -# -# License: GPL (version 3 or any later version). -# See LICENSE for details. -# --- END COPYRIGHT BLOCK --- -# -from lib389._constants import * -from lib389.mit_krb5 import MitKrb5, KrbClient -from lib389 import DirSrv, Entry -import pytest -import logging -import socket -import subprocess - -import ldap -import ldap.sasl - -logging.getLogger(__name__).setLevel(logging.DEBUG) -log = logging.getLogger(__name__) - -INSTANCE_PORT = 54321 -INSTANCE_SERVERID = 'standalone' -REALM = "EXAMPLE.COM" -TEST_USER = 'uid=test,%s' % DEFAULT_SUFFIX - -KEYTAB = "/tmp/test.keytab" -CCACHE = "FILE:/tmp/test.ccache" - - -class TopologyInstance(object): - def __init__(self, instance): - instance.open() - self.instance = instance - - -@pytest.fixture(scope="module") -def topology(request): - # Create the realm - krb = MitKrb5(realm=REALM) - instance = DirSrv(verbose=False) - instance.log.debug("Instance allocated") - # WARNING: If this test fails it's like a hostname issue!!! - args = {SER_HOST: socket.gethostname(), - SER_PORT: INSTANCE_PORT, - SER_REALM: REALM, - SER_SERVERID_PROP: INSTANCE_SERVERID} - instance.allocate(args) - if instance.exists(): - instance.delete() - # Its likely our realm exists too - # Remove the old keytab - if os.path.exists(KEYTAB): - os.remove(KEYTAB) - if krb.check_realm(): - krb.destroy_realm() - # This will automatically create the krb entries - krb.create_realm() - instance.create() - instance.open() - - def fin(): - if instance.exists(): - instance.delete() - if krb.check_realm(): - krb.destroy_realm() - if os.path.exists(KEYTAB): - os.remove(KEYTAB) - if os.path.exists(CCACHE): - os.remove(CCACHE) - request.addfinalizer(fin) - - return TopologyInstance(instance) - - -@pytest.fixture(scope="module") -def add_user(topology): - """ - Create a user entry - """ - - log.info('Create a user entry: %s' % TEST_USER) - uentry = Entry(TEST_USER) - uentry.setValues('objectclass', 'top', 'extensibleobject') - uentry.setValues('uid', 'test') - topology.instance.add_s(uentry) - # This doesn't matter that we re-open the realm - krb = MitKrb5(realm=REALM) - krb.create_principal("test") - # We extract the kt so we can kinit from it - krb.create_keytab("test", "/tmp/test.keytab") - - -def test_gssapi(topology, add_user): - """ - Check that our bind completese with ldapwhoami correctly mapped from - the principal to our test user object. - """ - # Init our local ccache - kclient = KrbClient("test@%s" % REALM, KEYTAB, CCACHE) - # Probably need to change this to NOT be raw python ldap - # conn = ldap.initialize("ldap://%s:%s" % (LOCALHOST, INSTANCE_PORT)) - conn = ldap.initialize("ldap://%s:%s" % (socket.gethostname(), INSTANCE_PORT)) - sasl = ldap.sasl.gssapi("test@%s" % REALM) - try: - conn.sasl_interactive_bind_s('', sasl) - except Exception as e: - try: - print("%s" % subprocess.check_output(['klist'])) - except Exception as ex: - print("%s" % ex) - print("%s" % os.environ) - print("IF THIS TEST FAILS ITS LIKELY A HOSTNAME ISSUE") - raise e - assert(conn.whoami_s() == "dn: uid=test,dc=example,dc=com") - - print("Error case 1. Broken Kerberos uid mapping") - uidmapping = 'cn=Kerberos uid mapping,cn=mapping,cn=sasl,cn=config' - topology.instance.modify_s(uidmapping, [(ldap.MOD_REPLACE, 'nsSaslMapFilterTemplate', '(cn=\1)')]) - conn0 = ldap.initialize("ldap://%s:%s" % (socket.gethostname(), INSTANCE_PORT)) - try: - conn0.sasl_interactive_bind_s('', sasl) - except Exception as e: - print("Exception (expected): %s" % type(e).__name__) - print('Desc ' + str(e['desc'])) - assert isinstance(e, ldap.INVALID_CREDENTIALS) - - # undo - topology.instance.modify_s(uidmapping, [(ldap.MOD_REPLACE, 'nsSaslMapFilterTemplate', '(uid=\1)')]) - - print("Error case 2. Delete %s from DS" % TEST_USER) - topology.instance.delete_s(TEST_USER) - try: - conn0.sasl_interactive_bind_s('', sasl) - except Exception as e: - print("Exception (expected): %s" % type(e).__name__) - print('Desc ' + str(e['desc'])) - assert isinstance(e, ldap.INVALID_CREDENTIALS) - - print("SUCCESS") - - -if __name__ == "__main__": - CURRENT_FILE = os.path.realpath(__file__) - pytest.main("-s -v %s" % CURRENT_FILE) diff --git a/src/lib389/lib389/topologies.py b/src/lib389/lib389/topologies.py index 69c71ee..730e0c4 100644 --- a/src/lib389/lib389/topologies.py +++ b/src/lib389/lib389/topologies.py @@ -10,13 +10,19 @@ import os import logging import time +# For hostname detection for GSSAPI tests +import socket + import pytest from lib389 import DirSrv from lib389.utils import generate_ds_params from lib389.replica import Replicas +from lib389.mit_krb5 import MitKrb5 +from lib389.saslmap import SaslMappings + from lib389._constants import (args_instance, SER_HOST, SER_PORT, SER_SERVERID_PROP, SER_CREATION_SUFFIX, - ReplicaRole, DEFAULT_SUFFIX, REPLICA_ID) + ReplicaRole, DEFAULT_SUFFIX, REPLICA_ID, DEFAULT_REALM) DEBUGGING = os.getenv('DEBUGGING', default=False) if DEBUGGING: @@ -186,6 +192,65 @@ def topology_st(request): return topology +@pytest.fixture(scope="module") +def topology_st_gssapi(request): + """Create a DS standalone instance with GSSAPI enabled. + + This will alter the instance to remove the secure port, to allow + GSSAPI to function. + """ + # If our hostname does not EXACTLY match a specific setup, we CAN NOT + # OPERATE. + + hostname = socket.gethostname().split('.', 1) + + # IF THIS FAILS YOUR HOST DOMAIN NAME IS INCORRECT. + assert hostname[1] == 'example.com' + + topology = create_topology({ReplicaRole.STANDALONE: 1}) + + # Fix the hostname. + topology.standalone.host = socket.gethostname() + + krb = MitKrb5(realm=DEFAULT_REALM, debug=DEBUGGING) + + # Destroy existing realm. + if krb.check_realm(): + krb.destroy_realm() + krb.create_realm() + + # Now add krb to our instance. + krb.create_principal(principal='ldap/%s' % topology.standalone.host) + krb.create_keytab(principal='ldap/%s' % topology.standalone.host, keytab='/etc/krb5.keytab') + os.chown('/etc/krb5.keytab', topology.standalone.get_user_uid(), topology.standalone.get_group_gid()) + + # Add sasl mappings + saslmappings = SaslMappings(topology.standalone) + saslmappings.create(properties={ + 'cn': 'suffix map', + # Don't add the realm due to a SASL bug + # 'nsSaslMapRegexString': '\\(.*\\)@%s' % self.realm, + 'nsSaslMapRegexString': '\\(.*\\)', + 'nsSaslMapBaseDNTemplate': topology.standalone.creation_suffix, + 'nsSaslMapFilterTemplate': '(uid=\\1)' + }) + topology.standalone.realm = krb + + topology.standalone.sslport = None + + topology.standalone.restart() + + def fin(): + if DEBUGGING: + topology.standalone.stop() + else: + topology.standalone.delete() + krb.destroy_realm() + + request.addfinalizer(fin) + + return topology + @pytest.fixture(scope="module") def topology_i2(request): -- 1.8.3.1