Skip to content

Commit

Permalink
hi again florian
Browse files Browse the repository at this point in the history
  • Loading branch information
SKairinos committed Jan 23, 2024
1 parent 0602d6b commit 7061b74
Show file tree
Hide file tree
Showing 12 changed files with 258 additions and 7 deletions.
51 changes: 51 additions & 0 deletions .vscode/workspace.code-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"module.docstring": {
"prefix": [
"module.docstring",
"\"\"\"",
"'''"
],
"scope": "python",
"body": [
"\"\"\"",
"© Ocado Group",
"Created on $CURRENT_DATE/$CURRENT_MONTH/$CURRENT_YEAR at $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND($CURRENT_TIMEZONE_OFFSET)."
"",
"${1:__description__}",
"\"\"\""
]
},
"module.doccomment": {
"prefix": [
"module.doccomment",
"/"
],
"scope": "javascript,typescript,javascriptreact,typescriptreact",
"body": [
"/**",
" * © Ocado Group",
" * Created on $CURRENT_DATE/$CURRENT_MONTH/$CURRENT_YEAR at $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND($CURRENT_TIMEZONE_OFFSET)."
" *",
" * ${1:__description__}",
" */"
]
},
"pylint.disable-next": {
"prefix": [
"# pylint"
],
"scope": "python",
"body": [
"# pylint: disable-next=${1:__code_name__}"
]
},
"mypy.ignore": {
"prefix": [
"# type"
],
"scope": "python",
"body": [
"# type: ignore[${1:__code_name__}]"
]
}
}
2 changes: 1 addition & 1 deletion backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ google-cloud-logging = "==1.*"
google-auth = "==2.*"
google-cloud-container = "==2.3.0"
"django-anymail[amazon_ses]" = "==7.0.*"
codeforlife = {ref = "code-checkers", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
codeforlife = {ref = "enable_disable_otp", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
django = "==3.2.20"
djangorestframework = "==3.13.1"
django-cors-headers = "==4.1.0"
Expand Down
4 changes: 2 additions & 2 deletions backend/Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions backend/api/serializers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
"""
© Ocado Group
Created on 23/01/2024 at 16:13:13(+00:00).
"""

from .auth_factor import AuthFactorSerializer
from .klass import ClassSerializer
from .school import SchoolSerializer
from .user import UserSerializer
26 changes: 26 additions & 0 deletions backend/api/serializers/auth_factor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""
© Ocado Group
Created on 23/01/2024 at 11:05:37(+00:00).
"""

from codeforlife.request import Request
from codeforlife.serializers import ModelSerializer
from codeforlife.user.models import AuthFactor


# pylint: disable-next=missing-class-docstring
class AuthFactorSerializer(ModelSerializer[AuthFactor]):
class Meta:
model = AuthFactor
fields = [
"id",
"type",
]
extra_kwargs = {
"id": {"read_only": True},
}

def create(self, validated_data):
request: Request = self.context["request"]
validated_data["user"] = request.user
return super().create(validated_data)
6 changes: 6 additions & 0 deletions backend/api/serializers/school.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"""
© Ocado Group
Created on 23/01/2024 at 11:05:41(+00:00).
"""

from codeforlife.user.serializers import SchoolSerializer as _SchoolSerializer


# pylint: disable-next=missing-class-docstring
class SchoolSerializer(_SchoolSerializer):
class Meta(_SchoolSerializer.Meta):
pass
17 changes: 15 additions & 2 deletions backend/api/signals/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,27 @@
All signals for the User model.
"""

from codeforlife.models.signals.pre_save import previous_values_are_unequal
from codeforlife.user.models import User
from codeforlife.models.signals.pre_save import (
previous_values_are_unequal,
was_created,
)
from codeforlife.user.models import User, UserProfile
from django.db.models.signals import post_save, pre_save
from django.dispatch import receiver
import pyotp

# pylint: disable=unused-argument


@receiver(pre_save, sender=UserProfile)
def user__pre_save__otp_secret(sender, instance: UserProfile, *args, **kwargs):
"""Set the OTP secret for new users."""

# TODO: move this to User.otp_secret.default when restructuring.
if not was_created(instance):
instance.otp_secret = pyotp.random_base32()


@receiver(pre_save, sender=User)
def user__pre_save__email(sender, instance: User, *args, **kwargs):
"""Before a user's email field is updated."""
Expand Down
19 changes: 18 additions & 1 deletion backend/api/tests/signals/test_user.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
from codeforlife.user.models import User
from codeforlife.user.models import User, UserProfile
from django.test import TestCase


class TestUser(TestCase):
def test_pre_save__otp_secret(self):
"""
Creating a new user sets their OTP secret.
"""

user = User.objects.create_user(
username="[email protected]",
email="[email protected]",
password="password",
first_name="John",
last_name="Doe",
)

profile = UserProfile.objects.create(user=user)

assert profile.otp_secret is not None

def test_pre_save__email(self):
"""
Updating the email field also updates the username field.
Expand Down
94 changes: 94 additions & 0 deletions backend/api/tests/views/test_auth_factor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""
© Ocado Group
Created on 23/01/2024 at 11:22:16(+00:00).
"""

from codeforlife.tests import ModelViewSetTestCase
from codeforlife.user.models import AuthFactor, User, UserProfile
from rest_framework import status

from ...serializers import AuthFactorSerializer
from ...views import AuthFactorViewSet


# pylint: disable-next=missing-class-docstring
class TestAuthFactorViewSet(
ModelViewSetTestCase[AuthFactorViewSet, AuthFactorSerializer, AuthFactor]
):
basename = "auth-factor"

def setUp(self):
self.one_factor_credentials = {
"email": "[email protected]",
"password": "password",
}
self.one_factor_user = User.objects.create_user(
first_name="One",
last_name="Factor",
username=self.one_factor_credentials["email"],
**self.one_factor_credentials,
)
UserProfile.objects.create(user=self.one_factor_user)

self.two_factor_credentials = {
"email": "[email protected]",
"password": "password",
}
self.two_factor_user = User.objects.create_user(
first_name="Two",
last_name="Factor",
username=self.two_factor_credentials["email"],
**self.two_factor_credentials,
)
UserProfile.objects.create(user=self.two_factor_user)
self.two_auth_factor = AuthFactor.objects.create(
user=self.two_factor_user,
type=AuthFactor.Type.OTP,
)

def test_retrieve(self):
"""
Retrieving a single auth factor is forbidden.
"""

user = self.client.login(**self.two_factor_credentials)
assert user == self.two_factor_user

self.client.retrieve(self.two_auth_factor, status.HTTP_403_FORBIDDEN)

def test_list(self):
"""
Can list enabled auth-factors.
"""

user = self.client.login(**self.two_factor_credentials)
assert user == self.two_factor_user

# Need to have another two auth-factor user to ensure some data is
# filtered out.
AuthFactor.objects.create(
user=self.one_factor_user,
type=AuthFactor.Type.OTP,
)

self.client.list([self.two_auth_factor])

def test_create(self):
"""
Can enable a auth-factor.
"""

user = self.client.login(**self.one_factor_credentials)
assert user == self.one_factor_user

self.client.create({"type": "otp"})

def test_destroy(self):
"""
Can disable a auth-factor.
"""

user = self.client.login(**self.two_factor_credentials)
assert user == self.two_factor_user

self.client.destroy(self.two_auth_factor)
7 changes: 6 additions & 1 deletion backend/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@

from rest_framework.routers import DefaultRouter

from .views import ClassViewSet, SchoolViewSet, UserViewSet
from .views import AuthFactorViewSet, ClassViewSet, SchoolViewSet, UserViewSet

router = DefaultRouter()
router.register(
"auth-factors",
AuthFactorViewSet,
basename="auth-factor",
)
router.register(
"classes",
ClassViewSet,
Expand Down
6 changes: 6 additions & 0 deletions backend/api/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
"""
© Ocado Group
Created on 23/01/2024 at 16:13:58(+00:00).
"""

from .auth_factor import AuthFactorViewSet
from .klass import ClassViewSet
from .school import SchoolViewSet
from .user import UserViewSet
27 changes: 27 additions & 0 deletions backend/api/views/auth_factor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""
© Ocado Group
Created on 23/01/2024 at 11:04:44(+00:00).
"""

from codeforlife.permissions import AllowNone
from codeforlife.user.models import AuthFactor, User
from rest_framework.viewsets import ModelViewSet

from ..serializers import AuthFactorSerializer


# pylint: disable-next=missing-class-docstring,too-many-ancestors
class AuthFactorViewSet(ModelViewSet):
http_method_names = ["get", "post", "delete"]
serializer_class = AuthFactorSerializer
# permission_classes = [IsAuthenticated, ListOnly] TODO

def get_queryset(self):
user: User = self.request.user # type: ignore[assignment]
return AuthFactor.objects.filter(user=user)

def get_permissions(self):
if self.action == "retrieve":
return [AllowNone()]

return super().get_permissions()

0 comments on commit 7061b74

Please sign in to comment.