diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 1eda7a3..f055892 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -18,7 +18,6 @@ from rest_framework.permissions import AllowAny from core import models -from ..resource_server.authentication import ResourceServerAuthentication from . import permissions, serializers SIMILARITY_THRESHOLD = 0.04 @@ -248,26 +247,6 @@ class TeamViewSet( queryset = models.Team.objects.all() pagination_class = None - def _get_service_provider_audience(self): - """Return the audience of the Service Provider from the OIDC introspected token.""" - if not isinstance( - self.request.successful_authenticator, ResourceServerAuthentication - ): - # We could check request.resource_server_token_audience here, but it's - # more explicit to check the authenticator type and assert the attribute - # existence. - return None - - # When used as a resource server, the request has a token audience - service_provider_audience = self.request.resource_server_token_audience - - if not service_provider_audience: # should not happen - raise exceptions.AuthenticationFailed( - "Resource server token audience not found in request" - ) - - return service_provider_audience - def get_queryset(self): """Custom queryset to get user related teams.""" user_role_query = models.TeamAccess.objects.filter( @@ -282,18 +261,6 @@ class TeamViewSet( .annotate(user_role=Subquery(user_role_query)) ) - def get_serializer_context(self): - """Extra context provided to the serializer class.""" - context = super().get_serializer_context() - - # When used as a resource server, we need to know the audience to automatically: - # - add the Service Provider to the team "scope" on creation - context["from_service_provider_audience"] = ( - self._get_service_provider_audience() - ) - - return context - def perform_create(self, serializer): """Set the current user as owner of the newly created team.""" team = serializer.save() diff --git a/src/backend/core/resource_server/mixins.py b/src/backend/core/resource_server/mixins.py new file mode 100644 index 0000000..ae3b03a --- /dev/null +++ b/src/backend/core/resource_server/mixins.py @@ -0,0 +1,53 @@ +""" +Mixins for resource server views. +""" + +from rest_framework import exceptions as drf_exceptions + +from .authentication import ResourceServerAuthentication + + +class ResourceServerMixin: + """ + Mixin for resource server views: + - Restrict the authentication class to ResourceServerAuthentication. + - Adds the Service Provider ID to the serializer context. + - Fetch the Service Provider ID from the OIDC introspected token stored + in the request. + + This Mixin *must* be used in every view that should act as a resource server. + """ + + authentication_classes = [ResourceServerAuthentication] + + def get_serializer_context(self): + """Extra context provided to the serializer class.""" + context = super().get_serializer_context() + + # When used as a resource server, we need to know the audience to automatically: + # - add the Service Provider to the team "scope" on creation + context["from_service_provider_audience"] = ( + self._get_service_provider_audience() + ) + + return context + + def _get_service_provider_audience(self): + """Return the audience of the Service Provider from the OIDC introspected token.""" + if not isinstance( + self.request.successful_authenticator, ResourceServerAuthentication + ): + # We could check request.resource_server_token_audience here, but it's + # more explicit to check the authenticator type and assert the attribute + # existence. + return None + + # When used as a resource server, the request has a token audience + service_provider_audience = self.request.resource_server_token_audience + + if not service_provider_audience: # should not happen + raise drf_exceptions.AuthenticationFailed( + "Resource server token audience not found in request" + ) + + return service_provider_audience diff --git a/src/backend/people/resource_server_urls.py b/src/backend/people/resource_server_urls.py new file mode 100644 index 0000000..b775b01 --- /dev/null +++ b/src/backend/people/resource_server_urls.py @@ -0,0 +1,30 @@ +"""Resource server API URL Configuration""" + +from django.urls import include, path + +from rest_framework.routers import DefaultRouter + +from core.resource_server.urls import urlpatterns as resource_server_urls +from core.resource_server_api import viewsets + +# - Main endpoints +# Contacts will be added later +# Users will be added later +router = DefaultRouter() +router.register("teams", viewsets.TeamViewSet, basename="teams") + + +# - Routes nested under a team +# Invitations will be added later + +urlpatterns = [ + path( + "resource-server/v1.0/", + include( + [ + *router.urls, + *resource_server_urls, + ] + ), + ), +] diff --git a/src/backend/people/settings.py b/src/backend/people/settings.py index c73175a..7eb27e8 100755 --- a/src/backend/people/settings.py +++ b/src/backend/people/settings.py @@ -230,7 +230,10 @@ class Base(Configuration): REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( - "core.resource_server.authentication.ResourceServerAuthentication", + # "core.resource_server.authentication.ResourceServerAuthentication", + # The resource server authentication is added on a per-view basis + # to enforce the filtering adapted from the introspected token. + # See ResourceServerMixin usage for more details. "mozilla_django_oidc.contrib.drf.OIDCAuthentication", "rest_framework.authentication.SessionAuthentication", ), diff --git a/src/backend/people/urls.py b/src/backend/people/urls.py index 11f15c1..8978885 100644 --- a/src/backend/people/urls.py +++ b/src/backend/people/urls.py @@ -14,13 +14,17 @@ from drf_spectacular.views import ( from debug import urls as debug_urls -from . import api_urls +from . import api_urls, resource_server_urls API_VERSION = settings.API_VERSION -urlpatterns = [ - path("admin/", admin.site.urls), -] + api_urls.urlpatterns +urlpatterns = ( + [ + path("admin/", admin.site.urls), + ] + + api_urls.urlpatterns + + resource_server_urls.urlpatterns +) if settings.DEBUG: urlpatterns = (