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 unittest import mock
from django.contrib.auth.models import AnonymousUser, Group, Permission, User from django.contrib.auth.models import Group, Permission, User
from django.core import signing
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import RequestFactory, TestCase from django.test import RequestFactory, TestCase
@ -13,7 +12,6 @@ from .backends import AccountBackend, GenericBackend
from .middleware import TemporaryAuthMiddleware from .middleware import TemporaryAuthMiddleware
from .models import GenericTeamToken from .models import GenericTeamToken
from .utils import get_kfet_generic_user from .utils import get_kfet_generic_user
from .views import GenericLoginView
## ##
# Forms # Forms
@ -144,7 +142,7 @@ class GenericLoginViewTests(TestCase):
def test_url(self): def test_url(self):
self.assertEqual(self.url, "/k-fet/login/generic") 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. 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.assertEqual(r.status_code, 200)
self.assertTemplateUsed(r, "kfet/confirm_form.html") 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 The kfet generic user is logged in.
logout url.
""" """
self.client.login(username="team", password="team") self.client.login(username="team", password="team")
r = self.client.post(self.url) r = self.client.post(self.url)
self.assertRedirects( self.assertRedirects(r, reverse("kfet.kpsul"))
r, "/logout?next={}".format(self.url), fetch_redirect_response=False 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. Logged in user must be a team user to initiate login as generic user.
""" """
@ -177,74 +175,28 @@ class GenericLoginViewTests(TestCase):
# With GET. # With GET.
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertRedirects( 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. # Also with POST.
r = self.client.post(self.url) r = self.client.post(self.url)
self.assertRedirects( 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): def test_post_redirect(self):
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):
""" """
A team user is logged in as the kfet generic user. A team user is logged in as the kfet generic user.
""" """
self.client.login(username="team", password="team") 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.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 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.decorators import permission_required
from django.contrib.auth.models import Group, User 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.contrib.messages.views import SuccessMessageMixin
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
from django.db.models import Prefetch from django.db.models import Prefetch
from django.http import QueryDict
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext_lazy as _ 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 import View
from django.views.generic.edit import CreateView, UpdateView from django.views.generic.edit import CreateView, UpdateView
from kfet.decorators import teamkfet_required
from .forms import GroupForm from .forms import GroupForm
from .models import GenericTeamToken from .models import GenericTeamToken
@ -21,91 +19,50 @@ from .models import GenericTeamToken
class GenericLoginView(View): class GenericLoginView(View):
""" """
View to authenticate as kfet generic user. 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): def dispatch(self, request, *args, **kwargs):
token = request.get_signed_cookie(self.TOKEN_COOKIE_NAME, None) return super().dispatch(request, *args, **kwargs)
if not token:
if not request.user.has_perm("kfet.is_team"):
return redirect_to_login(request.get_full_path())
if request.method == "POST": def get(self, request, *args, **kwargs):
# Step 1: set token and logout user. """
return self.prepare_auth() GET requests should not change server/client states. Prompt user for
else: confirmation.
# GET request should not change server/client states. Send a """
# confirmation template to emit a POST request. return render(
return render( request,
request, "kfet/confirm_form.html",
"kfet/confirm_form.html", {
{ "title": _("Ouvrir une session partagée"),
"title": _("Ouvrir une session partagée"), "text": _(
"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): def post(self, request, *args, **kwargs):
# Issue token. # Issue token, used by GenericBackend.
token = GenericTeamToken.objects.create_token() token = GenericTeamToken.objects.create_token()
# Prepare callback of logout. logout(self.request)
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_url = reverse("account_logout") # Authenticate with GenericBackend. Should always return the kfet
logout_qd = QueryDict(mutable=True) # generic user.
logout_qd["next"] = here_url user = authenticate(request=self.request, kfet_token=token.token)
logout_url += "?{}".format(logout_qd.urlencode(safe="/"))
resp = redirect(logout_url) if not user:
resp.set_signed_cookie(self.TOKEN_COOKIE_NAME, token.token, httponly=True) return redirect(self.request.get_full_path())
return resp
def validate_auth(self, token): # Log in generic user.
# Authenticate with GenericBackend. login(self.request, user)
user = authenticate(request=self.request, kfet_token=token) messages.success(self.request, _("K-Fêt — Ouverture d'une session partagée."))
return redirect(self.get_next_url())
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
def get_next_url(self): def get_next_url(self):
return self.request.GET.get("next", reverse("kfet.kpsul")) 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") @permission_required("kfet.manage_perms")