diff --git a/backend/Pipfile b/backend/Pipfile index 162707b..07d8745 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -4,7 +4,7 @@ verify_ssl = true name = "pypi" [packages] -codeforlife = {ref = "v0.8.0", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} +codeforlife = {ref = "v0.8.3", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} django = "==3.2.20" djangorestframework = "==3.13.1" django-cors-headers = "==4.1.0" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 520d772..115a4d6 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9b5dd7292210fab043685afe18e5fc517103bc85902a80699ebf09b26c760ee3" + "sha256": "35a1f0e3d41bff90adb93acc781bbf0db41767aac9a22db6baadcdcfccd9490c" }, "pipfile-spec": 6, "requires": { @@ -155,7 +155,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "5fb23069bb2ca1ccd9a1e9e5d3db1841d2758fd1" + "ref": "f11c55524e8ef4a78a07f996978f2a11b553f326" }, "codeforlife-portal": { "hashes": [ @@ -1141,11 +1141,11 @@ }, "platformdirs": { "hashes": [ - "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d", - "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d" + "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", + "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e" ], "markers": "python_version >= '3.7'", - "version": "==3.10.0" + "version": "==3.11.0" }, "pluggy": { "hashes": [ diff --git a/backend/api/permissions.py b/backend/api/permissions.py new file mode 100644 index 0000000..d82aa0f --- /dev/null +++ b/backend/api/permissions.py @@ -0,0 +1,12 @@ +from codeforlife.user.models import User +from rest_framework.permissions import BasePermission +from rest_framework.request import Request +from rest_framework.views import View + + +class UserHasSessionAuthFactors(BasePermission): + def has_permission(self, request: Request, view: View): + return ( + isinstance(request.user, User) + and request.user.session.session_auth_factors.exists() + ) diff --git a/backend/api/tests/test_views.py b/backend/api/tests/test_views.py index e58dbca..4591149 100644 --- a/backend/api/tests/test_views.py +++ b/backend/api/tests/test_views.py @@ -4,6 +4,7 @@ from codeforlife.tests import CronTestCase from codeforlife.user.models import AuthFactor, User from django.core import management +from django.http import HttpResponse from django.test import TestCase from django.urls import reverse from django.utils import timezone @@ -13,6 +14,15 @@ class TestLoginView(TestCase): def setUp(self): self.user = User.objects.get(id=2) + def _get_session_auth_factors(self, response: HttpResponse): + return [ + auth_factor + for auth_factor in response.cookies[ + "sessionid_httponly_false" + ].value.split(",") + if auth_factor != "" + ] + def test_post__otp(self): AuthFactor.objects.create( user=self.user, @@ -28,9 +38,7 @@ def test_post__otp(self): ) assert response.status_code == 200 - self.assertDictEqual( - response.json(), {"auth_factors": [AuthFactor.Type.OTP]} - ) + assert self._get_session_auth_factors(response) == [AuthFactor.Type.OTP] self.user.userprofile.otp_secret = pyotp.random_base32() self.user.userprofile.save() @@ -45,7 +53,7 @@ def test_post__otp(self): ) assert response.status_code == 200 - self.assertDictEqual(response.json(), {"auth_factors": []}) + assert self._get_session_auth_factors(response) == [] class TestClearExpiredView(CronTestCase): diff --git a/backend/api/urls.py b/backend/api/urls.py index 6df0a75..7218a4a 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -1,16 +1,28 @@ from django.urls import include, path, re_path -from .views import ClearExpiredView, LoginView +from .views import ClearExpiredView, LoginOptionsView, LoginView urlpatterns = [ path( "session/", include( [ - re_path( - r"^login/(?P