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")