Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: view property, type hints, linting, email case insensitive search #68

Merged
merged 1 commit into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions codeforlife/serializers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import typing as t

from django.contrib.auth.models import AnonymousUser
from django.views import View
from rest_framework.serializers import BaseSerializer as _BaseSerializer

from ..request import Request
Expand Down Expand Up @@ -39,3 +40,9 @@ def request_anon_user(self):
"""

return t.cast(AnonymousUser, self.request.user)

@property
def view(self):
"""The view that instantiated this serializer."""

return t.cast(View, self.context["view"])
33 changes: 29 additions & 4 deletions codeforlife/serializers/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,26 @@ class ModelSerializer(
):
"""Base model serializer for all model serializers."""

instance: AnyModel

@property
def view(self):
# NOTE: import outside top-level to avoid circular imports.
# pylint: disable-next=import-outside-toplevel
from ..views import ModelViewSet

return t.cast(ModelViewSet[AnyModel], super().view)

# pylint: disable-next=useless-parent-delegation
def update(self, instance, validated_data: t.Dict[str, t.Any]):
def update(
self,
instance: AnyModel,
validated_data: t.Dict[str, t.Any],
) -> AnyModel:
return super().update(instance, validated_data)

# pylint: disable-next=useless-parent-delegation
def create(self, validated_data: t.Dict[str, t.Any]):
def create(self, validated_data: t.Dict[str, t.Any]) -> AnyModel:
return super().create(validated_data)

def validate(self, attrs: t.Dict[str, t.Any]):
Expand Down Expand Up @@ -61,6 +75,14 @@ class Meta:
instance: t.List[AnyModel]
batch_size: t.Optional[int] = None

@property
def view(self):
# NOTE: import outside top-level to avoid circular imports.
# pylint: disable-next=import-outside-toplevel
from ..views import ModelViewSet

return t.cast(ModelViewSet[AnyModel], super().view)

@classmethod
def get_model_class(cls) -> t.Type[AnyModel]:
"""Get the model view set's class.
Expand All @@ -74,7 +96,10 @@ def get_model_class(cls) -> t.Type[AnyModel]:
0
]

def create(self, validated_data: t.List[t.Dict[str, t.Any]]):
def create(
self,
validated_data: t.List[t.Dict[str, t.Any]],
) -> t.List[AnyModel]:
"""Bulk create many instances of a model.

https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-create
Expand All @@ -96,7 +121,7 @@ def update(
self,
instance: t.List[AnyModel],
validated_data: t.List[t.Dict[str, t.Any]],
):
) -> t.List[AnyModel]:
"""Bulk update many instances of a model.

https://www.django-rest-framework.org/api-guide/serializers/#customizing-multiple-update
Expand Down
5 changes: 5 additions & 0 deletions codeforlife/user/auth/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
"""
© Ocado Group
Created on 01/02/2024 at 14:48:57(+00:00).
"""

from .email_and_password import EmailAndPasswordBackend
from .otp import OtpBackend
from .otp_bypass_token import OtpBypassTokenBackend
Expand Down
23 changes: 16 additions & 7 deletions codeforlife/user/auth/backends/email_and_password.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,40 @@
"""
© Ocado Group
Created on 01/02/2024 at 14:34:04(+00:00).
"""

import typing as t

from django.contrib.auth.backends import BaseBackend

from ....request import WSGIRequest
from ....request import HttpRequest
from ...models import User


class EmailAndPasswordBackend(BaseBackend):
def authenticate(
"""Authenticate a user by checking their email and password."""

def authenticate( # type: ignore[override]
self,
request: WSGIRequest,
request: t.Optional[HttpRequest],
email: t.Optional[str] = None,
password: t.Optional[str] = None,
**kwargs
):
if email is None or password is None:
return
return None

try:
user = User.objects.get(email=email)
user = User.objects.get(email__iexact=email)
if user.check_password(password):
return user
except User.DoesNotExist:
return
return None

return None

def get_user(self, user_id: int):
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return
return None
22 changes: 16 additions & 6 deletions codeforlife/user/auth/backends/otp.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
"""
© Ocado Group
Created on 01/02/2024 at 14:41:20(+00:00).
"""

import typing as t

import pyotp
from django.contrib.auth.backends import BaseBackend
from django.utils import timezone

from ....request import WSGIRequest
from ....request import HttpRequest
from ...models import AuthFactor, User


class OtpBackend(BaseBackend):
def authenticate(
"""Check a user's multi-factor OTP authentication."""

def authenticate( # type: ignore[override]
self,
request: WSGIRequest,
request: t.Optional[HttpRequest],
otp: t.Optional[str] = None,
**kwargs,
):
Expand All @@ -20,13 +27,14 @@ def authenticate(

if (
otp is None
or request is None
or not isinstance(request.user, User)
or not request.user.userprofile.otp_secret
or not request.user.session.session_auth_factors.filter(
auth_factor__type=AuthFactor.Type.OTP
).exists()
):
return
return None

totp = pyotp.TOTP(request.user.userprofile.otp_secret)

Expand All @@ -35,7 +43,7 @@ def authenticate(
# Deny replay attacks by rejecting the otp for last time.
last_otp_for_time = request.user.userprofile.last_otp_for_time
if last_otp_for_time and totp.verify(otp, last_otp_for_time):
return
return None
request.user.userprofile.last_otp_for_time = now
request.user.userprofile.save()

Expand All @@ -46,8 +54,10 @@ def authenticate(

return request.user

return None

def get_user(self, user_id: int):
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return
return None
22 changes: 17 additions & 5 deletions codeforlife/user/auth/backends/otp_bypass_token.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
"""
© Ocado Group
Created on 01/02/2024 at 14:39:16(+00:00).
"""

import typing as t

from django.contrib.auth.backends import BaseBackend

from ....request import WSGIRequest
from ....request import HttpRequest
from ...models import AuthFactor, User


class OtpBypassTokenBackend(BaseBackend):
def authenticate(
"""
Bypass a user's multi-factor OTP authentication with a single-use token.
"""

def authenticate( # type: ignore[override]
self,
request: WSGIRequest,
request: t.Optional[HttpRequest],
token: t.Optional[str] = None,
**kwargs,
):
if (
token is None
or request is None
or not isinstance(request.user, User)
or not request.user.session.session_auth_factors.filter(
auth_factor__type=AuthFactor.Type.OTP
).exists()
):
return
return None

for otp_bypass_token in request.user.otp_bypass_tokens.all():
if otp_bypass_token.check_token(token):
Expand All @@ -31,8 +41,10 @@ def authenticate(

return request.user

return None

def get_user(self, user_id: int):
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return
return None
19 changes: 14 additions & 5 deletions codeforlife/user/auth/backends/user_id_and_login_id.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
"""
© Ocado Group
Created on 01/02/2024 at 14:44:16(+00:00).
"""

import typing as t

from common.helpers.generators import get_hashed_login_id
from common.models import Student
from django.contrib.auth.backends import BaseBackend

from ....request import WSGIRequest
from ....request import HttpRequest
from ...models import User


class UserIdAndLoginIdBackend(BaseBackend):
def authenticate(
"""Authenticate a student using their ID and auto-generated password."""

def authenticate( # type: ignore[override]
self,
request: WSGIRequest,
request: t.Optional[HttpRequest],
user_id: t.Optional[int] = None,
login_id: t.Optional[str] = None,
**kwargs
):
if user_id is None or login_id is None:
return
return None

user = self.get_user(user_id)
if user:
Expand All @@ -30,8 +37,10 @@ def authenticate(
):
return user

return None

def get_user(self, user_id: int):
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return
return None
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
"""
© Ocado Group
Created on 01/02/2024 at 14:48:17(+00:00).
"""

import typing as t

from django.contrib.auth.backends import BaseBackend

from ....request import WSGIRequest
from ....request import HttpRequest
from ...models import User


class UsernameAndPasswordAndClassIdBackend(BaseBackend):
def authenticate(
"""Authenticate a student using their username, password and class ID."""

def authenticate( # type: ignore[override]
self,
request: WSGIRequest,
request: t.Optional[HttpRequest],
username: t.Optional[str] = None,
password: t.Optional[str] = None,
class_id: t.Optional[str] = None,
**kwargs
):
if username is None or password is None or class_id is None:
return
return None

try:
user = User.objects.get(
Expand All @@ -26,10 +33,12 @@ def authenticate(
if user.check_password(password):
return user
except User.DoesNotExist:
return
return None

return None

def get_user(self, user_id: int):
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return
return None
Loading