From c0ce515a4e5557c757e19dc8aa15ebb0437bc887 Mon Sep 17 00:00:00 2001 From: Stefan Kairinos Date: Wed, 19 Jun 2024 09:36:24 +0100 Subject: [PATCH 1/4] fix: auth flow (#120) * update paths * try entrypoint script * remove manage script * load fixtures command * fix: backends * allow anyone to get a CSRF cookie * rename session cookie * rename cookie --- codeforlife/settings/django.py | 12 +++--- codeforlife/user/auth/backends/__init__.py | 9 ++--- .../{email_and_password.py => email.py} | 2 +- ...nd_password_and_class_id.py => student.py} | 2 +- ...ser_id_and_login_id.py => student_auto.py} | 26 +++++++------ .../user/management/commands/load_fixtures.py | 38 +++++++++++++++++++ codeforlife/views/csrf.py | 3 ++ 7 files changed, 68 insertions(+), 24 deletions(-) rename codeforlife/user/auth/backends/{email_and_password.py => email.py} (94%) rename codeforlife/user/auth/backends/{first_name_and_password_and_class_id.py => student.py} (94%) rename codeforlife/user/auth/backends/{user_id_and_login_id.py => student_auto.py} (55%) create mode 100644 codeforlife/user/management/commands/load_fixtures.py diff --git a/codeforlife/settings/django.py b/codeforlife/settings/django.py index 35edba60..68a71c31 100644 --- a/codeforlife/settings/django.py +++ b/codeforlife/settings/django.py @@ -66,11 +66,11 @@ def get_databases(base_dir: Path): # pragma: no cover # https://docs.djangoproject.com/en/3.2/ref/settings/#authentication-backends AUTHENTICATION_BACKENDS = [ - "codeforlife.user.auth.backends.EmailAndPasswordBackend", + "codeforlife.user.auth.backends.EmailBackend", "codeforlife.user.auth.backends.OtpBackend", "codeforlife.user.auth.backends.OtpBypassTokenBackend", - "codeforlife.user.auth.backends.UserIdAndLoginIdBackend", - "codeforlife.user.auth.backends.FirstNameAndPasswordAndClassIdBackend", + "codeforlife.user.auth.backends.StudentBackend", + "codeforlife.user.auth.backends.StudentAutoBackend", ] # Sessions @@ -79,7 +79,7 @@ def get_databases(base_dir: Path): # pragma: no cover SESSION_ENGINE = "codeforlife.user.models.session" SESSION_SAVE_EVERY_REQUEST = True SESSION_EXPIRE_AT_BROWSER_CLOSE = True -SESSION_COOKIE_NAME = "sessionid_httponly_true" +SESSION_COOKIE_NAME = "session_key" SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_AGE = 60 * 60 SESSION_COOKIE_SECURE = True @@ -144,12 +144,12 @@ def get_databases(base_dir: Path): # pragma: no cover # URLs # https://docs.djangoproject.com/en/3.2/ref/settings/#root-urlconf -ROOT_URLCONF = "src.service.urls" +ROOT_URLCONF = "src.urls" # App # https://docs.djangoproject.com/en/3.2/ref/settings/#wsgi-application -WSGI_APPLICATION = "src.service.wsgi.application" +WSGI_APPLICATION = "main.app" # Password validation # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators diff --git a/codeforlife/user/auth/backends/__init__.py b/codeforlife/user/auth/backends/__init__.py index 8c7f34ae..8d1e5266 100644 --- a/codeforlife/user/auth/backends/__init__.py +++ b/codeforlife/user/auth/backends/__init__.py @@ -2,11 +2,10 @@ © Ocado Group Created on 01/02/2024 at 14:48:57(+00:00). """ + # TODO: Create a custom auth backend for Django admin permissions -from .email_and_password import EmailAndPasswordBackend -from .first_name_and_password_and_class_id import ( - FirstNameAndPasswordAndClassIdBackend, -) +from .email import EmailBackend from .otp import OtpBackend from .otp_bypass_token import OtpBypassTokenBackend -from .user_id_and_login_id import UserIdAndLoginIdBackend +from .student import StudentBackend +from .student_auto import StudentAutoBackend diff --git a/codeforlife/user/auth/backends/email_and_password.py b/codeforlife/user/auth/backends/email.py similarity index 94% rename from codeforlife/user/auth/backends/email_and_password.py rename to codeforlife/user/auth/backends/email.py index a7cb8650..58f215e7 100644 --- a/codeforlife/user/auth/backends/email_and_password.py +++ b/codeforlife/user/auth/backends/email.py @@ -9,7 +9,7 @@ from .base import BaseBackend -class EmailAndPasswordBackend(BaseBackend): +class EmailBackend(BaseBackend): """Authenticate a user by checking their email and password.""" def authenticate( # type: ignore[override] diff --git a/codeforlife/user/auth/backends/first_name_and_password_and_class_id.py b/codeforlife/user/auth/backends/student.py similarity index 94% rename from codeforlife/user/auth/backends/first_name_and_password_and_class_id.py rename to codeforlife/user/auth/backends/student.py index 387f1f11..aed1cb0b 100644 --- a/codeforlife/user/auth/backends/first_name_and_password_and_class_id.py +++ b/codeforlife/user/auth/backends/student.py @@ -10,7 +10,7 @@ from .base import BaseBackend -class FirstNameAndPasswordAndClassIdBackend(BaseBackend): +class StudentBackend(BaseBackend): """Authenticate a student using their first name, password and class ID.""" user_class = StudentUser diff --git a/codeforlife/user/auth/backends/user_id_and_login_id.py b/codeforlife/user/auth/backends/student_auto.py similarity index 55% rename from codeforlife/user/auth/backends/user_id_and_login_id.py rename to codeforlife/user/auth/backends/student_auto.py index 65651747..1d48e33e 100644 --- a/codeforlife/user/auth/backends/user_id_and_login_id.py +++ b/codeforlife/user/auth/backends/student_auto.py @@ -17,7 +17,7 @@ from .base import BaseBackend -class UserIdAndLoginIdBackend(BaseBackend): +class StudentAutoBackend(BaseBackend): """Authenticate a student using their ID and auto-generated password.""" user_class = StudentUser @@ -25,22 +25,26 @@ class UserIdAndLoginIdBackend(BaseBackend): def authenticate( # type: ignore[override] self, request: t.Optional[HttpRequest], - user_id: t.Optional[int] = None, - login_id: t.Optional[str] = None, + student_id: t.Optional[int] = None, + auto_gen_password: t.Optional[str] = None, **kwargs ): - if user_id is None or login_id is None: + if student_id is None or auto_gen_password is None: return None - user = self.get_user(user_id) - if user: + try: + student = Student.objects.get(id=student_id) + except Student.DoesNotExist: + student = None + + if student: + # TODO: refactor this # Check the url against the student's stored hash. - student = Student.objects.get(new_user=user) if ( - student.login_id - # TODO: refactor this - and get_hashed_login_id(login_id) == student.login_id + student.new_user + and student.login_id + and get_hashed_login_id(auto_gen_password) == student.login_id ): - return user + return student.new_user return None diff --git a/codeforlife/user/management/commands/load_fixtures.py b/codeforlife/user/management/commands/load_fixtures.py new file mode 100644 index 00000000..2346c933 --- /dev/null +++ b/codeforlife/user/management/commands/load_fixtures.py @@ -0,0 +1,38 @@ +""" +© Ocado Group +Created on 10/06/2024 at 10:44:45(+01:00). +""" + +import os +import typing as t + +from django.apps import apps +from django.core.management import call_command +from django.core.management.base import BaseCommand + + +# pylint: disable-next=missing-class-docstring +class Command(BaseCommand): + help = "Loads all the fixtures of the specified apps." + + def add_arguments(self, parser): + parser.add_argument("app_labels", nargs="*", type=str) + + def handle(self, *args, **options): + fixture_labels: t.List[str] = [] + for app_label in {*options["app_labels"], "user"}: + app_config = apps.app_configs[app_label] + fixtures_path = os.path.join(app_config.path, "fixtures") + + self.stdout.write(f"{app_label} fixtures ({fixtures_path}):") + for fixture_label in os.listdir(fixtures_path): + if fixture_label in fixture_labels: + self.stderr.write(f"Duplicate fixture: {fixture_label}") + return + + self.stdout.write(f" - {fixture_label}") + fixture_labels.append(fixture_label) + + self.stdout.write() + + call_command("loaddata", *fixture_labels) diff --git a/codeforlife/views/csrf.py b/codeforlife/views/csrf.py index be1ed404..8a149cf5 100644 --- a/codeforlife/views/csrf.py +++ b/codeforlife/views/csrf.py @@ -9,11 +9,14 @@ from rest_framework.response import Response from rest_framework.views import APIView +from ..permissions import AllowAny + class CookieView(APIView): """A view to get a CSRF cookie.""" http_method_names = ["get"] + permission_classes = [AllowAny] @method_decorator(ensure_csrf_cookie) def get(self, request: Request): From 06bbc5b18422b6952c72ce561f0988fc4e35e915 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Wed, 19 Jun 2024 08:38:56 +0000 Subject: [PATCH 2/4] 0.16.11 Automatically generated by python-semantic-release --- CHANGELOG.md | 6 ++++++ codeforlife/version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bfb53a0..abf55124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ +## v0.16.11 (2024-06-19) + +### Fix + +* Auth flow ([#120](https://github.com/ocadotechnology/codeforlife-package-python/issues/120)) ([`c0ce515`](https://github.com/ocadotechnology/codeforlife-package-python/commit/c0ce515a4e5557c757e19dc8aa15ebb0437bc887)) + ## v0.16.10 (2024-06-07) ### Fix diff --git a/codeforlife/version.py b/codeforlife/version.py index 377c1c27..1bdc7440 100644 --- a/codeforlife/version.py +++ b/codeforlife/version.py @@ -5,4 +5,4 @@ # Do NOT set manually! # This is auto-updated by python-semantic-release in the pipeline. -__version__ = "0.16.10" +__version__ = "0.16.11" From 1e4855c5b1c7c7d4a2dfac9b29901efe50e654f5 Mon Sep 17 00:00:00 2001 From: SKairinos Date: Wed, 19 Jun 2024 15:32:13 +0000 Subject: [PATCH 3/4] fix: override logout view --- codeforlife/urls.py | 5 ++--- codeforlife/views/__init__.py | 2 +- codeforlife/views/{csrf.py => common.py} | 11 ++++++++++- 3 files changed, 13 insertions(+), 5 deletions(-) rename codeforlife/views/{csrf.py => common.py} (66%) diff --git a/codeforlife/urls.py b/codeforlife/urls.py index 07394c28..886b08a0 100644 --- a/codeforlife/urls.py +++ b/codeforlife/urls.py @@ -6,13 +6,12 @@ import typing as t from django.contrib import admin -from django.contrib.auth.views import LogoutView from django.http import HttpResponse from django.urls import URLPattern, URLResolver, include, path, re_path from rest_framework import status from .settings import SERVICE_IS_ROOT, SERVICE_NAME -from .views import csrf +from .views import CsrfCookieView, LogoutView UrlPatterns = t.List[t.Union[URLResolver, URLPattern]] @@ -39,7 +38,7 @@ def get_urlpatterns( ), path( "api/csrf/cookie/", - csrf.CookieView.as_view(), + CsrfCookieView.as_view(), name="get-csrf-cookie", ), path( diff --git a/codeforlife/views/__init__.py b/codeforlife/views/__init__.py index 149f5b39..1822c7e9 100644 --- a/codeforlife/views/__init__.py +++ b/codeforlife/views/__init__.py @@ -4,6 +4,6 @@ """ from .api import APIView -from .csrf import CookieView +from .common import CsrfCookieView, LogoutView from .decorators import action, cron_job from .model import ModelViewSet diff --git a/codeforlife/views/csrf.py b/codeforlife/views/common.py similarity index 66% rename from codeforlife/views/csrf.py rename to codeforlife/views/common.py index 8a149cf5..ac84de68 100644 --- a/codeforlife/views/csrf.py +++ b/codeforlife/views/common.py @@ -3,6 +3,8 @@ Created on 12/04/2024 at 16:51:36(+01:00). """ +from django.contrib.auth.views import LogoutView as _LogoutView +from django.http import JsonResponse from django.utils.decorators import method_decorator from django.views.decorators.csrf import ensure_csrf_cookie from rest_framework.request import Request @@ -12,7 +14,7 @@ from ..permissions import AllowAny -class CookieView(APIView): +class CsrfCookieView(APIView): """A view to get a CSRF cookie.""" http_method_names = ["get"] @@ -24,3 +26,10 @@ def get(self, request: Request): Return a response which Django will auto-insert a CSRF cookie into. """ return Response() + + +class LogoutView(_LogoutView): + """Override Django's logout view to always return a JSON response.""" + + def render_to_response(self, context, **response_kwargs): + return JsonResponse({}) From 567b2377019dedcdad5c0651c6d0bd576dd65759 Mon Sep 17 00:00:00 2001 From: semantic-release Date: Wed, 19 Jun 2024 15:35:13 +0000 Subject: [PATCH 4/4] 0.16.12 Automatically generated by python-semantic-release --- CHANGELOG.md | 6 ++++++ codeforlife/version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abf55124..b74f6f31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ +## v0.16.12 (2024-06-19) + +### Fix + +* Override logout view ([`1e4855c`](https://github.com/ocadotechnology/codeforlife-package-python/commit/1e4855c5b1c7c7d4a2dfac9b29901efe50e654f5)) + ## v0.16.11 (2024-06-19) ### Fix diff --git a/codeforlife/version.py b/codeforlife/version.py index 1bdc7440..b96ed5ea 100644 --- a/codeforlife/version.py +++ b/codeforlife/version.py @@ -5,4 +5,4 @@ # Do NOT set manually! # This is auto-updated by python-semantic-release in the pipeline. -__version__ = "0.16.11" +__version__ = "0.16.12"