🔒️(backend) restrict resource server views

We don't want every Service Provider to be able to request
every endpoint if those are not implementing a filtering on
the data returned. To prevent any data leak we enforce the
developers to manually "whitelist" each endpoint and add
the proper filtering when needed.
This commit is contained in:
Quentin BEY
2024-11-14 17:10:08 +01:00
committed by BEY Quentin
parent a041296f8a
commit a991737a59
5 changed files with 95 additions and 38 deletions

View File

@@ -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()

View File

@@ -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

View File

@@ -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,
]
),
),
]

View File

@@ -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",
),

View File

@@ -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 = (