Skip to content

Commit

Permalink
create otp bypass tokens (#257)
Browse files Browse the repository at this point in the history
* test

* test

* validate pr refs

* reset api app

* add .venv

* add code checkers

* set cwd

* new deps

* remove unused config

* fix pylint_django bug

* quick save

* finalize

* move logic to signal

* feedback

* split run into separate files

* update test cases

* house keeping

* house keeping

* hi again florian

* Merge branch 'development' into enable_disable_otp

* house keeping

* otp bypass token view set

* remove todo

* Merge branch 'enable_disable_otp' into create_otp_bypass_tokens

* merge from dev

* generate otp bypass tokens

* py package v0.9.6
  • Loading branch information
SKairinos authored Jan 25, 2024
1 parent 558feaf commit cca63cf
Show file tree
Hide file tree
Showing 13 changed files with 190 additions and 21 deletions.
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 = "v0.9.5", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"}
codeforlife = {ref = "v0.9.6", 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
22 changes: 11 additions & 11 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/klass.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
"""
© Ocado Group
Created on 24/01/2024 at 12:14:21(+00:00).
"""

from codeforlife.user.serializers import ClassSerializer as _ClassSerializer


# pylint: disable-next=missing-class-docstring
class ClassSerializer(_ClassSerializer):
class Meta(_ClassSerializer.Meta):
pass
6 changes: 2 additions & 4 deletions backend/api/tests/views/test_auth_factor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@
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]
):
class TestAuthFactorViewSet(ModelViewSetTestCase[AuthFactor]):
basename = "auth-factor"
model_view_set_class = AuthFactorViewSet

def setUp(self):
self.one_factor_credentials = {
Expand Down
89 changes: 89 additions & 0 deletions backend/api/tests/views/test_otp_bypass_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""
© Ocado Group
Created on 24/01/2024 at 09:47:04(+00:00).
"""

from unittest.mock import patch

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

from ...views import OtpBypassTokenViewSet


# pylint: disable-next=missing-class-docstring
class TestOtpBypassTokenViewSet(ModelViewSetTestCase[OtpBypassToken]):
basename = "otp-bypass-token"
model_view_set_class = OtpBypassTokenViewSet

def setUp(self):
self.user = User.objects.get(email="[email protected]")
assert not self.user.otp_bypass_tokens.exists()

self.otp_bypass_tokens = OtpBypassToken.objects.bulk_create(
[
OtpBypassToken(user=self.user, token=token)
for token in OtpBypassToken.generate_tokens()
]
)

def test_generate(self):
"""
Generate max number of OTP bypass tokens.
"""

user = self.client.login_teacher(
email="[email protected]",
password="Password1",
)
assert user == self.user

tokens = {
"aaaaaaaa",
"bbbbbbbb",
"cccccccc",
"dddddddd",
"eeeeeeee",
"ffffffff",
"gggggggg",
"hhhhhhhh",
"iiiiiiii",
"jjjjjjjj",
}
assert len(tokens) == OtpBypassToken.max_count

with patch.object(
OtpBypassToken, "generate_tokens", return_value=tokens
) as generate_tokens:
response = self.client.post(
self.client.reverse("generate"),
status_code_assertion=status.HTTP_201_CREATED,
)

generate_tokens.assert_called_once()

# We received the expected tokens.
assert set(response.json()) == tokens

# The user's pre-existing tokens were deleted.
assert (
OtpBypassToken.objects.filter(
pk__in=[
otp_bypass_token.pk
for otp_bypass_token in self.otp_bypass_tokens
]
).count()
== 0
)

# The new tokens all check out.
for otp_bypass_token in OtpBypassToken.objects.filter(user=user):
found_token = False
for token in tokens.copy():
found_token = otp_bypass_token.check_token(token)
if found_token:
tokens.remove(token)
break

assert found_token
4 changes: 2 additions & 2 deletions backend/api/tests/views/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@
from codeforlife.tests import ModelViewSetTestCase
from codeforlife.user.models import User

from ...serializers import UserSerializer
from ...views import UserViewSet


class TestUserViewSet(ModelViewSetTestCase[UserViewSet, UserSerializer, User]):
class TestUserViewSet(ModelViewSetTestCase[User]):
"""
Base naming convention:
test_{action}
Expand All @@ -20,6 +19,7 @@ class TestUserViewSet(ModelViewSetTestCase[UserViewSet, UserSerializer, User]):
"""

basename = "user"
model_view_set_class = UserViewSet

def test_is_unique_email(self):
"""
Expand Down
13 changes: 12 additions & 1 deletion backend/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@

from rest_framework.routers import DefaultRouter

from .views import AuthFactorViewSet, ClassViewSet, SchoolViewSet, UserViewSet
from .views import (
AuthFactorViewSet,
ClassViewSet,
OtpBypassTokenViewSet,
SchoolViewSet,
UserViewSet,
)

router = DefaultRouter()
router.register(
Expand All @@ -18,6 +24,11 @@
ClassViewSet,
basename="class",
)
router.register(
"otp-bypass-tokens",
OtpBypassTokenViewSet,
basename="otp-bypass-token",
)
router.register(
"schools",
SchoolViewSet,
Expand Down
1 change: 1 addition & 0 deletions backend/api/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@

from .auth_factor import AuthFactorViewSet
from .klass import ClassViewSet
from .otp_bypass_token import OtpBypassTokenViewSet
from .school import SchoolViewSet
from .user import UserViewSet
4 changes: 2 additions & 2 deletions backend/api/views/auth_factor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@

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

from ..serializers import AuthFactorSerializer


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

Expand Down
6 changes: 6 additions & 0 deletions backend/api/views/klass.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
"""
© Ocado Group
Created on 23/01/2024 at 17:53:37(+00:00).
"""

from codeforlife.user.views import ClassViewSet as _ClassViewSet

from ..serializers import ClassSerializer


# pylint: disable-next=missing-class-docstring,too-many-ancestors
class ClassViewSet(_ClassViewSet):
http_method_names = ["get", "post", "patch", "delete"]
serializer_class = ClassSerializer
46 changes: 46 additions & 0 deletions backend/api/views/otp_bypass_token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""
© Ocado Group
Created on 23/01/2024 at 17:54:08(+00:00).
"""

import typing as t

from codeforlife.permissions import AllowNone
from codeforlife.request import Request
from codeforlife.user.models import OtpBypassToken, User
from codeforlife.user.permissions import IsTeacher
from codeforlife.views import ModelViewSet
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response


# pylint: disable-next=missing-class-docstring,too-many-ancestors
class OtpBypassTokenViewSet(ModelViewSet[OtpBypassToken]):
http_method_names = ["post"]

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

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

return [IsTeacher()]

@action(detail=False, methods=["post"])
def generate(self, request: Request):
"""Generates some OTP bypass tokens for a user."""

user = t.cast(User, request.user)

OtpBypassToken.objects.filter(user=user).delete()

tokens = OtpBypassToken.generate_tokens()

OtpBypassToken.objects.bulk_create(
[OtpBypassToken(user=user, token=token) for token in tokens]
)

return Response(tokens, status.HTTP_201_CREATED)
6 changes: 6 additions & 0 deletions backend/api/views/school.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
"""
© Ocado Group
Created on 23/01/2024 at 17:53:50(+00:00).
"""

from codeforlife.user.views import SchoolViewSet as _SchoolViewSet

from ..serializers import SchoolSerializer


# pylint: disable-next=missing-class-docstring,too-many-ancestors
class SchoolViewSet(_SchoolViewSet):
http_method_names = ["get", "post", "patch"]
serializer_class = SchoolSerializer
6 changes: 6 additions & 0 deletions backend/api/views/user.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
"""
© Ocado Group
Created on 23/01/2024 at 17:53:44(+00:00).
"""

import typing as t

from codeforlife.user.models import User
Expand All @@ -9,6 +14,7 @@
from ..serializers import UserSerializer


# pylint: disable-next=missing-class-docstring,too-many-ancestors
class UserViewSet(_UserViewSet):
http_method_names = ["get", "post", "patch", "delete"]
serializer_class = UserSerializer
Expand Down

0 comments on commit cca63cf

Please sign in to comment.