From d96fda3f5f04b49c4b15e65c37ec522d1b440bbf Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Sun, 20 Jan 2013 17:09:32 +0100 Subject: [PATCH 01/19] Early getting of the searches from the registry To avoid useless activation of the context managers both in Connector.ldap_login_query() and Connector.user_groups() --- pyramid_ldap/__init__.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index ba56ea6..cb6f9a6 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -108,12 +108,12 @@ def authenticate(self, login, password): If :meth:`pyramid.config.Configurator.ldap_set_login_query` was not called, using this function will raise an :exc:`pyramid.exceptions.ConfiguratorError`.""" + search = getattr(self.registry, 'ldap_login_query', None) + if search is None: + raise ConfigurationError( + 'ldap_set_login_query was not called during setup') + with self.manager.connection() as conn: - search = getattr(self.registry, 'ldap_login_query', None) - if search is None: - raise ConfigurationError( - 'ldap_set_login_query was not called during setup') - result = search.execute(conn, login=login, password=password) if len(result) == 1: login_dn = result[0][0] @@ -144,11 +144,11 @@ def user_groups(self, userdn): called, using this function will raise an :exc:`pyramid.exceptions.ConfiguratorError` """ + search = getattr(self.registry, 'ldap_groups_query', None) + if search is None: + raise ConfigurationError( + 'set_ldap_groups_query was not called during setup') with self.manager.connection() as conn: - search = getattr(self.registry, 'ldap_groups_query', None) - if search is None: - raise ConfigurationError( - 'set_ldap_groups_query was not called during setup') try: result = search.execute(conn, userdn=userdn) return _ldap_decode(result) From d31c9a9f6f8e42cfa212f2240b9e6e40bc24cb1e Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Sun, 20 Jan 2013 17:15:39 +0100 Subject: [PATCH 02/19] Wrap both LDAP operations in the same try: block to correctly check exception from the first search too. --- pyramid_ldap/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index cb6f9a6..0e42393 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -113,13 +113,13 @@ def authenticate(self, login, password): raise ConfigurationError( 'ldap_set_login_query was not called during setup') - with self.manager.connection() as conn: - result = search.execute(conn, login=login, password=password) - if len(result) == 1: - login_dn = result[0][0] - else: - return None try: + with self.manager.connection() as conn: + result = search.execute(conn, login=login, password=password) + if len(result) == 1: + login_dn = result[0][0] + else: + return None with self.manager.connection(login_dn, password) as conn: # must invoke the __enter__ of this thing for it to connect return _ldap_decode(result[0]) From 9d17d4d6e1f2b080fafa04ff8aa4bdad5cdbe241 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Sun, 20 Jan 2013 17:30:32 +0100 Subject: [PATCH 03/19] Be nicer to the directory server by setting a sizelimit on authentication searches. --- pyramid_ldap/__init__.py | 23 +++++++++++++++-------- pyramid_ldap/tests.py | 7 +++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index 0e42393..5368ccd 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -15,7 +15,7 @@ class ldap(object): from pyramid.compat import bytes_ try: - from ldappool import ConnectionManager + from ldappool import ConnectionManager, BackendError except ImportError as e: # pragma: no cover class ConnectionManager(object): def __init__(self, *arg, **kw): @@ -27,17 +27,20 @@ def __init__(self, *arg, **kw): class _LDAPQuery(object): """ Represents an LDAP query. Provides rudimentary in-RAM caching of query results.""" - def __init__(self, base_dn, filter_tmpl, scope, cache_period): + def __init__(self, base_dn, filter_tmpl, scope, cache_period, + search_after_bind=False): self.base_dn = base_dn self.filter_tmpl = filter_tmpl self.scope = scope self.cache_period = cache_period self.last_timeslice = 0 self.cache = {} + self.search_after_bind = search_after_bind def __str__(self): return ('base_dn=%(base_dn)s, filter_tmpl=%(filter_tmpl)s, ' - 'scope=%(scope)s, cache_period=%(cache_period)s' % + 'scope=%(scope)s, cache_period=%(cache_period)s ' + 'search_after_bind=%(search_after_bind)s'% self.__dict__) def query_cache(self, cache_key): @@ -57,7 +60,7 @@ def query_cache(self, cache_key): return result - def execute(self, conn, **kw): + def execute(self, conn, sizelimit=0, **kw): cache_key = ( bytes_(self.base_dn % kw, 'utf-8'), self.scope, @@ -73,10 +76,10 @@ def execute(self, conn, **kw): (cache_key,) ) else: - result = conn.search_s(*cache_key) + result = conn.search_ext_s(*cache_key, sizelimit=sizelimit) self.cache[cache_key] = result else: - result = conn.search_s(*cache_key) + result = conn.search_ext_s(*cache_key, sizelimit=sizelimit) logger.debug('search result: %r' % (result,)) @@ -115,7 +118,7 @@ def authenticate(self, login, password): try: with self.manager.connection() as conn: - result = search.execute(conn, login=login, password=password) + result = search.execute(conn, login=login, password=password, sizelimit=1) if len(result) == 1: login_dn = result[0][0] else: @@ -123,7 +126,11 @@ def authenticate(self, login, password): with self.manager.connection(login_dn, password) as conn: # must invoke the __enter__ of this thing for it to connect return _ldap_decode(result[0]) - except ldap.LDAPError: + except (ldap.LDAPError, ldap.SIZELIMIT_EXCEEDED, ldap.INVALID_CREDENTIALS): + logger.debug('Exception in authenticate with login %r - - ' % login, + exc_info=True) + return None + except BackendError: logger.debug('Exception in authenticate with login %r' % login, exc_info=True) return None diff --git a/pyramid_ldap/tests.py b/pyramid_ldap/tests.py index 72d8b4c..0f1d650 100644 --- a/pyramid_ldap/tests.py +++ b/pyramid_ldap/tests.py @@ -289,3 +289,10 @@ def search_s(self, *arg): self.arg = arg return self.result + def search_ext_s(self, *arg, **kw): + import ldap + sizelimit = kw.get('sizelimit', 0) + self.arg = arg + if sizelimit and len(self.result) > sizelimit: + raise ldap.SIZELIMIT_EXCEEDED + return self.result From 6f1de90b6e2fcda4bf47f164a88ba20f7a164d4c Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Sun, 20 Jan 2013 15:36:47 +0100 Subject: [PATCH 04/19] Skip server searches on empty query filter. The API has been preserved by refactoring the caching and connection searching into a new _LDAPQuery.execute_cache method that gets called in turn by _LDAPQuery.execute --- pyramid_ldap/__init__.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index 5368ccd..d091d09 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -60,13 +60,8 @@ def query_cache(self, cache_key): return result - def execute(self, conn, sizelimit=0, **kw): - cache_key = ( - bytes_(self.base_dn % kw, 'utf-8'), - self.scope, - bytes_(self.filter_tmpl % kw, 'utf-8') - ) - + def execute_cache(self, conn, *cache_key, **kw): + sizelimit = kw.get('sizelimit', 0) logger.debug('searching for %r' % (cache_key,)) if self.cache_period: @@ -85,6 +80,26 @@ def execute(self, conn, sizelimit=0, **kw): return result + def execute(self, conn, sizelimit=0, **kw): + """ Returns an entry set resulting from querying the connected backend + for entries matching parameters in kw. + + Skip the real query and return an hard-coded result based on string + interpolation of ``base_dn`` if the ``filter_tmpl`` attribute is empty""" + search_filter = self.filter_tmpl % kw + search_base = self.base_dn % kw + if search_filter: + cache_key = ( + bytes_(search_base, 'utf-8'), + self.scope, + bytes_(search_filter, 'utf-8') + ) + return self.execute_cache(conn, *cache_key, sizelimit=sizelimit) + + result = [(search_base, {})] + logger.debug('result generated by string interpolation: %r' % result) + return result + def _timeslice(period, when=None): if when is None: # pragma: no cover when = time.time() From 74f65da06686247208a24027b2a6226a9cf2fb9f Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Sun, 20 Jan 2013 18:03:02 +0100 Subject: [PATCH 05/19] Allow skipping/postponing the dn search by refactoring the _LDAPQuery class: - slightly refactor _LDAPQuery.execute by splitting it into a new method _LDAPQuery.execute_cache, which does the real work of searching and caching, and a replacement _LDAPQuery.execute which will skip the call to execute_cache when filter_tmpl is empty. - directly call _LDAPQuery.execute_cache after entering the user-bind self.manager.connection() context manager --- pyramid_ldap/__init__.py | 68 ++++++++++++++++++++++++++++++---------- pyramid_ldap/tests.py | 3 +- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index d091d09..540af8d 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -140,6 +140,10 @@ def authenticate(self, login, password): return None with self.manager.connection(login_dn, password) as conn: # must invoke the __enter__ of this thing for it to connect + if search.search_after_bind: + result = search.execute_cache(conn, login_dn, + ldap.SCOPE_BASE, + '(objectClass=*)') return _ldap_decode(result[0]) except (ldap.LDAPError, ldap.SIZELIMIT_EXCEEDED, ldap.INVALID_CREDENTIALS): logger.debug('Exception in authenticate with login %r - - ' % login, @@ -181,14 +185,23 @@ def user_groups(self, userdn): return None def ldap_set_login_query(config, base_dn, filter_tmpl, - scope=ldap.SCOPE_ONELEVEL, cache_period=0): - """ Configurator method to set the LDAP login search. ``base_dn`` is the - DN at which to begin the search. ``filter_tmpl`` is a string which can - be used as an LDAP filter: it should contain the replacement value - ``%(login)s``. Scope is any valid LDAP scope value - (e.g. ``ldap.SCOPE_ONELEVEL``). ``cache_period`` is the number of seconds - to cache login search results; if it is 0, login search results will not - be cached. + scope=ldap.SCOPE_ONELEVEL, cache_period=0, + search_after_bind=False): + """ Configurator method to set the LDAP login search. + + - **base_dn**: the DN at which to begin the search **[mandatory]** + - **filter_tmpl**: an LDAP search filter **[mandatory]** + + At least one of these parameters should contain the replacement value + ``%(login)s`` + + - **scope**: A valid ldap search scope + **default**: ``ldap.SCOPE_ONELEVEL`` + - **cache_period**: the number of seconds to cache login search results + if 0, results will not be cached + **default**: ``0`` + - **search_after_bind**: do a base search on the entry itself after + a successful bind Example:: @@ -200,8 +213,28 @@ def ldap_set_login_query(config, base_dn, filter_tmpl, The registered search must return one and only one value to be considered a valid login. + + If the ``filter_tmpl`` is empty, the directory will not be searched, and + the entry dn will be assumed to be equal to the ``%(login)s``-replaced + ``base_dn``, and no entry's attribute will be fetched from the LDAP server, + leading to faster operation. + Both in this case, and in the case of servers configured to only allow + reading some needed entry's attribute only to the bound entry itself, + ``search_after_bind`` can be set to ``True`` if there is a need to read + the entry's attribute. + + Example:: + + config.set_ldap_login_query( + base_dn='sAMAccountName=%(login)s,CN=Users,DC=example,DC=com', + filter_tmpl='' + scope=ldap.SCOPE_ONELEVEL, + search_after_bind=True + ) + """ - query = _LDAPQuery(base_dn, filter_tmpl, scope, cache_period) + query = _LDAPQuery(base_dn, filter_tmpl, scope, cache_period, + search_after_bind=search_after_bind) def register(): config.registry.ldap_login_query = query @@ -216,13 +249,16 @@ def register(): def ldap_set_groups_query(config, base_dn, filter_tmpl, scope=ldap.SCOPE_SUBTREE, cache_period=0): - """ Configurator method to set the LDAP groups search. ``base_dn`` is - the DN at which to begin the search. ``filter_tmpl`` is a string which - can be used as an LDAP filter: it should contain the replacement value - ``%(userdn)s``. Scope is any valid LDAP scope value - (e.g. ``ldap.SCOPE_SUBTREE``). ``cache_period`` is the number of seconds - to cache groups search results; if it is 0, groups search results will - not be cached. + """ Configurator method to set the LDAP groups search. + + - **base_dn**: the DN at which to begin the search **[mandatory]** + - **filter_tmpl**: a string which can be used as an LDAP filter: + it should contain the replacement value ``%(userdn)s`` **[mandatory]** + - **scope**: A valid ldap search scope + **default**: ``ldap.SCOPE_SUBTREE`` + - **cache_period**: the number of seconds to cache login search results + if 0, results will not be cached + **default**: ``0`` Example:: diff --git a/pyramid_ldap/tests.py b/pyramid_ldap/tests.py index 0f1d650..46d7826 100644 --- a/pyramid_ldap/tests.py +++ b/pyramid_ldap/tests.py @@ -271,9 +271,10 @@ def connection(self, username=None, password=None): raise e class DummySearch(object): - def __init__(self, result, exc=None): + def __init__(self, result, exc=None, search_after_bind=False): self.result = result self.exc = exc + self.search_after_bind = search_after_bind def execute(self, conn, **kw): if self.exc is not None: From 2232f87dbee21ffa3443a07d4062ec1996a6fb98 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Sun, 20 Jan 2013 18:43:02 +0100 Subject: [PATCH 06/19] Escape login identifier before searching the entry. This will avoid trivial DOS and ldap.FILTER_ERROR exceptions on attempted logins by users sporting "funny" login names, like 'user*name' or 'user(middle)name'. This is a forward port of lmctv/pyramid_ldap@f3057446181106 to silence merge conflicts. --- pyramid_ldap/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index 540af8d..916a004 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -1,5 +1,6 @@ try: import ldap + import ldap.filter except ImportError: # pragma: no cover # this is for benefit of being able to build the docs on rtd.org class ldap(object): @@ -132,8 +133,10 @@ def authenticate(self, login, password): 'ldap_set_login_query was not called during setup') try: + login = login or '' + escaped_login = ldap.filter.escape_filter_chars(login) with self.manager.connection() as conn: - result = search.execute(conn, login=login, password=password, sizelimit=1) + result = search.execute(conn, login=escaped_login, password=password, sizelimit=1) if len(result) == 1: login_dn = result[0][0] else: From d8b5b6b1e51df69c20d67fc044657e9212b6af62 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Tue, 22 Jan 2013 19:50:23 +0100 Subject: [PATCH 07/19] Create context-named connections This way, it becomes possible to add distinct ldap connectors to the same pyramid app, and reference them by adding a context=... discriminator to the api calls: ldap_setup() ldap_set_login_query() ldap_set_groups_query() get_ldap_connector() and to the Connector's __init__ method. Feature comes complete with unit tests... --- pyramid_ldap/__init__.py | 73 ++++++++++++++++++++++++++++------------ pyramid_ldap/tests.py | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 21 deletions(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index 916a004..c7dcd1b 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -106,11 +106,26 @@ def _timeslice(period, when=None): when = time.time() return when - (when % period) +def _activity_identifier(base_identifier, context=''): + if context: + return '-'.join((base_identifier, context)) + else: + return base_identifier + +def _registry_identifier(base_identifier, context=''): + if context: + return '_'.join((base_identifier, context)) + else: + return base_identifier + class Connector(object): """ Provides API methods for accessing LDAP authentication information.""" - def __init__(self, registry, manager): + def __init__(self, registry, manager, context=''): self.registry = registry self.manager = manager + self.context = context + self.login_qry_identif = _registry_identifier('ldap_login_query', context) + self.group_qry_identif = _registry_identifier('ldap_groups_query', context) def authenticate(self, login, password): """ Given a login name and a password, return a tuple of ``(dn, @@ -127,7 +142,7 @@ def authenticate(self, login, password): If :meth:`pyramid.config.Configurator.ldap_set_login_query` was not called, using this function will raise an :exc:`pyramid.exceptions.ConfiguratorError`.""" - search = getattr(self.registry, 'ldap_login_query', None) + search = getattr(self.registry, self.login_qry_identif, None) if search is None: raise ConfigurationError( 'ldap_set_login_query was not called during setup') @@ -173,7 +188,7 @@ def user_groups(self, userdn): called, using this function will raise an :exc:`pyramid.exceptions.ConfiguratorError` """ - search = getattr(self.registry, 'ldap_groups_query', None) + search = getattr(self.registry, self.group_qry_identif, None) if search is None: raise ConfigurationError( 'set_ldap_groups_query was not called during setup') @@ -189,7 +204,7 @@ def user_groups(self, userdn): def ldap_set_login_query(config, base_dn, filter_tmpl, scope=ldap.SCOPE_ONELEVEL, cache_period=0, - search_after_bind=False): + search_after_bind=False, context=''): """ Configurator method to set the LDAP login search. - **base_dn**: the DN at which to begin the search **[mandatory]** @@ -236,22 +251,26 @@ def ldap_set_login_query(config, base_dn, filter_tmpl, ) """ + query_identif = _registry_identifier('ldap_login_query', context) + intr_identif = _registry_identifier('pyramid_ldap', context) + act_identif = _activity_identifier('pyramid_ldap', context) + query = _LDAPQuery(base_dn, filter_tmpl, scope, cache_period, search_after_bind=search_after_bind) def register(): - config.registry.ldap_login_query = query + setattr(config.registry, query_identif, query) intr = config.introspectable( - 'pyramid_ldap login query', + '%s login query' % intr_identif, None, str(query), - 'pyramid_ldap login query' + 'login query' ) - config.action('ldap-set-login-query', register, introspectables=(intr,)) + config.action(act_identif, register, introspectables=(intr,)) def ldap_set_groups_query(config, base_dn, filter_tmpl, - scope=ldap.SCOPE_SUBTREE, cache_period=0): + scope=ldap.SCOPE_SUBTREE, cache_period=0, context=''): """ Configurator method to set the LDAP groups search. - **base_dn**: the DN at which to begin the search **[mandatory]** @@ -272,19 +291,26 @@ def ldap_set_groups_query(config, base_dn, filter_tmpl, ) """ + query_identif = _registry_identifier('ldap_groups_query', context) + intr_identif = _registry_identifier('pyramid_ldap', context) + act_identif = _activity_identifier('ldap-set-groups-query', context) + query = _LDAPQuery(base_dn, filter_tmpl, scope, cache_period) + def register(): - config.registry.ldap_groups_query = query + setattr(config.registry, query_identif, query) + intr = config.introspectable( - 'pyramid_ldap groups query', + '%s groups query' % intr_identif, None, str(query), - 'pyramid_ldap groups query' + '%s groups query' % intr_identif ) - config.action('ldap-set-groups-query', register, introspectables=(intr,)) + + config.action(act_identif, register, introspectables=(intr,)) def ldap_setup(config, uri, bind=None, passwd=None, pool_size=10, retry_max=3, - retry_delay=.1, use_tls=False, timeout=-1, use_pool=True): + retry_delay=.1, use_tls=False, timeout=-1, use_pool=True, context=''): """ Configurator method to set up an LDAP connection pool. - **uri**: ldap server uri **[mandatory]** @@ -300,6 +326,10 @@ def ldap_setup(config, uri, bind=None, passwd=None, pool_size=10, retry_max=3, - **use_pool**: activates the pool. If False, will recreate a connector each time. **default: True** """ + conn_identif = _registry_identifier('ldap_connector', context) + intr_identif = _registry_identifier('pyramid_ldap', context) + act_identif = _activity_identifier('ldap-setup', context) + vals = dict( uri=uri, bind=bind, passwd=passwd, size=pool_size, retry_max=retry_max, retry_delay=retry_delay, use_tls=use_tls, @@ -310,23 +340,24 @@ def ldap_setup(config, uri, bind=None, passwd=None, pool_size=10, retry_max=3, def get_connector(request): registry = request.registry - return Connector(registry, manager) + return Connector(registry, manager, context) - config.set_request_property(get_connector, 'ldap_connector', reify=True) + config.set_request_property(get_connector, conn_identif, reify=True) intr = config.introspectable( - 'pyramid_ldap setup', + '%s setup' % intr_identif, None, pprint.pformat(vals), - 'pyramid_ldap setup' + '%s setup' % intr_identif, ) - config.action('ldap-setup', None, introspectables=(intr,)) + config.action(act_identif, None, introspectables=(intr,)) -def get_ldap_connector(request): +def get_ldap_connector(request, context=''): """ Return the LDAP connector attached to the request. If :meth:`pyramid.config.Configurator.ldap_setup` was not called, using this function will raise an :exc:`pyramid.exceptions.ConfigurationError`.""" - connector = getattr(request, 'ldap_connector', None) + conn_name = _registry_identifier('ldap_connector', context) + connector = getattr(request, conn_name, None) if connector is None: raise ConfigurationError( 'You must call Configurator.ldap_setup during setup ' diff --git a/pyramid_ldap/tests.py b/pyramid_ldap/tests.py index 46d7826..ccd8969 100644 --- a/pyramid_ldap/tests.py +++ b/pyramid_ldap/tests.py @@ -137,6 +137,72 @@ def test_it_defaults(self): ldap.SCOPE_ONELEVEL) self.assertEqual(config.registry.ldap_login_query.cache_period, 0) +class Test_get_ldap_connector_w_context(unittest.TestCase): + named_context = 'CONTEXT' + def _callFUT(self, request): + from pyramid_ldap import get_ldap_connector + return get_ldap_connector(request, context=self.named_context) + + def test_no_connector(self): + request = testing.DummyRequest() + self.assertRaises(ConfigurationError, self._callFUT, request) + + def test_with_connector(self): + request = testing.DummyRequest() + setattr(request, 'ldap_connector_%s' % self.named_context, True) + result = self._callFUT(request) + self.assertEqual(result, True) + +class Test_ldap_setup_w_context(unittest.TestCase): + named_context = 'CONTEXT' + def _callFUT(self, config, uri, **kw): + from pyramid_ldap import ldap_setup + return ldap_setup(config, uri, context=self.named_context, **kw) + + def test_it_defaults(self): + from pyramid_ldap import Connector + config = DummyConfig() + self._callFUT(config, 'ldap://') + self.assertEqual(config.prop_name, 'ldap_connector_%s' % self.named_context) + self.assertEqual(config.prop_reify, True) + request = testing.DummyRequest() + self.assertEqual(config.prop(request).__class__, Connector) + +class Test_ldap_set_groups_query_w_context(unittest.TestCase): + named_context = 'CONTEXT' + def _callFUT(self, config, base_dn, filter_tmpl, **kw): + from pyramid_ldap import ldap_set_groups_query + return ldap_set_groups_query(config, base_dn, filter_tmpl, context=self.named_context, **kw) + + def test_it_defaults(self): + import ldap + config = DummyConfig() + self._callFUT(config, 'dn', 'tmpl') + qry = getattr(config.registry, 'ldap_groups_query_%s' % self.named_context) + self.assertRaises(AttributeError, lambda: config.registry.ldap_groups_query.base_dn) + self.assertEqual(qry.base_dn, 'dn') + self.assertEqual(qry.filter_tmpl, 'tmpl') + self.assertEqual(qry.scope, + ldap.SCOPE_SUBTREE) + self.assertEqual(qry.cache_period, 0) + +class Test_ldap_set_login_query_w_context(unittest.TestCase): + named_context = 'CONTEXT' + def _callFUT(self, config, base_dn, filter_tmpl, **kw): + from pyramid_ldap import ldap_set_login_query + return ldap_set_login_query(config, base_dn, filter_tmpl, context=self.named_context, **kw) + + def test_it_defaults(self): + import ldap + config = DummyConfig() + self._callFUT(config, 'dn', 'tmpl') + qry = getattr(config.registry, 'ldap_login_query_%s' % self.named_context) + self.assertEqual(qry.base_dn, 'dn') + self.assertEqual(qry.filter_tmpl, 'tmpl') + self.assertEqual(qry.scope, + ldap.SCOPE_ONELEVEL) + self.assertEqual(qry.cache_period, 0) + class TestConnector(unittest.TestCase): def _makeOne(self, registry, manager): from pyramid_ldap import Connector From c1df1ff1592ba5c9a9a0858ff447a953c793d7b9 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Tue, 22 Jan 2013 19:56:59 +0100 Subject: [PATCH 08/19] Change authenticated_user's representation to help groupfinder discriminate between configured backend contexts --- pyramid_ldap/__init__.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index c7dcd1b..e58a555 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -1,6 +1,7 @@ try: import ldap import ldap.filter + import ldapurl except ImportError: # pragma: no cover # this is for benefit of being able to build the docs on rtd.org class ldap(object): @@ -162,7 +163,7 @@ def authenticate(self, login, password): result = search.execute_cache(conn, login_dn, ldap.SCOPE_BASE, '(objectClass=*)') - return _ldap_decode(result[0]) + return _ldap_tag_dn_decode(result, context=self.context)[0] except (ldap.LDAPError, ldap.SIZELIMIT_EXCEEDED, ldap.INVALID_CREDENTIALS): logger.debug('Exception in authenticate with login %r - - ' % login, exc_info=True) @@ -195,7 +196,7 @@ def user_groups(self, userdn): with self.manager.connection() as conn: try: result = search.execute(conn, userdn=userdn) - return _ldap_decode(result) + return _ldap_tag_dn_decode(result) except ldap.LDAPError: logger.debug( 'Exception in user_groups with userdn %r' % userdn, @@ -370,8 +371,9 @@ def groupfinder(userdn, request): each group belonging to the user specified by ``userdn`` to as a principal in the list of results; if the user does not exist, it returns None.""" - connector = get_ldap_connector(request) - group_list = connector.user_groups(userdn) + parsed = ldapurl.LDAPUrl(userdn) + connector = get_ldap_connector(request, context=parsed.hostport) + group_list = connector.user_groups(parsed.dn) if group_list is None: return None group_dns = [] @@ -384,6 +386,13 @@ def _ldap_decode(result): using the utf-8 encoding """ return _Decoder().decode(result) +def _ldap_tag_dn_decode(result, context=''): + + return [('ldap://%s/%s' % (context, ldapurl.ldapUrlEscape(dn)), + attributes) + for dn, attributes in _ldap_decode(result) + ] + class _Decoder(object): """ Stolen from django-auth-ldap. From f5ba93dd5cb39abc6d877289dd728f186206d605 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Tue, 22 Jan 2013 19:58:57 +0100 Subject: [PATCH 09/19] Adapt tests to the new representation. --- pyramid_ldap/tests.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyramid_ldap/tests.py b/pyramid_ldap/tests.py index ccd8969..de54ec9 100644 --- a/pyramid_ldap/tests.py +++ b/pyramid_ldap/tests.py @@ -69,13 +69,13 @@ def _callFUT(self, dn, request): def test_no_group_list(self): request = testing.DummyRequest() request.ldap_connector = DummyLDAPConnector('dn', None) - result = self._callFUT('dn', request) + result = self._callFUT('ldap:///dn', request) self.assertEqual(result, None) def test_with_group_list(self): request = testing.DummyRequest() request.ldap_connector = DummyLDAPConnector('dn', [('groupdn', None)]) - result = self._callFUT('dn', request) + result = self._callFUT('ldap:///dn', request) self.assertEqual(result, ['groupdn']) class Test_get_ldap_connector(unittest.TestCase): @@ -225,7 +225,7 @@ def test_authenticate_search_returns_one_result(self): registry = Dummy() registry.ldap_login_query = DummySearch([('a', 'b')]) inst = self._makeOne(registry, manager) - self.assertEqual(inst.authenticate(None, None), ('a', 'b')) + self.assertEqual(inst.authenticate(None, None), ('ldap:///a', 'b')) def test_authenticate_search_bind_raises(self): import ldap @@ -245,7 +245,7 @@ def test_user_groups_search_returns_result(self): registry = Dummy() registry.ldap_groups_query = DummySearch([('a', 'b')]) inst = self._makeOne(registry, manager) - self.assertEqual(inst.user_groups(None), [('a', 'b')]) + self.assertEqual(inst.user_groups(None), [('ldap:///a', 'b')]) def test_user_groups_execute_raises(self): import ldap From 8410b3d052cb3c2cb06bacecd3e19162b8f9a14f Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Sat, 2 Mar 2013 09:25:32 +0100 Subject: [PATCH 10/19] Factor-out a get_connector_name to simplify downstream integration. --- pyramid_ldap/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index e58a555..239a350 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -353,11 +353,14 @@ def get_connector(request): ) config.action(act_identif, None, introspectables=(intr,)) +def get_ldap_connector_name(context=''): + return _registry_identifier('ldap_connector', context) + def get_ldap_connector(request, context=''): """ Return the LDAP connector attached to the request. If :meth:`pyramid.config.Configurator.ldap_setup` was not called, using this function will raise an :exc:`pyramid.exceptions.ConfigurationError`.""" - conn_name = _registry_identifier('ldap_connector', context) + conn_name = get_ldap_connector_name(context) connector = getattr(request, conn_name, None) if connector is None: raise ConfigurationError( From aba32284c638db7133308430adf8ed77fc02044d Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Sat, 2 Mar 2013 10:12:25 +0100 Subject: [PATCH 11/19] Add documentation for get_ldap_connector_name. --- pyramid_ldap/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index 239a350..61d9009 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -354,6 +354,8 @@ def get_connector(request): config.action(act_identif, None, introspectables=(intr,)) def get_ldap_connector_name(context=''): + """ Return the name of the connector attached to the request + for the named **context**.""" return _registry_identifier('ldap_connector', context) def get_ldap_connector(request, context=''): From d377b9d68185f00d0180585fe565f48b37854ee9 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Sun, 9 Jun 2013 10:16:02 +0200 Subject: [PATCH 12/19] Docstring changes Shorten one line and correct one typo. --- pyramid_ldap/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index 61d9009..835cf08 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -87,7 +87,8 @@ def execute(self, conn, sizelimit=0, **kw): for entries matching parameters in kw. Skip the real query and return an hard-coded result based on string - interpolation of ``base_dn`` if the ``filter_tmpl`` attribute is empty""" + interpolation of ``base_dn`` if the ``filter_tmpl`` attribute + is empty""" search_filter = self.filter_tmpl % kw search_base = self.base_dn % kw if search_filter: @@ -138,7 +139,7 @@ def authenticate(self, login, password): dictionary mapping LDAP user attributes to sequences of values. The keys and values in the dictionary values provided will be decoded from UTF-8, recursively, where possible. The dictionary returned is - a case-insensitive dictionary implemenation. + a case-insensitive dictionary implementation. If :meth:`pyramid.config.Configurator.ldap_set_login_query` was not called, using this function will raise an From 82be5c4761e5770503c74d9b524b4b264b606ef2 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Sun, 9 Jun 2013 10:19:55 +0200 Subject: [PATCH 13/19] Add myself to CONTRIBUTORS --- CONTRIBUTORS.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 80fbb23..06d3f66 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -103,4 +103,4 @@ Contributors ------------ - Chris McDonough, 2013/04/24 - +- Lorenzo M. Catucci, 2013/06/09 From 830efef6a006f8a25315dbeea462230475f7f150 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Sun, 9 Jun 2013 10:18:11 +0200 Subject: [PATCH 14/19] Set default values for login and password --- pyramid_ldap/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index 835cf08..aadb6bd 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -129,7 +129,7 @@ def __init__(self, registry, manager, context=''): self.login_qry_identif = _registry_identifier('ldap_login_query', context) self.group_qry_identif = _registry_identifier('ldap_groups_query', context) - def authenticate(self, login, password): + def authenticate(self, login='', password=''): """ Given a login name and a password, return a tuple of ``(dn, attrdict)`` if the matching user if the user exists and his password is correct. Otherwise return ``None``. @@ -150,7 +150,6 @@ def authenticate(self, login, password): 'ldap_set_login_query was not called during setup') try: - login = login or '' escaped_login = ldap.filter.escape_filter_chars(login) with self.manager.connection() as conn: result = search.execute(conn, login=escaped_login, password=password, sizelimit=1) From 5a0a4221f9f346dea7b575d5b8c2708ff365af35 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Tue, 1 Jul 2014 18:36:30 +0200 Subject: [PATCH 15/19] Skip escaping on false-ish values of login --- pyramid_ldap/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index e4347be..2b46240 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -159,7 +159,10 @@ def authenticate(self, login='', password=''): 'ldap_set_login_query was not called during setup') try: - escaped_login = ldap.filter.escape_filter_chars(login) + if login: + escaped_login = ldap.filter.escape_filter_chars(login) + else: + escaped_login = '' with self.manager.connection() as conn: result = search.execute(conn, login=escaped_login, password=password, sizelimit=1) if len(result) == 1: From aee13bc0801296ebd853b603ef5e6fd83e52e0e1 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Tue, 1 Jul 2014 19:14:25 +0200 Subject: [PATCH 16/19] Module users should know what changed without being forced to read the vcs log --- CHANGES.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 673127e..fe812bb 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,18 @@ Next release ------------ +- Support creation of multiple co-existing ldap contexts in the same + application. + +- Add a "search_after_bind" attribute to the _LDAPQuery class, to + help in reading directory entry attributes when the directory server + is configured to hide them in the before login search phase. + +- Set a size limit on pre-login entry dn searches. + +- Escape user-provided login names, avoiding server errors or trivial + DOSs caused by user names like 'user*name' or 'user(middle)name' + - Prevent the use of zero-length password authentication. See https://github.com/Pylons/pyramid_ldap/pull/13 From f4486df16d4d90540cdc52b3a75a8c83b21cd716 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Tue, 1 Jul 2014 19:21:15 +0200 Subject: [PATCH 17/19] Add a reference to upstream pull requests. --- CHANGES.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index fe812bb..75035bf 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,11 +2,12 @@ Next release ------------ - Support creation of multiple co-existing ldap contexts in the same - application. + application. See https://github.com/Pylons/pyramid_ldap/pull/6 - Add a "search_after_bind" attribute to the _LDAPQuery class, to help in reading directory entry attributes when the directory server is configured to hide them in the before login search phase. + See https://github.com/Pylons/pyramid_ldap/pull/5 - Set a size limit on pre-login entry dn searches. From bc809d50003effde186ecfe2b9b347883e2d2790 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Wed, 2 Jul 2014 11:46:16 +0200 Subject: [PATCH 18/19] =?UTF-8?q?Rename=20context=20=E2=86=92=20realm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyramid_ldap/__init__.py | 64 ++++++++++++++++++++-------------------- pyramid_ldap/tests.py | 32 ++++++++++---------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index 2b46240..c63bbb3 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -108,26 +108,26 @@ def _timeslice(period, when=None): when = time.time() return when - (when % period) -def _activity_identifier(base_identifier, context=''): - if context: - return '-'.join((base_identifier, context)) +def _activity_identifier(base_identifier, realm=''): + if realm: + return '-'.join((base_identifier, realm)) else: return base_identifier -def _registry_identifier(base_identifier, context=''): - if context: - return '_'.join((base_identifier, context)) +def _registry_identifier(base_identifier, realm=''): + if realm: + return '_'.join((base_identifier, realm)) else: return base_identifier class Connector(object): """ Provides API methods for accessing LDAP authentication information.""" - def __init__(self, registry, manager, context=''): + def __init__(self, registry, manager, realm=''): self.registry = registry self.manager = manager - self.context = context - self.login_qry_identif = _registry_identifier('ldap_login_query', context) - self.group_qry_identif = _registry_identifier('ldap_groups_query', context) + self.realm = realm + self.login_qry_identif = _registry_identifier('ldap_login_query', realm) + self.group_qry_identif = _registry_identifier('ldap_groups_query', realm) def authenticate(self, login='', password=''): """ Given a login name and a password, return a tuple of ``(dn, @@ -175,7 +175,7 @@ def authenticate(self, login='', password=''): result = search.execute_cache(conn, login_dn, ldap.SCOPE_BASE, '(objectClass=*)') - return _ldap_tag_dn_decode(result, context=self.context)[0] + return _ldap_tag_dn_decode(result, realm=self.realm)[0] except (ldap.LDAPError, ldap.SIZELIMIT_EXCEEDED, ldap.INVALID_CREDENTIALS): logger.debug('Exception in authenticate with login %r - - ' % login, exc_info=True) @@ -217,7 +217,7 @@ def user_groups(self, userdn): def ldap_set_login_query(config, base_dn, filter_tmpl, scope=ldap.SCOPE_ONELEVEL, cache_period=0, - search_after_bind=False, context=''): + search_after_bind=False, realm=''): """ Configurator method to set the LDAP login search. - **base_dn**: the DN at which to begin the search **[mandatory]** @@ -264,9 +264,9 @@ def ldap_set_login_query(config, base_dn, filter_tmpl, ) """ - query_identif = _registry_identifier('ldap_login_query', context) - intr_identif = _registry_identifier('pyramid_ldap', context) - act_identif = _activity_identifier('pyramid_ldap', context) + query_identif = _registry_identifier('ldap_login_query', realm) + intr_identif = _registry_identifier('pyramid_ldap', realm) + act_identif = _activity_identifier('pyramid_ldap', realm) query = _LDAPQuery(base_dn, filter_tmpl, scope, cache_period, search_after_bind=search_after_bind) @@ -283,7 +283,7 @@ def register(): config.action(act_identif, register, introspectables=(intr,)) def ldap_set_groups_query(config, base_dn, filter_tmpl, - scope=ldap.SCOPE_SUBTREE, cache_period=0, context=''): + scope=ldap.SCOPE_SUBTREE, cache_period=0, realm=''): """ Configurator method to set the LDAP groups search. - **base_dn**: the DN at which to begin the search **[mandatory]** @@ -304,9 +304,9 @@ def ldap_set_groups_query(config, base_dn, filter_tmpl, ) """ - query_identif = _registry_identifier('ldap_groups_query', context) - intr_identif = _registry_identifier('pyramid_ldap', context) - act_identif = _activity_identifier('ldap-set-groups-query', context) + query_identif = _registry_identifier('ldap_groups_query', realm) + intr_identif = _registry_identifier('pyramid_ldap', realm) + act_identif = _activity_identifier('ldap-set-groups-query', realm) query = _LDAPQuery(base_dn, filter_tmpl, scope, cache_period) @@ -323,7 +323,7 @@ def register(): config.action(act_identif, register, introspectables=(intr,)) def ldap_setup(config, uri, bind=None, passwd=None, pool_size=10, retry_max=3, - retry_delay=.1, use_tls=False, timeout=-1, use_pool=True, context=''): + retry_delay=.1, use_tls=False, timeout=-1, use_pool=True, realm=''): """ Configurator method to set up an LDAP connection pool. - **uri**: ldap server uri **[mandatory]** @@ -339,9 +339,9 @@ def ldap_setup(config, uri, bind=None, passwd=None, pool_size=10, retry_max=3, - **use_pool**: activates the pool. If False, will recreate a connector each time. **default: True** """ - conn_identif = _registry_identifier('ldap_connector', context) - intr_identif = _registry_identifier('pyramid_ldap', context) - act_identif = _activity_identifier('ldap-setup', context) + conn_identif = _registry_identifier('ldap_connector', realm) + intr_identif = _registry_identifier('pyramid_ldap', realm) + act_identif = _activity_identifier('ldap-setup', realm) vals = dict( uri=uri, bind=bind, passwd=passwd, size=pool_size, @@ -353,7 +353,7 @@ def ldap_setup(config, uri, bind=None, passwd=None, pool_size=10, retry_max=3, def get_connector(request): registry = request.registry - return Connector(registry, manager, context) + return Connector(registry, manager, realm) config.set_request_property(get_connector, conn_identif, reify=True) @@ -365,16 +365,16 @@ def get_connector(request): ) config.action(act_identif, None, introspectables=(intr,)) -def get_ldap_connector_name(context=''): +def get_ldap_connector_name(realm=''): """ Return the name of the connector attached to the request - for the named **context**.""" - return _registry_identifier('ldap_connector', context) + for the named **realm**.""" + return _registry_identifier('ldap_connector', realm) -def get_ldap_connector(request, context=''): +def get_ldap_connector(request, realm=''): """ Return the LDAP connector attached to the request. If :meth:`pyramid.config.Configurator.ldap_setup` was not called, using this function will raise an :exc:`pyramid.exceptions.ConfigurationError`.""" - conn_name = get_ldap_connector_name(context) + conn_name = get_ldap_connector_name(realm) connector = getattr(request, conn_name, None) if connector is None: raise ConfigurationError( @@ -389,7 +389,7 @@ def groupfinder(userdn, request): principal in the list of results; if the user does not exist, it returns None.""" parsed = ldapurl.LDAPUrl(userdn) - connector = get_ldap_connector(request, context=parsed.hostport) + connector = get_ldap_connector(request, realm=parsed.hostport) group_list = connector.user_groups(parsed.dn) if group_list is None: return None @@ -403,9 +403,9 @@ def _ldap_decode(result): using the utf-8 encoding """ return _Decoder().decode(result) -def _ldap_tag_dn_decode(result, context=''): +def _ldap_tag_dn_decode(result, realm=''): - return [('ldap://%s/%s' % (context, ldapurl.ldapUrlEscape(dn)), + return [('ldap://%s/%s' % (realm, ldapurl.ldapUrlEscape(dn)), attributes) for dn, attributes in _ldap_decode(result) ] diff --git a/pyramid_ldap/tests.py b/pyramid_ldap/tests.py index 596cfb6..edfd338 100644 --- a/pyramid_ldap/tests.py +++ b/pyramid_ldap/tests.py @@ -137,11 +137,11 @@ def test_it_defaults(self): ldap.SCOPE_ONELEVEL) self.assertEqual(config.registry.ldap_login_query.cache_period, 0) -class Test_get_ldap_connector_w_context(unittest.TestCase): - named_context = 'CONTEXT' +class Test_get_ldap_connector_w_realm(unittest.TestCase): + named_realm = 'REALM' def _callFUT(self, request): from pyramid_ldap import get_ldap_connector - return get_ldap_connector(request, context=self.named_context) + return get_ldap_connector(request, realm=self.named_realm) def test_no_connector(self): request = testing.DummyRequest() @@ -149,36 +149,36 @@ def test_no_connector(self): def test_with_connector(self): request = testing.DummyRequest() - setattr(request, 'ldap_connector_%s' % self.named_context, True) + setattr(request, 'ldap_connector_%s' % self.named_realm, True) result = self._callFUT(request) self.assertEqual(result, True) -class Test_ldap_setup_w_context(unittest.TestCase): - named_context = 'CONTEXT' +class Test_ldap_setup_w_realm(unittest.TestCase): + named_realm = 'REALM' def _callFUT(self, config, uri, **kw): from pyramid_ldap import ldap_setup - return ldap_setup(config, uri, context=self.named_context, **kw) + return ldap_setup(config, uri, realm=self.named_realm, **kw) def test_it_defaults(self): from pyramid_ldap import Connector config = DummyConfig() self._callFUT(config, 'ldap://') - self.assertEqual(config.prop_name, 'ldap_connector_%s' % self.named_context) + self.assertEqual(config.prop_name, 'ldap_connector_%s' % self.named_realm) self.assertEqual(config.prop_reify, True) request = testing.DummyRequest() self.assertEqual(config.prop(request).__class__, Connector) -class Test_ldap_set_groups_query_w_context(unittest.TestCase): - named_context = 'CONTEXT' +class Test_ldap_set_groups_query_w_realm(unittest.TestCase): + named_realm = 'REALM' def _callFUT(self, config, base_dn, filter_tmpl, **kw): from pyramid_ldap import ldap_set_groups_query - return ldap_set_groups_query(config, base_dn, filter_tmpl, context=self.named_context, **kw) + return ldap_set_groups_query(config, base_dn, filter_tmpl, realm=self.named_realm, **kw) def test_it_defaults(self): import ldap config = DummyConfig() self._callFUT(config, 'dn', 'tmpl') - qry = getattr(config.registry, 'ldap_groups_query_%s' % self.named_context) + qry = getattr(config.registry, 'ldap_groups_query_%s' % self.named_realm) self.assertRaises(AttributeError, lambda: config.registry.ldap_groups_query.base_dn) self.assertEqual(qry.base_dn, 'dn') self.assertEqual(qry.filter_tmpl, 'tmpl') @@ -186,17 +186,17 @@ def test_it_defaults(self): ldap.SCOPE_SUBTREE) self.assertEqual(qry.cache_period, 0) -class Test_ldap_set_login_query_w_context(unittest.TestCase): - named_context = 'CONTEXT' +class Test_ldap_set_login_query_w_realm(unittest.TestCase): + named_realm = 'REALM' def _callFUT(self, config, base_dn, filter_tmpl, **kw): from pyramid_ldap import ldap_set_login_query - return ldap_set_login_query(config, base_dn, filter_tmpl, context=self.named_context, **kw) + return ldap_set_login_query(config, base_dn, filter_tmpl, realm=self.named_realm, **kw) def test_it_defaults(self): import ldap config = DummyConfig() self._callFUT(config, 'dn', 'tmpl') - qry = getattr(config.registry, 'ldap_login_query_%s' % self.named_context) + qry = getattr(config.registry, 'ldap_login_query_%s' % self.named_realm) self.assertEqual(qry.base_dn, 'dn') self.assertEqual(qry.filter_tmpl, 'tmpl') self.assertEqual(qry.scope, From 438d57c284cf9f428c5477fc2dd0f5bdfd61f303 Mon Sep 17 00:00:00 2001 From: "Lorenzo M. Catucci" Date: Sat, 5 Jul 2014 18:12:17 +0200 Subject: [PATCH 19/19] Document presence and usage of ``realm`` parameter in `ldap_setup`, `get_ldap_connector`, `ldap_set_login_query` and `ldap_set_groups_query` API calls. --- pyramid_ldap/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pyramid_ldap/__init__.py b/pyramid_ldap/__init__.py index c63bbb3..d8682af 100644 --- a/pyramid_ldap/__init__.py +++ b/pyramid_ldap/__init__.py @@ -233,6 +233,8 @@ def ldap_set_login_query(config, base_dn, filter_tmpl, **default**: ``0`` - **search_after_bind**: do a base search on the entry itself after a successful bind + - **realm**: A connection identifier + **default**: ``''`` Example:: @@ -294,6 +296,8 @@ def ldap_set_groups_query(config, base_dn, filter_tmpl, - **cache_period**: the number of seconds to cache login search results if 0, results will not be cached **default**: ``0`` + - **realm**: A connection identifier + **default**: ``''`` Example:: @@ -338,6 +342,8 @@ def ldap_setup(config, uri, bind=None, passwd=None, pool_size=10, retry_max=3, - **timeout**: connector timeout. **default: -1** - **use_pool**: activates the pool. If False, will recreate a connector each time. **default: True** + - **realm**: A connection identifier + **default**: ``''`` """ conn_identif = _registry_identifier('ldap_connector', realm) intr_identif = _registry_identifier('pyramid_ldap', realm) @@ -371,9 +377,11 @@ def get_ldap_connector_name(realm=''): return _registry_identifier('ldap_connector', realm) def get_ldap_connector(request, realm=''): - """ Return the LDAP connector attached to the request. If - :meth:`pyramid.config.Configurator.ldap_setup` was not called, using - this function will raise an :exc:`pyramid.exceptions.ConfigurationError`.""" + """ Return the LDAP connector attached to the request for the connection + identified by the ``realm`` name. + If :meth:`pyramid.config.Configurator.ldap_setup` was not called for the + named ``realm``, using this function will raise + an :exc:`pyramid.exceptions.ConfigurationError`.""" conn_name = get_ldap_connector_name(realm) connector = getattr(request, conn_name, None) if connector is None: