From cae2aab1abec0c307159d3db7d242ae851bacaa8 Mon Sep 17 00:00:00 2001 From: Quentin BEY Date: Fri, 2 May 2025 17:11:51 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7(resource-server)=20Allow=20documen?= =?UTF-8?q?ts=20API=20via=20RS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This provides a base configuration to allow to access all "/documents" API via OIDC resource server authentication. --- src/backend/core/api/permissions.py | 37 ++++++++++++++++++ src/backend/core/api/viewsets.py | 18 +++++++++ src/backend/core/urls.py | 2 + src/backend/impress/settings.py | 59 +++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) diff --git a/src/backend/core/api/permissions.py b/src/backend/core/api/permissions.py index 43a0465f..ec996603 100644 --- a/src/backend/core/api/permissions.py +++ b/src/backend/core/api/permissions.py @@ -1,9 +1,11 @@ """Permission handlers for the impress core app.""" +from django.conf import settings from django.core import exceptions from django.db.models import Q from django.http import Http404 +from lasuite.oidc_resource_server.authentication import ResourceServerAuthentication from rest_framework import permissions from core.models import DocumentAccess, RoleChoices, get_trashbin_cutoff @@ -134,3 +136,38 @@ class DocumentAccessPermission(AccessPermission): raise Http404 return has_permission + + +class ResourceServerClientPermission(permissions.BasePermission): + """ + Permission class for resource server views. + + This provides a way to open the resource server views to a limited set of + Service Providers. + + Note: we might add a more complex permission system in the future, based on + the Service Provider ID and the requested scopes. + """ + + def has_permission(self, request, view): + """ + Check if the user is authenticated and the token introspection + provides an authorized Service Provider. + """ + if not isinstance( + request.successful_authenticator, ResourceServerAuthentication + ): + # Not a resource server request + return True + + # Check if the user is authenticated + if not request.user.is_authenticated: + return False + + if view.action not in view.resource_server_actions: + return False + + # When used as a resource server, the request has a token audience + return ( + request.resource_server_token_audience in settings.OIDC_RS_ALLOWED_AUDIENCES + ) diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 578c49d6..cd79f4c8 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -25,6 +25,7 @@ import requests import rest_framework as drf from botocore.exceptions import ClientError from lasuite.malware_detection import malware_detection +from lasuite.oidc_resource_server.authentication import ResourceServerAuthentication from rest_framework import filters, status, viewsets from rest_framework import response as drf_response from rest_framework.permissions import AllowAny @@ -431,6 +432,7 @@ class DocumentViewSet( pagination_class = Pagination permission_classes = [ permissions.DocumentAccessPermission, + permissions.ResourceServerClientPermission, ] queryset = models.Document.objects.all() serializer_class = serializers.DocumentSerializer @@ -440,6 +442,22 @@ class DocumentViewSet( list_serializer_class = serializers.ListDocumentSerializer trashbin_serializer_class = serializers.ListDocumentSerializer tree_serializer_class = serializers.ListDocumentSerializer + resource_server_actions = { + "list", + "retrieve", + "create_for_owner", + } + + def get_authenticators(self): + """Allow resource server authentication for very specific actions.""" + authenticators = super().get_authenticators() + + # self.action does not exist yet + action = self.action_map[self.request.method.lower()] + if action in self.resource_server_actions: + authenticators.append(ResourceServerAuthentication()) + + return authenticators def annotate_is_favorite(self, queryset): """ diff --git a/src/backend/core/urls.py b/src/backend/core/urls.py index 05441895..fae84820 100644 --- a/src/backend/core/urls.py +++ b/src/backend/core/urls.py @@ -4,6 +4,7 @@ from django.conf import settings from django.urls import include, path, re_path from lasuite.oidc_login.urls import urlpatterns as oidc_urls +from lasuite.oidc_resource_server.urls import urlpatterns as resource_server_urls from rest_framework.routers import DefaultRouter from core.api import viewsets @@ -44,6 +45,7 @@ urlpatterns = [ [ *router.urls, *oidc_urls, + *resource_server_urls, re_path( r"^documents/(?P[0-9a-z-]*)/", include(document_related_router.urls), diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py index 57269b16..16bbf9a6 100755 --- a/src/backend/impress/settings.py +++ b/src/backend/impress/settings.py @@ -586,6 +586,65 @@ class Base(Configuration): default=True, environ_name="ALLOW_LOGOUT_GET_METHOD", environ_prefix=None ) + # OIDC - Docs as a resource server + OIDC_OP_URL = values.Value( + default=None, environ_name="OIDC_OP_URL", environ_prefix=None + ) + OIDC_OP_INTROSPECTION_ENDPOINT = values.Value( + environ_name="OIDC_OP_INTROSPECTION_ENDPOINT", environ_prefix=None + ) + OIDC_VERIFY_SSL = values.BooleanValue( + default=True, environ_name="OIDC_VERIFY_SSL", environ_prefix=None + ) + OIDC_TIMEOUT = values.IntegerValue( + default=3, environ_name="OIDC_TIMEOUT", environ_prefix=None + ) + OIDC_PROXY = values.Value(None, environ_name="OIDC_PROXY", environ_prefix=None) + + OIDC_RS_BACKEND_CLASS = "lasuite.oidc_resource_server.backend.ResourceServerBackend" + OIDC_RS_AUDIENCE_CLAIM = values.Value( # The claim used to identify the audience + default="client_id", environ_name="OIDC_RS_AUDIENCE_CLAIM", environ_prefix=None + ) + OIDC_RS_PRIVATE_KEY_STR = values.Value( + default=None, + environ_name="OIDC_RS_PRIVATE_KEY_STR", + environ_prefix=None, + ) + OIDC_RS_ENCRYPTION_KEY_TYPE = values.Value( + default="RSA", + environ_name="OIDC_RS_ENCRYPTION_KEY_TYPE", + environ_prefix=None, + ) + OIDC_RS_ENCRYPTION_ALGO = values.Value( + default="RSA-OAEP", + environ_name="OIDC_RS_ENCRYPTION_ALGO", + environ_prefix=None, + ) + OIDC_RS_ENCRYPTION_ENCODING = values.Value( + default="A256GCM", + environ_name="OIDC_RS_ENCRYPTION_ENCODING", + environ_prefix=None, + ) + OIDC_RS_CLIENT_ID = values.Value( + None, environ_name="OIDC_RS_CLIENT_ID", environ_prefix=None + ) + OIDC_RS_CLIENT_SECRET = values.Value( + None, + environ_name="OIDC_RS_CLIENT_SECRET", + environ_prefix=None, + ) + OIDC_RS_SIGNING_ALGO = values.Value( + default="ES256", environ_name="OIDC_RS_SIGNING_ALGO", environ_prefix=None + ) + OIDC_RS_SCOPES = values.ListValue( + [], environ_name="OIDC_RS_SCOPES", environ_prefix=None + ) + OIDC_RS_ALLOWED_AUDIENCES = values.ListValue( + default=[], + environ_name="OIDC_RS_ALLOWED_AUDIENCES", + environ_prefix=None, + ) + # AI service AI_FEATURE_ENABLED = values.BooleanValue( default=False, environ_name="AI_FEATURE_ENABLED", environ_prefix=None