On branche authens

This commit is contained in:
Tom Hubrecht 2021-01-26 14:26:35 +01:00
parent c70fcefa86
commit 6a59163dea
12 changed files with 280 additions and 66 deletions

View file

@ -116,5 +116,8 @@ class User(AbstractUser):
def connection_method(self):
if self.election is None:
if self.username.split("__")[0] == "pwd":
return _("mot de passe")
return _("CAS")
return _("identifiants spécifiques")

View file

@ -12,6 +12,8 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
import os
from django.urls import reverse_lazy
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@ -35,6 +37,7 @@ INSTALLED_APPS = [
"django.contrib.messages",
"kadenios.apps.IgnoreSrcStaticFilesConfig",
"elections",
"authens",
]
MIDDLEWARE = [
@ -68,7 +71,7 @@ TEMPLATES = [
WSGI_APPLICATION = "kadenios.wsgi.application"
# Password validation
# Password validation and authentication
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
@ -88,12 +91,15 @@ AUTH_PASSWORD_VALIDATORS = [
AUTH_USER_MODEL = "elections.User"
AUTHENTICATION_BACKENDS = [
"shared.auth.backends.ENSCASBackend",
"shared.auth.backends.PwdBackend",
"shared.auth.backends.CASBackend",
"shared.auth.backends.ElectionBackend",
]
LOGIN_URL = "auth.cas"
LOGIN_REDIRECT_URL = "kadenios"
LOGIN_URL = reverse_lazy("authens:login")
LOGIN_REDIRECT_URL = "/"
AUTHENS_USE_OLDCAS = False # On n'utilise que le CAS normal pour l'instant
# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/

View file

@ -9,6 +9,7 @@ urlpatterns = [
path("admin/", admin.site.urls),
path("elections/", include("elections.urls")),
path("auth/", include("shared.auth.urls")),
path("authens/", include("authens.urls")),
]
if "debug_toolbar" in settings.INSTALLED_APPS:

View file

@ -1,62 +1,37 @@
from authens.backends import ENSCASBackend
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from .utils import get_cas_client
UserModel = get_user_model()
class ENSCASBackend:
"""ENS CAS authentication backend.
Implement standard CAS v3 authentication
"""
def authenticate(self, request, ticket=None):
cas_client = get_cas_client(request)
cas_login, attributes, _ = cas_client.verify_ticket(ticket)
if cas_login is None:
# Authentication failed
return None
cas_login = self.clean_cas_login(cas_login)
if request:
request.session["CASCONNECTED"] = True
return self._get_or_create(cas_login, attributes)
class CASBackend(ENSCASBackend):
"""ENS CAS authentication backend, customized to get the full name at connection."""
def clean_cas_login(self, cas_login):
return cas_login.strip().lower()
def _get_or_create(self, cas_login, attributes):
"""Handles account retrieval and creation for CAS authentication.
- If no CAS account exists, create one;
- If a matching CAS account exists, retrieve it.
"""
return f"cas__{cas_login.strip().lower()}"
def create_user(self, username, attributes):
email = attributes.get("email")
name = attributes.get("name")
try:
user = UserModel.objects.get(username=cas_login)
except UserModel.DoesNotExist:
user = None
return UserModel.objects.create_user(
username=username, email=email, full_name=name
)
if user is None:
user = UserModel.objects.create_user(
username=cas_login, email=email, full_name=name
)
return user
# Django boilerplate.
def get_user(self, user_id):
try:
return UserModel.objects.get(pk=user_id)
except UserModel.DoesNotExist:
class PwdBackend(ModelBackend):
"""Password authentication"""
def authenticate(self, request, username=None, password=None):
if username is None or password is None:
return None
return super().authenticate(
request, username=f"pwd__{username}", password=password
)
class ElectionBackend(ModelBackend):
"""Authentication for a specific election.
@ -70,17 +45,12 @@ class ElectionBackend(ModelBackend):
return None
try:
user = UserModel.objects.get(username=f"{election_id}__{login}")
user = UserModel.objects.get(
username=f"{election_id}__{login}", election=election_id
)
except UserModel.DoesNotExist:
return None
if user.check_password(password):
return user
return None
# Django boilerplate.
def get_user(self, user_id):
try:
return UserModel.objects.get(pk=user_id)
except UserModel.DoesNotExist:
return None

View file

@ -1,8 +1,11 @@
from django import forms
from django.contrib.auth import authenticate
from django.contrib.auth import forms as auth_forms
from django.contrib.auth import get_user_model
from django.utils.translation import gettext_lazy as _
UserModel = get_user_model()
class ElectionAuthForm(forms.Form):
"""Adapts Django's AuthenticationForm to allow for an election specific login."""
@ -50,3 +53,11 @@ class ElectionAuthForm(forms.Form):
),
code="invalid_login",
)
class PwdResetForm(auth_forms.PasswordResetForm):
"""Restricts the search for password users, i.e. whose username starts with pwd__."""
def get_users(self, email):
users = super().get_users(email)
return (u for u in users if u.username.split("__")[0] == "pwd")

View file

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% load i18n %}
{% block auth %}{% endblock %}
{% block content %}
<h1 class="title">{% trans "Choisissez la méthode de connexion" %}</h1>
<hr>
{# Indications de connexion #}
{% comment %}
{% if method %}
<div class="message is-warning">
<div class="message-body">
{% if method == "PWD" %}
{% trans "Pour voter lors de cette élection, vous devez vous connecter à l'aide des identifiants reçus par mail. Choisissez la connexion par mot de passe." %}
{% elif method == "CAS" %}
{% trans "Pour voter lors de cette élection, vous devez vous connecter à l'aide du CAS élève." %}
{% endif %}
</div>
</div>
{% endif %}
{% endcomment %}
<div class="tile is-ancestor">
<div class="tile is-parent">
<a class="tile is-child notification is-primary" href="{% url "authens:login.cas" %}?next={{ next }}">
<div class="subtitle has-text-centered mb-2">
<span class="icon has-text-white">
<i class="fas fa-school"></i>
</span>
<span class="ml-3">{% trans "Connexion via CAS" %}</span>
</div>
</a>
</div>
<div class="tile is-parent">
<a class="tile is-child notification" href="{% url "authens:login.pwd" %}?next={{ next }}">
<div class="subtitle has-text-centered mb-2">
<span class="icon">
<i class="fas fa-key"></i>
</span>
<span class="ml-3">{% trans "Connexion par mot de passe" %}</span>
</div>
</a>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,56 @@
{% extends "base.html" %}
{% load i18n %}
{% block auth %}{% endblock %}
{% block content %}
<h1 class="title">{% trans "Connexion par mot de passe" %}</h1>
<hr>
<div class="columns is-centered">
<div class="column is-two-thirds">
<form action="" method="post">
{% csrf_token %}
{% include "forms/form.html" with errors=True %}
<div class="field is-grouped is-centered">
<div class="control is-expanded">
<button class="button is-fullwidth is-outlined is-primary is-light">
<span class="icon is-small">
<i class="fas fa-check"></i>
</span>
<span>{% trans "Enregistrer" %}</span>
</button>
</div>
<div class="control">
<a class="button is-primary" href="{% url 'authens:login' %}?next={{ next }}">
<span class="icon is-small">
<i class="fas fa-undo-alt"></i>
</span>
<span>{% trans "Retour" %}</span>
</a>
</div>
</div>
<div class="field is-centered">
<div class="control">
<div class="help has-text-centered">
<span>{% trans "Mot de passe oublié :" %}</span>
<a class="tag has-text-primary" href="{% url 'authens:reset.pwd' %}">
<span>{% trans "Réinitialiser mon mot de passe." %}</span>
<span class="icon is-small">
<i class="fas fa-lock-open"></i>
</span>
</a>
</div>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,42 @@
{% extends "base.html" %}
{% load i18n %}
{% block auth %}{% endblock %}
{% block content %}
<h1 class="title">{% trans "Réinitialisation du mot de passe" %}</h1>
<hr>
<div class="columns is-centered">
<div class="column is-two-thirds">
<form action="" method="post">
{% csrf_token %}
{% include "forms/form.html" with errors=True %}
<div class="field is-grouped is-centered">
<div class="control is-expanded">
<button class="button is-fullwidth is-outlined is-primary is-light">
<span class="icon is-small">
<i class="fas fa-check"></i>
</span>
<span>{% trans "Envoyer un mail" %}</span>
</button>
</div>
<div class="control">
<a class="button is-primary" href="{% url 'authens:login.pwd' %}?next={{ next }}">
<span class="icon is-small">
<i class="fas fa-undo-alt"></i>
</span>
<span>{% trans "Retour" %}</span>
</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,33 @@
{% extends "base.html" %}
{% load i18n %}
{% block auth %}{% endblock %}
{% block content %}
<h1 class="title">{% trans "Réinitialisation du mot de passe" %}</h1>
<hr>
<div class="columns is-centered">
<div class="column is-two-thirds">
<form action="" method="post">
{% csrf_token %}
{% include "forms/form.html" with errors=True %}
<div class="field is-grouped is-centered">
<div class="control is-expanded">
<button class="button is-fullwidth is-outlined is-primary is-light">
<span class="icon is-small">
<i class="fas fa-check"></i>
</span>
<span>{% trans "Enregistrer" %}</span>
</button>
</div>
</div>
</form>
</div>
</div>
{% endblock %}

View file

@ -55,22 +55,19 @@
</div>
<div class="level-item ml-3">
<a class="icon is-size-1 has-text-white" href="{% url 'auth.logout' %}?next={% if view.get_next_url %}{{ view.get_next_url }}{% else %}/{% endif %}">
<a class="icon is-size-1 has-text-white" href="{% url 'authens:logout' %}?next={% if view.get_next_url %}{{ view.get_next_url }}{% else %}/{% endif %}">
<i class="fas fa-sign-out-alt"></i>
</a>
</div>
{% else %}
<div class="level-item">
{% if election %}
<a class="icon is-size-1 has-text-white" href="{% url 'auth.select' %}?next={{ request.path }}&election_id={{ election.id }}&method={{ election.preferred_method }}">
<i class="fas fa-sign-in-alt"></i>
<a class="tag has-text-primary is-size-5" href="{% url 'authens:login' %}?next={{ request.path }}">
<span>{% trans "Se connecter" %}</span>
<span class="icon">
<i class="fas fa-sign-in-alt"></i>
</span>
</a>
{% else %}
<a class="icon is-size-1 has-text-white" href="{% url 'auth.cas' %}?next={{ request.path }}">
<i class="fas fa-sign-in-alt"></i>
</a>
{% endif %}
</div>
{% endif %}
</div>

View file

@ -12,8 +12,8 @@
{% endfor %}
{% if field.help_text %}
<p class="help">
<div class="help">
{{ field.help_text|safe }}
</p>
</div>
{% endif %}
</div>

View file

@ -0,0 +1,42 @@
{% extends "base.html" %}
{% load i18n %}
{% block auth %}{% endblock %}
{% block content %}
<h1 class="title">{% trans "Déconnexion réussie" %}</h1>
<hr>
<div class="columns is-centered">
<div class="column is-two-thirds">
<form action="" method="post">
{% csrf_token %}
{% include "forms/form.html" with errors=True %}
<div class="field is-grouped is-centered">
<div class="control is-expanded">
<a class="button is-fullwidth is-outlined is-primary is-light" href="{% url 'authens:login' %}">
<span>{% trans "Se reconnecter" %}</span>
<span class="icon is-small">
<i class="fas fa-unlock"></i>
</span>
</a>
</div>
<div class="control">
<a class="button is-primary" href="{% url 'kadenios' %}">
<span>{% trans "Accueil" %}</span>
<span class="icon is-small">
<i class="fas fa-home"></i>
</span>
</a>
</div>
</div>
</form>
</div>
</div>
{% endblock %}