From 8f65318d32d55cf587ba5b19e81a5568f5116125 Mon Sep 17 00:00:00 2001 From: jo Date: Sat, 4 Feb 2023 21:55:47 +0100 Subject: [PATCH] fi(api): improve auth/preferences schema generation Applying recommendation from https://github.com/tfranzel/drf-spectacular/issues/874 --- api/config/schema.py | 106 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 22 deletions(-) diff --git a/api/config/schema.py b/api/config/schema.py index 99e4555fa..f5a4ef633 100644 --- a/api/config/schema.py +++ b/api/config/schema.py @@ -1,35 +1,97 @@ import os -from drf_spectacular.contrib.django_oauth_toolkit import OpenApiAuthenticationExtension -from drf_spectacular.plumbing import build_bearer_security_scheme_object +from drf_spectacular.contrib.django_oauth_toolkit import ( + DjangoOAuthToolkitScheme, + OpenApiAuthenticationExtension, +) +from drf_spectacular.extensions import ( + OpenApiSerializerExtension, + OpenApiSerializerFieldExtension, +) +from drf_spectacular.plumbing import ( + OpenApiTypes, + build_basic_type, + build_bearer_security_scheme_object, +) -class CustomOAuthExt(OpenApiAuthenticationExtension): +class CustomRelatedFieldScheme(OpenApiSerializerFieldExtension): + target_class = "funkwhale_api.common.serializers.RelatedField" + match_subclasses = True + + def map_serializer_field(self, auto_schema, direction): + if direction == "request": + return build_basic_type(OpenApiTypes.UUID) + elif direction == "response" and self.target.serializer: + component = auto_schema.resolve_serializer( + self.target.serializer, direction + ) + return component.ref + else: + # happens for + # UserViewSet: UserWriteSerializer: not sure how this works for: avatar + # AlbumViewSet: AlbumCreateSerializer: not sure how this works for: artist + return build_basic_type(OpenApiTypes.UUID) + + +class PreferenceSerializerScheme(OpenApiSerializerExtension): + target_class = "dynamic_preferences.api.serializers.PreferenceSerializer" + match_subclasses = True + + def map_serializer(self, auto_schema, direction): + from dynamic_preferences.api.serializers import PreferenceSerializer + + class Fix(PreferenceSerializer): + def get_default(self, o) -> str: + pass + + def get_verbose_name(self, o) -> str: + pass + + def get_identifier(self, o) -> str: + pass + + def get_help_text(self, o) -> str: + pass + + def get_additional_data(self, o) -> dict: + pass + + def get_field(self, o) -> dict: + pass + + return auto_schema._map_serializer(Fix, direction, bypass_extensions=True) + + +class CustomOAuthScheme(DjangoOAuthToolkitScheme): target_class = "funkwhale_api.common.authentication.OAuth2Authentication" - name = "oauth2" - def get_security_definition(self, auto_schema): - from drf_spectacular.settings import spectacular_settings - from oauth2_provider.scopes import get_scopes_backend + def get_security_requirement(self, auto_schema): + from funkwhale_api.users.oauth.permissions import ( + METHOD_SCOPE_MAPPING, + ScopePermission, + ) - flows = {} - for flow_type in spectacular_settings.OAUTH2_FLOWS: - flows[flow_type] = {} - if flow_type in ("implicit", "authorizationCode"): - flows[flow_type][ - "authorizationUrl" - ] = spectacular_settings.OAUTH2_AUTHORIZATION_URL - if flow_type in ("password", "clientCredentials", "authorizationCode"): - flows[flow_type]["tokenUrl"] = spectacular_settings.OAUTH2_TOKEN_URL - if spectacular_settings.OAUTH2_REFRESH_URL: - flows[flow_type]["refreshUrl"] = spectacular_settings.OAUTH2_REFRESH_URL - scope_backend = get_scopes_backend() - flows[flow_type]["scopes"] = scope_backend.get_all_scopes() + for permission in auto_schema.view.get_permissions(): + if isinstance(permission, ScopePermission): + scope_config = getattr(auto_schema.view, "required_scope", "noopscope") - return {"type": "oauth2", "flows": flows} + if isinstance(scope_config, str): + scope_config = { + "read": f"read:{scope_config}", + "write": f"write:{scope_config}", + } + action = METHOD_SCOPE_MAPPING[ + auto_schema.view.request.method.lower() + ] + required_scope = scope_config[action] + else: + required_scope = scope_config[auto_schema.view.action] + + return {self.name: [required_scope]} -class CustomApplicationTokenExt(OpenApiAuthenticationExtension): +class CustomApplicationTokenScheme(OpenApiAuthenticationExtension): target_class = "funkwhale_api.common.authentication.ApplicationTokenAuthentication" name = "ApplicationToken"