From c0ce515a4e5557c757e19dc8aa15ebb0437bc887 Mon Sep 17 00:00:00 2001 From: Stefan Kairinos Date: Wed, 19 Jun 2024 09:36:24 +0100 Subject: [PATCH] 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):