From a265dd54ccb504b39fcccb705ad5cae16b84126d Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 21 May 2021 21:04:56 +0200 Subject: [PATCH] stages/authenticator_*: fix Permission Error when disabling Authenticator as non-superuser Signed-off-by: Jens Langhammer --- authentik/core/api/propertymappings.py | 2 +- authentik/core/api/providers.py | 2 +- authentik/core/api/sources.py | 2 +- authentik/core/api/users.py | 2 +- authentik/events/api/notification.py | 13 +-- authentik/flows/api/stages.py | 2 +- authentik/policies/api/policies.py | 2 +- .../sources/oauth/api/source_connection.py | 26 +++-- authentik/stages/authenticator_static/api.py | 26 ++--- .../stages/authenticator_static/tests.py | 20 ++++ authentik/stages/authenticator_totp/api.py | 28 +++--- authentik/stages/authenticator_totp/tests.py | 20 ++++ .../stages/authenticator_webauthn/api.py | 28 +++--- .../stages/authenticator_webauthn/tests.py | 20 ++++ swagger.yaml | 96 ------------------- 15 files changed, 122 insertions(+), 167 deletions(-) create mode 100644 authentik/stages/authenticator_static/tests.py create mode 100644 authentik/stages/authenticator_totp/tests.py create mode 100644 authentik/stages/authenticator_webauthn/tests.py diff --git a/authentik/core/api/propertymappings.py b/authentik/core/api/propertymappings.py index fc1954b49..ccca8b52a 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() @swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)}) diff --git a/authentik/core/api/providers.py b/authentik/core/api/providers.py index f4f8ec22d..b515899cd 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() @swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)}) diff --git a/authentik/core/api/sources.py b/authentik/core/api/sources.py index e1da3a2fb..4438fa7e4 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() @swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)}) diff --git a/authentik/core/api/users.py b/authentik/core/api/users.py index a3f315678..87018fd6b 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) @swagger_auto_schema(responses={200: SessionUserSerializer(many=False)}) 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 749e4a7ac..762c4e8f9 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() @swagger_auto_schema(responses={200: TypeCreateSerializer(many=True)}) diff --git a/authentik/policies/api/policies.py b/authentik/policies/api/policies.py index 9a089e93a..f03da21dc 100644 --- a/authentik/policies/api/policies.py +++ b/authentik/policies/api/policies.py @@ -91,7 +91,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/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 555a42bab..e2f39511d 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 -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 @@ -37,23 +38,22 @@ class StaticDeviceSerializer(ModelSerializer): depth = 2 -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/swagger.yaml b/swagger.yaml index f631bd9ac..5e544285f 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -595,30 +595,6 @@ paths: $ref: '#/definitions/GenericError' tags: - authenticators - post: - operationId: authenticators_static_create - description: Viewset for static authenticator devices - parameters: - - name: data - in: body - required: true - schema: - $ref: '#/definitions/StaticDevice' - responses: - '201': - description: '' - schema: - $ref: '#/definitions/StaticDevice' - '400': - description: Invalid input. - schema: - $ref: '#/definitions/ValidationError' - '403': - description: Authentication credentials were invalid, absent or insufficient. - schema: - $ref: '#/definitions/GenericError' - tags: - - authenticators parameters: [] /authenticators/static/{id}/: get: @@ -797,30 +773,6 @@ paths: $ref: '#/definitions/GenericError' tags: - authenticators - post: - operationId: authenticators_totp_create - description: Viewset for totp authenticator devices - parameters: - - name: data - in: body - required: true - schema: - $ref: '#/definitions/TOTPDevice' - responses: - '201': - description: '' - schema: - $ref: '#/definitions/TOTPDevice' - '400': - description: Invalid input. - schema: - $ref: '#/definitions/ValidationError' - '403': - description: Authentication credentials were invalid, absent or insufficient. - schema: - $ref: '#/definitions/GenericError' - tags: - - authenticators parameters: [] /authenticators/totp/{id}/: get: @@ -999,30 +951,6 @@ paths: $ref: '#/definitions/GenericError' tags: - authenticators - post: - operationId: authenticators_webauthn_create - description: Viewset for WebAuthn authenticator devices - parameters: - - name: data - in: body - required: true - schema: - $ref: '#/definitions/WebAuthnDevice' - responses: - '201': - description: '' - schema: - $ref: '#/definitions/WebAuthnDevice' - '400': - description: Invalid input. - schema: - $ref: '#/definitions/ValidationError' - '403': - description: Authentication credentials were invalid, absent or insufficient. - schema: - $ref: '#/definitions/GenericError' - tags: - - authenticators parameters: [] /authenticators/webauthn/{id}/: get: @@ -10425,30 +10353,6 @@ paths: $ref: '#/definitions/GenericError' tags: - sources - post: - operationId: sources_oauth_user_connections_create - description: Source Viewset - parameters: - - name: data - in: body - required: true - schema: - $ref: '#/definitions/UserOAuthSourceConnection' - responses: - '201': - description: '' - schema: - $ref: '#/definitions/UserOAuthSourceConnection' - '400': - description: Invalid input. - schema: - $ref: '#/definitions/ValidationError' - '403': - description: Authentication credentials were invalid, absent or insufficient. - schema: - $ref: '#/definitions/GenericError' - tags: - - sources parameters: [] /sources/oauth_user_connections/{id}/: get: