Skip to content

Commit

Permalink
Have JWTAuthBackend extend BaseAuthentication (Amsterdam#1405)
Browse files Browse the repository at this point in the history
* Have JWTAuthBackend extend BaseAuthentication

* Sync mypy baseline

* Move docs inside method

* Fix broken test
  • Loading branch information
4c0n authored Oct 23, 2023
1 parent 1bd2ad7 commit 0c4928c
Show file tree
Hide file tree
Showing 3 changed files with 17 additions and 42 deletions.
27 changes: 0 additions & 27 deletions app/mypy-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -84,59 +84,32 @@ 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]
signals/apps/api/serializers/signal_context.py:0: error: Skipping analyzing "rest_framework_gis.fields": module is installed, but missing library stubs or py.typed marker [import-untyped]
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]
28 changes: 14 additions & 14 deletions app/signals/auth/backend.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
# 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

USER_NOT_AUTHORIZED = "User {} is not authorized"
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)
Expand All @@ -38,23 +40,21 @@ def get_user(user_id):
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
"""
@staticmethod # noqa: C901
def authenticate(request):
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')
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
Expand Down
4 changes: 3 additions & 1 deletion app/signals/auth/tests/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'))

0 comments on commit 0c4928c

Please sign in to comment.