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:
parent
05eeb6a25c
commit
e56200a569
2 changed files with 49 additions and 140 deletions
|
@ -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/")
|
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Add table
Reference in a new issue