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

@ -168,3 +168,10 @@ class OldCASBackend:
# Taken from Django's ModelBackend # Taken from Django's ModelBackend
is_active = getattr(user, "is_active", None) is_active = getattr(user, "is_active", None)
return is_active or is_active is 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") verbose_name_plural = _("Comptes CAS")
def __str__(self): 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, "cas_login": self.cas_login,
"entrance_year": self.entrance_year, "entrance_year": self.entrance_year,
"user": self.user.username, "user": self.user.username,
@ -74,7 +76,7 @@ class OldCASAccount(models.Model):
def __str__(self): def __str__(self):
return _( 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, "cas_login": self.cas_login,
"entrance_year": self.entrance_year, "entrance_year": self.entrance_year,

View file

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

View file

@ -13,11 +13,9 @@
{% trans "Mot de passe" %} {% trans "Mot de passe" %}
</div> </div>
</a> </a>
{% comment %} TODO: https://git.eleves.ens.fr/klub-dev-ens/authens/issues/9 <a href="{% url "authens:login.oldcas" %}?next={{ next| urlencode }}">
<a href="#TODO">
<div class="big-button oldcas"> <div class="big-button oldcas">
{% trans "Vieilleux" %} {% trans "Vieilleux" %}
</div> </div>
</a> </a>
{% endcomment %}
{% endblock %} {% 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): class TestOldCASBackend(TestCase):
def test_simple_auth(self): def test_simple_auth(self):
user = UserModel.objects.create_user(username="johndoe31", password="password") user = UserModel.objects.create_user(username="johndoe31", password="password")
wrong_user = UserModel.objects.create_user("johndoe", "password") # Decoy user that may be authenticated by mistake
OldCASAccount.objects.create(user=user, cas_login="johndoe", entrance_year=2019) UserModel.objects.create_user(username="johndoe", password="password")
OldCASAccount.objects.create(user=user, cas_login="johndoe", entrance_year=2014)
auth_user = authenticate( 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) 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.test import Client, TestCase
from django.urls import reverse from django.urls import reverse
from authens.models import CASAccount from authens.models import CASAccount, OldCASAccount
from authens.tests.cas_utils import FakeCASClient from authens.tests.cas_utils import FakeCASClient
UserModel = get_user_model() UserModel = get_user_model()
@ -44,6 +44,39 @@ class TestLoginViews(TestCase):
response = Client().get(reverse("authens:login")) response = Client().get(reverse("authens:login"))
self.assertEqual(response.status_code, 200) 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): class TestLogoutView(TestCase):
def test_regular_logout(self): def test_regular_logout(self):

View file

@ -7,5 +7,6 @@ urlpatterns = [
path("login/choose", views.LoginSwitchView.as_view(), name="login"), path("login/choose", views.LoginSwitchView.as_view(), name="login"),
path("login/cas", views.CASLoginView.as_view(), name="login.cas"), path("login/cas", views.CASLoginView.as_view(), name="login.cas"),
path("login/pwd", views.PasswordLoginView.as_view(), name="login.pwd"), 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"), 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 django.utils.translation import gettext_lazy as _
from authens.utils import get_cas_client from authens.utils import get_cas_client
from authens.forms import OldCASAuthForm
class NextPageMixin: class NextPageMixin:
@ -79,6 +80,11 @@ class PasswordLoginView(auth_views.LoginView):
template_name = "authens/pwd_login.html" 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): class LogoutView(auth_views.LogoutView):
"""Logout view of AuthENS. """Logout view of AuthENS.

View file

@ -125,6 +125,7 @@ INSTALLED_APPS += ["example", "authens"]
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend", "django.contrib.auth.backends.ModelBackend",
"authens.backends.ENSCASBackend", "authens.backends.ENSCASBackend",
"authens.backends.OldCASBackend",
] ]
LOGIN_URL = reverse_lazy("authens:login") 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