From 558feaffbd2060161dc83a65904e97267db3c06a Mon Sep 17 00:00:00 2001 From: Stefan Kairinos Date: Wed, 24 Jan 2024 12:03:35 +0000 Subject: [PATCH] Enable disable otp (#256) * 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 * remove todo * remove spaces * remove more spaces * feedback and rename scripts --- .vscode/workspace.code-snippets | 51 +++++++++++ backend/Pipfile | 4 +- backend/Pipfile.lock | 59 +++++++++---- backend/api/serializers/__init__.py | 6 ++ backend/api/serializers/auth_factor.py | 26 ++++++ backend/api/serializers/school.py | 6 ++ backend/api/signals/user.py | 17 +++- backend/api/tests/signals/test_user.py | 21 ++++- backend/api/tests/views/test_auth_factor.py | 94 +++++++++++++++++++++ backend/api/urls.py | 7 +- backend/api/views/__init__.py | 6 ++ backend/api/views/auth_factor.py | 26 ++++++ run.sh => run | 0 setup.sh => setup | 0 14 files changed, 300 insertions(+), 23 deletions(-) create mode 100644 .vscode/workspace.code-snippets create mode 100644 backend/api/serializers/auth_factor.py create mode 100644 backend/api/tests/views/test_auth_factor.py create mode 100644 backend/api/views/auth_factor.py rename run.sh => run (100%) rename setup.sh => setup (100%) diff --git a/.vscode/workspace.code-snippets b/.vscode/workspace.code-snippets new file mode 100644 index 00000000..3e6a570c --- /dev/null +++ b/.vscode/workspace.code-snippets @@ -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__}]" + ] + } +} \ No newline at end of file diff --git a/backend/Pipfile b/backend/Pipfile index 58685af1..986d8ff6 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -27,8 +27,8 @@ django-import-export = "*" 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"} +# "django-anymail[amazon_ses]" = "==7.0.*" +codeforlife = {ref = "v0.9.5", git = "https://github.com/ocadotechnology/codeforlife-package-python.git"} django = "==3.2.20" djangorestframework = "==3.13.1" django-cors-headers = "==4.1.0" diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 777302f2..17388bae 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "036a427d2cf39db6e2ffac5b0faff4939024e5ef4c7e42399d55f56f88f59285" + "sha256": "f13456df29f1ee78415489207b55fc24d23c5097ecb75808bdb142f8433fe9d6" }, "pipfile-spec": 6, "requires": { @@ -170,7 +170,7 @@ }, "codeforlife": { "git": "https://github.com/ocadotechnology/codeforlife-package-python.git", - "ref": "efe62bc039d28965727909213887316592c41bf1" + "ref": "77d64137bebb6e53814e204bfa97e68092185fe2" }, "codeforlife-portal": { "hashes": [ @@ -202,19 +202,9 @@ "sha256:dec2a116787b8e14962014bf78e120bba454135108e1af9e9b91ade7b2964c40" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==3.2.20" }, - "django-anymail": { - "extras": [ - "amazon-ses" - ], - "hashes": [ - "sha256:7930d5f841c9be7e044a9e6bf3492aedf7aa641716b6c1f8f52411658f674131", - "sha256:dfa4a00a1608d40893cf818ed3632046a0bb01cf2a2bb3a64d31ff146151533a" - ], - "index": "pypi", - "version": "==7.0.0" - }, "django-classy-tags": { "hashes": [ "sha256:25eb4f95afee396148683bfb4811b83b3f5729218d73ad0a3399271a6f9fcc49", @@ -229,6 +219,7 @@ "sha256:88a4bfae24b6404dd0e0640203cb27704a2a57fd546a429e5d821dfa53dd1acf" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.1.0" }, "django-countries": { @@ -269,6 +260,7 @@ "sha256:b1b7385627ed61063cd9764e8c19cce3ce8945626f7953262df8162b0feec376" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.3.6" }, "django-js-reverse": { @@ -315,6 +307,7 @@ "sha256:857e797f23de948b204a31dba9d88aea3ce731b7a5d926d0240c772e19b5486f" ], "index": "pypi", + "markers": "python_version >= '3.4'", "version": "==3.0.1" }, "django-recaptcha": { @@ -352,6 +345,7 @@ "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==3.13.1" }, "dnspython": { @@ -402,6 +396,7 @@ "sha256:97327dbbf58cccb58fc5a1712bba403ae76668e64814eb30f7316f7e27126b81" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.26.2" }, "google-cloud-container": { @@ -410,6 +405,7 @@ "sha256:2d5365a1d8679573bd27446ee8cb0c2174fc559a441869a2b930635231049d0b" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==2.3.0" }, "google-cloud-core": { @@ -426,9 +422,13 @@ "sha256:d9fcb0f5807931ed305d46079ccca8301775bee7239dd9ccc388922724d8e726" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.3" }, "googleapis-common-protos": { + "extras": [ + "grpc" + ], "hashes": [ "sha256:4750113612205514f9f6aa4cb00d523a94f3e8c06c5ad2fee466387dc4875f07", "sha256:83f0ece9f94e5672cced82f592d2a5edf527a96ed1794f0bab36d5735c996277" @@ -596,6 +596,7 @@ "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.13.0" }, "itsdangerous": { @@ -669,6 +670,7 @@ "sha256:f1efc1b612299c88aec9e39d6ca0c266d360daa5b19d9430bdeaffffa86993f9" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==0.22.0" }, "markuppy": { @@ -749,6 +751,7 @@ "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713" ], "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==8.7.0" }, "mypy-extensions": { @@ -1110,6 +1113,7 @@ "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==5.4.1" }, "qrcode": { @@ -1177,6 +1181,7 @@ "sha256:ff09a0a1e5cef05309ac09dfc5185e8151d927bcf45470d2f540c96260f8a355" ], "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4'", "version": "==3.6.13" }, "requests": { @@ -1185,6 +1190,7 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "requests-oauthlib": { @@ -1232,6 +1238,7 @@ "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c" ], "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==0.4.4" }, "tablib": { @@ -1302,6 +1309,7 @@ "sha256:16468e9ad2189f09f4a8c635a9031cc9bb2cdbc8e5e53365407acf99f7ade9ec" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==6.5.0" }, "xlrd": { @@ -1371,6 +1379,7 @@ "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==23.12.1" }, "certifi": { @@ -1576,6 +1585,7 @@ "sha256:dec2a116787b8e14962014bf78e120bba454135108e1af9e9b91ade7b2964c40" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==3.2.20" }, "django-import-export": { @@ -1584,6 +1594,7 @@ "sha256:b1b7385627ed61063cd9764e8c19cce3ce8945626f7953262df8162b0feec376" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.3.6" }, "django-selenium-clean": { @@ -1602,7 +1613,7 @@ "sha256:2fcd257884a68dfa02de41ee5410ec805264d9b07d9b5b119e4dea82c7b8345e", "sha256:e60b43de662a199db4b15c803c06669e0ac5035614af291cbd3b91591f7dcc94" ], - "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==4.2.6" }, "django-stubs-ext": { @@ -1619,6 +1630,7 @@ "sha256:9e8b9b4364fef70dde10a5f85c5a75d447ca2189ec648325610fab1268daec97" ], "index": "pypi", + "markers": "python_version >= '3.6' and python_version < '4.0'", "version": "==1.2.0" }, "djangorestframework-stubs": { @@ -1629,7 +1641,7 @@ "sha256:5be8275dd05d6629b3d1688929586ef7b6bc66b4f3f728b5e0389305f07c7a7f", "sha256:8ee8719bfeb647b92cc200e15b3cc9813d2e4468c8190777a55a121542a4b2d4" ], - "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.14.4" }, "et-xmlfile": { @@ -1685,6 +1697,7 @@ "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" ], "index": "pypi", + "markers": "python_full_version >= '3.8.0'", "version": "==5.13.2" }, "markuppy": { @@ -1732,6 +1745,7 @@ "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==1.6.1" }, "mypy-extensions": { @@ -1795,6 +1809,7 @@ "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda" ], "index": "pypi", + "markers": "python_full_version >= '3.8.0'", "version": "==3.0.2" }, "pylint-django": { @@ -1803,6 +1818,7 @@ "sha256:5abd5c2228e0e5e2a4cb6d0b4fc1d1cef1e773d0be911412f4dd4fc1a1a440b7" ], "index": "pypi", + "markers": "python_version >= '3.7' and python_version < '4.0'", "version": "==2.5.5" }, "pylint-plugin-utils": { @@ -1818,7 +1834,7 @@ "sha256:6315e7a6bd49afd695925c243d72876e2dbfb774ecd551b3115e87d18df29599", "sha256:9122a6441f6c10b92b0262a95f9c04f552fe4498f26e7dc6e3d26fc0a58153ce" ], - "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==2.10.6" }, "pytest": { @@ -1827,6 +1843,7 @@ "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==7.4.4" }, "pytest-cov": { @@ -1835,6 +1852,7 @@ "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==4.1.0" }, "pytest-django": { @@ -1843,6 +1861,7 @@ "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2" ], "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==4.5.2" }, "pytest-env": { @@ -1851,6 +1870,7 @@ "sha256:baed9b3b6bae77bd75b9238e0ed1ee6903a42806ae9d6aeffb8754cd5584d4ff" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.8.2" }, "pytest-mock": { @@ -1859,6 +1879,7 @@ "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9" ], "index": "pypi", + "markers": "python_version >= '3.8'", "version": "==3.12.0" }, "pytest-order": { @@ -1867,6 +1888,7 @@ "sha256:9d65c3b6dc6d6ee984d6ae2c6c4aa4f1331e5b915116219075c888c8bcbb93b8" ], "index": "pypi", + "markers": "python_version >= '3.6'", "version": "==1.2.0" }, "pytest-xdist": { @@ -1875,6 +1897,7 @@ "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==3.5.0" }, "pytz": { @@ -1925,6 +1948,7 @@ "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", "version": "==5.4.1" }, "requests": { @@ -1933,6 +1957,7 @@ "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==2.31.0" }, "responses": { @@ -1941,6 +1966,7 @@ "sha256:380cad4c1c1dc942e5e8a8eaae0b4d4edf708f4f010db8b7bcfafad1fcd254ff" ], "index": "pypi", + "markers": "python_version >= '3.7'", "version": "==0.18.0" }, "selenium": { @@ -1973,6 +1999,7 @@ "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c" ], "index": "pypi", + "markers": "python_version >= '3.5'", "version": "==0.4.4" }, "tablib": { diff --git a/backend/api/serializers/__init__.py b/backend/api/serializers/__init__.py index d577449b..94753e37 100644 --- a/backend/api/serializers/__init__.py +++ b/backend/api/serializers/__init__.py @@ -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 diff --git a/backend/api/serializers/auth_factor.py b/backend/api/serializers/auth_factor.py new file mode 100644 index 00000000..e04f5299 --- /dev/null +++ b/backend/api/serializers/auth_factor.py @@ -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) diff --git a/backend/api/serializers/school.py b/backend/api/serializers/school.py index 6047a143..1930bbe5 100644 --- a/backend/api/serializers/school.py +++ b/backend/api/serializers/school.py @@ -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 diff --git a/backend/api/signals/user.py b/backend/api/signals/user.py index 9b20adb3..26578dee 100644 --- a/backend/api/signals/user.py +++ b/backend/api/signals/user.py @@ -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 +import pyotp +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 # 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.""" diff --git a/backend/api/tests/signals/test_user.py b/backend/api/tests/signals/test_user.py index 5a98040d..c44c7041 100644 --- a/backend/api/tests/signals/test_user.py +++ b/backend/api/tests/signals/test_user.py @@ -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="john.doe@codeforlife.com", + email="john.doe@codeforlife.com", + 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. @@ -23,4 +40,4 @@ def test_post_save__email(self): Updating the email field sends a verification email. """ - raise NotImplementedError() # TODO + raise NotImplementedError() # TODO: implement diff --git a/backend/api/tests/views/test_auth_factor.py b/backend/api/tests/views/test_auth_factor.py new file mode 100644 index 00000000..78ae6044 --- /dev/null +++ b/backend/api/tests/views/test_auth_factor.py @@ -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": "one.factor@codeforlife.com", + "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": "two.factor@codeforlife.com", + "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) diff --git a/backend/api/urls.py b/backend/api/urls.py index 8718b966..2e3ce51e 100644 --- a/backend/api/urls.py +++ b/backend/api/urls.py @@ -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, diff --git a/backend/api/views/__init__.py b/backend/api/views/__init__.py index ffcd7b4e..8059ded9 100644 --- a/backend/api/views/__init__.py +++ b/backend/api/views/__init__.py @@ -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 diff --git a/backend/api/views/auth_factor.py b/backend/api/views/auth_factor.py new file mode 100644 index 00000000..1d960c2c --- /dev/null +++ b/backend/api/views/auth_factor.py @@ -0,0 +1,26 @@ +""" +© 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 + + 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() diff --git a/run.sh b/run similarity index 100% rename from run.sh rename to run diff --git a/setup.sh b/setup similarity index 100% rename from setup.sh rename to setup