Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add search after bind parameter #5

Open
wants to merge 6 commits into from

1 participant

@lmctv

This is the first part of the changes I've tried to send yesterday.

I've undone the part which seemed more controversial even to myself,
in the hope these changes could get in with less friction...

This request is meant to replace both

#3
#4

Thank you,

     lorenzo

@anilj: with this pull request,the base dn in itself is templated; I'm not really sure about
adding the means to enable passing an arbitrary dictionary into Connector.authenticate() and
Connector.user_groups(). Some more details on your use-case?

lmctv added some commits
@lmctv lmctv 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()
d96fda3
@lmctv lmctv Wrap both LDAP operations in the same try: block
to correctly check exception from the first search too.
d31c9a9
@lmctv lmctv 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
6f1de90
@lmctv lmctv Be nicer to the directory server
by setting a sizelimit on authentication searches.
9d17d4d
@lmctv lmctv 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
74f65da
@lmctv lmctv 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@f305744
to silence merge conflicts.
2232f87
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 21, 2013
  1. @lmctv

    Early getting of the searches from the registry

    lmctv authored
    To avoid useless activation of the context managers both in
    Connector.ldap_login_query() and Connector.user_groups()
  2. @lmctv

    Wrap both LDAP operations in the same try: block

    lmctv authored
    to correctly check exception from the first search too.
  3. @lmctv

    Skip server searches on empty query filter.

    lmctv authored
    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
  4. @lmctv

    Be nicer to the directory server

    lmctv authored
    by setting a sizelimit on authentication searches.
  5. @lmctv

    Allow skipping/postponing the dn search

    lmctv authored
    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
  6. @lmctv

    Escape login identifier before searching the entry.

    lmctv authored
    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@f305744
    to silence merge conflicts.
This page is out of date. Refresh to see the latest.
Showing with 114 additions and 45 deletions.
  1. +105 −44 pyramid_ldap/__init__.py
  2. +9 −1 pyramid_ldap/tests.py
View
149 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):
@@ -15,7 +16,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 +28,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,13 +61,8 @@ def query_cache(self, cache_key):
return result
- def execute(self, conn, **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:
@@ -73,15 +72,35 @@ 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,))
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()
@@ -108,22 +127,32 @@ 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`."""
- 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]
- else:
- return None
+ search = getattr(self.registry, 'ldap_login_query', None)
+ if search is None:
+ raise ConfigurationError(
+ '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)
+ 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
+ if search.search_after_bind:
+ result = search.execute_cache(conn, login_dn,
+ ldap.SCOPE_BASE,
+ '(objectClass=*)')
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
@@ -144,11 +173,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)
@@ -159,14 +188,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::
@@ -178,8 +216,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
@@ -194,13 +252,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::
View
10 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:
@@ -289,3 +290,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
Something went wrong with that request. Please try again.