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.
This commit is contained in:
Aurélien Delobelle 2017-11-13 15:49:29 +01:00 committed by Aurélien Delobelle
parent 05eeb6a25c
commit e56200a569
2 changed files with 49 additions and 140 deletions

View file

@ -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/")
##

View file

@ -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.
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 ?"
"Êtes-vous sûr·e de vouloir ouvrir une session " "partagée ?"
),
},
)
else:
# Step 2: validate token.
return self.validate_auth(token)
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
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")