Merge branch 'Aufinal/old_cas_view' into 'master'

Vues pour l'authentification vieilleux

See merge request klub-dev-ens/authens!12
This commit is contained in:
Martin Pepin 2020-06-20 17:06:31 +02:00
commit ab49a6479f
12 changed files with 181 additions and 12 deletions

View file

@ -142,7 +142,7 @@ class ENSCASBackend:
class OldCASBackend:
"""Authentication backend for old CAS accounts.
Given a CAS login, an entrance year and a password, first finds the matching
Given a CAS login, an entrance year and a password, first finds the matching
OldCASAccount instance (if it exists), then checks the given password with
the user associated to this account.
"""
@ -168,3 +168,10 @@ class OldCASBackend:
# Taken from Django's ModelBackend
is_active = getattr(user, "is_active", None)
return is_active or is_active is None
# Django boilerplate.
def get_user(self, user_id):
try:
return UserModel.objects.get(pk=user_id)
except UserModel.DoesNotExist:
return None

61
authens/forms.py Normal file
View file

@ -0,0 +1,61 @@
from django import forms
from django.contrib.auth import forms as auth_forms, authenticate
from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
def promo_choices():
return [(r, r) for r in range(2000, timezone.now().year + 1)]
class OldCASAuthForm(forms.Form):
""" Adapts Django's AuthenticationForm to allow for OldCAS login.
"""
cas_login = auth_forms.UsernameField(
label=_("Ancien login clipper"), max_length=1023
)
password = forms.CharField(
label=_("Mot de passe"),
strip=False,
widget=forms.PasswordInput(attrs={"autocomplete": "current-password"}),
)
entrance_year = forms.TypedChoiceField(
label=_("Promotion"), choices=promo_choices, coerce=int
)
def __init__(self, request=None, *args, **kwargs):
self.request = request
self.user_cache = None
super().__init__(*args, **kwargs)
def clean(self):
cas_login = self.cleaned_data.get("cas_login")
password = self.cleaned_data.get("password")
entrance_year = self.cleaned_data.get("entrance_year")
if cas_login is not None and password:
self.user_cache = authenticate(
self.request,
cas_login=cas_login,
password=password,
entrance_year=entrance_year,
)
if self.user_cache is None:
raise self.get_invalid_login_error()
return self.cleaned_data
def get_user(self):
# Necessary API for LoginView
return self.user_cache
def get_invalid_login_error(self):
return forms.ValidationError(
_(
"Aucun utilisateur n'existe avec ce clipper, cette promo et/ou ce mot "
"de passe. Veuillez vérifier votre saisie. Attention, tous les champs "
"sont sensibles à la casse !"
),
code="invalid_login",
)

View file

@ -32,7 +32,9 @@ class CASAccount(models.Model):
verbose_name_plural = _("Comptes CAS")
def __str__(self):
return _("compte CAS %(cas_login) (promo %(entrance_year)s) lié à %(user)s") % {
return _(
"compte CAS %(cas_login)s (promo %(entrance_year)s) lié à %(user)s"
) % {
"cas_login": self.cas_login,
"entrance_year": self.entrance_year,
"user": self.user.username,
@ -74,7 +76,7 @@ class OldCASAccount(models.Model):
def __str__(self):
return _(
"Ancien compte CAS %(cas_login) (promo %(entrance_year)s) lié à %(user)s"
"Ancien compte CAS %(cas_login)s (promo %(entrance_year)s) lié à %(user)s"
) % {
"cas_login": self.cas_login,
"entrance_year": self.entrance_year,

View file

@ -59,7 +59,7 @@ form {
}
form table {
margin: auto;
margin: 20px auto;
border-spacing: 0.3em;
}
@ -90,6 +90,15 @@ input[type="submit"]:hover {
border-color: white;
}
select {
border: 0;
font-size: 1em;
background-color: white;
width: 100%;
padding:5px;
text-align: end;
}
a {
flex: 1;
height: 200px;

View file

@ -13,11 +13,9 @@
{% trans "Mot de passe" %}
</div>
</a>
{% comment %} TODO: https://git.eleves.ens.fr/klub-dev-ens/authens/issues/9
<a href="#TODO">
<a href="{% url "authens:login.oldcas" %}?next={{ next| urlencode }}">
<div class="big-button oldcas">
{% trans "Vieilleux" %}
</div>
</a>
{% endcomment %}
{% endblock %}

View file

@ -0,0 +1,27 @@
{% extends "authens/base.html" %}
{% load i18n %}
{% block content %}
<h2>{% if request.site.name %}{{ request.site.name }}{% else %}AuthENS{% endif %} - {% trans "Connexion vieilleux" %}</h2>
{% if form.errors %}
<p class="error">{% trans "Login CAS, promotion et/ou mot de passe incorrect" %}</p>
{% endif %}
<form class="oldcas" method="post" action="">
{% csrf_token %}
<table>
<tbody>
{% for field in form %}
<tr>
<th>{{ field.label_tag }}</th>
<td>{{ field }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<input type="submit" value="{% trans "Se connecter" %}">
<input type="hidden" name="next" value="{{ next }}">
</form>
{% endblock %}

View file

@ -115,10 +115,11 @@ class TestCASBackend(TestCase):
class TestOldCASBackend(TestCase):
def test_simple_auth(self):
user = UserModel.objects.create_user(username="johndoe31", password="password")
wrong_user = UserModel.objects.create_user("johndoe", "password")
OldCASAccount.objects.create(user=user, cas_login="johndoe", entrance_year=2019)
# Decoy user that may be authenticated by mistake
UserModel.objects.create_user(username="johndoe", password="password")
OldCASAccount.objects.create(user=user, cas_login="johndoe", entrance_year=2014)
auth_user = authenticate(
None, cas_login="johndoe", entrance_year=2019, password="password"
None, cas_login="johndoe", entrance_year=2014, password="password"
)
self.assertEqual(auth_user, user)

View file

@ -7,7 +7,7 @@ from django.contrib.sessions.models import Session
from django.test import Client, TestCase
from django.urls import reverse
from authens.models import CASAccount
from authens.models import CASAccount, OldCASAccount
from authens.tests.cas_utils import FakeCASClient
UserModel = get_user_model()
@ -44,6 +44,39 @@ class TestLoginViews(TestCase):
response = Client().get(reverse("authens:login"))
self.assertEqual(response.status_code, 200)
def test_oldcas_login(self):
url = reverse("authens:login.oldcas")
client = Client()
user = UserModel.objects.create_user(username="johndoe31", password="password")
# Decoy user that may be authenticated by mistake
UserModel.objects.create_user(username="johndoe", password="password")
OldCASAccount.objects.create(user=user, cas_login="johndoe", entrance_year=2014)
post_data = dict(cas_login="johndoe", password="password", entrance_year=2014)
response = client.post(url, post_data)
self.assertRedirects(response, settings.LOGIN_REDIRECT_URL)
def test_oldcas_login_error(self):
url = reverse("authens:login.oldcas")
client = Client()
user = UserModel.objects.create_user(username="johndoe31", password="password")
OldCASAccount.objects.create(user=user, cas_login="johndoe", entrance_year=2014)
wrong_year = dict(cas_login="johndoe", password="password", entrance_year=2015)
wrong_login = dict(
cas_login="johndoe31", password="password", entrance_year=2014
)
response = client.post(url, wrong_year, follow=True)
non_field_errors = response.context["form"].non_field_errors().as_data()
self.assertEqual(non_field_errors[0].code, "invalid_login")
response = client.post(url, wrong_login, follow=True)
non_field_errors = response.context["form"].non_field_errors().as_data()
self.assertEqual(non_field_errors[0].code, "invalid_login")
class TestLogoutView(TestCase):
def test_regular_logout(self):

View file

@ -7,5 +7,6 @@ urlpatterns = [
path("login/choose", views.LoginSwitchView.as_view(), name="login"),
path("login/cas", views.CASLoginView.as_view(), name="login.cas"),
path("login/pwd", views.PasswordLoginView.as_view(), name="login.pwd"),
path("login/oldcas", views.OldCASLoginView.as_view(), name="login.oldcas"),
path("logout", views.LogoutView.as_view(), name="logout"),
]

View file

@ -9,6 +9,7 @@ from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
from authens.utils import get_cas_client
from authens.forms import OldCASAuthForm
class NextPageMixin:
@ -79,6 +80,11 @@ class PasswordLoginView(auth_views.LoginView):
template_name = "authens/pwd_login.html"
class OldCASLoginView(auth_views.LoginView):
template_name = "authens/old_cas_login.html"
authentication_form = OldCASAuthForm
class LogoutView(auth_views.LogoutView):
"""Logout view of AuthENS.

View file

@ -118,13 +118,14 @@ STATIC_URL = "/static/"
# The only modifications to the default settings are here #
# ------------------------------------------------------- #
from django.urls import reverse_lazy # noqa
from django.urls import reverse_lazy # noqa
# This is mandatory
INSTALLED_APPS += ["example", "authens"]
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
"authens.backends.ENSCASBackend",
"authens.backends.OldCASBackend",
]
LOGIN_URL = reverse_lazy("authens:login")

23
setup.cfg Normal file
View file

@ -0,0 +1,23 @@
[flake8]
exclude = migrations
max-line-length = 88
ignore =
# whitespace before ':' (not PEP8-compliant for slicing)
E203,
# lambda expression
E731,
# line break before binary operator (not PEP8-compliant)
W503
[isort]
# For black compat: https://github.com/ambv/black#how-black-wraps-lines
combine_as_imports = true
default_section = THIRDPARTY
force_grid_wrap = 0
include_trailing_comma = true
known_django = django
known_first_party = authens,tests
line_length = 88
multi_line_output = 3
not_skip = __init__.py
sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER