From e56200a569588a11c63d2d0954cb184c86e52309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 13 Nov 2017 15:49:29 +0100 Subject: [PATCH] kfet -- LoginGenericView directly disconnects users. Since allauth is installed, users are not automatically logged out of CAS when logging out GestioCOF. This change simplifies the view and avoid being stuck because of the redirect to the logout page, which happened via a GET request and so prompting to confirm. --- kfet/auth/tests.py | 82 +++++++--------------------------- kfet/auth/views.py | 107 ++++++++++++++------------------------------- 2 files changed, 49 insertions(+), 140 deletions(-) diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index a8c81360..812a0ef0 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -1,7 +1,6 @@ from unittest import mock -from django.contrib.auth.models import AnonymousUser, Group, Permission, User -from django.core import signing +from django.contrib.auth.models import Group, Permission, User from django.core.urlresolvers import reverse from django.test import RequestFactory, TestCase @@ -13,7 +12,6 @@ from .backends import AccountBackend, GenericBackend from .middleware import TemporaryAuthMiddleware from .models import GenericTeamToken from .utils import get_kfet_generic_user -from .views import GenericLoginView ## # Forms @@ -144,7 +142,7 @@ class GenericLoginViewTests(TestCase): def test_url(self): self.assertEqual(self.url, "/k-fet/login/generic") - def test_notoken_get(self): + def test_get(self): """ Send confirmation for user to emit POST request, instead of GET. """ @@ -155,20 +153,20 @@ class GenericLoginViewTests(TestCase): self.assertEqual(r.status_code, 200) self.assertTemplateUsed(r, "kfet/confirm_form.html") - def test_notoken_post(self): + def test_post(self): """ - POST request without token in COOKIES sets a token and redirects to - logout url. + The kfet generic user is logged in. """ self.client.login(username="team", password="team") r = self.client.post(self.url) - self.assertRedirects( - r, "/logout?next={}".format(self.url), fetch_redirect_response=False - ) + self.assertRedirects(r, reverse("kfet.kpsul")) + self.assertEqual(r.wsgi_request.user, self.generic_user) + with self.assertRaises(GenericTeamToken.DoesNotExist): + GenericTeamToken.objects.get() - def test_notoken_not_team(self): + def test_not_team(self): """ Logged in user must be a team user to initiate login as generic user. """ @@ -177,74 +175,28 @@ class GenericLoginViewTests(TestCase): # With GET. r = self.client.get(self.url) self.assertRedirects( - r, "/login?next={}".format(self.url), fetch_redirect_response=False + r, "/profil/login/?next={}".format(self.url), fetch_redirect_response=False ) # Also with POST. r = self.client.post(self.url) self.assertRedirects( - r, "/login?next={}".format(self.url), fetch_redirect_response=False + r, "/profil/login/?next={}".format(self.url), fetch_redirect_response=False ) - def _set_signed_cookie(self, client, key, value): - signed_value = signing.get_cookie_signer(salt=key).sign(value) - client.cookies.load({key: signed_value}) - - def _is_cookie_deleted(self, client, key): - try: - self.assertNotIn(key, client.cookies) - except AssertionError: - try: - cookie = client.cookies[key] - # It also can be emptied. - self.assertEqual(cookie.value, "") - self.assertEqual(cookie["expires"], "Thu, 01-Jan-1970 00:00:00 GMT") - self.assertEqual(cookie["max-age"], 0) - except AssertionError: - raise AssertionError("The cookie '%s' still exists." % key) - - def test_withtoken_valid(self): - """ - The kfet generic user is logged in. - """ - token = GenericTeamToken.objects.create(token="valid") - self._set_signed_cookie( - self.client, GenericLoginView.TOKEN_COOKIE_NAME, "valid" - ) - - r = self.client.get(self.url) - - self.assertRedirects(r, reverse("kfet.kpsul")) - self.assertEqual(r.wsgi_request.user, self.generic_user) - self._is_cookie_deleted(self.client, GenericLoginView.TOKEN_COOKIE_NAME) - with self.assertRaises(GenericTeamToken.DoesNotExist): - token.refresh_from_db() - - def test_withtoken_invalid(self): - """ - If token is invalid, delete it and try again. - """ - self._set_signed_cookie( - self.client, GenericLoginView.TOKEN_COOKIE_NAME, "invalid" - ) - - r = self.client.get(self.url) - - self.assertRedirects(r, self.url, fetch_redirect_response=False) - self.assertEqual(r.wsgi_request.user, AnonymousUser()) - self._is_cookie_deleted(self.client, GenericLoginView.TOKEN_COOKIE_NAME) - - def test_flow_ok(self): + def test_post_redirect(self): """ A team user is logged in as the kfet generic user. """ self.client.login(username="team", password="team") - next_url = "/k-fet/" - r = self.client.post("{}?next={}".format(self.url, next_url), follow=True) + next_url = "/any-url/" + url = self.url + "?next=" + next_url + r = self.client.post(url) + + self.assertRedirects(r, next_url, fetch_redirect_response=False) self.assertEqual(r.wsgi_request.user, self.generic_user) - self.assertEqual(r.wsgi_request.path, "/k-fet/") ## diff --git a/kfet/auth/views.py b/kfet/auth/views.py index 75c4c672..36258d88 100644 --- a/kfet/auth/views.py +++ b/kfet/auth/views.py @@ -1,19 +1,17 @@ from django.contrib import messages -from django.contrib.auth import authenticate, login +from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import permission_required from django.contrib.auth.models import Group, User -from django.contrib.auth.views import redirect_to_login from django.contrib.messages.views import SuccessMessageMixin from django.core.urlresolvers import reverse, reverse_lazy from django.db.models import Prefetch -from django.http import QueryDict from django.shortcuts import redirect, render -from django.utils.decorators import method_decorator from django.utils.translation import ugettext_lazy as _ -from django.views.decorators.http import require_http_methods from django.views.generic import View from django.views.generic.edit import CreateView, UpdateView +from kfet.decorators import teamkfet_required + from .forms import GroupForm from .models import GenericTeamToken @@ -21,91 +19,50 @@ from .models import GenericTeamToken class GenericLoginView(View): """ View to authenticate as kfet generic user. - - It is a 2-step view. First, issue a token if user is a team member and send - him to the logout view (for proper disconnect) with callback url to here. - Then authenticate the token to log in as the kfet generic user. - - Token is stored in COOKIES to avoid share it with the authentication - provider, which can be external. Session is unusable as it will be cleared - on logout. """ - TOKEN_COOKIE_NAME = "kfettoken" - - @method_decorator(require_http_methods(["GET", "POST"])) def dispatch(self, request, *args, **kwargs): - token = request.get_signed_cookie(self.TOKEN_COOKIE_NAME, None) - if not token: - if not request.user.has_perm("kfet.is_team"): - return redirect_to_login(request.get_full_path()) + return super().dispatch(request, *args, **kwargs) - if request.method == "POST": - # Step 1: set token and logout user. - return self.prepare_auth() - else: - # GET request should not change server/client states. Send a - # confirmation template to emit a POST request. - return render( - request, - "kfet/confirm_form.html", - { - "title": _("Ouvrir une session partagée"), - "text": _( - "Êtes-vous sûr·e de vouloir ouvrir une session " - "partagée ?" - ), - }, - ) - else: - # Step 2: validate token. - return self.validate_auth(token) + def get(self, request, *args, **kwargs): + """ + GET requests should not change server/client states. Prompt user for + confirmation. + """ + return render( + request, + "kfet/confirm_form.html", + { + "title": _("Ouvrir une session partagée"), + "text": _( + "Êtes-vous sûr·e de vouloir ouvrir une session " "partagée ?" + ), + }, + ) - def prepare_auth(self): - # Issue token. + def post(self, request, *args, **kwargs): + # Issue token, used by GenericBackend. token = GenericTeamToken.objects.create_token() - # Prepare callback of logout. - here_url = reverse(login_generic) - if "next" in self.request.GET: - # Keep given next page. - here_qd = QueryDict(mutable=True) - here_qd["next"] = self.request.GET["next"] - here_url += "?{}".format(here_qd.urlencode()) + logout(self.request) - logout_url = reverse("account_logout") - logout_qd = QueryDict(mutable=True) - logout_qd["next"] = here_url - logout_url += "?{}".format(logout_qd.urlencode(safe="/")) + # Authenticate with GenericBackend. Should always return the kfet + # generic user. + user = authenticate(request=self.request, kfet_token=token.token) - resp = redirect(logout_url) - resp.set_signed_cookie(self.TOKEN_COOKIE_NAME, token.token, httponly=True) - return resp + if not user: + return redirect(self.request.get_full_path()) - def validate_auth(self, token): - # Authenticate with GenericBackend. - user = authenticate(request=self.request, kfet_token=token) - - if user: - # Log in generic user. - login(self.request, user) - messages.success( - self.request, _("K-Fêt — Ouverture d'une session partagée.") - ) - resp = redirect(self.get_next_url()) - else: - # Try again. - resp = redirect(self.request.get_full_path()) - - # Prevents blocking due to an invalid COOKIE. - resp.delete_cookie(self.TOKEN_COOKIE_NAME) - return resp + # Log in generic user. + login(self.request, user) + messages.success(self.request, _("K-Fêt — Ouverture d'une session partagée.")) + return redirect(self.get_next_url()) def get_next_url(self): return self.request.GET.get("next", reverse("kfet.kpsul")) -login_generic = GenericLoginView.as_view() +login_generic = teamkfet_required(GenericLoginView.as_view()) @permission_required("kfet.manage_perms")