From f92f9bbe081128b98588c712b8320a8343ef269b Mon Sep 17 00:00:00 2001 From: Dominik George <dominik.george@teckids.org> Date: Sun, 7 Feb 2021 22:53:45 +0100 Subject: [PATCH] Set password of LDAP-logged-in user in database Having a local password is needed to make changing passwords easier. In order to catch password changes in a universal way and forward them to backends (like LDAP, in this case), getting the old password first is necessary to authenticate as that user to LDAP. We buy the small insecurity of having a hash of the password in the Django database in order to not require it to have global admin permissions on the LDAP directory. In addition, we fail early by raising PermissionDenied if LDAP cannot authenticate the user, so as to not allow logins with ghost users that were removed in LDAP or changed their password there. --- aleksis/core/settings.py | 9 ++++++++- aleksis/core/util/ldap.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 aleksis/core/util/ldap.py diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index c3705b3ca..142e2c837 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -234,7 +234,7 @@ if _settings.get("ldap.uri", None): ) # Enable Django's integration to LDAP - AUTHENTICATION_BACKENDS.append("django_auth_ldap.backend.LDAPBackend") + AUTHENTICATION_BACKENDS.append("aleksis.core.util.ldap.LDAPBackend") AUTH_LDAP_SERVER_URI = _settings.get("ldap.uri") @@ -243,6 +243,13 @@ if _settings.get("ldap.uri", None): AUTH_LDAP_BIND_DN = _settings.get("ldap.bind.dn") AUTH_LDAP_BIND_PASSWORD = _settings.get("ldap.bind.password") + # Keep local password for users to be required to proveide their old password on change + AUTH_LDAP_SET_USABLE_PASSWORD = True + + # Keep bound as the authenticating user + # Ensures proper read permissions, and ability to change password without admin + AUTH_LDAP_BIND_AS_AUTHENTICATING_USER = True + # The TOML config might contain either one table or an array of tables _AUTH_LDAP_USER_SETTINGS = _settings.get("ldap.users.search") if not isinstance(_AUTH_LDAP_USER_SETTINGS, list): diff --git a/aleksis/core/util/ldap.py b/aleksis/core/util/ldap.py new file mode 100644 index 000000000..5a17cbbb5 --- /dev/null +++ b/aleksis/core/util/ldap.py @@ -0,0 +1,34 @@ +"""Utilities and extensions for django_auth_ldap.""" + +from django.core.exceptions import PermissionDenied + +from django_auth_ldap.backend import LDAPBackend as _LDAPBackend + + +class LDAPBackend(_LDAPBackend): + default_settings = {"SET_USABLE_PASSWORD": False} + + def authenticate_ldap_user(self, ldap_user, password): + """Authenticate user against LDAP and set local password if successful. + + Having a local password is needed to make changing passwords easier. In + order to catch password changes in a universal way and forward them to + backends (like LDAP, in this case), getting the old password first is + necessary to authenticate as that user to LDAP. + + We buy the small insecurity of having a hash of the password in the + Django database in order to not require it to have global admin permissions + on the LDAP directory. + """ + user = ldap_user.authenticate(password) + + if not user: + # Fail early and do not try other backends + raise PermissionDenied("LDAP failed to authenticate user") + + if self.settings.SET_USABLE_PASSWORD: + # Set a usable password so users can change their LDAP password + user.set_password(password) + user.save() + + return user -- GitLab