From 26455ae5c61b760c3da3678bdd681a560839828b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 15 Sep 2020 00:44:42 +0200 Subject: [PATCH 1/5] Monkey-patch authens to make it look like GestioCOF --- cof/settings/bds_prod.py | 4 +--- cof/settings/common.py | 3 +++ .../static/gestioncof/css/authens_extra.css | 15 +++++++++++++++ gestioncof/templates/authens/base.html | 8 ++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 gestioncof/static/gestioncof/css/authens_extra.css create mode 100644 gestioncof/templates/authens/base.html diff --git a/cof/settings/bds_prod.py b/cof/settings/bds_prod.py index 12f5a552..3d6ac5a8 100644 --- a/cof/settings/bds_prod.py +++ b/cof/settings/bds_prod.py @@ -11,7 +11,7 @@ from .common import INSTALLED_APPS ALLOWED_HOSTS = ["bds.ens.fr", "www.bds.ens.fr", "dev.cof.ens.fr"] -INSTALLED_APPS += ["bds", "events", "clubs", "authens"] +INSTALLED_APPS += ["bds", "events", "clubs"] STATIC_ROOT = "/srv/bds.ens.fr/public/gestion2/static" STATIC_URL = "/gestion2/static/" @@ -29,8 +29,6 @@ AUTHENTICATION_BACKENDS = [ "authens.backends.OldCASBackend", ] -AUTHENS_USE_OLDCAS = False - LOGIN_URL = "authens:login" LOGIN_REDIRECT_URL = "bds:home" LOGOUT_REDIRECT_URL = "bds:home" diff --git a/cof/settings/common.py b/cof/settings/common.py index 4636ace3..f8645b98 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -69,6 +69,7 @@ INSTALLED_APPS = [ "django_cas_ng", "bootstrapform", "widget_tweaks", + "authens", ] MIDDLEWARE = [ @@ -141,3 +142,5 @@ CAS_LOGIN_MSG = None CAS_IGNORE_REFERER = True CAS_REDIRECT_URL = "/" CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" + +AUTHENS_USE_OLDCAS = False diff --git a/gestioncof/static/gestioncof/css/authens_extra.css b/gestioncof/static/gestioncof/css/authens_extra.css new file mode 100644 index 00000000..c0204201 --- /dev/null +++ b/gestioncof/static/gestioncof/css/authens_extra.css @@ -0,0 +1,15 @@ +html, body { + background-color : #A7D4CD; + font-family: 'Roboto', sans-serif; + font-weight: 300; +} + +#container-title, input[type="submit"] { + background: #4F504B; +} + +.cas { background: #49A5E3; } +.big-button.cas:hover { background: #71B5E3; } + +.exte { background: #E36748; } +.big-button.exte:hover { background: #E38871; } diff --git a/gestioncof/templates/authens/base.html b/gestioncof/templates/authens/base.html new file mode 100644 index 00000000..4219fa21 --- /dev/null +++ b/gestioncof/templates/authens/base.html @@ -0,0 +1,8 @@ +{% extends "authens/base.html" %} + +{% load static %} + +{% block extra_css %} + + +{% endblock %} -- 2.47.1 From a98e56d3169625a1a1b463074862aab27038f95d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 17 Sep 2020 21:35:15 +0200 Subject: [PATCH 2/5] Switch to Authens for GestioCOF authentication --- cof/settings/bds_prod.py | 12 --- cof/settings/cof_prod.py | 10 +-- cof/settings/common.py | 14 ++-- .../templates/gestioncof/base_header.html | 2 +- gestioncof/urls.py | 18 +---- gestioncof/views.py | 78 +------------------ kfet/auth/views.py | 2 +- kfet/templates/kfet/base_nav.html | 6 +- 8 files changed, 18 insertions(+), 124 deletions(-) diff --git a/cof/settings/bds_prod.py b/cof/settings/bds_prod.py index 3d6ac5a8..a99185e4 100644 --- a/cof/settings/bds_prod.py +++ b/cof/settings/bds_prod.py @@ -18,17 +18,5 @@ STATIC_URL = "/gestion2/static/" MEDIA_ROOT = "/srv/bds.ens.fr/gestion2/media" MEDIA_URL = "/gestion2/media/" - -# --- -# Auth-related stuff -# --- - -AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", - "authens.backends.ENSCASBackend", - "authens.backends.OldCASBackend", -] - -LOGIN_URL = "authens:login" LOGIN_REDIRECT_URL = "bds:home" LOGOUT_REDIRECT_URL = "bds:home" diff --git a/cof/settings/cof_prod.py b/cof/settings/cof_prod.py index 47fa3954..f0b56319 100644 --- a/cof/settings/cof_prod.py +++ b/cof/settings/cof_prod.py @@ -4,7 +4,6 @@ The settings that are not listed here are imported from .common """ import os -from .common import * # NOQA from .common import ( AUTHENTICATION_BACKENDS, BASE_DIR, @@ -14,6 +13,8 @@ from .common import ( import_secret, ) +from .common import * # NOQA + # --- # COF-specific secrets # --- @@ -108,13 +109,10 @@ CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.e # Auth-related stuff # --- -AUTHENTICATION_BACKENDS += [ - "gestioncof.shared.COFCASBackend", - "kfet.auth.backends.GenericBackend", -] +AUTHENTICATION_BACKENDS.append("kfet.auth.backends.GenericBackend") -LOGIN_URL = "cof-login" LOGIN_REDIRECT_URL = "home" +LOGOUT_REDIRECT_URL = "home" # --- # Cache settings diff --git a/cof/settings/common.py b/cof/settings/common.py index f8645b98..3d61c6f8 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -134,13 +134,11 @@ FORMAT_MODULE_PATH = "cof.locale" # Auth-related stuff # --- -AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.ModelBackend"] - -CAS_SERVER_URL = "https://cas.eleves.ens.fr/" -CAS_VERSION = "2" -CAS_LOGIN_MSG = None -CAS_IGNORE_REFERER = True -CAS_REDIRECT_URL = "/" -CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" +AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "authens.backends.ENSCASBackend", +] AUTHENS_USE_OLDCAS = False + +LOGIN_URL = "authens:login" diff --git a/gestioncof/templates/gestioncof/base_header.html b/gestioncof/templates/gestioncof/base_header.html index e5f757a7..2358a3a2 100644 --- a/gestioncof/templates/gestioncof/base_header.html +++ b/gestioncof/templates/gestioncof/base_header.html @@ -11,7 +11,7 @@

{% if user.first_name %}{{ user.first_name }}{% else %}{{ user.username }}{% endif %}, {% if user.profile.is_cof %}au COF{% else %}non-COF{% endif %}

diff --git a/gestioncof/urls.py b/gestioncof/urls.py index 14fb101f..7df204e7 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -1,7 +1,7 @@ +from authens.views import LogoutView from django.contrib.auth import views as django_auth_views from django.urls import include, path from django.views.generic.base import TemplateView -from django_cas_ng import views as django_cas_views from gestioncof import csv_views, views @@ -96,21 +96,7 @@ urlpatterns = [ TemplateView.as_view(template_name="cof-denied.html"), name="cof-denied", ), - path("cas/login", django_cas_views.LoginView.as_view(), name="cas_login_view"), - path("cas/logout", django_cas_views.LogoutView.as_view()), - path( - "outsider/login", - views.LoginExtView.as_view(), - name="ext_login_view", - ), - path( - "outsider/logout", - django_auth_views.LogoutView.as_view(), - {"next_page": "home"}, - ), - path("login", views.login, name="cof-login"), - path("logout", views.logout, name="cof-logout"), - path("admin/logout/", views.logout), + path("admin/logout/", LogoutView.as_view()), # ----- # Infos persos # ----- diff --git a/gestioncof/views.py b/gestioncof/views.py index d4b6a5be..aa47b652 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -2,17 +2,12 @@ import csv import uuid from datetime import timedelta from smtplib import SMTPRecipientsRefused -from urllib.parse import parse_qs, urlencode, urlparse, urlunparse from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User -from django.contrib.auth.views import ( - LoginView as DjangoLoginView, - LogoutView as DjangoLogoutView, - redirect_to_login, -) +from django.contrib.auth.views import redirect_to_login from django.contrib.sites.models import Site from django.core.mail import send_mail from django.http import Http404, HttpResponse, HttpResponseForbidden @@ -22,7 +17,6 @@ from django.urls import reverse_lazy from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from django.views.generic import FormView, TemplateView -from django_cas_ng.views import LogoutView as CasLogoutView from icalendar import Calendar, Event as Vevent from bda.models import Spectacle, Tirage @@ -34,7 +28,6 @@ from gestioncof.forms import ( EventForm, EventFormset, EventStatusFilterForm, - ExteAuthenticationForm, GestioncofConfigForm, PhoneForm, ProfileForm, @@ -79,75 +72,6 @@ class HomeView(LoginRequiredMixin, TemplateView): return context -def login(request): - if request.user.is_authenticated: - return redirect("home") - context = {} - if request.method == "GET" and "next" in request.GET: - context["next"] = request.GET["next"] - return render(request, "login_switch.html", context) - - -class LoginExtView(DjangoLoginView): - template_name = "login.html" - form_class = ExteAuthenticationForm - - def form_invalid(self, form): - for e in form.non_field_errors().as_data(): - if e.code in ["has_clipper", "no_password"]: - return render(self.request, "login_error.html", {"error_code": e.code}) - return super().form_invalid(form) - - -class CustomCasLogoutView(CasLogoutView): - """ - Actuellement, le CAS de l'ENS est pété et n'a pas le bon paramètre GET - pour rediriger après déconnexion. On change la redirection à la main - dans la vue de logout. - """ - - def get(self, request): - # CasLogoutView.get() retourne un HttpResponseRedirect - response = super().get(request) - parse_result = urlparse(response.url) - qd = parse_qs(parse_result.query) - - if "url" in qd.keys(): - # Le 2e pop est nécessaire car CAS n'aime pas - # les paramètres sous forme de liste - qd["service"] = qd.pop("url").pop() - - # La méthode _replace est documentée ! - new_url = parse_result._replace(query=urlencode(qd)) - - return redirect(urlunparse(new_url)) - - -@login_required -def logout(request, next_page=None): - if next_page is None: - next_page = request.GET.get("next", None) - - profile = getattr(request.user, "profile", None) - - if profile and profile.login_clipper: - if next_page is None: - # On ne voit pas les messages quand on se déconnecte de CAS - msg = None - else: - msg = _("Déconnexion de GestioCOF et CAS réussie. À bientôt {}.") - logout_view = CustomCasLogoutView.as_view() - else: - msg = _("Déconnexion de GestioCOF réussie. À bientôt {}.") - logout_view = DjangoLogoutView.as_view( - next_page=next_page, template_name="logout.html" - ) - - if msg is not None: - messages.success(request, msg.format(request.user.get_short_name())) - return logout_view(request) - - @login_required def survey(request, survey_id): survey = get_object_or_404( diff --git a/kfet/auth/views.py b/kfet/auth/views.py index f57e8415..2ae79e83 100644 --- a/kfet/auth/views.py +++ b/kfet/auth/views.py @@ -73,7 +73,7 @@ class GenericLoginView(View): here_qd["next"] = self.request.GET["next"] here_url += "?{}".format(here_qd.urlencode()) - logout_url = reverse("cof-logout") + logout_url = reverse("authens:logout") logout_qd = QueryDict(mutable=True) logout_qd["next"] = here_url logout_url += "?{}".format(logout_qd.urlencode(safe="/")) diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html index 1cded20b..6e6db51d 100644 --- a/kfet/templates/kfet/base_nav.html +++ b/kfet/templates/kfet/base_nav.html @@ -101,7 +101,7 @@ {% endif %}
  • - + Déconnexion
  • @@ -110,13 +110,13 @@ {% endif %} {% if user.is_authenticated and not perms.kfet.is_team %}
  • - +
  • {% elif not user.is_authenticated %}
  • - + Connexion -- 2.47.1 From 7b8d1575c006f66d4e949123c5bde765abd846e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 17 Sep 2020 21:36:17 +0200 Subject: [PATCH 3/5] Break a redirection loop in teamkfet_required --- kfet/decorators.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/kfet/decorators.py b/kfet/decorators.py index 70848820..e7d383a5 100644 --- a/kfet/decorators.py +++ b/kfet/decorators.py @@ -1,8 +1,15 @@ from django.contrib.auth.decorators import user_passes_test +from django.core.exceptions import PermissionDenied def kfet_is_team(user): - return user.has_perm("kfet.is_team") + if user.is_authenticated: + if user.has_perm("kfet.is_team"): + return True + else: + raise PermissionDenied + else: + return False teamkfet_required = user_passes_test(kfet_is_team) -- 2.47.1 From 0815f79739af44c592c2fe9ef0b6eb3d8ad3da95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 17 Sep 2020 21:36:53 +0200 Subject: [PATCH 4/5] Update auth urls in tests --- cof/urls.py | 2 +- kfet/auth/tests.py | 12 +++++++++--- kfet/tests/test_statistic.py | 4 ++-- kfet/tests/test_views.py | 12 ++++++------ kfet/tests/testcases.py | 4 +++- shared/tests/mixins.py | 2 +- 6 files changed, 22 insertions(+), 14 deletions(-) diff --git a/cof/urls.py b/cof/urls.py index 1de437ed..b305967a 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -31,7 +31,7 @@ app_dict = { "bda": "gestion/bda/", "petitscours": "gestion/petitcours/", "events": "gestion/event_v2/", # the events module is still experimental ! - "authens": "gestion/authens/", + "authens": "gestion/auth/", } for (app_name, url_prefix) in app_dict.items(): if app_name in settings.INSTALLED_APPS: diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index 32e04812..01ceebe4 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -168,7 +168,9 @@ class GenericLoginViewTests(TestCase): r = self.client.post(self.url) self.assertRedirects( - r, "/gestion/logout?next={}".format(self.url), fetch_redirect_response=False + r, + "/gestion/auth/logout?next={}".format(self.url), + fetch_redirect_response=False, ) def test_notoken_not_team(self): @@ -180,13 +182,17 @@ class GenericLoginViewTests(TestCase): # With GET. r = self.client.get(self.url) self.assertRedirects( - r, "/gestion/login?next={}".format(self.url), fetch_redirect_response=False + r, + "/gestion/auth/login/choose?next={}".format(self.url), + fetch_redirect_response=False, ) # Also with POST. r = self.client.post(self.url) self.assertRedirects( - r, "/gestion/login?next={}".format(self.url), fetch_redirect_response=False + r, + "/gestion/auth/login/choose?next={}".format(self.url), + fetch_redirect_response=False, ) def _set_signed_cookie(self, client, key, value): diff --git a/kfet/tests/test_statistic.py b/kfet/tests/test_statistic.py index 6d8ecb47..1cd86743 100644 --- a/kfet/tests/test_statistic.py +++ b/kfet/tests/test_statistic.py @@ -61,7 +61,7 @@ class TestStats(TestCase): self.assertEqual(404, resp2.status_code) # 2. FOO is a member of the team and can get these pages but BAR - # receives a Redirect response + # receives a 403 response articles_urls = [ "/k-fet/articles/{}/stat/sales/list".format(article.pk), "/k-fet/articles/{}/stat/sales?{}".format( @@ -80,4 +80,4 @@ class TestStats(TestCase): resp = client.get(url) self.assertEqual(200, resp.status_code) resp2 = client2.get(url, follow=True) - self.assertRedirects(resp2, "/gestion/") + self.assertEqual(403, resp2.status_code) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 47382aa1..fcebef8c 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -232,7 +232,7 @@ class AccountReadViewTests(ViewTestCaseMixin, TestCase): response = client.get(url) self.assertRedirects( response, - "/gestion/login?next={}".format(url), + "/gestion/auth/login/choose?next={}".format(url), fetch_redirect_response=False, ) else: @@ -344,7 +344,7 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): response = meth(url) self.assertRedirects( response, - "/gestion/login?next={}".format(url), + "/gestion/auth/login/choose?next={}".format(url), fetch_redirect_response=False, ) else: @@ -629,7 +629,7 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase): response = client.get(url) self.assertRedirects( response, - "/gestion/login?next={}".format(url), + "/gestion/auth/login/choose?next={}".format(url), fetch_redirect_response=False, ) else: @@ -723,7 +723,7 @@ class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase): response = client.get(url) self.assertRedirects( response, - "/gestion/login?next={}".format(url), + "/gestion/auth/login/choose?next={}".format(url), fetch_redirect_response=False, ) else: @@ -764,7 +764,7 @@ class AccountStatBalanceListViewTests(ViewTestCaseMixin, TestCase): response = client.get(url) self.assertRedirects( response, - "/gestion/login?next={}".format(url), + "/gestion/auth/login/choose?next={}".format(url), fetch_redirect_response=False, ) else: @@ -830,7 +830,7 @@ class AccountStatBalanceViewTests(ViewTestCaseMixin, TestCase): response = client.get(url) self.assertRedirects( response, - "/gestion/login?next={}".format(url), + "/gestion/auth/login/choose?next={}".format(url), fetch_redirect_response=False, ) else: diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py index 4912023e..a6781387 100644 --- a/kfet/tests/testcases.py +++ b/kfet/tests/testcases.py @@ -39,7 +39,9 @@ class TestCaseMixin: querystring = QueryDict(mutable=True) querystring["next"] = full_path - login_url = "/gestion/login?" + querystring.urlencode(safe="/") + login_url = "/gestion/auth/login/choose?{}".format( + querystring.urlencode(safe="/") + ) # We don't focus on what the login view does. # So don't fetch the redirect. diff --git a/shared/tests/mixins.py b/shared/tests/mixins.py index ea83616a..6f056d20 100644 --- a/shared/tests/mixins.py +++ b/shared/tests/mixins.py @@ -173,7 +173,7 @@ class TestCaseMixin: querystring["next"] = full_path login_url = "{}?{}".format( - reverse("cof-login"), querystring.urlencode(safe="/") + reverse("authens:login"), querystring.urlencode(safe="/") ) # We don't focus on what the login view does. -- 2.47.1 From 3967831e92c7f9c9517691a47f6b83dfb108e6b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 22 Sep 2020 20:53:27 +0200 Subject: [PATCH 5/5] Migrate django_cas_ng accounts to authens --- .../migrations/0018_django_cas_to_authens.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 gestioncof/migrations/0018_django_cas_to_authens.py diff --git a/gestioncof/migrations/0018_django_cas_to_authens.py b/gestioncof/migrations/0018_django_cas_to_authens.py new file mode 100644 index 00000000..9b689a7e --- /dev/null +++ b/gestioncof/migrations/0018_django_cas_to_authens.py @@ -0,0 +1,40 @@ +import sys + +from authens.shortcuts import fetch_cas_account +from django.db import migrations + + +def to_authens(apps, schema_editor): + User = apps.get_model("auth", "User") + CASAccount = apps.get_model("authens", "CASAccount") + + failures = [] + for user in User.objects.select_related("profile").all(): + login_clipper = user.profile.login_clipper + if login_clipper: + ldap_info = fetch_cas_account(login_clipper) + if ldap_info is None: + failures.append(user) + else: + entrance_year = ldap_info["entrance_year"] + CASAccount.objects.create( + user=user, cas_login=login_clipper, entrance_year=entrance_year + ) + + if failures: + sys.stderr.write(" ---------- \n") + sys.stderr.write("Some accounts could not be linked to a CAS account:\n") + for user in failures: + sys.stderr.write(f"- {user.username}\n") + sys.stderr.write(" ---------- \n") + + +class Migration(migrations.Migration): + + dependencies = [ + ("gestioncof", "0017_petitscours_uniqueness"), + ] + + operations = [ + migrations.RunPython(to_authens, migrations.RunPython.noop), + ] -- 2.47.1