diff --git a/authentik/api/auth.py b/authentik/api/authentication.py similarity index 96% rename from authentik/api/auth.py rename to authentik/api/authentication.py index 412b69d5e..82a4c1b01 100644 --- a/authentik/api/auth.py +++ b/authentik/api/authentication.py @@ -43,7 +43,7 @@ def token_from_header(raw_header: bytes) -> Optional[Token]: return tokens.first() -class AuthentikTokenAuthentication(BaseAuthentication): +class TokenAuthentication(BaseAuthentication): """Token-based authentication using HTTP Bearer authentication""" def authenticate(self, request: Request) -> Union[tuple[User, Any], None]: @@ -61,7 +61,7 @@ class AuthentikTokenAuthentication(BaseAuthentication): class TokenSchema(OpenApiAuthenticationExtension): """Auth schema""" - target_class = AuthentikTokenAuthentication + target_class = TokenAuthentication name = "authentik" def get_security_definition(self, auto_schema): diff --git a/authentik/api/authorization.py b/authentik/api/authorization.py new file mode 100644 index 000000000..765f94637 --- /dev/null +++ b/authentik/api/authorization.py @@ -0,0 +1,35 @@ +"""API Authorization""" +from django.db.models import Model +from django.db.models.query import QuerySet +from rest_framework.filters import BaseFilterBackend +from rest_framework.permissions import BasePermission +from rest_framework.request import Request + + +class OwnerFilter(BaseFilterBackend): + """Filter objects by their owner""" + + owner_key = "user" + + def filter_queryset(self, request: Request, queryset: QuerySet, view) -> QuerySet: + return queryset.filter(**{self.owner_key: request.user}) + + +class OwnerPermissions(BasePermission): + """Authorize requests by an object's owner matching the requesting user""" + + owner_key = "user" + + def has_permission(self, request: Request, view) -> bool: + """If the user is authenticated, we allow all requests here. For listing, the + object-level permissions are done by the filter backend""" + return request.user.is_authenticated + + def has_object_permission(self, request: Request, view, obj: Model) -> bool: + """Check if the object's owner matches the currently logged in user""" + if not hasattr(obj, self.owner_key): + return False + owner = getattr(obj, self.owner_key) + if owner != request.user: + return False + return True diff --git a/authentik/api/tests/test_auth.py b/authentik/api/tests/test_auth.py index 4f6a6120f..b7ef750cc 100644 --- a/authentik/api/tests/test_auth.py +++ b/authentik/api/tests/test_auth.py @@ -5,7 +5,7 @@ from django.test import TestCase from guardian.shortcuts import get_anonymous_user from rest_framework.exceptions import AuthenticationFailed -from authentik.api.auth import token_from_header +from authentik.api.authentication import token_from_header from authentik.core.models import Token, TokenIntents diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py index 54666fe63..6c7971836 100644 --- a/authentik/api/v2/urls.py +++ b/authentik/api/v2/urls.py @@ -161,9 +161,19 @@ router.register("propertymappings/scope", ScopeMappingViewSet) router.register("authenticators/static", StaticDeviceViewSet) router.register("authenticators/totp", TOTPDeviceViewSet) router.register("authenticators/webauthn", WebAuthnDeviceViewSet) -router.register("authenticators/admin/static", StaticAdminDeviceViewSet) -router.register("authenticators/admin/totp", TOTPAdminDeviceViewSet) -router.register("authenticators/admin/webauthn", WebAuthnAdminDeviceViewSet) +router.register( + "authenticators/admin/static", + StaticAdminDeviceViewSet, + basename="admin-staticdevice", +) +router.register( + "authenticators/admin/totp", TOTPAdminDeviceViewSet, basename="admin-totpdevice" +) +router.register( + "authenticators/admin/webauthn", + WebAuthnAdminDeviceViewSet, + basename="admin-webauthndevice", +) router.register("stages/all", StageViewSet) router.register("stages/authenticator/static", AuthenticatorStaticStageViewSet) diff --git a/authentik/core/api/propertymappings.py b/authentik/core/api/propertymappings.py index 798a7a722..4eff31150 100644 --- a/authentik/core/api/propertymappings.py +++ b/authentik/core/api/propertymappings.py @@ -78,7 +78,7 @@ class PropertyMappingViewSet( filterset_fields = {"managed": ["isnull"]} ordering = ["name"] - def get_queryset(self): + def get_queryset(self): # pragma: no cover return PropertyMapping.objects.select_subclasses() @extend_schema(responses={200: TypeCreateSerializer(many=True)}) diff --git a/authentik/core/api/providers.py b/authentik/core/api/providers.py index a6a7c2e88..86e210736 100644 --- a/authentik/core/api/providers.py +++ b/authentik/core/api/providers.py @@ -63,7 +63,7 @@ class ProviderViewSet( "application__name", ] - def get_queryset(self): + def get_queryset(self): # pragma: no cover return Provider.objects.select_subclasses() @extend_schema(responses={200: TypeCreateSerializer(many=True)}) diff --git a/authentik/core/api/sources.py b/authentik/core/api/sources.py index def370b2a..4ee717ed3 100644 --- a/authentik/core/api/sources.py +++ b/authentik/core/api/sources.py @@ -61,7 +61,7 @@ class SourceViewSet( serializer_class = SourceSerializer lookup_field = "slug" - def get_queryset(self): + def get_queryset(self): # pragma: no cover return Source.objects.select_subclasses() @extend_schema(responses={200: TypeCreateSerializer(many=True)}) diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index 39aeb696c..f36ecc258 100644 --- a/authentik/core/api/users.py +++ b/authentik/core/api/users.py @@ -139,7 +139,7 @@ class UserViewSet(ModelViewSet): search_fields = ["username", "name", "is_active"] filterset_class = UsersFilter - def get_queryset(self): + def get_queryset(self): # pragma: no cover return User.objects.all().exclude(pk=get_anonymous_user().pk) @extend_schema(responses={200: SessionUserSerializer(many=False)}) diff --git a/authentik/core/channels.py b/authentik/core/channels.py index cb124e4b4..a081ec985 100644 --- a/authentik/core/channels.py +++ b/authentik/core/channels.py @@ -4,7 +4,7 @@ from channels.generic.websocket import JsonWebsocketConsumer from rest_framework.exceptions import AuthenticationFailed from structlog.stdlib import get_logger -from authentik.api.auth import token_from_header +from authentik.api.authentication import token_from_header from authentik.core.models import User LOGGER = get_logger() diff --git a/authentik/events/api/notification.py b/authentik/events/api/notification.py index 546754104..07b2ac49e 100644 --- a/authentik/events/api/notification.py +++ b/authentik/events/api/notification.py @@ -1,12 +1,12 @@ """Notification API Views""" from django_filters.rest_framework import DjangoFilterBackend -from guardian.utils import get_anonymous_user from rest_framework import mixins from rest_framework.fields import ReadOnlyField from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import GenericViewSet +from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.events.api.event import EventSerializer from authentik.events.models import Notification @@ -49,12 +49,5 @@ class NotificationViewSet( "event", "seen", ] - filter_backends = [ - DjangoFilterBackend, - OrderingFilter, - SearchFilter, - ] - - def get_queryset(self): - user = self.request.user if self.request else get_anonymous_user() - return Notification.objects.filter(user=user.pk) + permission_classes = [OwnerPermissions] + filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] diff --git a/authentik/flows/api/stages.py b/authentik/flows/api/stages.py index a700e3b38..3968e3b6f 100644 --- a/authentik/flows/api/stages.py +++ b/authentik/flows/api/stages.py @@ -65,7 +65,7 @@ class StageViewSet( search_fields = ["name"] filterset_fields = ["name"] - def get_queryset(self): + def get_queryset(self): # pragma: no cover return Stage.objects.select_subclasses() @extend_schema(responses={200: TypeCreateSerializer(many=True)}) diff --git a/authentik/lib/views.py b/authentik/lib/views.py index bfa28414e..f444dd2d6 100644 --- a/authentik/lib/views.py +++ b/authentik/lib/views.py @@ -2,28 +2,6 @@ from django.http import HttpRequest from django.template.response import TemplateResponse from django.utils.translation import gettext_lazy as _ -from django.views.generic import CreateView -from guardian.shortcuts import assign_perm - - -class CreateAssignPermView(CreateView): - """Assign permissions to object after creation""" - - permissions = [ - "%s.view_%s", - "%s.change_%s", - "%s.delete_%s", - ] - - def form_valid(self, form): - response = super().form_valid(form) - for permission in self.permissions: - full_permission = permission % ( - self.object._meta.app_label, - self.object._meta.model_name, - ) - assign_perm(full_permission, self.request.user, self.object) - return response def bad_request_message( diff --git a/authentik/policies/api/policies.py b/authentik/policies/api/policies.py index 9de4e0b66..1f08cc6ec 100644 --- a/authentik/policies/api/policies.py +++ b/authentik/policies/api/policies.py @@ -92,7 +92,7 @@ class PolicyViewSet( } search_fields = ["name"] - def get_queryset(self): + def get_queryset(self): # pragma: no cover return Policy.objects.select_subclasses().prefetch_related( "bindings", "promptstage_set" ) diff --git a/authentik/root/settings.py b/authentik/root/settings.py index a52475a73..039e6bef6 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -174,7 +174,7 @@ REST_FRAMEWORK = { "rest_framework.permissions.DjangoObjectPermissions", ), "DEFAULT_AUTHENTICATION_CLASSES": ( - "authentik.api.auth.AuthentikTokenAuthentication", + "authentik.api.authentication.TokenAuthentication", "rest_framework.authentication.SessionAuthentication", ), "DEFAULT_RENDERER_CLASSES": [ diff --git a/authentik/sources/oauth/api/source_connection.py b/authentik/sources/oauth/api/source_connection.py index fb140fcc8..c7e1d036b 100644 --- a/authentik/sources/oauth/api/source_connection.py +++ b/authentik/sources/oauth/api/source_connection.py @@ -1,9 +1,10 @@ """OAuth Source Serializer""" from django_filters.rest_framework import DjangoFilterBackend -from guardian.utils import get_anonymous_user +from rest_framework import mixins from rest_framework.filters import OrderingFilter, SearchFilter -from rest_framework.viewsets import ModelViewSet +from rest_framework.viewsets import GenericViewSet +from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.core.api.sources import SourceSerializer from authentik.sources.oauth.models import UserOAuthSourceConnection @@ -21,20 +22,17 @@ class UserOAuthSourceConnectionSerializer(SourceSerializer): ] -class UserOAuthSourceConnectionViewSet(ModelViewSet): +class UserOAuthSourceConnectionViewSet( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + GenericViewSet, +): """Source Viewset""" queryset = UserOAuthSourceConnection.objects.all() serializer_class = UserOAuthSourceConnectionSerializer filterset_fields = ["source__slug"] - filter_backends = [ - DjangoFilterBackend, - OrderingFilter, - SearchFilter, - ] - - def get_queryset(self): - user = self.request.user if self.request else get_anonymous_user() - if user.is_superuser: - return super().get_queryset() - return super().get_queryset().filter(user=user.pk) + permission_classes = [OwnerPermissions] + filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] diff --git a/authentik/stages/authenticator_static/api.py b/authentik/stages/authenticator_static/api.py index 340d2b4df..d0fdea081 100644 --- a/authentik/stages/authenticator_static/api.py +++ b/authentik/stages/authenticator_static/api.py @@ -1,12 +1,13 @@ """AuthenticatorStaticStage API Views""" from django_filters.rest_framework import DjangoFilterBackend from django_otp.plugins.otp_static.models import StaticDevice, StaticToken -from guardian.utils import get_anonymous_user +from rest_framework import mixins from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.permissions import IsAdminUser from rest_framework.serializers import ModelSerializer -from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet +from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet +from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.flows.api.stages import StageSerializer from authentik.stages.authenticator_static.models import AuthenticatorStaticStage @@ -47,23 +48,22 @@ class StaticDeviceSerializer(ModelSerializer): fields = ["name", "token_set", "pk"] -class StaticDeviceViewSet(ModelViewSet): +class StaticDeviceViewSet( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + GenericViewSet, +): """Viewset for static authenticator devices""" - queryset = StaticDevice.objects.none() + queryset = StaticDevice.objects.all() serializer_class = StaticDeviceSerializer + permission_classes = [OwnerPermissions] + filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] search_fields = ["name"] filterset_fields = ["name"] ordering = ["name"] - filter_backends = [ - DjangoFilterBackend, - OrderingFilter, - SearchFilter, - ] - - def get_queryset(self): - user = self.request.user if self.request else get_anonymous_user() - return StaticDevice.objects.filter(user=user.pk) class StaticAdminDeviceViewSet(ReadOnlyModelViewSet): diff --git a/authentik/stages/authenticator_static/tests.py b/authentik/stages/authenticator_static/tests.py new file mode 100644 index 000000000..3c0cf0663 --- /dev/null +++ b/authentik/stages/authenticator_static/tests.py @@ -0,0 +1,20 @@ +"""Test Static API""" +from django.urls import reverse +from django_otp.plugins.otp_static.models import StaticDevice +from rest_framework.test import APITestCase + +from authentik.core.models import User + + +class AuthenticatorStaticStage(APITestCase): + """Test Static API""" + + def test_api_delete(self): + """Test api delete""" + user = User.objects.create(username="foo") + self.client.force_login(user) + dev = StaticDevice.objects.create(user=user) + response = self.client.delete( + reverse("authentik_api:staticdevice-detail", kwargs={"pk": dev.pk}) + ) + self.assertEqual(response.status_code, 204) diff --git a/authentik/stages/authenticator_totp/api.py b/authentik/stages/authenticator_totp/api.py index 4329d4a4a..52689de37 100644 --- a/authentik/stages/authenticator_totp/api.py +++ b/authentik/stages/authenticator_totp/api.py @@ -1,12 +1,13 @@ """AuthenticatorTOTPStage API Views""" -from django_filters.rest_framework import DjangoFilterBackend +from django_filters.rest_framework.backends import DjangoFilterBackend from django_otp.plugins.otp_totp.models import TOTPDevice -from guardian.utils import get_anonymous_user +from rest_framework import mixins from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.permissions import IsAdminUser from rest_framework.serializers import ModelSerializer -from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet +from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet +from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.flows.api.stages import StageSerializer from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage @@ -40,23 +41,22 @@ class TOTPDeviceSerializer(ModelSerializer): depth = 2 -class TOTPDeviceViewSet(ModelViewSet): +class TOTPDeviceViewSet( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + GenericViewSet, +): """Viewset for totp authenticator devices""" - queryset = TOTPDevice.objects.none() + queryset = TOTPDevice.objects.all() serializer_class = TOTPDeviceSerializer + permission_classes = [OwnerPermissions] + filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] search_fields = ["name"] filterset_fields = ["name"] ordering = ["name"] - filter_backends = [ - DjangoFilterBackend, - OrderingFilter, - SearchFilter, - ] - - def get_queryset(self): - user = self.request.user if self.request else get_anonymous_user() - return TOTPDevice.objects.filter(user=user.pk) class TOTPAdminDeviceViewSet(ReadOnlyModelViewSet): diff --git a/authentik/stages/authenticator_totp/tests.py b/authentik/stages/authenticator_totp/tests.py new file mode 100644 index 000000000..f89745bda --- /dev/null +++ b/authentik/stages/authenticator_totp/tests.py @@ -0,0 +1,20 @@ +"""Test TOTP API""" +from django.urls import reverse +from django_otp.plugins.otp_totp.models import TOTPDevice +from rest_framework.test import APITestCase + +from authentik.core.models import User + + +class AuthenticatorTOTPStage(APITestCase): + """Test TOTP API""" + + def test_api_delete(self): + """Test api delete""" + user = User.objects.create(username="foo") + self.client.force_login(user) + dev = TOTPDevice.objects.create(user=user) + response = self.client.delete( + reverse("authentik_api:totpdevice-detail", kwargs={"pk": dev.pk}) + ) + self.assertEqual(response.status_code, 204) diff --git a/authentik/stages/authenticator_webauthn/api.py b/authentik/stages/authenticator_webauthn/api.py index 3afd59a80..9f9314bf8 100644 --- a/authentik/stages/authenticator_webauthn/api.py +++ b/authentik/stages/authenticator_webauthn/api.py @@ -1,11 +1,12 @@ """AuthenticateWebAuthnStage API Views""" -from django_filters.rest_framework import DjangoFilterBackend -from guardian.utils import get_anonymous_user +from django_filters.rest_framework.backends import DjangoFilterBackend +from rest_framework import mixins from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.permissions import IsAdminUser from rest_framework.serializers import ModelSerializer -from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet +from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet +from authentik.api.authorization import OwnerFilter, OwnerPermissions from authentik.flows.api.stages import StageSerializer from authentik.stages.authenticator_webauthn.models import ( AuthenticateWebAuthnStage, @@ -39,23 +40,22 @@ class WebAuthnDeviceSerializer(ModelSerializer): depth = 2 -class WebAuthnDeviceViewSet(ModelViewSet): +class WebAuthnDeviceViewSet( + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + GenericViewSet, +): """Viewset for WebAuthn authenticator devices""" - queryset = WebAuthnDevice.objects.none() + queryset = WebAuthnDevice.objects.all() serializer_class = WebAuthnDeviceSerializer search_fields = ["name"] filterset_fields = ["name"] ordering = ["name"] - filter_backends = [ - DjangoFilterBackend, - OrderingFilter, - SearchFilter, - ] - - def get_queryset(self): - user = self.request.user if self.request else get_anonymous_user() - return WebAuthnDevice.objects.filter(user=user.pk) + permission_classes = [OwnerPermissions] + filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter] class WebAuthnAdminDeviceViewSet(ReadOnlyModelViewSet): diff --git a/authentik/stages/authenticator_webauthn/tests.py b/authentik/stages/authenticator_webauthn/tests.py new file mode 100644 index 000000000..9d4441bba --- /dev/null +++ b/authentik/stages/authenticator_webauthn/tests.py @@ -0,0 +1,20 @@ +"""Test WebAuthn API""" +from django.urls import reverse +from rest_framework.test import APITestCase + +from authentik.core.models import User +from authentik.stages.authenticator_webauthn.models import WebAuthnDevice + + +class AuthenticatorWebAuthnStage(APITestCase): + """Test WebAuthn API""" + + def test_api_delete(self): + """Test api delete""" + user = User.objects.create(username="foo") + self.client.force_login(user) + dev = WebAuthnDevice.objects.create(user=user) + response = self.client.delete( + reverse("authentik_api:webauthndevice-detail", kwargs={"pk": dev.pk}) + ) + self.assertEqual(response.status_code, 204) diff --git a/authentik/stages/dummy/tests.py b/authentik/stages/dummy/tests.py index b943d391e..1bfdef559 100644 --- a/authentik/stages/dummy/tests.py +++ b/authentik/stages/dummy/tests.py @@ -1,5 +1,5 @@ """dummy tests""" -from django.test import Client, TestCase +from django.test import TestCase from django.urls import reverse from django.utils.encoding import force_str @@ -14,7 +14,6 @@ class TestDummyStage(TestCase): def setUp(self): super().setUp() self.user = User.objects.create(username="unittest", email="test@beryju.org") - self.client = Client() self.flow = Flow.objects.create( name="test-dummy", diff --git a/schema.yml b/schema.yml index 0072788e9..a1446dc34 100644 --- a/schema.yml +++ b/schema.yml @@ -444,37 +444,6 @@ paths: $ref: '#/components/schemas/ValidationError' '403': $ref: '#/components/schemas/GenericError' - post: - operationId: authenticators_static_create - description: Viewset for static authenticator devices - tags: - - authenticators - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/StaticDeviceRequest' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/StaticDeviceRequest' - multipart/form-data: - schema: - $ref: '#/components/schemas/StaticDeviceRequest' - required: true - security: - - authentik: [] - - cookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/StaticDevice' - description: '' - '400': - $ref: '#/components/schemas/ValidationError' - '403': - $ref: '#/components/schemas/GenericError' /api/v2beta/authenticators/static/{id}/: get: operationId: authenticators_static_retrieve @@ -648,37 +617,6 @@ paths: $ref: '#/components/schemas/ValidationError' '403': $ref: '#/components/schemas/GenericError' - post: - operationId: authenticators_totp_create - description: Viewset for totp authenticator devices - tags: - - authenticators - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/TOTPDeviceRequest' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/TOTPDeviceRequest' - multipart/form-data: - schema: - $ref: '#/components/schemas/TOTPDeviceRequest' - required: true - security: - - authentik: [] - - cookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/TOTPDevice' - description: '' - '400': - $ref: '#/components/schemas/ValidationError' - '403': - $ref: '#/components/schemas/GenericError' /api/v2beta/authenticators/totp/{id}/: get: operationId: authenticators_totp_retrieve @@ -852,37 +790,6 @@ paths: $ref: '#/components/schemas/ValidationError' '403': $ref: '#/components/schemas/GenericError' - post: - operationId: authenticators_webauthn_create - description: Viewset for WebAuthn authenticator devices - tags: - - authenticators - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/WebAuthnDeviceRequest' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/WebAuthnDeviceRequest' - multipart/form-data: - schema: - $ref: '#/components/schemas/WebAuthnDeviceRequest' - required: true - security: - - authentik: [] - - cookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/WebAuthnDevice' - description: '' - '400': - $ref: '#/components/schemas/ValidationError' - '403': - $ref: '#/components/schemas/GenericError' /api/v2beta/authenticators/webauthn/{id}/: get: operationId: authenticators_webauthn_retrieve @@ -10117,37 +10024,6 @@ paths: $ref: '#/components/schemas/ValidationError' '403': $ref: '#/components/schemas/GenericError' - post: - operationId: sources_oauth_user_connections_create - description: Source Viewset - tags: - - sources - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UserOAuthSourceConnectionRequest' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/UserOAuthSourceConnectionRequest' - multipart/form-data: - schema: - $ref: '#/components/schemas/UserOAuthSourceConnectionRequest' - required: true - security: - - authentik: [] - - cookieAuth: [] - responses: - '201': - content: - application/json: - schema: - $ref: '#/components/schemas/UserOAuthSourceConnection' - description: '' - '400': - $ref: '#/components/schemas/ValidationError' - '403': - $ref: '#/components/schemas/GenericError' /api/v2beta/sources/oauth_user_connections/{id}/: get: operationId: sources_oauth_user_connections_retrieve