Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Have JWTAuthBackend extend BaseAuthentication #1405

Merged
merged 4 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'))