Skip to content
Snippets Groups Projects
Verified Commit 534e3ec1 authored by Nik | Klampfradler's avatar Nik | Klampfradler
Browse files

Allow using local Django accounts and LDAP accounts at the same time

This fixes #470, where local Django accoutns were generally locked if
LDAP accoutns were used together with password handling to protect
against deleted/locked LDAP users being able to still login using a
shadow copy of their account in the Django database.

The fix introduces user account attributes, and the LDAP
authentication code keeps a record of users who used to authenticate
with LDAP in the past. If a suer is known to have been using LDAP in
the past, they are denied if they cannot be authenticated in the
future; if a user tries to authenticate who has not used LDAP in the
past, they are allowed in.
parent 452c17e6
No related branches found
No related tags found
No related merge requests found
# Generated by Django 3.2.10 on 2021-12-25 10:59
import aleksis.core.mixins
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('core', '0029_invitations'),
]
operations = [
migrations.CreateModel(
name='UserAdditionalAttributes',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('attributes', models.JSONField(default=dict, verbose_name='Additional attributes')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='additional_attributes', to=settings.AUTH_USER_MODEL, verbose_name='Linked user')),
],
bases=(models.Model, aleksis.core.mixins.PureDjangoModel),
),
]
# flake8: noqa: DJ01 # flake8: noqa: DJ01
import hmac import hmac
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from typing import Iterable, List, Optional, Sequence, Union from typing import Any, Iterable, List, Optional, Sequence, Union
from urllib.parse import urlparse from urllib.parse import urlparse
from django.conf import settings from django.conf import settings
...@@ -1131,6 +1131,46 @@ class TaskUserAssignment(ExtensibleModel): ...@@ -1131,6 +1131,46 @@ class TaskUserAssignment(ExtensibleModel):
verbose_name_plural = _("Task user assignments") verbose_name_plural = _("Task user assignments")
class UserAdditionalAttributes(models.Model, PureDjangoModel):
"""Additional attributes for Django user accounts.
These attributes are explicitly linked to a User, not to a Person.
"""
user = models.OneToOneField(
get_user_model(),
on_delete=models.CASCADE,
related_name="additional_attributes",
verbose_name=_("Linked user"),
)
attributes = models.JSONField(verbose_name=_("Additional attributes"), default=dict)
@classmethod
def get_user_attribute(
cls, username: str, attribute: str, default: Optional[Any] = None
) -> Any:
"""Get a user attribute for a user by name."""
try:
attributes = cls.objects.get(user__username=username)
except cls.DoesNotExist:
return default
return attributes.attributes.get(attribute, default)
@classmethod
def set_user_attribute(cls, username: str, attribute: str, value: Any):
"""Set a user attribute for a user by name.
Raises DoesNotExist if a username for a non-existing Django user is passed.
"""
user = get_user_model().objects.get(username=username)
attributes, __ = cls.objects.update_or_create(user=user)
attributes.attributes[attribute] = value
attributes.save()
class OAuthApplication(AbstractApplication): class OAuthApplication(AbstractApplication):
"""Modified OAuth application class that supports Grant Flows configured in preferences.""" """Modified OAuth application class that supports Grant Flows configured in preferences."""
......
"""Utilities and extensions for django_auth_ldap.""" """Utilities and extensions for django_auth_ldap."""
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django_auth_ldap.backend import LDAPBackend as _LDAPBackend from django_auth_ldap.backend import LDAPBackend as _LDAPBackend
from ..models import UserAdditionalAttributes
class LDAPBackend(_LDAPBackend): class LDAPBackend(_LDAPBackend):
default_settings = {"SET_USABLE_PASSWORD": False} default_settings = {"SET_USABLE_PASSWORD": False}
...@@ -24,11 +27,28 @@ class LDAPBackend(_LDAPBackend): ...@@ -24,11 +27,28 @@ class LDAPBackend(_LDAPBackend):
if self.settings.SET_USABLE_PASSWORD: if self.settings.SET_USABLE_PASSWORD:
if not user: if not user:
# Fail early and do not try other backends # The user could not be authenticated against LDAP.
raise PermissionDenied("LDAP failed to authenticate user") # We need to make sure to let other backends handle it, but also that
# we do not let actually deleted/locked LDAP users fall through to a
# backend that cached a valid password
if UserAdditionalAttributes.get_user_attribute(
ldap_user._username, "ldap_authenticated", False
):
# User was LDAP-authenticated in the past, so we fail authentication now
# to not let other backends override a legitimate deletion
raise PermissionDenied("LDAP failed to authenticate user")
else:
# No note about LDAP authentication in the past
# The user can continue authentication like before if they exist
return user
# Set a usable password so users can change their LDAP password # Set a usable password so users can change their LDAP password
user.set_password(password) user.set_password(password)
user.save() user.save()
# Not that we LDAP-autenticated the user so we can check this in the future
UserAdditionalAttributes.set_user_attribute(
ldap_user._username, "ldap_authenticated", True
)
return user return user
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment