From 17222ced2341cb5f4a5ecd755230b1670a464a03 Mon Sep 17 00:00:00 2001 From: Youri Westerman Date: Mon, 23 Oct 2023 16:13:29 +0200 Subject: [PATCH 1/4] Have JWTAuthBackend extend BaseAuthentication --- app/signals/auth/backend.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/signals/auth/backend.py b/app/signals/auth/backend.py index 8281301f8..3132b91f2 100644 --- a/app/signals/auth/backend.py +++ b/app/signals/auth/backend.py @@ -1,9 +1,11 @@ # SPDX-License-Identifier: MPL-2.0 -# Copyright (C) 2018 - 2021 Gemeente Amsterdam +# Copyright (C) 2018 - 2023 Gemeente Amsterdam from django.conf import settings from django.contrib.auth.models import User from django.core.cache import cache from rest_framework import exceptions +from rest_framework.authentication import BaseAuthentication +from rest_framework.request import Request from .tokens import JWTAccessToken @@ -11,12 +13,12 @@ USER_DOES_NOT_EXIST = -1 -class JWTAuthBackend(): +class JWTAuthBackend(BaseAuthentication): """ Retrieve user from backend and cache the result """ - @staticmethod # noqa: C901 - def get_user(user_id): + @staticmethod + def get_user(user_id: int) -> User: # Now we know we have a Amsterdam municipal employee (may or may not be allowed acceess) # or external user with access to the `signals` application, we retrieve the Django user. user = cache.get(user_id) @@ -42,19 +44,17 @@ def get_user(user_id): Authenticate. Check if required scope is present and get user_email from JWT token. use ALWAYS_OK = True to skip token verification. Useful for local dev/testing """ - @staticmethod # noqa: C901 - def authenticate(request): + def authenticate(self, request: Request) -> tuple[User, str]: auth_header = request.META.get('HTTP_AUTHORIZATION') - claims, user_id = JWTAccessToken.token_data(auth_header) + _, user_id = JWTAccessToken.token_data(auth_header) if user_id == "ALWAYS_OK": user_id = settings.TEST_LOGIN auth_user = JWTAuthBackend.get_user(user_id) - # We return only when we have correct scope, and user is known to `signals`. - # TODO remove default empty scope + return auth_user, '' - def authenticate_header(self, request): + def authenticate_header(self, request: Request) -> str: """ Return a string to be used as the value of the `WWW-Authenticate` header in a `401 Unauthenticated` response, or `None` if the From 1ae0562cffe5d9c5170fa38f3701431c9b71d1c4 Mon Sep 17 00:00:00 2001 From: Youri Westerman Date: Mon, 23 Oct 2023 16:14:17 +0200 Subject: [PATCH 2/4] Sync mypy baseline --- app/mypy-baseline.txt | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/app/mypy-baseline.txt b/app/mypy-baseline.txt index 126b6fa25..5f5ba60c9 100644 --- a/app/mypy-baseline.txt +++ b/app/mypy-baseline.txt @@ -84,7 +84,6 @@ signals/apps/users/migrations/0005_auto_20191113_1000.py:0: error: Unexpected ke signals/apps/users/migrations/0005_auto_20191113_1000.py:0: error: Unexpected keyword argument "null" for "ManyToManyField" [call-arg] /usr/local/lib/python3.11/site-packages/django-stubs/db/models/fields/related.pyi:0: note: "ManyToManyField" defined here signals/apps/signals/migrations/0192_auto_20230626_1312.py:0: error: Skipping analyzing "django_fsm": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/sigmax/rest_framework/views.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] signals/apps/search/apps.py:0: error: Skipping analyzing "elasticsearch_dsl": module is installed, but missing library stubs or py.typed marker [import-untyped] signals/apps/questionnaires/tests/services/test_question_graph.py:0: error: Skipping analyzing "networkx": module is installed, but missing library stubs or py.typed marker [import-untyped] signals/apps/api/serializers/signal_context.py:0: error: Skipping analyzing "datapunt_api.rest": module is installed, but missing library stubs or py.typed marker [import-untyped] @@ -92,51 +91,25 @@ signals/apps/api/serializers/signal_context.py:0: error: Skipping analyzing "res signals/apps/api/serializers/signal_context.py:0: error: Skipping analyzing "rest_framework_gis.serializers": module is installed, but missing library stubs or py.typed marker [import-untyped] signals/apps/api/serializers/attachment.py:0: error: Skipping analyzing "datapunt_api.rest": module is installed, but missing library stubs or py.typed marker [import-untyped] signals/apps/my_signals/rest_framework/views/signals.py:0: error: Skipping analyzing "rest_framework_extensions.mixins": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/api/views/source.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] signals/apps/api/generics/routers.py:0: error: Skipping analyzing "rest_framework_extensions.routers": module is installed, but missing library stubs or py.typed marker [import-untyped] signals/apps/api/views/signals/private/signal_reporters.py:0: error: Skipping analyzing "django_fsm": module is installed, but missing library stubs or py.typed marker [import-untyped] signals/apps/api/views/signals/private/signal_reporters.py:0: error: Skipping analyzing "rest_framework_extensions.mixins": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/api/views/signals/private/signal_reporters.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] signals/apps/users/rest_framework/views/user.py:0: error: Skipping analyzing "rest_framework_extensions.mixins": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/users/rest_framework/views/user.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] -signals/apps/users/rest_framework/views/user.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] -signals/apps/users/rest_framework/views/user.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] signals/apps/users/rest_framework/views/role.py:0: error: Skipping analyzing "rest_framework_extensions.mixins": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/users/rest_framework/views/role.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] signals/apps/users/rest_framework/views/permission.py:0: error: Skipping analyzing "rest_framework_extensions.mixins": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/users/rest_framework/views/permission.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] -signals/apps/reporting/rest_framework/views.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] -signals/apps/api/views/translations.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] -signals/apps/api/views/csv.py:0: error: Incompatible types in assignment (expression has type "tuple[type[JWTAuthBackend]]", base class "APIView" defined the type as "Sequence[type[BaseAuthentication]]") [assignment] -signals/apps/api/views/area.py:0: error: Incompatible types in assignment (expression has type "tuple[type[JWTAuthBackend]]", base class "APIView" defined the type as "Sequence[type[BaseAuthentication]]") [assignment] signals/apps/api/serializers/stored_signal_filter.py:0: error: Skipping analyzing "datapunt_api.rest": module is installed, but missing library stubs or py.typed marker [import-untyped] signals/apps/api/serializers/signal.py:0: error: Skipping analyzing "datapunt_api.rest": module is installed, but missing library stubs or py.typed marker [import-untyped] signals/apps/api/serializers/signal.py:0: error: Incompatible types in assignment (expression has type "CharField", base class "Field" defined the type as "Callable[..., Any] | str | None") [assignment] signals/apps/api/serializers/departments.py:0: error: Skipping analyzing "datapunt_api.rest": module is installed, but missing library stubs or py.typed marker [import-untyped] signals/apps/api/serializers/category.py:0: error: Skipping analyzing "datapunt_api.rest": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/api/views/status_message.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] -signals/apps/api/views/status_message.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] signals/apps/api/tests/test_translation_endpoints.py:0: error: "_MonkeyPatchedResponse" has no attribute "streaming_content" [attr-defined] signals/apps/api/serializers/expression.py:0: error: Skipping analyzing "datapunt_api.serializers": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/api/views/expression.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] signals/apps/search/rest_framework/views.py:0: error: Skipping analyzing "elasticsearch_dsl.query": module is installed, but missing library stubs or py.typed marker [import-untyped] signals/apps/search/rest_framework/views.py:0: error: Skipping analyzing "rest_framework_extensions.mixins": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/search/rest_framework/views.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] -signals/apps/search/rest_framework/views.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] signals/apps/search/rest_framework/views.py:0: error: Incompatible types in assignment (expression has type "str", target has type "list[str]") [assignment] signals/apps/api/views/stored_signal_filter.py:0: error: Skipping analyzing "datapunt_api.pagination": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/api/views/stored_signal_filter.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] -signals/apps/api/views/status_message_template.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] signals/apps/api/views/departments.py:0: error: Skipping analyzing "rest_framework_extensions.mixins": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/api/views/departments.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] -signals/apps/api/views/category_removed.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] signals/apps/api/views/category.py:0: error: Skipping analyzing "rest_framework_extensions.mixins": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/api/views/category.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] -signals/apps/api/views/category.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] signals/apps/api/views/attachment.py:0: error: Skipping analyzing "rest_framework_extensions.mixins": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/api/views/attachment.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] signals/apps/api/views/signals/public/signals.py:0: error: "type[PublicSignalGeographyFeature]" has no attribute "objects" [attr-defined] -signals/apps/api/views/signals/private/signals_promoted_to_parent.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] signals/apps/api/views/signals/private/signals.py:0: error: Skipping analyzing "rest_framework_extensions.mixins": module is installed, but missing library stubs or py.typed marker [import-untyped] -signals/apps/api/views/signals/private/signals.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] -signals/apps/api/views/signals/private/signal_context.py:0: error: List item 0 has incompatible type "type[JWTAuthBackend]"; expected "type[BaseAuthentication]" [list-item] From 2972b91418621c4458f8e3a5b95506b8fe4eaa2d Mon Sep 17 00:00:00 2001 From: Youri Westerman Date: Mon, 23 Oct 2023 16:17:27 +0200 Subject: [PATCH 3/4] Move docs inside method --- app/signals/auth/backend.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/signals/auth/backend.py b/app/signals/auth/backend.py index 3132b91f2..84a6e0443 100644 --- a/app/signals/auth/backend.py +++ b/app/signals/auth/backend.py @@ -40,11 +40,11 @@ def get_user(user_id: int) -> User: raise exceptions.AuthenticationFailed('User inactive') return user - """ - Authenticate. Check if required scope is present and get user_email from JWT token. - use ALWAYS_OK = True to skip token verification. Useful for local dev/testing - """ def authenticate(self, request: Request) -> tuple[User, str]: + """ + Authenticate. Check if required scope is present and get user_email from JWT token. + use ALWAYS_OK = True to skip token verification. Useful for local dev/testing + """ auth_header = request.META.get('HTTP_AUTHORIZATION') _, user_id = JWTAccessToken.token_data(auth_header) if user_id == "ALWAYS_OK": From d3d1f887be2fbb33c3f8e9c4f9ce693590d1b57d Mon Sep 17 00:00:00 2001 From: Youri Westerman Date: Mon, 23 Oct 2023 16:39:51 +0200 Subject: [PATCH 4/4] Fix broken test --- app/signals/auth/tests/test_backend.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/signals/auth/tests/test_backend.py b/app/signals/auth/tests/test_backend.py index 2050394c9..b4be18423 100644 --- a/app/signals/auth/tests/test_backend.py +++ b/app/signals/auth/tests/test_backend.py @@ -94,8 +94,10 @@ def test_user_does_not_exists(self, mock_cache, mock_request, mock_token_data): mock_token_data.return_value = claims, 'idonotexist' mock_cache.get.return_value = None + backend = JWTAuthBackend() + with self.assertRaises(AuthenticationFailed) as cm: - JWTAuthBackend.authenticate(mock_request) + backend.authenticate(mock_request) e = cm.exception self.assertEqual(str(e), 'User {} is not authorized'.format('idonotexist'))