From 5cac8f8275f294ca123947a7d372b163306dea87 Mon Sep 17 00:00:00 2001 From: Patrick Mayr Date: Sun, 26 Jan 2025 13:10:35 +0100 Subject: [PATCH] change ldap auth to ldap backend to be able to program without ldap --- fet2020/authentications/authentications.py | 103 ------------- fet2020/authentications/backends.py | 162 +++++++++++++++++++++ fet2020/authentications/forms.py | 43 +----- fet2020/authentications/views.py | 3 +- fet2020/fet2020/settings.py | 17 ++- fet2020/templates/admin/base.html | 2 +- 6 files changed, 179 insertions(+), 151 deletions(-) create mode 100644 fet2020/authentications/backends.py diff --git a/fet2020/authentications/authentications.py b/fet2020/authentications/authentications.py index cd09d144..959cdcce 100644 --- a/fet2020/authentications/authentications.py +++ b/fet2020/authentications/authentications.py @@ -11,109 +11,6 @@ host = "ldap://juri.fet.htu.tuwien.ac.at" port = 389 -def authentication(username, password): - # no empty passwords - if password is None or password.strip() == "": - return None - - server = Server(host, port=port) - userdn = f"uid={username},ou=user,dc=fet,dc=htu,dc=tuwien,dc=ac,dc=at" - - firstname = "" - surname = "" - mail = "" - - try: - c = Connection(server, user=userdn, password=password, auto_bind=True) - - if not c.extend.standard.who_am_i(): - logger.info("Username '%s' is not in the list.", username) - return None - - # get member infos from ldap - c.search( - search_base=userdn, - search_filter=f"(uid={username})", - search_scope="SUBTREE", - attributes=["givenName", "sn", "mail"], - ) - - firstname = c.response[0]["attributes"]["givenName"][0] - surname = c.response[0]["attributes"]["sn"][0] - mail = c.response[0]["attributes"]["mail"][0] - - except LDAPBindError as e: - logger.info("LDAP Bind error from username '%s'. Error: %s", username, e) - return None - - except Exception as e: - logger.info("Auth exception from username '%s'. Error: %s", username, e) - return None - - # get member or if not then create a new member - try: - member = Member.objects.get(mailaccount=mail) - - # set username if not equal - if member.username != username: - member.username = username - logger.info("User '%s' saved.", username) - member.save() - - except Member.DoesNotExist: - member = Member() - member.firstname = firstname - member.surname = surname - member.username = username - member.nickname = username - member.mailaccount = mail - logger.info("Member '%s' created.", username) - member.save() - - logger.info("User '%s' logged in.", username) - return username - - -def get_finance_perm(username, password): - # no empty passwords - if password is None or password.strip() == "": - return None - - server = Server(host, port=port) - userdn = f"uid={username},ou=user,dc=fet,dc=htu,dc=tuwien,dc=ac,dc=at" - - finance_perm = None - - try: - c = Connection(server, user=userdn, password=password, auto_bind=True) - - if not c.extend.standard.who_am_i(): - logger.info("Username '%s' is not in the list.", username) - return None - - # check if member has finance permission - c.search( - search_base="CN=finance,OU=Groups,dc=fet,dc=htu,dc=tuwien,dc=ac,dc=at", - search_filter="(objectClass=posixGroup)", - search_scope="SUBTREE", - attributes=["memberUid"], - ) - - if username in c.response[0]["attributes"]["memberUid"]: - logger.info("User '%s' has finance permission.", username) - finance_perm = True - - except LDAPBindError as e: - logger.info("LDAP Bind error from username '%s'. Error: %s", username, e) - return None - - except Exception as e: - logger.info("Auth exception from username '%s'. Error: %s", username, e) - return None - - return finance_perm - - def change_password(username, old_password, new_password): server = Server(host, port=port, use_ssl=True) userdn = f"uid={username},ou=user,dc=fet,dc=htu,dc=tuwien,dc=ac,dc=at" diff --git a/fet2020/authentications/backends.py b/fet2020/authentications/backends.py new file mode 100644 index 00000000..e7d14b3c --- /dev/null +++ b/fet2020/authentications/backends.py @@ -0,0 +1,162 @@ +import logging + +from django.contrib.auth.models import Group, User +from django.contrib.auth.backends import ModelBackend + +from ldap3 import Connection, Server +from ldap3.core.exceptions import LDAPBindError + +from members.models import Member + +logger = logging.getLogger(__name__) +host = "ldap://juri.fet.htu.tuwien.ac.at" +port = 389 + + +class LdapBackend(ModelBackend): + def _check_ldap_user(self, username: str, password: str) -> bool: + server = Server(host, port=port) + userdn = f"uid={username},ou=user,dc=fet,dc=htu,dc=tuwien,dc=ac,dc=at" + + try: + c = Connection(server, user=userdn, password=password, auto_bind=True) + + if not c.extend.standard.who_am_i(): + logger.info("Username '%s' is not in the list.", username) + return False + except LDAPBindError as e: + logger.info("LDAP Bind error from username '%s'. Error: %s", username, e) + return False + except Exception as e: + logger.info("Auth exception from username '%s'. Error: %s", username, e) + return False + + return True + + def _check_fet_member(self, username: str, password: str) -> bool: + server = Server(host, port=port) + userdn = f"uid={username},ou=user,dc=fet,dc=htu,dc=tuwien,dc=ac,dc=at" + + try: + c = Connection(server, user=userdn, password=password, auto_bind=True) + + # Get member infos from ldap + c.search( + search_base=userdn, + search_filter=f"(uid={username})", + search_scope="SUBTREE", + attributes=["givenName", "sn", "mail"], + ) + + firstname = c.response[0]["attributes"]["givenName"][0] + surname = c.response[0]["attributes"]["sn"][0] + mail = c.response[0]["attributes"]["mail"][0] + except LDAPBindError as e: + logger.info("LDAP Bind error from username '%s'. Error: %s", username, e) + return False + except Exception as e: + logger.info("Auth exception from username '%s'. Error: %s", username, e) + return False + + # Try to get the member. If it not exists, then create a new member. + try: + member = Member.objects.get(mailaccount=mail) + + # Set username if not equal + if member.username != username: + member.username = username + logger.info("Member '%s' saved.", username) + member.save() + except Member.DoesNotExist: + member = Member() + member.firstname = firstname + member.surname = surname + member.username = username + member.nickname = username + member.mailaccount = mail + logger.info("Member '%s' created.", username) + member.save() + + return True + + def _check_finance_perm(self, username: str, password: str) -> bool: + server = Server(host, port=port) + userdn = f"uid={username},ou=user,dc=fet,dc=htu,dc=tuwien,dc=ac,dc=at" + + try: + c = Connection(server, user=userdn, password=password, auto_bind=True) + + # Check if member has finance permission + c.search( + search_base="CN=finance,OU=Groups,dc=fet,dc=htu,dc=tuwien,dc=ac,dc=at", + search_filter="(objectClass=posixGroup)", + search_scope="SUBTREE", + attributes=["memberUid"], + ) + + if username in c.response[0]["attributes"]["memberUid"]: + logger.info("User '%s' has finance permission.", username) + return True + + except LDAPBindError as e: + logger.info("LDAP Bind error from username '%s'. Error: %s", username, e) + return False + + except Exception as e: + logger.info("Auth exception from username '%s'. Error: %s", username, e) + return False + + return False + + def authenticate(self, request, username=None, password=None): + if username is None or password is None: + return + + # Set username to lower because fet2020 can only work with lowercase letters. + username = username.lower() + + if not self._check_ldap_user(username, password): + return + + if not self._check_fet_member(username, password): + return + + try: + user = User.objects.get(username) + except User.DoesNotExist: + user = User.objects.create_user(username) + finally: + if not self.user_can_authenticate(user): + logger.info("User '%s' is inactive.", user.get_username()) + return + + # Add user to all groups + for elem in Group.objects.all(): + elem.user_set.add(user) + + # Delete finance group if no permission + if not self._check_finance_perm(username, password): + finance_group = Group.objects.get(name="finance") + finance_group.user_set.remove(user) + + return user + + +class DebugBackend(ModelBackend): + def authenticate(self, request, username, password, **kwargs): + if (user := super().authenticate(request, username, password, **kwargs)) is None: + logger.info("User '%s' can't login. The user may not be a superuser.", username) + return None + + # Try to get the member. If it not exists, then create a new member. + try: + member = Member.objects.get(mailaccount=user.email) + except Member.DoesNotExist: + member = Member() + member.username = username + member.nickname = username + member.mailaccount = user.email + logger.info("Member '%s' created.", username) + member.save() + + return user diff --git a/fet2020/authentications/forms.py b/fet2020/authentications/forms.py index 718d716e..a2d43bd8 100644 --- a/fet2020/authentications/forms.py +++ b/fet2020/authentications/forms.py @@ -1,54 +1,17 @@ import logging -from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm -from django.contrib.auth.models import Group, User +from django.contrib.auth.forms import PasswordChangeForm from django.core.exceptions import ValidationError -from .authentications import authentication, change_password, get_finance_perm +from .authentications import change_password logger = logging.getLogger(__name__) -class LoginForm(AuthenticationForm): - def clean(self): - username = self.cleaned_data.get("username").lower() - password = self.cleaned_data.get("password") - - if username is not None and password: - if (auth_user := authentication(username, password)) is None: - raise ValidationError( - "Bitte Benutzername und Passwort korrekt eingeben.", - code="invalid_login", - ) - - try: - self.user_cache = User.objects.get(username=auth_user.lower()) - except User.DoesNotExist: - self.user_cache = User.objects.create_user(auth_user.lower()) - finally: - self.confirm_login_allowed(self.user_cache) - - # add user to all groups - for elem in Group.objects.all(): - elem.user_set.add(self.user_cache) - - # delete finance group if no permission - if not get_finance_perm(username, password): - finance_group = Group.objects.get(name="finance") - finance_group.user_set.remove(self.user_cache) - - return self.cleaned_data - - +# TODO: fix me class LdapPasswordChangeForm(PasswordChangeForm): def clean_old_password(self): old_password = self.cleaned_data.get("old_password") - if not authentication(self.user, old_password): - raise ValidationError( - self.error_messages["password_incorrect"], - code="password_incorrect", - ) - return old_password def save(self): diff --git a/fet2020/authentications/views.py b/fet2020/authentications/views.py index 998921ab..e83e2338 100644 --- a/fet2020/authentications/views.py +++ b/fet2020/authentications/views.py @@ -10,11 +10,10 @@ from django.urls import reverse_lazy from documents.etherpadlib import del_ep_cookie from .decorators import authenticated_user -from .forms import LdapPasswordChangeForm, LoginForm +from .forms import LdapPasswordChangeForm class AuthLoginView(LoginView): - authentication_form = LoginForm redirect_authenticated_user = True template_name = "authentications/login.html" diff --git a/fet2020/fet2020/settings.py b/fet2020/fet2020/settings.py index f0ebc4d4..c82517b5 100644 --- a/fet2020/fet2020/settings.py +++ b/fet2020/fet2020/settings.py @@ -5,7 +5,8 @@ import environ env = environ.Env( # set casting, default value - DEBUG=(bool, True), + DEBUG=(str, "True"), + LDAP=(str, "False"), MYSQL_HOST=(str, "mysql"), MYSQL_PORT=(int, 3306), MYSQL_DATABASE=(str, "fet2020db"), @@ -23,7 +24,8 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # DEBUGGING -DEBUG = env("DEBUG") +DEBUG = (env("DEBUG").lower() == "true") +LDAP = (env("LDAP").lower() == "true") # MODELS @@ -63,9 +65,14 @@ INSTALLED_APPS = [ # AUTHENTICATIONS -AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", -] +if not DEBUG and LDAP: + AUTHENTICATION_BACKENDS = [ + "authentications.backends.LdapBackend", + ] +else: + AUTHENTICATION_BACKENDS = [ + "authentications.backends.DebugBackend", + ] LOGIN_REDIRECT_URL = "home" LOGIN_URL = "/auth/login" diff --git a/fet2020/templates/admin/base.html b/fet2020/templates/admin/base.html index d7df2347..6262be3c 100644 --- a/fet2020/templates/admin/base.html +++ b/fet2020/templates/admin/base.html @@ -17,7 +17,7 @@ {% if site_url %} Zurück zur FET Homepage {% endif %} - Passwort ändern + {% translate 'Log out' %} {% endblock %}