diff --git a/Makefile b/Makefile index 9d2a0c53b..5c8b41fd1 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ lint: pylint authentik tests lifecycle gen: - ./manage.py spectacular --file schema.yml + ./manage.py spectacular --file schema.yaml --validate --fail-on-warn docker run \ --rm -v ${PWD}:/local \ openapitools/openapi-generator-cli generate \ diff --git a/authentik/admin/api/tasks.py b/authentik/admin/api/tasks.py index 2e87b208b..43b77810e 100644 --- a/authentik/admin/api/tasks.py +++ b/authentik/admin/api/tasks.py @@ -4,7 +4,7 @@ from importlib import import_module from django.contrib import messages from django.http.response import Http404 from django.utils.translation import gettext_lazy as _ -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.decorators import action from rest_framework.fields import CharField, ChoiceField, DateTimeField, ListField from rest_framework.permissions import IsAdminUser @@ -34,8 +34,14 @@ class TaskViewSet(ViewSet): """Read-only view set that returns all background tasks""" permission_classes = [IsAdminUser] + serializer_class = TaskSerializer - @extend_schema(responses={200: TaskSerializer(many=False), 404: "Task not found"}) + @extend_schema( + responses={ + 200: TaskSerializer(many=False), + 404: OpenApiResponse(description="Task not found"), + } + ) # pylint: disable=invalid-name def retrieve(self, request: Request, pk=None) -> Response: """Get a single system task""" @@ -52,9 +58,9 @@ class TaskViewSet(ViewSet): @extend_schema( responses={ - 204: "Task retried successfully", - 404: "Task not found", - 500: "Failed to retry task", + 204: OpenApiResponse(description="Task retried successfully"), + 404: OpenApiResponse(description="Task not found"), + 500: OpenApiResponse(description="Failed to retry task"), } ) @action(detail=True, methods=["post"]) diff --git a/authentik/api/auth.py b/authentik/api/auth.py index 852f1d9db..8fa5ca963 100644 --- a/authentik/api/auth.py +++ b/authentik/api/auth.py @@ -3,11 +3,11 @@ from base64 import b64decode from binascii import Error from typing import Any, Optional, Union +from drf_spectacular.authentication import OpenApiAuthenticationExtension from rest_framework.authentication import BaseAuthentication, get_authorization_header from rest_framework.exceptions import AuthenticationFailed from rest_framework.request import Request from structlog.stdlib import get_logger -from drf_spectacular.authentication import OpenApiAuthenticationExtension from authentik.core.models import Token, TokenIntents, User @@ -60,11 +60,11 @@ class AuthentikTokenAuthentication(BaseAuthentication): class TokenSchema(OpenApiAuthenticationExtension): target_class = AuthentikTokenAuthentication - name = 'authentik' + name = "authentik" def get_security_definition(self, auto_schema): return { - 'type': 'apiKey', - 'in': 'header', - 'name': 'Authorization', + "type": "apiKey", + "in": "header", + "name": "Authorization", } diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index 9ae803332..ac6824c90 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -6,7 +6,7 @@ from django.db.models import QuerySet from django.http.response import HttpResponseBadRequest from django.shortcuts import get_object_or_404 from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter, extend_schema +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema from rest_framework.decorators import action from rest_framework.fields import SerializerMethodField from rest_framework.parsers import MultiPartParser @@ -94,8 +94,8 @@ class ApplicationViewSet(ModelViewSet): @extend_schema( responses={ - 204: "Access granted", - 403: "Access denied", + 204: OpenApiResponse(description="Access granted"), + 403: OpenApiResponse(description="Access denied"), } ) @action(detail=True, methods=["GET"]) @@ -161,7 +161,10 @@ class ApplicationViewSet(ModelViewSet): required=True, ) ], - responses={200: "Success", 400: "Bad request"}, + responses={ + 200: OpenApiResponse(description="Success"), + 400: OpenApiResponse(description="Bad request"), + }, ) @action( detail=True, diff --git a/authentik/core/api/propertymappings.py b/authentik/core/api/propertymappings.py index 8904ac32b..798a7a722 100644 --- a/authentik/core/api/propertymappings.py +++ b/authentik/core/api/propertymappings.py @@ -2,7 +2,7 @@ from json import dumps from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter, extend_schema +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema from guardian.shortcuts import get_objects_for_user from rest_framework import mixins from rest_framework.decorators import action @@ -102,7 +102,10 @@ class PropertyMappingViewSet( @permission_required("authentik_core.view_propertymapping") @extend_schema( request=PolicyTestSerializer(), - responses={200: PropertyMappingTestResultSerializer, 400: "Invalid parameters"}, + responses={ + 200: PropertyMappingTestResultSerializer, + 400: OpenApiResponse(description="Invalid parameters"), + }, parameters=[ OpenApiParameter( name="format_result", diff --git a/authentik/core/api/providers.py b/authentik/core/api/providers.py index 5e4042603..a6a7c2e88 100644 --- a/authentik/core/api/providers.py +++ b/authentik/core/api/providers.py @@ -22,7 +22,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer): component = SerializerMethodField() - def get_component(self, obj: Provider): # pragma: no cover + def get_component(self, obj: Provider) -> str: # pragma: no cover """Get object component so that we know how to edit the object""" # pyright: reportGeneralTypeIssues=false if obj.__class__ == Provider: diff --git a/authentik/core/api/sources.py b/authentik/core/api/sources.py index 7a4b795b7..def370b2a 100644 --- a/authentik/core/api/sources.py +++ b/authentik/core/api/sources.py @@ -24,7 +24,7 @@ class SourceSerializer(ModelSerializer, MetaNameSerializer): component = SerializerMethodField() - def get_component(self, obj: Source): + def get_component(self, obj: Source) -> str: """Get object component so that we know how to edit the object""" # pyright: reportGeneralTypeIssues=false if obj.__class__ == Source: diff --git a/authentik/core/api/tokens.py b/authentik/core/api/tokens.py index 793338271..9ef14ac9b 100644 --- a/authentik/core/api/tokens.py +++ b/authentik/core/api/tokens.py @@ -1,6 +1,6 @@ """Tokens API Viewset""" from django.http.response import Http404 -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.decorators import action from rest_framework.fields import CharField from rest_framework.request import Request @@ -70,7 +70,7 @@ class TokenViewSet(ModelViewSet): @extend_schema( responses={ 200: TokenViewSerializer(many=False), - 404: "Token not found or expired", + 404: OpenApiResponse(description="Token not found or expired"), } ) @action(detail=True, pagination_class=None, filter_backends=[]) diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index c92676162..39aeb696c 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -7,7 +7,7 @@ from django.urls import reverse_lazy from django.utils.http import urlencode from django_filters.filters import BooleanFilter, CharFilter from django_filters.filterset import FilterSet -from drf_spectacular.utils import extend_schema, extend_schema_field +from drf_spectacular.utils import OpenApiResponse, extend_schema, extend_schema_field from guardian.utils import get_anonymous_user from rest_framework.decorators import action from rest_framework.fields import CharField, JSONField, SerializerMethodField @@ -170,7 +170,10 @@ class UserViewSet(ModelViewSet): @permission_required("authentik_core.reset_user_password") @extend_schema( - responses={"200": LinkSerializer(many=False), "404": "No recovery flow found."}, + responses={ + "200": LinkSerializer(many=False), + "404": OpenApiResponse(description="No recovery flow found."), + }, ) @action(detail=True, pagination_class=None, filter_backends=[]) # pylint: disable=invalid-name, unused-argument diff --git a/authentik/crypto/api.py b/authentik/crypto/api.py index c277f3c45..e7b54cb68 100644 --- a/authentik/crypto/api.py +++ b/authentik/crypto/api.py @@ -6,7 +6,7 @@ from cryptography.x509 import load_pem_x509_certificate from django.http.response import HttpResponse from django.utils.translation import gettext_lazy as _ from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter, extend_schema +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema from rest_framework.decorators import action from rest_framework.fields import ( CharField, @@ -127,7 +127,10 @@ class CertificateKeyPairViewSet(ModelViewSet): @permission_required(None, ["authentik_crypto.add_certificatekeypair"]) @extend_schema( request=CertificateGenerationSerializer(), - responses={200: CertificateKeyPairSerializer, 400: "Bad request"}, + responses={ + 200: CertificateKeyPairSerializer, + 400: OpenApiResponse(description="Bad request"), + }, ) @action(detail=False, methods=["POST"]) def generate(self, request: Request) -> Response: diff --git a/authentik/events/api/event.py b/authentik/events/api/event.py index 307593e2e..5cf5e7992 100644 --- a/authentik/events/api/event.py +++ b/authentik/events/api/event.py @@ -1,8 +1,8 @@ """Events API Views""" -from drf_spectacular.types import OpenApiTypes import django_filters from django.db.models.aggregates import Count from django.db.models.fields.json import KeyTextTransform +from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter, extend_schema from guardian.shortcuts import get_objects_for_user from rest_framework.decorators import action @@ -116,7 +116,7 @@ class EventViewSet(ReadOnlyModelViewSet): location=OpenApiParameter.QUERY, required=False, ) - ] + ], ) @action(detail=False, methods=["GET"]) def top_per_user(self, request: Request): diff --git a/authentik/events/api/notification_transport.py b/authentik/events/api/notification_transport.py index 24225e39e..05b857e87 100644 --- a/authentik/events/api/notification_transport.py +++ b/authentik/events/api/notification_transport.py @@ -1,6 +1,6 @@ """NotificationTransport API Views""" from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.decorators import action from rest_framework.fields import CharField, ListField, SerializerMethodField from rest_framework.request import Request @@ -23,7 +23,7 @@ class NotificationTransportSerializer(ModelSerializer): mode_verbose = SerializerMethodField() - def get_mode_verbose(self, instance: NotificationTransport): + def get_mode_verbose(self, instance: NotificationTransport) -> str: """Return selected mode with a UI Label""" return TransportMode(instance.mode).label @@ -62,7 +62,7 @@ class NotificationTransportViewSet(ModelViewSet): @extend_schema( responses={ 200: NotificationTransportTestSerializer(many=False), - 503: "Failed to test transport", + 500: OpenApiResponse(description="Failed to test transport"), }, request=OpenApiTypes.NONE, ) @@ -84,4 +84,4 @@ class NotificationTransportViewSet(ModelViewSet): response.is_valid() return Response(response.data) except NotificationTransportError as exc: - return Response(str(exc.__cause__ or None), status=503) + return Response(str(exc.__cause__ or None), status=500) diff --git a/authentik/flows/api/flows.py b/authentik/flows/api/flows.py index 4d2f11455..5fba69aa4 100644 --- a/authentik/flows/api/flows.py +++ b/authentik/flows/api/flows.py @@ -7,12 +7,7 @@ from django.http.response import HttpResponseBadRequest, JsonResponse from django.urls import reverse from django.utils.translation import gettext as _ from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import ( - OpenApiParameter, - OpenApiResponse, - OpenApiSchemaBase, - extend_schema, -) +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema from guardian.shortcuts import get_objects_for_user from rest_framework.decorators import action from rest_framework.parsers import MultiPartParser @@ -46,7 +41,7 @@ class FlowSerializer(ModelSerializer): cache_count = SerializerMethodField() - def get_cache_count(self, flow: Flow): + def get_cache_count(self, flow: Flow) -> int: """Get count of cached flows""" return len(cache.keys(f"{cache_key(flow)}*")) @@ -111,7 +106,10 @@ class FlowViewSet(ModelViewSet): @permission_required(None, ["authentik_flows.clear_flow_cache"]) @extend_schema( request=OpenApiTypes.NONE, - responses={204: "Successfully cleared cache", 400: "Bad request"}, + responses={ + 204: OpenApiResponse(description="Successfully cleared cache"), + 400: OpenApiResponse(description="Bad request"), + }, ) @action(detail=False, methods=["POST"]) def cache_clear(self, request: Request) -> Response: @@ -148,7 +146,10 @@ class FlowViewSet(ModelViewSet): required=True, ) ], - responses={204: "Successfully imported flow", 400: "Bad request"}, + responses={ + 204: OpenApiResponse(description="Successfully imported flow"), + 400: OpenApiResponse(description="Bad request"), + }, ) @action(detail=False, methods=["POST"], parser_classes=(MultiPartParser,)) def import_flow(self, request: Request) -> Response: @@ -178,9 +179,7 @@ class FlowViewSet(ModelViewSet): ) @extend_schema( responses={ - "200": OpenApiResponse( - response=OpenApiParameter("File Attachment", type=OpenApiTypes.BINARY) - ), + "200": OpenApiResponse(response=OpenApiTypes.BINARY), }, ) @action(detail=True, pagination_class=None, filter_backends=[]) @@ -274,7 +273,10 @@ class FlowViewSet(ModelViewSet): required=True, ) ], - responses={200: "Success", 400: "Bad request"}, + responses={ + 200: OpenApiResponse(description="Success"), + 400: OpenApiResponse(description="Bad request"), + }, ) @action( detail=True, @@ -295,7 +297,10 @@ class FlowViewSet(ModelViewSet): return Response({}) @extend_schema( - responses={200: LinkSerializer(many=False), 400: "Flow not applicable"}, + responses={ + 200: LinkSerializer(many=False), + 400: OpenApiResponse(description="Flow not applicable"), + }, ) @action(detail=True, pagination_class=None, filter_backends=[]) # pylint: disable=unused-argument diff --git a/authentik/flows/views.py b/authentik/flows/views.py index d279d6211..cd42f77a1 100644 --- a/authentik/flows/views.py +++ b/authentik/flows/views.py @@ -11,7 +11,7 @@ from django.utils.decorators import method_decorator from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.generic import View from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter, extend_schema +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema from rest_framework.permissions import AllowAny from rest_framework.views import APIView from sentry_sdk import capture_exception @@ -128,7 +128,9 @@ class FlowExecutorView(APIView): @extend_schema( responses={ 200: Challenge(), - 404: "No Token found", # This error can be raised by the email stage + 404: OpenApiResponse( + description="No Token found" + ), # This error can be raised by the email stage }, request=OpenApiTypes.NONE, parameters=[ diff --git a/authentik/policies/api/policies.py b/authentik/policies/api/policies.py index f4cd0afdf..9de4e0b66 100644 --- a/authentik/policies/api/policies.py +++ b/authentik/policies/api/policies.py @@ -1,7 +1,7 @@ """policy API Views""" from django.core.cache import cache from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import OpenApiResponse, extend_schema from guardian.shortcuts import get_objects_for_user from rest_framework import mixins from rest_framework.decorators import action @@ -124,7 +124,10 @@ class PolicyViewSet( @permission_required(None, ["authentik_policies.clear_policy_cache"]) @extend_schema( request=OpenApiTypes.NONE, - responses={204: "Successfully cleared cache", 400: "Bad request"}, + responses={ + 204: OpenApiResponse(description="Successfully cleared cache"), + 400: OpenApiResponse(description="Bad request"), + }, ) @action(detail=False, methods=["POST"]) def cache_clear(self, request: Request) -> Response: @@ -140,7 +143,10 @@ class PolicyViewSet( @permission_required("authentik_policies.view_policy") @extend_schema( request=PolicyTestSerializer(), - responses={200: PolicyTestResultSerializer(), 400: "Invalid parameters"}, + responses={ + 200: PolicyTestResultSerializer(), + 400: OpenApiResponse(description="Invalid parameters"), + }, ) @action(detail=True, pagination_class=None, filter_backends=[], methods=["POST"]) # pylint: disable=unused-argument, invalid-name diff --git a/authentik/providers/oauth2/api/provider.py b/authentik/providers/oauth2/api/provider.py index d556e1bbe..cf239b4a4 100644 --- a/authentik/providers/oauth2/api/provider.py +++ b/authentik/providers/oauth2/api/provider.py @@ -2,7 +2,7 @@ from django.db.models.base import Model from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.decorators import action from rest_framework.fields import ReadOnlyField from rest_framework.generics import get_object_or_404 @@ -61,9 +61,6 @@ class OAuth2ProviderSetupURLs(PassiveSerializer): provider_info = ReadOnlyField() logout = ReadOnlyField() - class Meta: - model = Model - class OAuth2ProviderViewSet(ModelViewSet): """OAuth2Provider Viewset""" @@ -74,7 +71,7 @@ class OAuth2ProviderViewSet(ModelViewSet): @extend_schema( responses={ 200: OAuth2ProviderSetupURLs, - 404: "Provider has no application assigned", + 404: OpenApiResponse(description="Provider has no application assigned"), } ) @action(methods=["GET"], detail=True) diff --git a/authentik/providers/saml/api.py b/authentik/providers/saml/api.py index 2a66b4b77..51ee04077 100644 --- a/authentik/providers/saml/api.py +++ b/authentik/providers/saml/api.py @@ -6,7 +6,7 @@ from django.http.response import HttpResponse from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter, extend_schema +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema, extend_schema_field from rest_framework.decorators import action from rest_framework.fields import CharField, FileField, ReadOnlyField from rest_framework.parsers import MultiPartParser @@ -83,7 +83,7 @@ class SAMLProviderViewSet(ModelViewSet): @extend_schema( responses={ 200: SAMLMetadataSerializer(many=False), - 404: "Provider has no application assigned", + 404: OpenApiResponse(description="Provider has no application assigned"), }, parameters=[ OpenApiParameter( @@ -120,7 +120,10 @@ class SAMLProviderViewSet(ModelViewSet): ) @extend_schema( request=SAMLProviderImportSerializer(), - responses={204: "Successfully imported provider", 400: "Bad request"}, + responses={ + 204: OpenApiResponse(description="Successfully imported provider"), + 400: OpenApiResponse(description="Bad request"), + }, ) @action(detail=False, methods=["POST"], parser_classes=(MultiPartParser,)) def import_metadata(self, request: Request) -> Response: diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 21d5130a0..011f037af 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -151,6 +151,9 @@ SPECTACULAR_SETTINGS = { "name": "GNU GPLv3", "url": "https://github.com/goauthentik/authentik/blob/master/LICENSE", }, + "ENUM_NAME_OVERRIDES": { + "ChallengeChoices": "authentik.flows.challenge.ChallengeTypes" + }, } REST_FRAMEWORK = { diff --git a/authentik/sources/ldap/api.py b/authentik/sources/ldap/api.py index 61aca7c91..9196186f3 100644 --- a/authentik/sources/ldap/api.py +++ b/authentik/sources/ldap/api.py @@ -1,7 +1,7 @@ """Source API Views""" from django.http.response import Http404 from django.utils.text import slugify -from drf_spectacular.utils import extend_schema +from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.decorators import action from rest_framework.request import Request from rest_framework.response import Response @@ -48,7 +48,12 @@ class LDAPSourceViewSet(ModelViewSet): serializer_class = LDAPSourceSerializer lookup_field = "slug" - @extend_schema(responses={200: TaskSerializer(many=False), 404: "Task not found"}) + @extend_schema( + responses={ + 200: TaskSerializer(many=False), + 404: OpenApiResponse(description="Task not found"), + } + ) @action(methods=["GET"], detail=True) # pylint: disable=unused-argument def sync_status(self, request: Request, slug: str) -> Response: diff --git a/authentik/sources/plex/api.py b/authentik/sources/plex/api.py index 2210da72b..00200982a 100644 --- a/authentik/sources/plex/api.py +++ b/authentik/sources/plex/api.py @@ -1,7 +1,7 @@ """Plex Source Serializer""" from django.shortcuts import get_object_or_404 from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import OpenApiParameter, extend_schema +from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from rest_framework.fields import CharField @@ -54,8 +54,8 @@ class PlexSourceViewSet(ModelViewSet): request=PlexTokenRedeemSerializer(), responses={ 200: RedirectChallenge(), - 400: "Token not found", - 403: "Access denied", + 400: OpenApiResponse(description="Token not found"), + 403: OpenApiResponse(description="Access denied"), }, parameters=[ OpenApiParameter( diff --git a/authentik/stages/authenticator_static/api.py b/authentik/stages/authenticator_static/api.py index 555a42bab..2bbaf04a5 100644 --- a/authentik/stages/authenticator_static/api.py +++ b/authentik/stages/authenticator_static/api.py @@ -34,7 +34,6 @@ class StaticDeviceSerializer(ModelSerializer): model = StaticDevice fields = ["name", "token_set", "pk"] - depth = 2 class StaticDeviceViewSet(ModelViewSet): diff --git a/schema.yml b/schema.yml index e7f831aec..436453b63 100644 --- a/schema.yml +++ b/schema.yml @@ -105,6 +105,18 @@ paths: required: true tags: - admin + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/TaskRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TaskRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/TaskRequest' + required: true security: - authentik: [] - cookieAuth: [] @@ -21190,6 +21202,28 @@ components: - task_description - task_finish_timestamp - task_name + TaskRequest: + type: object + description: Serialize TaskInfo and TaskResult + properties: + task_name: + type: string + task_description: + type: string + task_finish_timestamp: + type: string + format: date-time + status: + $ref: '#/components/schemas/StatusEnum' + messages: + type: array + items: {} + required: + - messages + - status + - task_description + - task_finish_timestamp + - task_name Token: type: object description: Token Serializer