diff --git a/Lib/ldappool/README.md b/Lib/ldappool/README.md new file mode 100644 index 00000000..c926097a --- /dev/null +++ b/Lib/ldappool/README.md @@ -0,0 +1,65 @@ +# LDAP Pooling example + +## entries as dict + +```python +from ldappool import ConnectionPool +pool = ConnectionPool( + params={"keep": True, "autoBind": True, "retries": 2}, + max=5) +pool.set_uri("ldaps://ldap.example.com:636/dc=example,dc=com?uid,mail?sub?(|(uid=test)(mail=test@example.com))") +pool.set_credentials("binddn", "bindpw") +with pool.get() as conn: + for entry in conn.search_s(pool.basedn, + pool.scope, + pool.filter, + pool.attributes): + print(f"{entry[0]}: {entry[1].get('uid')} {entry[1].get('mail')}") + for member in entry[1].get("memberOf", []): + print(member) +``` + +## entry to dataclass example +```python +from ldappool import ConnectionPool +from ldappool import e2c +pool = ConnectionPool( + params={"keep": True, "autoBind": True, "retries": 2}, + max=5) +pool.set_uri("ldaps://ldap.example.com:636/dc=example,dc=com?uid,mail?sub?(|(uid=test)(mail=test@example.com))") +pool.set_credentials("binddn", "bindpw") +with pool.get() as conn: + for entry in map(e2c, conn.search_s(pool.basedn, + pool.scope, + pool.filter, + pool.attributes)): + print(f"{entry.dn}: {entry.uid} {entry.mail}") + for member in entry.memberOf: + print(member) +``` + +## changing the connection or credentials for the pool + +```python +from ldappool import ConnectionPool +from ldappool import e2c +pool = ConnectionPool( + params={"keep": True, "autoBind": True, "retries": 2}, + max=5) +pool.set_uri("ldaps://ldap.example.com:636/dc=example,dc=com?uid,mail?sub?(|(uid=test)(mail=test@example.com))") +pool.set_credentials("binddn", "bindpw") +with pool.get() as conn: + for entry in map(e2c, conn.search_s(pool.basedn, + pool.scope, + pool.filter, + pool.attributes)): + print(f"{entry.dn}: {entry.uid} {entry.mail}") + +pool.set_credentials(entry.dn, "changeme") +with pool.get() as conn: + for entry in map(e2c, conn.search_s(pool.basedn, + pool.scope, + pool.filter, + pool.attributes)): + print(f"{entry.dn}: {entry.uid} {entry.mail}") +``` diff --git a/Lib/ldappool/__init__.py b/Lib/ldappool/__init__.py new file mode 100644 index 00000000..8530e005 --- /dev/null +++ b/Lib/ldappool/__init__.py @@ -0,0 +1,467 @@ +import sys + +if sys.version_info.minor >= 8: + import dataclasses +import logging +import sys +import threading +from urllib.parse import urlparse + +import sys + +if sys.version_info.minor > 6: + from time import perf_counter_ns as perf_counter +elif sys.version_info.minor == 6: + from time import perf_counter + +import ldap +from ldapurl import LDAPUrl + +# nano seconds to ensure we know the locked time +ns = 1_000_000_000 +ns_locktimeout = 15.0 + +logging.basicConfig(level=logging.INFO, stream=sys.stdout) + + +class LDAPPoolExhausted(Exception): + pass + + +class LDAPPoolDown(Exception): + pass + + +class LDAPLockTimeout(Exception): + pass + + +if sys.version_info.minor >= 8: + + def e2c(entry): + try: + cls = dataclasses.make_dataclass( + "", ["dn"] + list(entry[1].keys()), frozen=True + ) + return cls(**dict(list([("dn", entry[0])] + list(entry[1].items())))) + except NameError as dcerror: + print(f"dataclasses not supported") + return entry + +else: + + def e2c(entry): + return f"dataclasses not support on python < {sys.version_info.minor}" + + +class Connection(object): + def __init__( + self, + uri: LDAPUrl, + binddn: str, + bindpw: str, + params: dict = {}, + ): + self.uri = uri + self.binddn = binddn + self.bindpw = bindpw + self.params = params + self.established = False + self.inUse = False + self._whoami = None + self._conn = False + self._lock = threading.Lock() + self._pool = None + self._health = 0.0 + (f"ConnectionPool new Connection {self}") + if self.params.get("prewarm", False): + self.__enter__() + + def __locktime(self): + if self._health == 0.0: + self._health = perf_counter() + return True + if (perf_counter() - self._health) / ns < ns_locktimeout: + return False + return True + + @property + def whoami(self): + return self._whoami + + def __whoami(self): + # do not stress the connection too often + if not self.__locktime(): + return + for r in range(self.params.get("retries", 3)): + try: + self._whoami = self._conn.whoami_s() + return + except ldap.SERVER_DOWN as ldaperr: + logging.error(f"__whoami ConnectionPool {ldaperr}") + self.established = False + # just catch that error until we finished iterating + try: + self.__enter__() + except: + continue + raise ldap.SERVER_DOWN(f"max retries {self.params.get('retries', 3)} reached") + + @property + def conn(self): + if self._conn == False: + self.__enter__() + try: + if self.established: + self.__whoami() + except ldap.SERVER_DOWN as ldaperr: + self.established = False + raise LDAPPoolDown( + f"could not establish connection with {self.uri.initializeUrl()}" + + f" with max retries of {self.params.get('retries', 3)}" + ) + return self._conn + + def __lock_acquire(self): + try: + if self._lock.acquire(blocking=True, timeout=1): + return True + else: + raise LDAPLockTimeout() + except Exception as lockerr: + return False + + def __lock_release(self): + try: + self._lock.release() + return True + except Exception as lockerr: + return False + + def authenticate( + self, + binddn: str, + bindpw: str, + ): + + if not self.__lock_acquire(): + raise LDAPLockTimeout() + + try: + self.conn.simple_bind_s( + binddn, + bindpw, + ) + if not self.__lock_release(): + raise LDAPLockTimeout() + except ldap.INVALID_CREDENTIALS as ldaperr: + # rollback auth anyway + self.__lock_release() + self.__authenticate__() + raise ldap.INVALID_CREDENTIALS + # rollback auth anyway + self.__authenticate__() + return True + + def __authenticate__(self): + if not self.__lock_acquire(): + raise LDAPLockTimeout() + try: + self.conn.simple_bind_s( + self.binddn, + self.bindpw, + ) + logging.debug("__whoami from __authenticate__") + self.__whoami() + self.__lock_release() + except ldap.INVALID_CREDENTIALS as ldaperr: + self.__lock_release() + logging.info(ldaperr) + raise ldap.INVALID_CREDENTIALS + + def __set_connection_parameters__(self): + try: + self._conn.set_option( + ldap.OPT_REFERRALS, self.params.get("referrals", False) + ) + self._conn.set_option( + ldap.OPT_NETWORK_TIMEOUT, self.params.get("network_timeout", 10.0) + ) + self._conn.set_option(ldap.OPT_TIMEOUT, self.params.get("timeout", 10.0)) + self._conn.set_option( + ldap.OPT_X_KEEPALIVE_IDLE, self.params.get("keepalive_idle", 10) + ) + self._conn.set_option( + ldap.OPT_X_KEEPALIVE_INTERVAL, self.params.get("keepalive_interval", 5) + ) + self._conn.set_option( + ldap.OPT_X_KEEPALIVE_PROBES, self.params.get("keepalive_probes", 3) + ) + self._conn.set_option(ldap.OPT_RESTART, ldap.OPT_ON) + if self.params.get("allow_tls_fallback", False): + self._conn.set_option(ldap.OPT_X_TLS_TRY, 1) + self._conn.set_option(ldap.OPT_X_TLS_NEWCTX, ldap.OPT_OFF) + except Exception as connerr: + logging.error(f"cannot set LDAP option {connerr}") + + def __enter__(self): + self.inUse = True + if not self.established: + logging.debug( + f"ConnectionPool {self} initializin LDAP {self.uri.initializeUrl()}" + ) + try: + self._conn = ldap.initialize(self.uri.initializeUrl()) + self.__set_connection_parameters__() + if self.params.get("autoBind", False): + ( + f"ConnectionPool {self} autoBind with {self.binddn} password {'x'*len(self.bindpw)}" + ) + self.__authenticate__() + except Exception as ldaperr: + (ldaperr) + raise ldaperr + self.established = True + return self.conn + + def giveback(self, force=False): + try: + if force: + try: + self._conn.unbind_s() + except Exception as ldaperr: + logging.error( + "ConnectionPool unbind connection" + + f"{self} exception {ldaperr}" + ) + self.inUse = False + return + + if self.params.get("autoBind", False): + if not self.params.get("keep", False): + logging.debug(f"ConnectionPool unbind connection {self}") + try: + self._conn.unbind_s() + except Exception as ldaperr: + logging.error( + "ConnectionPool unbind connection" + + f"{self} exception {ldaperr}" + ) + self.inUse = False + except AttributeError: + self.inUse = False + + def __del__(self): + self.giveback() + if all([self._pool is not None, not self.params.get("keep", False)]): + logging.debug(f"ConnectionPool deleteing connection {self} from Pool") + self._pool.delete(self) + + def __exit__(self, type, value, traceback): + self.giveback() + if all([self._pool is not None, not self.params.get("keep", False)]): + self._pool.delete(self) + + def __cmp__(self, other): + if isinstance(other, Connection): + return self.uri.initializeUrl() == other.uri.initializeUrl() + return False + + def set_uri(self, uri: LDAPUrl): + self.uri = uri + return True + + def set_binddn(self, binddn: str): + self.binddn = binddn + return True + + def set_bindpw(self, bindpw: str): + self.bindpw = bindpw + return True + + def set_credentials(self, binddn: str, bindpw: str): + self.set_binddn(binddn) + self.set_bindpw(bindpw) + return True + + +class ConnectionPool(object): + def __init__( + self, + uri: LDAPUrl = LDAPUrl("ldap:///"), + binddn: str = "", + bindpw: str = "", + params: dict = {}, + max: int = 10, + ): + self.uri = uri + self.binddn = binddn + self.bindpw = bindpw + self.params = params + self.max = int(max) + self._lock = threading.Lock() + self._pool = [] + logging.debug(f"ConnectionPool {self} starting with {self.max} connections") + if self.params.get("prewarm", False): + self.scale + + @property + def basedn(self): + return self.uri.dn + + @property + def scope(self): + return self.uri.scope + + @property + def filter(self): + return self.uri.filterstr + + @property + def attributes(self): + return self.uri.attrs + + @property + def extensions(self): + return self.uri.extensions + + def set_uri(self, uri: LDAPUrl): + if not isinstance(uri, LDAPUrl): + uri = LDAPUrl(uri) + if len(self._pool) > 0: + list( + map( + lambda c: (c.set_uri(uri), c.giveback(force=True)), + filter(lambda cp: cp.uri != uri, self._pool), + ) + ) + self.uri = uri + return True + + def set_binddn(self, binddn: str): + if len(self._pool) > 0: + list( + map( + lambda c: (c.set_binddn(binddn), c.giveback(force=True)), + filter(lambda cp: cp.binddn != binddn, self._pool), + ) + ) + self.binddn = binddn + return True + + def set_bindpw(self, bindpw: str): + if len(self._pool) > 0: + list( + map( + lambda c: (c.set_bindpw(bindpw), c.giveback(force=True)), + filter(lambda cp: cp.bindpw != bindpw, self._pool), + ) + ) + self.bindpw = bindpw + return True + + def set_credentials(self, binddn: str, bindpw: str): + self.set_binddn(binddn) + self.set_bindpw(bindpw) + return True + + @property + def scale(self): + for _ in range(self.max - len(self._pool)): + self.put( + Connection( + uri=self.uri, + binddn=self.binddn, + bindpw=self.bindpw, + params=self.params, + ) + ) + + def __enter__(self): + if len(self._pool) == 0: + self.scale + with self.get() as conn: + yield conn + self.put(conn) + + @property + def ping(self): + with self.get() as conn: + try: + return True + except Exception as ldaperr: + try: + if conn.search_s("cn=config", ldap.SCOPE_ONELEVEL) != []: + return True + else: + # we might have ACI's in place + return True + except Exception as ldaperr: # collect with parent exception + pass + logging.error( + f"LDAP exception pinging server {self.uri.initializeUrl()} {ldaperr}" + ) + raise ldaperr + return True + + def get(self, binddn: str = "", bindpw: str = ""): + if len(self._pool) == 0: + self.scale + self._lock.acquire(timeout=1) + if len(self._pool) == 0: + self._lock.release() + logging.warning( + f"max connections {self.max} reached, consider increasing pool size" + ) + raise LDAPPoolExhausted( + f"max connections {self.max} reached, consider increasing pool size" + ) + try: + con = list(filter(lambda x: not x.inUse, self._pool))[0] + except IndexError: + self._lock.release() + logging.warning( + f"all connections {self.max} in use, consider increasing pool size" + ) + raise LDAPPoolExhausted( + f"all connections {self.max} in use, consider increasing pool size" + ) + con.inUse = True + self._lock.release() + if all([binddn != "", bindpw != ""]): + try: + con.authenticate(binddn, bindpw) + except ldap.INVALID_CREDENTIALS: + self.put(con) + raise ldap.INVALID_CREDENTIALS + return con + + def put(self, connection): + self._lock.acquire(timeout=1) + if connection.inUse: + connection.giveback() + if not connection in self._pool: + self._pool.append(connection) + connection._pool = self + self._lock.release() + return True + + def status(self): + self._lock.acquire(timeout=1) + for p in self._pool: + if p.inUse: + if sys.getrefcount(p) < 4: + p.giveback() + logging.info(f"Id {p} inUse {p.inUse} {p.established} {p.whoami}") + self._lock.release() + + def delete(self, connection, force=True): + self._lock.acquire(timeout=1) + if connection in self._pool: + if any([not self.params.get("keep", False), force]): + self._pool.remove(connection) + del connection + self._lock.release() + + def __len__(self): + return len(self._pool) diff --git a/Tests/__init__.py b/Tests/__init__.py index ea28d0ce..b1391a87 100644 --- a/Tests/__init__.py +++ b/Tests/__init__.py @@ -4,20 +4,7 @@ See https://www.python-ldap.org/ for details. """ - -from . import t_bind -from . import t_cext -from . import t_cidict -from . import t_ldap_dn -from . import t_ldap_filter -from . import t_ldap_functions -from . import t_ldap_modlist -from . import t_ldap_schema_tokenizer -from . import t_ldapurl -from . import t_ldif -from . import t_ldapobject -from . import t_edit -from . import t_ldap_schema_subentry -from . import t_untested_mods -from . import t_ldap_controls_libldap -from . import t_ldap_options +from . import (t_bind, t_cext, t_cidict, t_edit, t_ldap_controls_libldap, t_ldap_dn, + t_ldap_filter, t_ldap_functions, t_ldap_modlist, t_ldap_options, + t_ldap_schema_subentry, t_ldap_schema_tokenizer, t_ldapobject, + t_ldappool, t_ldapurl, t_ldif, t_untested_mods) diff --git a/Tests/t_ldappool.py b/Tests/t_ldappool.py new file mode 100644 index 00000000..6c3ddc60 --- /dev/null +++ b/Tests/t_ldappool.py @@ -0,0 +1,512 @@ +""" +Automatic tests for python-ldap's module ldappool + +See https://www.python-ldap.org/ for details. +""" + +import os +import sys +import unittest +import time + +# Switch off processing .ldaprc or ldap.conf before importing _ldap +os.environ["LDAPNOINIT"] = "1" + +import ldappool +from ldappool import Connection, ConnectionPool + +import ldap as _ldap +from ldapurl import LDAPUrl +import ldapurl + + +class ldapmock: + """Mocking some LDAP methods to avoid having a + full LDAP Setup for unittestst""" + + def __init__(self, fail=0, down=0): + self.fail = int(fail) + self.down = int(down) + + @property + def __whoami_s(self): + """if down was set when initializing + we fail for the count until we return success + """ + for _ in range(self.down): + self.down -= 1 + if self.down < 0: + self.down = 0 + raise _ldap.SERVER_DOWN() + return "cn=tester,dc=example,dc=com" + + def whoami_s(self): + """if down was set when initializing + we fail for the count until we return success + """ + for _ in range(self.down): + self.down -= 1 + if self.down < 0: + self.down = 0 + raise _ldap.SERVER_DOWN() + return "cn=tester,dc=example,dc=com" + + def initialize(self, uri): + return self + + def simple_bind_s(self, binddn, bindpw): + """if fail was set when initializing + we fail for the count until we return success + """ + for _ in range(self.fail): + self.fail -= 1 + if self.fail < 0: + self.fail = 0 + raise _ldap.INVALID_CREDENTIALS() + return (97, []) + + def set_option(self, *args, **kwargs): + return True + + def search_s(self, *args, **kwargs): + return True + + def authenticate(self, binddn, bindpw): + """if fail was set when initializing + we fail for the count until we return success + """ + for _ in range(self.fail): + self.fail -= 1 + if self.fail < 0: + self.fail = 0 + raise _ldap.INVALID_CREDENTIALS() + return (97, []) + + def unbind_s(self, *args, **kwargs): + return True + + def __enter__(self): + return + +class TestConnection(unittest.TestCase): + + def test_Connectionparams(self): + """test if a Connection handles parameters correctly""" + connection = Connection( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3}, + ) + assert connection.params.get("retries") == 3 + assert connection.inUse == False + assert connection.established == False + assert connection.binddn == "cn=tester,dc=example,dc=com" + assert connection.bindpw == "changeme" + + def test_Connectionhandling(self): + """test if a Connection changes state when in use""" + ldap = ldapmock(fail=0) + ldappool.ldap.initialize = ldap.initialize + connection = Connection( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3}, + ) + with connection as ctx: + assert connection.inUse == True + assert connection.established == True + assert connection.whoami == "cn=tester,dc=example,dc=com" + + def test_Connectionhandlingautherror(self): + """test if a connection raises exception if credentials are wrong""" + ldap = ldapmock(fail=2) + ldappool.ldap.initialize = ldap.initialize + connection = Connection( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "allow_tls_fallback": True}, + ) + with self.assertRaises(_ldap.INVALID_CREDENTIALS) as ctx: + connection.conn() + + def test_Connectionhandlingauthentication(self): + """test if a connection can authenticate for someone""" + ldap = ldapmock(fail=0, down=0) + ldappool.ldap.initialize = ldap.initialize + connection = Connection( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 1, "allow_tls_fallback": True}, + ) + connection._conn = ldap + with connection as ctx: + assert ctx.authenticate("test", "test") == (97, []) + + def test_Connectionhandlingserverdown(self): + """test if aconnection retries until params.retries reached""" + ldap = ldapmock(down=2) + ldappool.ldap.initialize = ldap.initialize + connection = Connection( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3}, + ) + with connection as ctx: + assert connection.whoami == "cn=tester,dc=example,dc=com" + + def test_ConnectionhandlingserverdownExceed(self): + """test to ensure we raise ldap.SERVER_DOWN after max retries + has been reached and we have not succeeded""" + ldap = ldapmock(down=10) + ldappool.ldap.initialize = ldap.initialize + connection = Connection( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3}, + ) + with self.assertRaises(_ldap.SERVER_DOWN) as ctx: + with connection as ctx: + connection.whoami + + """test to ensure without context ldap.SERVER_DOWN after max retries + has been reached and we have not succeeded""" + ldap = ldapmock(down=10) + ldappool.ldap.initialize = ldap.initialize + connection = Connection( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3}, + ) + with self.assertRaises(_ldap.SERVER_DOWN) as ctx: + connection.conn.search_s() + + def test_Connectionconfigchange(self): + """test if a connection updates configuration changes accordingly""" + ldap = ldapmock(down=2) + ldappool.ldap.initialize = ldap.initialize + connection = Connection( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3}, + ) + connection.set_credentials("cn=another,dc=example,dc=com", "changetoo") + assert connection.binddn == "cn=another,dc=example,dc=com" + assert connection.bindpw == "changetoo" + + def test_Connectionlocktime(self): + """test locktime which ensures we do not stress the connections too often""" + conn = Connection(LDAPUrl("ldap:///"), "", "") + assert conn._Connection__locktime() == True + assert conn._Connection__locktime() == False + time.sleep(15) + assert conn._Connection__locktime() == True + + + def test_Connectionmethods(self): + """test Connection methods which are there for + simplifying handling with the class""" + + """check set_uri""" + conn = Connection(LDAPUrl("ldap://127.0.0.1/"), "", "") + conn.set_uri(LDAPUrl("ldap://localhost/dc=example,dc=com")) + assert conn.uri == Connection(LDAPUrl("ldap://localhost/dc=example,dc=com"), "", "").uri + + """check set_binddn""" + conn = Connection(LDAPUrl("ldap://127.0.0.1/"), "", "") + conn.set_binddn("cn=Directory Manager") + assert conn.binddn == "cn=Directory Manager" + + """check set_bindpw""" + conn = Connection(LDAPUrl("ldap://127.0.0.1/"), "", "") + conn.set_bindpw("changeme") + assert conn.bindpw == "changeme" + + """check set_credentials""" + conn = Connection(LDAPUrl("ldap://127.0.0.1/"), "", "") + conn.set_credentials("cn=Directory Manager", "changeme") + assert conn.binddn == "cn=Directory Manager" + assert conn.bindpw == "changeme" + + +class TestConnectionPool(unittest.TestCase): + + def test_ConnectionPoolParams(self): + """test if ConnectionPool handles parameters correctly""" + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "autoBind": True}, + max=3, + ) + assert pool.params.get("retries") == 3 + assert pool.params.get("autoBind") == True + assert pool.binddn == "cn=tester,dc=example,dc=com" + assert pool.bindpw == "changeme" + assert pool.basedn == "dc=example,dc=com" + assert pool.scope == _ldap.SCOPE_SUBTREE + assert pool.filter == "(uid=tester)" + assert pool.attributes == ["uid", "mail"] + + def test_ConnectionPoolhandling(self): + """test if ConnectionPool context for Connection + is handled correctly""" + ldap = ldapmock(down=2) + ldappool.ldap.initialize = ldap.initialize + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "prewarm": True}, + max=3, + ) + assert len(pool._pool) == 3 + assert pool.ping + with pool.get() as conn: + assert conn.search_s("something") == True + assert conn.authenticate("test", "test") == (97, []) + + def test_ConnectionPoolCleanup(self): + """test if ConnectionPool removing Connection + without deleting it""" + ldap = ldapmock(down=2) + ldappool.ldap.initialize = ldap.initialize + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "prewarm": True, "autoBind": True}, + max=3, + ) + assert len(pool._pool) == 3 + assert pool.ping + conn = pool.get() + pool.delete(conn) + + def test_ConnectionPoolconfigchange(self): + """test if ConnectionPool delegates configuration changes + to Connections in pool during runtime change""" + ldap = ldapmock(down=2) + ldappool.ldap.initialize = ldap.initialize + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "prewarm": True}, + max=3, + ) + pool.scale + assert len(pool) == 3 + pool.set_credentials("cn=another,dc=example,dc=com", "changetoo") + assert pool.binddn == "cn=another,dc=example,dc=com" + assert pool.bindpw == "changetoo" + for conn in pool._pool: + assert conn.binddn == "cn=another,dc=example,dc=com" + assert conn.bindpw == "changetoo" + + def test_ConnectionPoolLDAPPoolExhausted(self): + """test if ConnectionPool raises Exception when + all connections are in use""" + ldap = ldapmock(down=2) + ldappool.ldap.initialize = ldap.initialize + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "prewarm": True}, + max=3, + ) + pool.scale + assert len(pool) == 3 + with self.assertRaises(ldappool.LDAPPoolExhausted) as ctx: + for _ in range(pool.max + 1): + c = pool.get() + + def test_ConnectionPoolLDAPPoolExhausted(self): + """test if all connections are free'ed when returned + to the Pool but connection kept established""" + ldap = ldapmock(down=2) + ldappool.ldap.initialize = ldap.initialize + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "prewarm": True}, + max=3, + ) + pool.scale + assert len(pool) == 3 + + conn = [] + for _ in range(pool.max - 1): + conn.append(pool.get()) + for c in conn: + pool.put(c) + assert c.inUse == False + assert c.established == True + + def test_ConnectionPoolLDAPPoolGiveback(self): + """test if ConnectionPool giveback returns + connections to the pool""" + ldap = ldapmock() + ldappool.ldap.initialize = ldap.initialize + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "prewarm": True}, + max=3, + ) + pool.scale + assert len(pool) == 3 + conn = pool.get() + conn.giveback() + assert conn.inUse == False + + """test if all connections are free'ed when returned + to the Pool but connection kept established""" + conn = [] + for _ in range(pool.max - 1): + conn.append(pool.get()) + for c in conn: + pool.put(c) + assert c.inUse == False + assert c.established == True + + def test_ConnectionPoolLDAPLockTimeout(self): + """test if ConnectionPool raises Locktimeout accordingly + when connection has been locked""" + ldap = ldapmock(down=2) + ldappool.ldap.initialize = ldap.initialize + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "prewarm": True}, + max=1, + ) + pool.scale + assert len(pool) == 1 + with self.assertRaises(ldappool.LDAPLockTimeout) as ctx: + c = pool.get() + if not c._lock.acquire(blocking=True, timeout=1): + raise ldappool.LDAPLockTimeout() + if not c._lock.acquire(blocking=True, timeout=1): + raise ldappool.LDAPLockTimeout() + c.authenticate("test", "test") == (97, []) + + """test if ConnectionPool releases lock when Connection + is used and returned""" + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "prewarm": True}, + max=1, + ) + pool.scale + assert len(pool) == 1 + with pool.get() as ctx: + pass + with pool.get() as ctx: + pass + + def test_ConnectionPoolmethods(self): + """test ConnectionPool methods which are there for + simplifying handling with the class""" + + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?sub?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "prewarm": True}, + max=1, + ) + assert pool.scope == _ldap.SCOPE_SUBTREE + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?base?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "prewarm": True}, + max=1, + ) + assert pool.scope == _ldap.SCOPE_BASE + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?one?(uid=tester)" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "prewarm": True}, + max=1, + ) + assert pool.scope == _ldap.SCOPE_ONELEVEL + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?one?(uid=tester)?extensiontest" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "prewarm": True}, + max=1, + ) + assert isinstance(pool.extensions, ldapurl.LDAPUrlExtensions) + pool = ConnectionPool( + uri=LDAPUrl( + "ldaps://localhost:636/dc=example,dc=com?uid,mail?one?(uid=tester)?extensiontest" + ), + binddn="cn=tester,dc=example,dc=com", + bindpw="changeme", + params={"retries": 3, "prewarm": True}, + max=3, + ) + pool.status + +if __name__ == "__main__": + unittest.main() diff --git a/pyproject.toml b/pyproject.toml index dda8dbc1..89596235 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,5 +4,5 @@ target-version = ['py36', 'py37', 'py38'] [tool.isort] line_length=88 -known_first_party=['ldap', '_ldap', 'ldapurl', 'ldif', 'slapdtest'] +known_first_party=['ldap', '_ldap', 'ldapurl', 'ldif', 'slapdtest', 'ldappool'] sections=['FUTURE', 'STDLIB', 'THIRDPARTY', 'FIRSTPARTY', 'LOCALFOLDER'] diff --git a/setup.py b/setup.py index 8e7963a1..46974b73 100644 --- a/setup.py +++ b/setup.py @@ -151,6 +151,7 @@ class OpenLDAP2: 'ldap.schema', 'slapdtest', 'slapdtest.certs', + 'ldappool', ], package_dir = {'': 'Lib',}, data_files = LDAP_CLASS.extra_files, diff --git a/tox.ini b/tox.ini index 22752067..c6def5c4 100644 --- a/tox.ini +++ b/tox.ini @@ -85,6 +85,7 @@ commands = Tests/t_ldap_modlist.py \ Tests/t_ldap_schema_tokenizer.py \ Tests/t_ldapurl.py \ + Tests/t_ldappool.py \ Tests/t_ldif.py \ Tests/t_untested_mods.py