diff --git a/Lib/ldap/compat.py b/Lib/ldap/compat.py index de0e110c..cbfeef57 100644 --- a/Lib/ldap/compat.py +++ b/Lib/ldap/compat.py @@ -1,6 +1,7 @@ """Compatibility wrappers for Py2/Py3.""" import sys +import os if sys.version_info[0] < 3: from UserDict import UserDict, IterableUserDict @@ -41,3 +42,72 @@ def reraise(exc_type, exc_value, exc_traceback): """ # In Python 3, all exception info is contained in one object. raise exc_value + +try: + from shutil import which +except ImportError: + # shutil.which() from Python 3.6 + # "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, + # 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software Foundation; + # All Rights Reserved" + def which(cmd, mode=os.F_OK | os.X_OK, path=None): + """Given a command, mode, and a PATH string, return the path which + conforms to the given mode on the PATH, or None if there is no such + file. + + `mode` defaults to os.F_OK | os.X_OK. `path` defaults to the result + of os.environ.get("PATH"), or can be overridden with a custom search + path. + + """ + # Check that a given file can be accessed with the correct mode. + # Additionally check that `file` is not a directory, as on Windows + # directories pass the os.access check. + def _access_check(fn, mode): + return (os.path.exists(fn) and os.access(fn, mode) + and not os.path.isdir(fn)) + + # If we're given a path with a directory part, look it up directly rather + # than referring to PATH directories. This includes checking relative to the + # current directory, e.g. ./script + if os.path.dirname(cmd): + if _access_check(cmd, mode): + return cmd + return None + + if path is None: + path = os.environ.get("PATH", os.defpath) + if not path: + return None + path = path.split(os.pathsep) + + if sys.platform == "win32": + # The current directory takes precedence on Windows. + if not os.curdir in path: + path.insert(0, os.curdir) + + # PATHEXT is necessary to check on Windows. + pathext = os.environ.get("PATHEXT", "").split(os.pathsep) + # See if the given file matches any of the expected path extensions. + # This will allow us to short circuit when given "python.exe". + # If it does match, only test that one, otherwise we have to try + # others. + if any(cmd.lower().endswith(ext.lower()) for ext in pathext): + files = [cmd] + else: + files = [cmd + ext for ext in pathext] + else: + # On other platforms you don't have things like PATHEXT to tell you + # what file suffixes are executable, so just pass on cmd as-is. + files = [cmd] + + seen = set() + for dir in path: + normdir = os.path.normcase(dir) + if not normdir in seen: + seen.add(normdir) + for thefile in files: + name = os.path.join(dir, thefile) + if _access_check(name, mode): + return name + return None diff --git a/Lib/slapdtest/__init__.py b/Lib/slapdtest/__init__.py index a2a875f9..306eaf7f 100644 --- a/Lib/slapdtest/__init__.py +++ b/Lib/slapdtest/__init__.py @@ -8,4 +8,5 @@ __version__ = '3.0.0b2' from slapdtest._slapdtest import SlapdObject, SlapdTestCase, SysLogHandler -from slapdtest._slapdtest import skip_unless_ci, requires_sasl, requires_tls +from slapdtest._slapdtest import requires_ldapi, requires_sasl, requires_tls +from slapdtest._slapdtest import skip_unless_ci diff --git a/Lib/slapdtest/_slapdtest.py b/Lib/slapdtest/_slapdtest.py index 4c7a9e45..484eb54b 100644 --- a/Lib/slapdtest/_slapdtest.py +++ b/Lib/slapdtest/_slapdtest.py @@ -9,6 +9,7 @@ import os import socket +import sys import time import subprocess import logging @@ -20,7 +21,7 @@ os.environ['LDAPNOINIT'] = '1' import ldap -from ldap.compat import quote_plus +from ldap.compat import quote_plus, which HERE = os.path.abspath(os.path.dirname(__file__)) @@ -56,6 +57,12 @@ LOCALHOST = '127.0.0.1' +CI_DISABLED = set(os.environ.get('CI_DISABLED', '').split(':')) +if 'LDAPI' in CI_DISABLED: + HAVE_LDAPI = False +else: + HAVE_LDAPI = hasattr(socket, 'AF_UNIX') + def identity(test_item): """Identity decorator @@ -69,7 +76,7 @@ def skip_unless_ci(reason, feature=None): """ if not os.environ.get('CI', False): return unittest.skip(reason) - elif feature in os.environ.get('CI_DISABLED', '').split(':'): + elif feature in CI_DISABLED: return unittest.skip(reason) else: # Don't skip on Travis @@ -95,6 +102,22 @@ def requires_sasl(): return identity +def requires_ldapi(): + if not HAVE_LDAPI: + return skip_unless_ci( + "test needs ldapi support (AF_UNIX)", feature='LDAPI') + else: + return identity + +def _add_sbin(path): + """Add /sbin and related directories to a command search path""" + directories = path.split(os.pathsep) + if sys.platform != 'win32': + for sbin in '/usr/local/sbin', '/sbin', '/usr/sbin': + if sbin not in directories: + directories.append(sbin) + return os.pathsep.join(directories) + def combined_logger( log_name, log_level=logging.WARN, @@ -149,8 +172,6 @@ class SlapdObject(object): root_dn = 'cn=%s,%s' % (root_cn, suffix) root_pw = 'password' slapd_loglevel = 'stats stats2' - # use SASL/EXTERNAL via LDAPI when invoking OpenLDAP CLI tools - cli_sasl_external = True local_host = '127.0.0.1' testrunsubdirs = ( 'schema', @@ -160,8 +181,6 @@ class SlapdObject(object): ) TMPDIR = os.environ.get('TMP', os.getcwd()) - SBINDIR = os.environ.get('SBIN', '/usr/sbin') - BINDIR = os.environ.get('BIN', '/usr/bin') if 'SCHEMA' in os.environ: SCHEMADIR = os.environ['SCHEMA'] elif os.path.isdir("/etc/openldap/schema"): @@ -170,12 +189,9 @@ class SlapdObject(object): SCHEMADIR = "/etc/ldap/schema" else: SCHEMADIR = None - PATH_LDAPADD = os.path.join(BINDIR, 'ldapadd') - PATH_LDAPDELETE = os.path.join(BINDIR, 'ldapdelete') - PATH_LDAPMODIFY = os.path.join(BINDIR, 'ldapmodify') - PATH_LDAPWHOAMI = os.path.join(BINDIR, 'ldapwhoami') - PATH_SLAPD = os.environ.get('SLAPD', os.path.join(SBINDIR, 'slapd')) - PATH_SLAPTEST = os.path.join(SBINDIR, 'slaptest') + + BIN_PATH = os.environ.get('BIN', os.environ.get('PATH', os.defpath)) + SBIN_PATH = os.environ.get('SBIN', _add_sbin(BIN_PATH)) # time in secs to wait before trying to access slapd via LDAP (again) _start_sleep = 1.5 @@ -192,8 +208,23 @@ def __init__(self): self._slapd_conf = os.path.join(self.testrundir, 'slapd.conf') self._db_directory = os.path.join(self.testrundir, "openldap-data") self.ldap_uri = "ldap://%s:%d/" % (LOCALHOST, self._port) - ldapi_path = os.path.join(self.testrundir, 'ldapi') - self.ldapi_uri = "ldapi://%s" % quote_plus(ldapi_path) + if HAVE_LDAPI: + ldapi_path = os.path.join(self.testrundir, 'ldapi') + self.ldapi_uri = "ldapi://%s" % quote_plus(ldapi_path) + self.default_ldap_uri = self.ldapi_uri + # use SASL/EXTERNAL via LDAPI when invoking OpenLDAP CLI tools + self.cli_sasl_external = True + else: + self.ldapi_uri = None + self.default_ldap_uri = self.ldap_uri + # Use simple bind via LDAP uri + self.cli_sasl_external = False + + self._find_commands() + + if self.SCHEMADIR is None: + raise ValueError('SCHEMADIR is None, ldap schemas are missing.') + # TLS certs self.cafile = os.path.join(HERE, 'certs/ca.pem') self.servercert = os.path.join(HERE, 'certs/server.pem') @@ -201,16 +232,31 @@ def __init__(self): self.clientcert = os.path.join(HERE, 'certs/client.pem') self.clientkey = os.path.join(HERE, 'certs/client.key') - def _check_requirements(self): - binaries = [ - self.PATH_LDAPADD, self.PATH_LDAPMODIFY, self.PATH_LDAPWHOAMI, - self.PATH_SLAPD, self.PATH_SLAPTEST - ] - for binary in binaries: - if not os.path.isfile(binary): - raise ValueError('Binary {} is missing.'.format(binary)) - if self.SCHEMADIR is None: - raise ValueError('SCHEMADIR is None, ldap schemas are missing.') + def _find_commands(self): + self.PATH_LDAPADD = self._find_command('ldapadd') + self.PATH_LDAPDELETE = self._find_command('ldapdelete') + self.PATH_LDAPMODIFY = self._find_command('ldapmodify') + self.PATH_LDAPWHOAMI = self._find_command('ldapwhoami') + + self.PATH_SLAPD = os.environ.get('SLAPD', None) + if not self.PATH_SLAPD: + self.PATH_SLAPD = self._find_command('slapd', in_sbin=True) + self.PATH_SLAPTEST = self._find_command('slaptest', in_sbin=True) + + def _find_command(self, cmd, in_sbin=False): + if in_sbin: + path = self.SBIN_PATH + var_name = 'SBIN' + else: + path = self.BIN_PATH + var_name = 'BIN' + command = which(cmd, path=path) + if command is None: + raise ValueError( + "Command '{}' not found. Set the {} environment variable to " + "override slapdtest's search path.".format(value, var_name) + ) + return command def setup_rundir(self): """ @@ -331,11 +377,14 @@ def _start_slapd(self): """ Spawns/forks the slapd process """ + urls = [self.ldap_uri] + if self.ldapi_uri: + urls.append(self.ldapi_uri) slapd_args = [ self.PATH_SLAPD, '-f', self._slapd_conf, '-F', self.testrundir, - '-h', '%s' % ' '.join((self.ldap_uri, self.ldapi_uri)), + '-h', ' '.join(urls), ] if self._log.isEnabledFor(logging.DEBUG): slapd_args.extend(['-d', '-1']) @@ -346,18 +395,21 @@ def _start_slapd(self): # Waits until the LDAP server socket is open, or slapd crashed # no cover to avoid spurious coverage changes, see # https://github.com/python-ldap/python-ldap/issues/127 - while 1: # pragma: no cover + for _ in range(10): # pragma: no cover if self._proc.poll() is not None: self._stopped() raise RuntimeError("slapd exited before opening port") time.sleep(self._start_sleep) try: - self._log.debug("slapd connection check to %s", self.ldapi_uri) + self._log.debug( + "slapd connection check to %s", self.default_ldap_uri + ) self.ldapwhoami() except RuntimeError: pass else: return + raise RuntimeError("slapd did not start properly") def start(self): """ @@ -365,7 +417,6 @@ def start(self): """ if self._proc is None: - self._check_requirements() # prepare directory structure atexit.register(self.stop) self._cleanup_rundir() @@ -435,9 +486,11 @@ def _cli_auth_args(self): # no cover to avoid spurious coverage changes def _cli_popen(self, ldapcommand, extra_args=None, ldap_uri=None, stdin_data=None): # pragma: no cover + if ldap_uri is None: + ldap_uri = self.default_ldap_uri args = [ ldapcommand, - '-H', ldap_uri or self.ldapi_uri, + '-H', ldap_uri, ] + self._cli_auth_args() + (extra_args or []) self._log.debug('Run command: %r', ' '.join(args)) proc = subprocess.Popen( diff --git a/Tests/t_ldap_sasl.py b/Tests/t_ldap_sasl.py index af6ed51a..d1044681 100644 --- a/Tests/t_ldap_sasl.py +++ b/Tests/t_ldap_sasl.py @@ -5,7 +5,6 @@ See https://www.python-ldap.org/ for details. """ import os -import pwd import socket import unittest @@ -14,7 +13,8 @@ from ldap.ldapobject import SimpleLDAPObject import ldap.sasl -from slapdtest import SlapdTestCase, requires_sasl, requires_tls +from slapdtest import SlapdTestCase +from slapdtest import requires_ldapi, requires_sasl, requires_tls LDIF = """ @@ -60,7 +60,7 @@ def setUpClass(cls): ) cls.server.ldapadd(ldif) - @unittest.skipUnless(hasattr(socket, 'AF_UNIX'), "needs Unix socket") + @requires_ldapi() def test_external_ldapi(self): # EXTERNAL authentication with LDAPI (AF_UNIX) ldap_conn = self.ldap_object_class(self.server.ldapi_uri) diff --git a/Tests/t_ldap_schema_subentry.py b/Tests/t_ldap_schema_subentry.py index 3c07d35b..4e1e09b2 100644 --- a/Tests/t_ldap_schema_subentry.py +++ b/Tests/t_ldap_schema_subentry.py @@ -16,7 +16,7 @@ from ldap.ldapobject import SimpleLDAPObject import ldap.schema from ldap.schema.models import ObjectClass -from slapdtest import SlapdTestCase +from slapdtest import SlapdTestCase, requires_ldapi HERE = os.path.abspath(os.path.dirname(__file__)) @@ -88,6 +88,7 @@ def test_urlfetch_ldap(self): dn, schema = ldap.schema.urlfetch(self.server.ldap_uri) self.assertSlapdSchema(dn, schema) + @requires_ldapi() def test_urlfetch_ldapi(self): dn, schema = ldap.schema.urlfetch(self.server.ldapi_uri) self.assertSlapdSchema(dn, schema) diff --git a/Tests/t_ldapobject.py b/Tests/t_ldapobject.py index 62591d77..50754687 100644 --- a/Tests/t_ldapobject.py +++ b/Tests/t_ldapobject.py @@ -22,8 +22,8 @@ import unittest import warnings import pickle -import warnings -from slapdtest import SlapdTestCase, requires_sasl, requires_tls +from slapdtest import SlapdTestCase +from slapdtest import requires_ldapi, requires_sasl, requires_tls # Switch off processing .ldaprc or ldap.conf before importing _ldap os.environ['LDAPNOINIT'] = '1' @@ -303,6 +303,7 @@ def test005_invalid_credentials(self): self.fail("expected INVALID_CREDENTIALS, got %r" % r) @requires_sasl() + @requires_ldapi() def test006_sasl_extenal_bind_s(self): l = self.ldap_object_class(self.server.ldapi_uri) l.sasl_external_bind_s() @@ -441,6 +442,7 @@ class Test01_ReconnectLDAPObject(Test00_SimpleLDAPObject): ldap_object_class = ReconnectLDAPObject @requires_sasl() + @requires_ldapi() def test101_reconnect_sasl_external(self): l = self.ldap_object_class(self.server.ldapi_uri) l.sasl_external_bind_s() @@ -450,7 +452,7 @@ def test101_reconnect_sasl_external(self): self.assertEqual(l.whoami_s(), authz_id) def test102_reconnect_simple_bind(self): - l = self.ldap_object_class(self.server.ldapi_uri) + l = self.ldap_object_class(self.server.ldap_uri) bind_dn = 'cn=user1,'+self.server.suffix l.simple_bind_s(bind_dn, 'user1_pw') self.assertEqual(l.whoami_s(), 'dn:'+bind_dn) @@ -458,7 +460,7 @@ def test102_reconnect_simple_bind(self): self.assertEqual(l.whoami_s(), 'dn:'+bind_dn) def test103_reconnect_get_state(self): - l1 = self.ldap_object_class(self.server.ldapi_uri) + l1 = self.ldap_object_class(self.server.ldap_uri) bind_dn = 'cn=user1,'+self.server.suffix l1.simple_bind_s(bind_dn, 'user1_pw') self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn) @@ -477,7 +479,7 @@ def test103_reconnect_get_state(self): str('_start_tls'): 0, str('_trace_level'): 0, str('_trace_stack_limit'): 5, - str('_uri'): self.server.ldapi_uri, + str('_uri'): self.server.ldap_uri, str('bytes_mode'): l1.bytes_mode, str('bytes_mode_hardfail'): l1.bytes_mode_hardfail, str('timeout'): -1, @@ -485,7 +487,7 @@ def test103_reconnect_get_state(self): ) def test104_reconnect_restore(self): - l1 = self.ldap_object_class(self.server.ldapi_uri) + l1 = self.ldap_object_class(self.server.ldap_uri) bind_dn = 'cn=user1,'+self.server.suffix l1.simple_bind_s(bind_dn, 'user1_pw') self.assertEqual(l1.whoami_s(), 'dn:'+bind_dn) diff --git a/tox.ini b/tox.ini index fcfc628b..58e3cf68 100644 --- a/tox.ini +++ b/tox.ini @@ -33,8 +33,8 @@ basepython = python2 deps = {[testenv]deps} passenv = {[testenv]passenv} setenv = - CI_DISABLED=TLS:SASL -# rebuild without SASL and TLS + CI_DISABLED=LDAPI:SASL:TLS +# rebuild without SASL and TLS, run without LDAPI commands = {envpython} \ -m coverage run --parallel setup.py \ clean --all \ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy