diff --git a/k5test/realm.py b/k5test/realm.py index 0dd8aba..d70b9e4 100644 --- a/k5test/realm.py +++ b/k5test/realm.py @@ -1,3 +1,4 @@ +# Copyright (C) 2021 by Red Hat, Inc. # Copyright (C) 2014 by Solly Ross # Copyright (C) 2010 by the Massachusetts Institute of Technology. # All rights reserved. @@ -21,9 +22,13 @@ # this software for any purpose. It is provided "as is" without express # or implied warranty. -# Changes from original: modified to work with Python's unittest +# Changes from original: +# - modified to work with Python's unittest +# - added Heimdal support +import abc import copy import os +import shlex import shutil import signal import socket @@ -63,42 +68,52 @@ def _cfg_merge(cfg1, cfg2): return result -_default_krb5_conf = { - 'libdefaults': { - 'default_realm': '$realm', - 'dns_lookup_kdc': 'false'}, - 'realms': { - '$realm': { - 'kdc': '$hostname:$port0', - 'admin_server': '$hostname:$port1', - 'kpasswd_server': '$hostname:$port2'}}} - - -_default_kdc_conf = { - 'realms': { - '$realm': { - 'database_module': 'db', - 'iprop_port': '$port4', - 'key_stash_file': '$tmpdir/stash', - 'acl_file': '$tmpdir/acl', - 'dictfile': '$tmpdir/dictfile', - 'kadmind_port': '$port1', - 'kpasswd_port': '$port2', - 'kdc_ports': '$port0', - 'kdc_tcp_ports': '$port0', - 'database_name': '$tmpdir/db'}}, - 'dbmodules': { - 'db_module_dir': os.path.join(_utils.find_plugin_dir(), - 'kdb'), - 'db': {'db_library': 'db2', 'database_name': '$tmpdir/db'}}, - 'logging': { - 'admin_server': 'FILE:$tmpdir/kadmind5.log', - 'kdc': 'FILE:$tmpdir/kdc.log', - 'default': 'FILE:$tmpdir/others.log'}} - - -class K5Realm(object): +def _discover_path(name, default, paths): + stderr_out = subprocess.DEVNULL + try: + path = subprocess.check_output(['which', name], + stderr=stderr_out).strip() + path = path.decode(sys.getfilesystemencoding() or + sys.getdefaultencoding()) + _LOG.debug(f"Using discovered path for {name} ({path})") + return path + except subprocess.CalledProcessError as e: + path = paths.get(name, default) + _LOG.debug(f"Using default path for {name} ({path}): {e}") + return path + + +class K5Realm(metaclass=abc.ABCMeta): """An object representing a functional krb5 test realm.""" + + def __new__(cls, *args, **kwargs): + provider_cls = cls + + if provider_cls == K5Realm: + krb5_config = _discover_path('krb5-config', + '/usr/bin/krb5-config', kwargs) + + try: + krb5_version = subprocess.check_output( + [krb5_config, '--version'], stderr=subprocess.STDOUT) + krb5_version = krb5_version.decode( + sys.getfilesystemencoding() or sys.getdefaultencoding()) + + # macOS output doesn't contain Heimdal + if 'heimdal' in krb5_version.lower() or ( + sys.platform == 'darwin' and + krb5_config == '/usr/bin/krb5-config'): + provider_cls = HeimdalRealm + else: + provider_cls = MITRealm + + except Exception as e: + _LOG.debug(f"Failed to determine gssapi provider, defaulting " + f"to MIT: {e}") + provider_cls = MITRealm + + return super(K5Realm, cls).__new__(provider_cls) + def __init__(self, realm='KRBTEST.COM', portbase=61000, krb5_conf=None, kdc_conf=None, create_kdb=True, krbtgt_keysalt=None, create_user=True, get_creds=True, @@ -123,8 +138,6 @@ def __init__(self, realm='KRBTEST.COM', portbase=61000, self.client_keytab = os.path.join(self.tmpdir, 'client_keytab') self.ccache = os.path.join(self.tmpdir, 'ccache') self.kadmin_ccache = os.path.join(self.tmpdir, 'kadmin_ccache') - self._krb5_conf = _cfg_merge(_default_krb5_conf, krb5_conf) - self._kdc_conf = _cfg_merge(_default_kdc_conf, kdc_conf) self._kdc_proc = None self._kadmind_proc = None krb5_conf_path = os.path.join(self.tmpdir, 'krb5.conf') @@ -135,19 +148,19 @@ def __init__(self, realm='KRBTEST.COM', portbase=61000, self._init_paths(**paths) - self._devnull = open(os.devnull, 'r') - if existing is None: - self._create_conf(self._krb5_conf, krb5_conf_path) - self._create_conf(self._kdc_conf, kdc_conf_path) + self._create_conf(_cfg_merge(self._krb5_conf, krb5_conf), + krb5_conf_path) + if self._kdc_conf or kdc_conf: + self._create_conf(_cfg_merge(self._kdc_conf, kdc_conf), + kdc_conf_path) self._create_acl() self._create_dictfile() if create_kdb: self.create_kdb() if krbtgt_keysalt and create_kdb: - self.run_kadminl('cpw -randkey -e %s %s' % - (krbtgt_keysalt, self.krbtgt_princ)) + self.change_password(self.krbtgt_princ, keysalt=krbtgt_keysalt) if create_user and create_kdb: self.addprinc(self.user_princ, self.password('user')) self.addprinc(self.admin_princ, self.password('admin')) @@ -164,36 +177,75 @@ def __init__(self, realm='KRBTEST.COM', portbase=61000, self.kinit(self.user_princ, self.password('user')) self.klist() - def _discover_path(self, name, default, paths): - stderr_out = getattr(subprocess, 'DEVNULL', subprocess.PIPE) - try: - path = subprocess.check_output(['which', name], - stderr=stderr_out).strip() - path = path.decode(sys.getfilesystemencoding() or - sys.getdefaultencoding()) - _LOG.debug("Using discovered path for {name} ({path}".format( - name=name, path=path)) - return path - except subprocess.CalledProcessError as e: - path = paths.get(name, default) - _LOG.debug("Using default path for {name} ({path}): {err}".format( - name=name, path=path, err=e)) - return path + @abc.abstractproperty + def provider(self): + pass + + @abc.abstractproperty + def _default_paths(self): + pass + + @abc.abstractproperty + def _krb5_conf(self): + pass + + @abc.abstractproperty + def _kdc_conf(self): + pass + + @abc.abstractmethod + def create_kdb(self): + pass + + @abc.abstractmethod + def addprinc(self, princname, password=None): + pass + + @abc.abstractmethod + def change_password(self, principal, password=None, keysalt=None): + pass + + @abc.abstractmethod + def extract_keytab(self, princname, keytab): + pass + + @abc.abstractmethod + def kinit(self, princname, password=None, flags=None, verbose=True, + **keywords): + pass + + @abc.abstractmethod + def klist(self, ccache=None, **keywords): + pass + + @abc.abstractclassmethod + def klist_keytab(self, keytab=None, **keywords): + pass + + @abc.abstractmethod + def prep_kadmin(self, princname=None, pw=None, flags=None): + pass + + @abc.abstractmethod + def run_kadmin(self, query, **keywords): + pass + + @abc.abstractmethod + def run_kadminl(self, query, **keywords): + pass + + @abc.abstractmethod + def start_kdc(self, args=None, env=None): + pass + + @abc.abstractmethod + def start_kadmind(self, env=None): + pass def _init_paths(self, **paths): - self.kdb5_util = self._discover_path('kdb5_util', - '/usr/sbin/kdb5_util', paths) - self.krb5kdc = self._discover_path('krb5kdc', - '/usr/sbin/krb5kdc', paths) - self.kadmin_local = self._discover_path('kadmin.local', - '/usr/sbin/kadmin.local', - paths) - self.kprop = self._discover_path('kprop', '/usr/sbin/kprop', paths) - self.kadmind = self._discover_path('kadmind', - '/usr/sbin/kadmind', paths) - - self._kinit = self._discover_path('kinit', '/usr/bin/kinit', paths) - self._klist = self._discover_path('klist', '/usr/bin/klist', paths) + for attr, name, default in self._default_paths: + value = _discover_path(name, default, paths) + setattr(self, attr, value) def _create_conf(self, profile, filename): with open(filename, 'w') as conf_file: @@ -225,7 +277,7 @@ def _write_cfg_section(self, conf_file, contents, indent_level): @property def hostname(self): - return socket.getfqdn() + return 'localhost' if sys.platform == 'darwin' else socket.getfqdn() def _subst_cfg_value(self, value): template = string.Template(value) @@ -273,7 +325,7 @@ def run(self, args, env=None, input=None, expected_code=0): if input: infile = subprocess.PIPE else: - infile = self._devnull + infile = subprocess.DEVNULL proc = subprocess.Popen(args, stdin=infile, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env) @@ -295,7 +347,7 @@ def run(self, args, env=None, input=None, expected_code=0): return outdata def __del__(self): - self._devnull.close() + pass def kprop_port(self): return self.portbase + 3 @@ -303,15 +355,15 @@ def kprop_port(self): def server_port(self): return self.portbase + 5 - def create_kdb(self): - self.run([self.kdb5_util, 'create', '-W', '-s', '-P', 'master']) + def _start_daemon(self, args, env=None, sentinel=None): + if env is None: + env = self.env - def _start_daemon(self, args, env, sentinel): - proc = subprocess.Popen(args, stdin=self._devnull, - stdout=subprocess.PIPE, + stdout = subprocess.PIPE if sentinel else subprocess.DEVNULL + proc = subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=stdout, stderr=subprocess.STDOUT, env=env) cmd = ' '.join(args) - while True: + while sentinel: line = proc.stdout.readline().decode() if line == "": code = proc.wait() @@ -329,13 +381,6 @@ def _start_daemon(self, args, env, sentinel): return proc - def start_kdc(self, args=[], env=None): - if env is None: - env = self.env - assert(self._kdc_proc is None) - self._kdc_proc = self._start_daemon([self.krb5kdc, '-n'] + args, env, - 'starting...') - def _stop_daemon(self, proc): proc.terminate() proc.communicate() @@ -346,17 +391,6 @@ def stop_kdc(self): self._stop_daemon(self._kdc_proc) self._kdc_proc = None - def start_kadmind(self, env=None): - if env is None: - env = self.env - assert(self._kadmind_proc is None) - dump_path = os.path.join(self.tmpdir, 'dump') - self._kadmind_proc = self._start_daemon([self.kadmind, '-nofork', '-W', - '-p', self.kdb5_util, - '-K', self.kprop, - '-F', dump_path], env, - 'starting...') - def stop_kadmind(self): assert(self._kadmind_proc is not None) self._stop_daemon(self._kadmind_proc) @@ -371,75 +405,331 @@ def stop(self): if self.tmpdir and not self.is_existing: shutil.rmtree(self.tmpdir) + def password(self, name): + """Get a weakly random password from name, consistent across calls.""" + return name + six.text_type(os.path.basename(self.tmpdir)) + + def special_env(self, name, has_kdc_conf, krb5_conf=None, kdc_conf=None): + krb5_conf_path = os.path.join(self.tmpdir, f'krb5.conf.{name}') + krb5_conf = _cfg_merge(self._krb5_conf, krb5_conf) + self._create_conf(krb5_conf, krb5_conf_path) + if has_kdc_conf and self._kdc_conf: + kdc_conf_path = os.path.join(self.tmpdir, f'kdc.conf.{name}') + kdc_conf = _cfg_merge(self._kdc_conf, kdc_conf) + self._create_conf(kdc_conf, kdc_conf_path) + else: + kdc_conf_path = None + return self._make_env(krb5_conf_path, kdc_conf_path) + + def kill_daemons(self): + # clean up daemons + for proc in self._daemons: + os.kill(proc.pid, signal.SIGTERM) + + +class MITRealm(K5Realm): + @property + def provider(self): + return 'mit' + + @property + def _default_paths(self): + return [ + ('kdb5_util', 'kdb5_util', '/usr/sbin/kdb5_util'), + ('krb5kdc', 'krb5kdc', '/usr/sbin/kdb5kdc'), + ('kadmin', 'kadmin', '/usr/bin/admin'), + ('kadmin_local', 'kadmin.local', '/usr/sbin/kadmin.local'), + ('kadmind', 'kadmind', '/usr/sbin/kadmind'), + ('kprop', 'kprop', '/usr/sbin/kprop'), + ('_kinit', 'kinit', '/usr/bin/kinit'), + ('_klist', 'klist', '/usr/bin/klist'), + ] + + @property + def _krb5_conf(self): + return { + 'libdefaults': { + 'default_realm': '$realm', + 'dns_lookup_kdc': 'false'}, + 'realms': { + '$realm': { + 'kdc': '$hostname:$port0', + 'admin_server': '$hostname:$port1', + 'kpasswd_server': '$hostname:$port2'}}} + + @property + def _kdc_conf(self): + plugin_dir = _utils.find_plugin_dir() + db_module_dir = None + if plugin_dir: + db_module_dir = os.path.join(plugin_dir, 'kdc') + + return { + 'realms': { + '$realm': { + 'database_module': 'db', + 'iprop_port': '$port4', + 'key_stash_file': '$tmpdir/stash', + 'acl_file': '$tmpdir/acl', + 'dict_file': '$tmpdir/dictfile', + 'kadmind_port': '$port1', + 'kpasswd_port': '$port2', + 'kdc_ports': '$port0', + 'kdc_tcp_ports': '$port0', + 'database_name': '$tmpdir/db'}}, + 'dbmodules': { + 'db_module_dir': db_module_dir, + 'db': {'db_library': 'db2', 'database_name': '$tmpdir/db'}}, + 'logging': { + 'admin_server': 'FILE:$tmpdir/kadmind5.log', + 'kdc': 'FILE:$tmpdir/kdc.log', + 'default': 'FILE:$tmpdir/others.log'}} + + def create_kdb(self): + self.run([self.kdb5_util, 'create', '-W', '-s', '-P', 'master']) + def addprinc(self, princname, password=None): + args = ['addprinc'] if password: - self.run_kadminl('addprinc -pw %s %s' % (password, princname)) + args.extend(['-pw', password]) else: - self.run_kadminl('addprinc -randkey %s' % princname) + args.append('-randkey') - def extract_keytab(self, princname, keytab): - self.run_kadminl('ktadd -k %s -norandkey %s' % (keytab, princname)) + args.append(princname) + self.run_kadminl(args) + + def change_password(self, principal, password=None, keysalt=None): + args = ['cpw'] - def kinit(self, princname, password=None, flags=[], verbose=True, - **keywords): if password: - input = password + "\n" + args.extend(['-pw', password]) else: - input = None + args.append('-randkey') + if keysalt: + args.extend('-e', keysalt) + + args.append(principal) + self.run_kadminl(args) + + def extract_keytab(self, princname, keytab): + self.run_kadminl(f'ktadd -k {keytab} -norandkey {princname}') + + def kinit(self, princname, password=None, flags=None, verbose=True, + **keywords): cmd = [self._kinit] if verbose: cmd.append('-V') - cmd.extend(flags) + if flags: + cmd.extend(flags) cmd.append(princname) + + input = password + "\n" if password else None return self.run(cmd, input=input, **keywords) def klist(self, ccache=None, **keywords): - if ccache is None: - ccache = self.ccache - ccachestr = ccache - if len(ccachestr) < 2 or ':' not in ccachestr[2:]: - ccachestr = 'FILE:' + ccachestr - return self.run([self._klist, ccache], **keywords) + return self.run([self._klist, ccache or self.ccache], **keywords) def klist_keytab(self, keytab=None, **keywords): - if keytab is None: - keytab = self.keytab - output = self.run([self._klist, '-k', keytab], **keywords) - return output - - def run_kadminl(self, query, env=None): - return self.run([self.kadmin_local, '-q', query], env=env) - - def password(self, name): - """Get a weakly random password from name, consistent across calls.""" - return name + six.text_type(os.path.basename(self.tmpdir)) + return self.run([self._klist, '-k', keytab or self.keytab], + **keywords) - def prep_kadmin(self, princname=None, pw=None, flags=[]): + def prep_kadmin(self, princname=None, pw=None, flags=None): if princname is None: princname = self.admin_princ pw = self.password('admin') return self.kinit(princname, pw, flags=['-S', 'kadmin/admin', - '-c', self.kadmin_ccache] + flags) + '-c', self.kadmin_ccache] + (flags or [])) def run_kadmin(self, query, **keywords): return self.run([self.kadmin, '-c', self.kadmin_ccache, '-q', query], **keywords) - def special_env(self, name, has_kdc_conf, krb5_conf=None, kdc_conf=None): - krb5_conf_path = os.path.join(self.tmpdir, 'krb5.conf.%s' % name) - krb5_conf = _cfg_merge(self._krb5_conf, krb5_conf) - self._create_conf(krb5_conf, krb5_conf_path) - if has_kdc_conf: - kdc_conf_path = os.path.join(self.tmpdir, 'kdc.conf.%s' % name) - kdc_conf = _cfg_merge(self._kdc_conf, kdc_conf) - self._create_conf(kdc_conf, kdc_conf_path) + def run_kadminl(self, query, **keywords): + if isinstance(query, list): + query = " ".join([shlex.quote(q) for q in query]) + + return self.run([self.kadmin_local, '-q', query], **keywords) + + def start_kdc(self, args=None, env=None): + if self._kdc_proc: + raise Exception("KDC has already started") + + start_args = [self.krb5kdc, '-n'] + if args: + start_args.extend(args) + self._kdc_proc = self._start_daemon(start_args, env, 'starting...') + + def start_kadmind(self, env): + if self._kadmind_proc: + raise Exception("kadmind has already started") + + dump_path = os.path.join(self.tmpdir, 'dump') + self._kadmind_proc = self._start_daemon([self.kadmind, '-nofork', '-W', + '-p', self.kdb5_util, + '-K', self.kprop, + '-F', dump_path], env, + 'starting...') + + +class HeimdalRealm(K5Realm): + @property + def provider(self): + return 'heimdal' + + @property + def _default_paths(self): + base = '/System/Library/PrivateFrameworks/Heimdal.framework/Helpers' + if sys.platform != 'darwin': + base = '/usr/libexec' + + return [ + ('krb5kdc', 'kdc', os.path.join(base, 'kdc')), + ('kadmin', 'kadmin', '/usr/bin/kadmin'), + ('kadmin_local', 'kadmin', '/usr/bin/kadmin'), + ('kadmind', 'kadmind', os.path.join(base, 'kadmind')), + ('_kinit', 'kinit', '/usr/bin/kinit'), + ('_klist', 'klist', '/usr/bin/klist'), + ('_ktutil', 'ktutil', '/usr/bin/ktutil'), + ] + + @property + def _krb5_conf(self): + return { + 'libdefaults': { + 'default_realm': '$realm', + 'default_keytab_name': 'FILE:$tmpdir/keytab', + 'dns_lookup_kdc': 'false', + 'dns_lookup_realm': 'false'}, + 'realms': { + '$realm': { + 'kdc': '$hostname:$port0', + 'admin_server': '$hostname:$port1', + 'kpasswd_server': '$hostname:$port2'}}, + 'logging': { + 'kadmind': 'FILE:$tmpdir/kadmind.log', + 'kdc': 'FILE:$tmpdir/kdc.log', + 'kpasswdd': 'FILE:$tmpdir/kpasswdd.log', + 'krb5': 'FILE:$tmpdir/krb5.log', + 'default': 'FILE:$tmpdir/others.log'}, + 'kdc': { + 'database': { + 'dbname': '$tmpdir/db', + 'mkey_file': '$tmpdir/stash', + 'acl_file': '$tmpdir/acl', + 'log_file': '$tmpdir/db.log'}, + 'ports': '$port0'}} + + @property + def _kdc_conf(self): + return + + def create_kdb(self): + self.run_kadminl(['stash', f'--key-file={self.tmpdir}/stash' + '--random-password']) + self.run_kadminl(['init', self.realm], input="\n\n") + + def addprinc(self, princname, password=None): + args = ['add', '--use-defaults'] + if password: + args.append(f'--password={password}') else: - kdc_conf_path = None - return self._make_env(krb5_conf_path, kdc_conf_path) + args.append('--random-key') - def kill_daemons(self): - # clean up daemons - for proc in self._daemons: - os.kill(proc.pid, signal.SIGTERM) + args.append(princname) + + self.run_kadminl(args) + + def change_password(self, principal, password=None, keysalt=None): + args = ['change_password'] + + if password: + args.append(f'--password={password}') + else: + args.append('--random-key') + + if keysalt: + args.extend('-e', keysalt) + + args.append(principal) + self.run_kadminl(args) + + def extract_keytab(self, princname, keytab): + self.run_kadminl(['ext', f'--keytab={keytab}', princname]) + + def kinit(self, princname, password=None, flags=None, verbose=True, + **keywords): + cmd = [self._kinit] + + input = None + if password: + input = password + "\n" + cmd.append('--password-file=STDIN') + + if flags: + cmd.extend(flags) + cmd.append(princname) + + return self.run(cmd, input=input, **keywords) + + def klist(self, ccache=None, **keywords): + return self.run([self._klist, '-c', ccache or self.ccache], + **keywords) + + def klist_keytab(self, keytab=None, **keywords): + return self.run([self._ktutil, '-k', keytab or self.keytab, 'list'], + **keywords) + + def prep_kadmin(self, princname=None, pw=None, flags=None): + raise NotImplementedError() # Not needed right now + + def run_kadmin(self, query, **keywords): + raise NotImplementedError() # Not needed right now + + def run_kadminl(self, query, **keywords): + if not isinstance(query, list): + query = [query] + + args = [self.kadmin_local, '--local'] + krb5_config = self.env.get('KRB5_CONFIG', None) + if krb5_config: + args.append(f'--config-file={krb5_config}') + + return self.run(args + query, **keywords) + + def start_kdc(self, args=None, env=None): + if self._kdc_proc: + raise Exception("KDC has already started") + + start_args = [self.krb5kdc] + + if sys.platform == 'darwin': + start_args.append('--no-sandbox') + + krb5_config = self.env.get('KRB5_CONFIG', None) + if krb5_config: + start_args.append('--config-file=%s' % krb5_config) + + if args: + start_args.extend(args) + + # The KDC won't display the output to stdout, so there's no sentinel + # to check. Instead, read the log file for it. + kdc_log = os.path.join(self.tmpdir, 'kdc.log') + with open(kdc_log, mode='w+') as log_fd: + self._kdc_proc = self._start_daemon(start_args, env) + + while True: + line = log_fd.readline() + if "KDC started" in line: + break + + def start_kadmind(self, env=None): + if self._kadmind_proc: + raise Exception("kadmind has already started") + + config_file = f'--config-file={self._krb5_conf}' + port = '--ports=%s' % (self.portbase + 1) + args = [self.kadmind, config_file, port] + self._kadmind_proc = self._start_daemon(args) diff --git a/k5test/unit.py b/k5test/unit.py index 4ee7f98..3c64b9e 100644 --- a/k5test/unit.py +++ b/k5test/unit.py @@ -35,7 +35,7 @@ def ext_test(self, *args, **kwargs): _KRB_VERSION = None -def krb_minversion_test(target_version, problem): +def krb_minversion_test(target_version, problem, provider=None): global _KRB_VERSION if _KRB_VERSION is None: _KRB_VERSION = _utils.get_output("krb5-config --version") @@ -43,7 +43,8 @@ def krb_minversion_test(target_version, problem): def make_ext_test(func): def ext_test(self, *args, **kwargs): - if _KRB_VERSION < target_version.split('.'): + if _KRB_VERSION < target_version.split('.') and ( + not provider or self.realm.lower() == provider.lower()): self.skipTest("Your GSSAPI (version %s) is known to have " "problems with %s" % (_KRB_VERSION, problem)) else: @@ -56,12 +57,15 @@ def ext_test(self, *args, **kwargs): def krb_plugin_test(plugin_type, plugin_name): # TODO(directxman12): add a way to make this look for # platform-specific library extensions - plugin_path = os.path.join(_utils.find_plugin_dir(), - plugin_type, '%s.so' % plugin_name) + krb5_plugin_path = _utils.find_plugin_dir() + plugin_path = None + if krb5_plugin_path: + plugin_path = os.path.join(krb5_plugin_path, plugin_type, + f"{plugin_name}.so") def make_krb_plugin_test(func): def krb_plugin_test(self, *args, **kwargs): - if not os.path.exists(plugin_path): + if not plugin_path or not os.path.exists(plugin_path): self.skipTest("You do not have the GSSAPI {type}" "plugin {name} installed".format( type=plugin_type, name=plugin_name)) @@ -71,3 +75,18 @@ def krb_plugin_test(self, *args, **kwargs): return krb_plugin_test return make_krb_plugin_test + + +def krb_provider_test(providers, problem): + def make_krb_provider_test(func): + def krb_provider_test(self, *args, **kwargs): + provider_list = [p.lower() for p in providers] + if self.realm.provider.lower() not in provider_list: + self.skipTest(f"Your GSSAPI (provider {self.realm.provider}) " + f"is known to have problems with {problem}") + else: + func(self, *args, **kwargs) + + return krb_provider_test + + return make_krb_provider_test diff --git a/setup.py b/setup.py index 0a4a97f..6d5e8d2 100755 --- a/setup.py +++ b/setup.py @@ -13,12 +13,12 @@ long_description=open('README.md').read(), license='LICENSE.txt', url="https://github.com/pythongssapi/k5test", + python_requires=">=3.6", classifiers=[ 'Development Status :: 4 - Beta', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.6', 'Intended Audience :: Developers', 'License :: OSI Approved :: ISC License (ISCL)', 'License :: OSI Approved :: MIT License',