From 5e3f026258c1e3cf7afbf567bf74d5f3de887a85 Mon Sep 17 00:00:00 2001 From: "Vitor M. A. da Cruz" Date: Tue, 16 Aug 2016 20:38:09 -0300 Subject: [PATCH 1/3] Added BIND_AS_AUTHENTICATING_USER option. This option allows the authentication of users with multiple unknown distinguished name (dn) formats, provided that there is at least one user whose dn is known and has the permissions to search for other users. --- README.md | 16 +++- django_auth_ldap3/backends.py | 136 ++++++++++++++++++++++------------ django_auth_ldap3/conf.py | 5 ++ 3 files changed, 109 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index ae7a3af..96ec8e9 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,20 @@ directory tree. It is yet to be implemented in this library. See [issue #2](https://github.com/sjkingo/django_auth_ldap3/issues/2). +### Method 3: Bind with one user, then search and bind + +The third method is used when the user's distinguished name (DN) format cannot be reliably predicted. +An user whose DN is known is used to search for the desired user's DN, which is then used to validate +her credentials. It may be seen as a subset of method 2. + +Relevant settings: +* `AUTH_LDAP_BIND_AS_AUTHENTICATING_USER`: Controls whether direct binding is used +* `AUTH_LDAP_BIND_DN`: The distinguished name of the user which will search for other users +* `AUTH_LDAP_BIND_PASSWORD`: Password for the user which will search for other users +* `AUTH_LDAP_USER_DN_FILTER_TEMPLATE`: Template of the search string used to query the server. For example: `(&(objectclass=person)(uid={username}))`. It must contain `{username}` somewhere which will be substituted for the username that is being authenticated. + +If using either of these settings, set `AUTH_LDAP_BIND_TEMPLATE` to `None`. + ## Group membership Sometimes it is desirable to restrict authentication to users that are members @@ -275,5 +289,5 @@ Default: `True` ## Caveats When using this library, it is strongly recommended to not manually -modify the usernames in the Django user table (either through the admin or modifying a +modify the usernames in the Django user table (either through the admin or modifying a `User.username` field). See issues [#7](https://github.com/sjkingo/django_auth_ldap3/issues/7) and [#9](https://github.com/sjkingo/django_auth_ldap3/issues/9) for more details. diff --git a/django_auth_ldap3/backends.py b/django_auth_ldap3/backends.py index c072548..02f6096 100644 --- a/django_auth_ldap3/backends.py +++ b/django_auth_ldap3/backends.py @@ -2,7 +2,6 @@ from django.contrib.auth.models import Group from django.contrib.auth import get_user_model -from ldap3.core.exceptions import LDAPSocketOpenError import hashlib import ldap3 import logging @@ -12,6 +11,7 @@ logger = logging.getLogger('django_auth_ldap3') + class LDAPUser(object): """ A class representing an LDAP user returned from the directory. @@ -40,6 +40,7 @@ def __str__(self): def username(self): return getattr(self, settings.UID_ATTRIB) + class LDAPBackend(object): """ An authentication backend for LDAP directories. @@ -107,14 +108,15 @@ def authenticate(self, username=None, password=None): if not django_user: # Create new user. We use `ldap_user.username` here as it is the # case-sensitive version - django_user = User(username=ldap_user.username, - password=hashlib.sha1().hexdigest(), - first_name=ldap_user.givenName, - last_name=ldap_user.sn, - email=ldap_user.mail, - is_superuser=False, - is_staff=admin, - is_active=True + django_user = User( + username=ldap_user.username, + password=hashlib.sha1().hexdigest(), + first_name=ldap_user.givenName, + last_name=ldap_user.sn, + email=ldap_user.mail, + is_superuser=False, + is_staff=admin, + is_active=True ) django_user.save() else: @@ -158,8 +160,10 @@ def check_group_membership(self, ldap_user, group_dn): # primaryGroupToken. This will return 0 results in OpenLDAP and hence # be ignored. pgt = None - group_attribs = self.search_ldap(ldap_user.connection, '(distinguishedName={})' \ - .format(group_dn), attributes=['primaryGroupToken']) + group_attribs = self.search_ldap( + ldap_user.connection, '(distinguishedName={})'.format(group_dn), + attributes=['primaryGroupToken'] + ) if group_attribs: pgt = group_attribs.get('primaryGroupToken', None) if type(pgt) == list: @@ -168,7 +172,8 @@ def check_group_membership(self, ldap_user, group_dn): # Now perform our group membership test. If the primary group token is not-None, # then we wrap the filter in an OR and test for that too. search_filter = '(&(objectClass=user)({}={})(memberof={}))'.format( - settings.UID_ATTRIB, str(ldap_user), group_dn) + settings.UID_ATTRIB, str(ldap_user), group_dn + ) if pgt: search_filter = '(|{}(&(cn={})(primaryGroupID={})))'.format(search_filter, ldap_user.cn, pgt) @@ -213,50 +218,38 @@ def bind_ldap_user(self, username, password): """ # Construct the user to bind as - if settings.BIND_TEMPLATE: - # Full CN - ldap_bind_user = settings.BIND_TEMPLATE.format(username=username, - base_dn=settings.BASE_DN) - elif settings.USERNAME_PREFIX: - # Prepend a prefix: useful for DOMAIN\user - ldap_bind_user = settings.USERNAME_PREFIX + username - elif settings.USERNAME_SUFFIX: - # Append a suffix: useful for user@domain - ldap_bind_user = username + settings.USERNAME_SUFFIX - logger.debug('Attempting to authenticate to LDAP by binding as ' + ldap_bind_user) + if settings.BIND_AS_AUTHENTICATING_USER: + if settings.BIND_TEMPLATE: + # Full CN + ldap_bind_user = settings.BIND_TEMPLATE.format( + username=username, base_dn=settings.BASE_DN + ) + elif settings.USERNAME_PREFIX: + # Prepend a prefix: useful for DOMAIN\user + ldap_bind_user = settings.USERNAME_PREFIX + username + elif settings.USERNAME_SUFFIX: + # Append a suffix: useful for user@domain + ldap_bind_user = username + settings.USERNAME_SUFFIX + elif settings.BIND_DN and settings.BIND_PASSWORD: + ldap_bind_user = self.find_user_dn( + settings.BIND_DN, settings.BIND_PASSWORD, username=username + ) - try: - c = ldap3.Connection(self.backend, - read_only=True, - lazy=False, - auto_bind=True, - client_strategy=ldap3.SYNC, - authentication=ldap3.SIMPLE, - user=ldap_bind_user, - password=password) - except ldap3.core.exceptions.LDAPSocketOpenError as e: - logger.error('LDAP connection error: ' + str(e)) - return None - except ldap3.core.exceptions.LDAPBindError as e: - if 'invalidCredentials' in str(e): - # Invalid bind DN or password - return None - else: - logger.error('LDAP bind error: ' + str(e)) - return None - except Exception as e: - logger.exception('Caught exception when trying to connect and bind to LDAP') - raise + logger.debug( + 'Attempting to authenticate to LDAP by binding as ' + ldap_bind_user + ) + + conn = self._maybe_bind(ldap_bind_user, password) # Search for the user using their full DN search_filter = '({}={})'.format(settings.UID_ATTRIB, username) - attributes = self.search_ldap(c, search_filter, attributes=LDAPUser._attrib_keys, size_limit=1) + attributes = self.search_ldap(conn, search_filter, attributes=LDAPUser._attrib_keys, size_limit=1) if not attributes: logger.error('LDAP search error: no results for ' + search_filter) return None # Construct an LDAPUser instance for this user - return LDAPUser(c, attributes) + return LDAPUser(conn, attributes) def update_group_membership(self, ldap_user, django_user): """Update the user's group memberships @@ -287,3 +280,52 @@ def update_group_membership(self, ldap_user, django_user): getattr(django_user.groups, operation)(g) django_user.save() + + def find_user_dn(self, bind_user, bind_password, username=None): + conn = self._maybe_bind(bind_user, bind_password) + search_filter = settings.USER_DN_FILTER_TEMPLATE.format( + username=username + ) + conn.search(settings.BASE_DN, search_filter) + num_results = len(conn.entries) + if num_results == 0: + logger.error('User with username {}. not found. Filter used was "{}"'.format( + username, search_filter + )) + return None + elif num_results > 1: + logger.error('Error searching for username {}. {} matches found. Filter used was "{}"'.format( + username, len(num_results), search_filter + )) + return None + else: + match = conn.entries[0] + return match.entry_get_dn() + + def _maybe_bind(self, user, password): + try: + conn = ldap3.Connection( + self.backend, + read_only=True, + lazy=False, + auto_bind=True, + client_strategy=ldap3.SYNC, + authentication=ldap3.SIMPLE, + user=user, + password=password + ) + except ldap3.core.exceptions.LDAPSocketOpenError as e: + logger.error('LDAP connection error: ' + str(e)) + return None + except ldap3.core.exceptions.LDAPBindError as e: + if 'invalidCredentials' in str(e): + # Invalid bind DN or password + return None + else: + logger.error('LDAP bind error: ' + str(e)) + return None + except Exception as e: + logger.exception('Caught exception when trying to connect and bind to LDAP') + raise + + return conn diff --git a/django_auth_ldap3/conf.py b/django_auth_ldap3/conf.py index 62ff498..fe6d719 100644 --- a/django_auth_ldap3/conf.py +++ b/django_auth_ldap3/conf.py @@ -1,5 +1,6 @@ from django.conf import settings as django_settings + class LDAPSettings(object): """ Class that provides access to the LDAP settings specified in Django's @@ -13,7 +14,11 @@ class LDAPSettings(object): defaults = { 'ADMIN_GROUP': None, 'BASE_DN': 'dc=example,dc=com', + 'BIND_AS_AUTHENTICATING_USER': True, + 'BIND_DN': '', + 'BIND_PASSWORD': '', 'BIND_TEMPLATE': 'uid={username},{base_dn}', + 'USER_DN_FILTER_TEMPLATE': '(&(objectclass=person)(uid={username}))', 'GROUP_MAP': None, 'LOGIN_GROUP': '*', 'UID_ATTRIB': 'uid', From a63ce796c2d7e984f6e88eca4c37081ff639ab7f Mon Sep 17 00:00:00 2001 From: "Vitor M. A. da Cruz" Date: Tue, 16 Aug 2016 21:18:02 -0300 Subject: [PATCH 2/3] bugfix: Handling bind failures correctly --- django_auth_ldap3/backends.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/django_auth_ldap3/backends.py b/django_auth_ldap3/backends.py index 02f6096..676ffd9 100644 --- a/django_auth_ldap3/backends.py +++ b/django_auth_ldap3/backends.py @@ -240,6 +240,8 @@ def bind_ldap_user(self, username, password): ) conn = self._maybe_bind(ldap_bind_user, password) + if conn is None: + return None # Search for the user using their full DN search_filter = '({}={})'.format(settings.UID_ATTRIB, username) @@ -283,6 +285,9 @@ def update_group_membership(self, ldap_user, django_user): def find_user_dn(self, bind_user, bind_password, username=None): conn = self._maybe_bind(bind_user, bind_password) + if conn is None: + return None + search_filter = settings.USER_DN_FILTER_TEMPLATE.format( username=username ) From deeb0d934c2a80f5a9719a4b43a95a99d642eae0 Mon Sep 17 00:00:00 2001 From: "Vitor M. A. da Cruz" Date: Wed, 14 Dec 2016 12:56:43 -0200 Subject: [PATCH 3/3] Updating for `ldap3 == 2.1.1` Replacing references to `EntryBase.entry_get_dn()` with `EntryBase.entry_dn`, as this method does not exist anymore. Using a specific version of ldap3 from now on, as this api change caught us off guard. --- django_auth_ldap3/__init__.py | 2 +- django_auth_ldap3/backends.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/django_auth_ldap3/__init__.py b/django_auth_ldap3/__init__.py index fac5bf4..d0564e5 100644 --- a/django_auth_ldap3/__init__.py +++ b/django_auth_ldap3/__init__.py @@ -1 +1 @@ -__version__ = '0.9.6' +__version__ = '0.9.7' diff --git a/django_auth_ldap3/backends.py b/django_auth_ldap3/backends.py index 676ffd9..cde3667 100644 --- a/django_auth_ldap3/backends.py +++ b/django_auth_ldap3/backends.py @@ -305,7 +305,7 @@ def find_user_dn(self, bind_user, bind_password, username=None): return None else: match = conn.entries[0] - return match.entry_get_dn() + return match.entry_dn def _maybe_bind(self, user, password): try: diff --git a/setup.py b/setup.py index d0bff04..38e1057 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ url='https://github.com/sjkingo/django_auth_ldap3', install_requires=[ 'Django >= 1.6.10', - 'ldap3 >= 0.9.7.1', + 'ldap3 == 2.1.1', ], packages=find_packages(), classifiers=[