feat(accounts): Allow divergence between cas_login and username
- Adds a Translation table between cas_login and the effective username - Show more descriptive errors when the connection cannot happen TODO: The translation mechanism is currently fragile, we need to update usernames when a translation is created/deleted and also disable updates to a translation
This commit is contained in:
parent
5381b0379b
commit
9119ad38b0
5 changed files with 134 additions and 26 deletions
|
@ -2,7 +2,7 @@ from django.contrib import admin
|
|||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from unfold.admin import ModelAdmin
|
||||
|
||||
from dgsi.models import Bylaws, Service, Statutes, User
|
||||
from dgsi.models import Bylaws, Service, Statutes, Translation, User
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
|
@ -10,6 +10,6 @@ class UserAdmin(BaseUserAdmin, ModelAdmin):
|
|||
pass
|
||||
|
||||
|
||||
@admin.register(Bylaws, Service, Statutes)
|
||||
@admin.register(Bylaws, Service, Statutes, Translation)
|
||||
class AdminClass(ModelAdmin):
|
||||
compressed_fields = True
|
||||
|
|
32
src/dgsi/migrations/0006_translation.py
Normal file
32
src/dgsi/migrations/0006_translation.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 4.2.12 on 2024-09-26 09:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("dgsi", "0005_alter_bylaws_options_alter_statutes_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Translation",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("cas_login", models.CharField(max_length=255, unique=True)),
|
||||
("username", models.CharField(max_length=255, unique=True)),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Correspondance de login",
|
||||
"verbose_name_plural": "Correspondances de login",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -69,6 +69,18 @@ class Bylaws(LegalDocument):
|
|||
verbose_name_plural = _("Règlements Intérieurs")
|
||||
|
||||
|
||||
class Translation(models.Model):
|
||||
cas_login = models.CharField(max_length=255, unique=True)
|
||||
username = models.CharField(max_length=255, unique=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.cas_login} → {self.username}"
|
||||
|
||||
class Meta: # pyright: ignore
|
||||
verbose_name = _("Correspondance de login")
|
||||
verbose_name_plural = _("Correspondances de login")
|
||||
|
||||
|
||||
@dataclass
|
||||
class KanidmProfile:
|
||||
person: Person
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
from functools import lru_cache
|
||||
from typing import Optional
|
||||
|
||||
from allauth.core.exceptions import ImmediateHttpResponse
|
||||
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
|
||||
from allauth.socialaccount.models import SocialLogin
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.contrib import messages
|
||||
from django.http import HttpRequest, HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dgsi.models import User
|
||||
from dgsi.models import Translation, User
|
||||
|
||||
|
||||
class SharedAccountAdapter(DefaultSocialAccountAdapter):
|
||||
|
@ -12,42 +17,101 @@ class SharedAccountAdapter(DefaultSocialAccountAdapter):
|
|||
Overrides the Account Adapter, to allow a simpler connection via CAS.
|
||||
"""
|
||||
|
||||
def pre_social_login(self, request, sociallogin):
|
||||
match sociallogin.account.provider:
|
||||
# TODO: Add a correspondance table between ENS logins and ours
|
||||
case "ens_cas":
|
||||
# In this case, the username is located in extra_data["uid"]
|
||||
username = sociallogin.account.extra_data["uid"]
|
||||
@lru_cache
|
||||
def _get_username(self, request: HttpRequest, sociallogin: SocialLogin) -> str:
|
||||
"""
|
||||
Returns the required username
|
||||
"""
|
||||
|
||||
# Validate that the user is a regular one
|
||||
match sociallogin.account.provider:
|
||||
case "ens_cas":
|
||||
cas_login = sociallogin.account.extra_data["uid"]
|
||||
|
||||
# Verify that this user can indeed connect to the website
|
||||
home = sociallogin.account.extra_data["homeDirectory"].split("/")
|
||||
|
||||
if (home[1] != "users") or (
|
||||
home[2]
|
||||
in ["absint", "algo", "grecc", "guests", "spi", "spi1", "staffs"]
|
||||
):
|
||||
messages.error(request, _("Catégorie de compte ENS interdite."))
|
||||
raise ImmediateHttpResponse(
|
||||
HttpResponseRedirect(reverse("dgsi:dgn-forbidden_account"))
|
||||
)
|
||||
|
||||
# Continue with the login flow
|
||||
try:
|
||||
return Translation.objects.get(cas_login=cas_login).username
|
||||
except Translation.DoesNotExist:
|
||||
return cas_login
|
||||
|
||||
case "kanidm":
|
||||
username = sociallogin.account.extra_data["preferred_username"]
|
||||
case _p:
|
||||
raise KeyError(f"No sociallogin '{_p}' is supposed to exist.")
|
||||
return sociallogin.account.extra_data["preferred_username"]
|
||||
|
||||
try:
|
||||
# Connect an existing user if the login already exists, even if it
|
||||
# with another social method
|
||||
user = User.objects.get(username=username)
|
||||
sociallogin.connect(request, user)
|
||||
except User.DoesNotExist:
|
||||
return
|
||||
case _:
|
||||
# INFO: This should never happen
|
||||
messages.error(request, _("Méthode de connexion invalide."))
|
||||
raise ImmediateHttpResponse(
|
||||
HttpResponseRedirect(reverse("dgsi:dgn-forbidden_account"))
|
||||
)
|
||||
|
||||
# We now know that a user exists, copy the distant attributes
|
||||
user.is_staff = user.is_admin
|
||||
user.is_superuser = user.is_admin
|
||||
def _get_user(
|
||||
self, request: HttpRequest, sociallogin: SocialLogin
|
||||
) -> Optional[User]:
|
||||
"""
|
||||
Returns the required user for completing the login
|
||||
"""
|
||||
|
||||
user.save()
|
||||
# The user is already linked to the social login, no reason to change it
|
||||
if sociallogin.is_existing:
|
||||
return sociallogin.user
|
||||
|
||||
# No user is currently linked to this social login, either the user has already
|
||||
# logged in with another method, or it truly does not exist
|
||||
return User.objects.filter(
|
||||
username=self._get_username(request, sociallogin)
|
||||
).first()
|
||||
|
||||
def _update_user(self, request: HttpRequest, sociallogin: SocialLogin):
|
||||
"""
|
||||
Updates the required attributes of the user:
|
||||
- username
|
||||
- permissions
|
||||
"""
|
||||
|
||||
u = sociallogin.user
|
||||
|
||||
assert isinstance(u, User)
|
||||
|
||||
# Update the username first, so that calls to kanidm return the correct information
|
||||
u.username = self._get_username(request, sociallogin)
|
||||
|
||||
# Update the global permissions
|
||||
u.is_staff = u.is_admin
|
||||
u.is_superuser = u.is_admin
|
||||
|
||||
# Save the updated user if needed
|
||||
if sociallogin.is_existing:
|
||||
u.save()
|
||||
|
||||
def pre_social_login(self, request, sociallogin: SocialLogin):
|
||||
###
|
||||
# The flow is the following:
|
||||
# - Get the correct user
|
||||
# - Do the connection if possible
|
||||
# - Update the required attributes
|
||||
|
||||
user = self._get_user(request, sociallogin)
|
||||
|
||||
if user is not None:
|
||||
sociallogin.user = user
|
||||
|
||||
# If the user exists, connect to it
|
||||
# FIXME: May not be necessary
|
||||
# if sociallogin.is_existing:
|
||||
# sociallogin.connect(request, sociallogin.user)
|
||||
|
||||
self._update_user(request, sociallogin)
|
||||
|
||||
def populate_user(self, request, sociallogin, data):
|
||||
return super().populate_user(request, sociallogin, data)
|
||||
|
|
|
@ -7,6 +7,6 @@
|
|||
<hr>
|
||||
|
||||
<div class="notification is-warning is-light px-5 py-5 has-text-centered is-size-5">
|
||||
{% blocktrans %}Votre catégorie de compte ENS ne permet pas de vous identifier auprès de la DGNum.<br>Si vous pensez qu'il s'agit une erreur, merci de contacter la DGNum à l'adresse : <a href="mailto:contact@dgnum.eu">contact@dgnum.eu</a>{% endblocktrans %}
|
||||
{% blocktrans %}Vos informations ne permettent pas de vous identifier auprès de la DGNum.<br>Si vous pensez qu'il s'agit une erreur, merci de nous contacter à l'adresse : <a href="mailto:contact@dgnum.eu">contact@dgnum.eu</a>{% endblocktrans %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
|
Loading…
Reference in a new issue