From 77e3257d3d53eb0a000fabacadbd82471da9f2a9 Mon Sep 17 00:00:00 2001 From: Davide Date: Fri, 4 Aug 2023 12:35:38 +0200 Subject: [PATCH] Allow to reload DN when using user-bind and DN-template Useful when interacting with Microsoft AD, which may rely on User Principal Name instead of the Distinguished Name as the attribute to identify a user. --- django_auth_ldap/backend.py | 49 ++++++++++++++++++++++--------------- django_auth_ldap/config.py | 1 + docs/reference.rst | 16 ++++++++++++ tests/tests.ldif | 18 ++++++++++++++ tests/tests.py | 28 +++++++++++++++++++++ 5 files changed, 92 insertions(+), 20 deletions(-) diff --git a/django_auth_ldap/backend.py b/django_auth_ldap/backend.py index 529ec6dc..fc37ef23 100644 --- a/django_auth_ldap/backend.py +++ b/django_auth_ldap/backend.py @@ -484,6 +484,12 @@ def _authenticate_user_dn(self, password): self._bind_as(self.dn, password, sticky=sticky) except ldap.INVALID_CREDENTIALS: raise self.AuthenticationFailed("user DN/password rejected by LDAP server.") + if ( + self._using_simple_bind_mode() + and sticky + and self.settings.REFRESH_DN_ON_BIND + ): + self._user_dn = self._search_for_user_dn() def _load_user_attrs(self): if self.dn is not None: @@ -507,15 +513,7 @@ def _load_user_dn(self): if self._using_simple_bind_mode(): self._user_dn = self._construct_simple_user_dn() else: - if self.settings.CACHE_TIMEOUT > 0: - cache_key = valid_cache_key( - "django_auth_ldap.user_dn.{}".format(self._username) - ) - self._user_dn = cache.get_or_set( - cache_key, self._search_for_user_dn, self.settings.CACHE_TIMEOUT - ) - else: - self._user_dn = self._search_for_user_dn() + self._user_dn = self._search_for_user_dn() def _using_simple_bind_mode(self): return self.settings.USER_DN_TEMPLATE is not None @@ -530,19 +528,30 @@ def _search_for_user_dn(self): Searches the directory for a user matching AUTH_LDAP_USER_SEARCH. Populates self._user_dn and self._user_attrs. """ - search = self.settings.USER_SEARCH - if search is None: - raise ImproperlyConfigured( - "AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance." - ) - results = search.execute(self.connection, {"user": self._username}) - if results is not None and len(results) == 1: - (user_dn, self._user_attrs) = next(iter(results)) - else: - user_dn = None + def _search_for_user(): + search = self.settings.USER_SEARCH + if search is None: + raise ImproperlyConfigured( + "AUTH_LDAP_USER_SEARCH must be an LDAPSearch instance." + ) + + results = search.execute(self.connection, {"user": self._username}) + if results is not None and len(results) == 1: + (user_dn, self._user_attrs) = next(iter(results)) + else: + user_dn = None + + return user_dn - return user_dn + if self.settings.CACHE_TIMEOUT > 0: + cache_key = valid_cache_key( + "django_auth_ldap.user_dn.{}".format(self._username) + ) + return cache.get_or_set( + cache_key, _search_for_user, self.settings.CACHE_TIMEOUT + ) + return _search_for_user() def _check_requirements(self): """ diff --git a/django_auth_ldap/config.py b/django_auth_ldap/config.py index 13207512..6dcdefa8 100644 --- a/django_auth_ldap/config.py +++ b/django_auth_ldap/config.py @@ -55,6 +55,7 @@ class LDAPSettings: "ALWAYS_UPDATE_USER": True, "AUTHORIZE_ALL_USERS": False, "BIND_AS_AUTHENTICATING_USER": False, + "REFRESH_DN_ON_BIND": False, "BIND_DN": "", "BIND_PASSWORD": "", "CONNECTION_OPTIONS": {}, diff --git a/docs/reference.rst b/docs/reference.rst index 1727f1fc..a989a152 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -44,6 +44,22 @@ requests where the user is authenticated. Thus, the downside to this setting is that LDAP results may vary based on whether the user was authenticated earlier in the Django view, which could be surprising to code not directly concerned with authentication. +Remember to set :setting:`AUTH_LDAP_USER_DN_TEMPLATE` to avoid initial connection +to LDAP with default bind credentials. + + +.. setting:: AUTH_LDAP_REFRESH_DN_ON_BIND + +AUTH_LDAP_REFRESH_DN_ON_BIND +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Default: ``False`` + +If ``True`` and :setting:`AUTH_LDAP_BIND_AS_AUTHENTICATING_USER` is ``True`` and +:setting:`AUTH_LDAP_USER_DN_TEMPLATE` is set, after performing bind login it refresh +the DN attribute of the user. This is meant for such cases in which users authenticates +via `userPrincipalName` and in which `distinguishedName` is not inferrable by that +attribute. .. setting:: AUTH_LDAP_BIND_DN diff --git a/tests/tests.ldif b/tests/tests.ldif index 1902373e..abe3772d 100644 --- a/tests/tests.ldif +++ b/tests/tests.ldif @@ -74,6 +74,24 @@ gidNumber: 50 sn: nobody homeDirectory: /home/nobody +dn: cn=charlie_cooper,ou=people,o=test +userPrincipalName: charlie@people.test +objectClass: person +objectClass: user +objectClass: organizationalPerson +objectClass: posixAccount +cn: charlie_cooper +uid: charlie +userPassword: password +uidNumber: 1004 +gidNumber: 50 +givenName: Charlie +sn: Cooper +instanceType: test +nTSecurityDescriptor: test +objectCategory: test +homeDirectory: /home/bob + dn: uid=nonposix,ou=people,o=test objectClass: person objectClass: organizationalPerson diff --git a/tests/tests.py b/tests/tests.py index e5c1bb6f..326bcabc 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -136,6 +136,7 @@ def setUpClass(cls): "cosine.ldif", "inetorgperson.ldif", "nis.ldif", + "msuser.ldif", ] cls.server.start() with open(os.path.join(here, "tests.ldif")) as fp: @@ -653,6 +654,33 @@ def test_bind_as_user(self): self.assertEqual(user.first_name, "Alice") self.assertEqual(user.last_name, "Adams") + def test_bind_as_user_with_dn_refetch(self): + self._init_settings( + USER_DN_TEMPLATE="%(user)s@people.test", + USER_SEARCH=LDAPSearch( + "ou=people,o=test", ldap.SCOPE_SUBTREE, "(uid=%(user)s)" + ), + USER_ATTR_MAP={"first_name": "givenName", "last_name": "sn"}, + BIND_AS_AUTHENTICATING_USER=True, + REFRESH_DN_ON_BIND=True, + ) + + # need override to mimic Microsoft AD bind + # since openldap does not accepts UPN for login + def _bind_as(_self, bind_dn, bind_password, sticky=False): + _self._get_connection().simple_bind_s( + "cn=charlie_cooper,ou=people,o=test", bind_password + ) + _self._connection_bound = sticky + + with mock.patch("django_auth_ldap.backend._LDAPUser._bind_as", _bind_as): + user = authenticate(username="charlie", password="password") + + self.assertEqual(user.username, "charlie") + self.assertEqual(user.first_name, "Charlie") + self.assertEqual(user.last_name, "Cooper") + self.assertEqual(user.ldap_user.dn, "cn=charlie_cooper,ou=people,o=test") + def test_signal_populate_user(self): self._init_settings(USER_DN_TEMPLATE="uid=%(user)s,ou=people,o=test") with catch_signal(populate_user) as handler: pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy