From a9519a4a6855684cf1c90a756c3ec3f4963d1c45 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 21 May 2021 00:03:02 +0200 Subject: [PATCH 1/7] g: set x-forwarded-proto based on upstream TLS Status Signed-off-by: Jens Langhammer --- internal/web/web_proxy.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/internal/web/web_proxy.go b/internal/web/web_proxy.go index 9bcd03d15..b4cb13d20 100644 --- a/internal/web/web_proxy.go +++ b/internal/web/web_proxy.go @@ -9,7 +9,20 @@ import ( func (ws *WebServer) configureProxy() { // Reverse proxy to the application server u, _ := url.Parse("http://localhost:8000") - rp := httputil.NewSingleHostReverseProxy(u) + director := func(req *http.Request) { + req.URL.Scheme = u.Scheme + req.URL.Host = u.Host + if _, ok := req.Header["User-Agent"]; !ok { + // explicitly disable User-Agent so it's not set to default value + req.Header.Set("User-Agent", "") + } + if req.TLS != nil { + req.Header.Set("X-Forwarded-Proto", "https") + } else { + req.Header.Set("X-Forwarded-Proto", "http") + } + } + rp := &httputil.ReverseProxy{Director: director} rp.ErrorHandler = ws.proxyErrorHandler rp.ModifyResponse = ws.proxyModifyResponse ws.m.PathPrefix("/").Handler(rp) From 75f252b53008bdb82661c1ab0ffec5ab0d952612 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 21 May 2021 12:06:39 +0200 Subject: [PATCH 2/7] flows: rename oob to oobe Signed-off-by: Jens Langhammer --- authentik/flows/migrations/0018_oob_flows.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/authentik/flows/migrations/0018_oob_flows.py b/authentik/flows/migrations/0018_oob_flows.py index e89f3083b..5d6395e44 100644 --- a/authentik/flows/migrations/0018_oob_flows.py +++ b/authentik/flows/migrations/0018_oob_flows.py @@ -21,7 +21,7 @@ context["user_backend"] = "django.contrib.auth.backends.ModelBackend" return True""" -def create_default_oob_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): +def create_default_oobe_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): from authentik.stages.prompt.models import FieldTypes User = apps.get_model("authentik_core", "User") @@ -52,20 +52,20 @@ def create_default_oob_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) # Create a policy that sets the flow's user prefill_policy, _ = ExpressionPolicy.objects.using(db_alias).update_or_create( - name="default-oob-prefill-user", + name="default-oobe-prefill-user", defaults={"expression": PREFILL_POLICY_EXPRESSION}, ) password_usable_policy, _ = ExpressionPolicy.objects.using( db_alias ).update_or_create( - name="default-oob-password-usable", + name="default-oobe-password-usable", defaults={"expression": PW_USABLE_POLICY_EXPRESSION}, ) prompt_header, _ = Prompt.objects.using(db_alias).update_or_create( - field_key="oob-header-text", + field_key="oobe-header-text", defaults={ - "label": "oob-header-text", + "label": "oobe-header-text", "type": FieldTypes.STATIC, "placeholder": "Welcome to authentik! Please set a password for the default admin user, akadmin.", "order": 100, @@ -84,7 +84,7 @@ def create_default_oob_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) password_second = Prompt.objects.using(db_alias).get(field_key="password_repeat") prompt_stage, _ = PromptStage.objects.using(db_alias).update_or_create( - name="default-oob-password", + name="default-oobe-password", ) prompt_stage.fields.set( [prompt_header, prompt_email, password_first, password_second] @@ -102,7 +102,7 @@ def create_default_oob_flow(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) slug="initial-setup", designation=FlowDesignation.STAGE_CONFIGURATION, defaults={ - "name": "default-oob-setup", + "name": "default-oobe-setup", "title": "Welcome to authentik!", }, ) @@ -146,5 +146,5 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(create_default_oob_flow), + migrations.RunPython(create_default_oobe_flow), ] From 41a1305555bf5947e442f6970ea93d359d3414ce Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 21 May 2021 19:10:15 +0200 Subject: [PATCH 3/7] policies: improve debug logging Signed-off-by: Jens Langhammer --- authentik/policies/engine.py | 11 ++++++++--- authentik/policies/types.py | 7 ++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/authentik/policies/engine.py b/authentik/policies/engine.py index 492594fad..b8ac67fa2 100644 --- a/authentik/policies/engine.py +++ b/authentik/policies/engine.py @@ -105,16 +105,21 @@ class PolicyEngine: if cached_policy and self.use_cache: self.logger.debug( "P_ENG: Taking result from cache", - policy=binding.policy, + binding=binding, cache_key=key, + request=self.request, ) self.__cached_policies.append(cached_policy) continue - self.logger.debug("P_ENG: Evaluating policy", policy=binding.policy) + self.logger.debug( + "P_ENG: Evaluating policy", binding=binding, request=self.request + ) our_end, task_end = Pipe(False) task = PolicyProcess(binding, self.request, task_end) task.daemon = False - self.logger.debug("P_ENG: Starting Process", policy=binding.policy) + self.logger.debug( + "P_ENG: Starting Process", binding=binding, request=self.request + ) if not CURRENT_PROCESS._config.get("daemon"): task.run() else: diff --git a/authentik/policies/types.py b/authentik/policies/types.py index d69748944..5fec74fe8 100644 --- a/authentik/policies/types.py +++ b/authentik/policies/types.py @@ -51,7 +51,12 @@ class PolicyRequest: LOGGER.warning("failed to get geoip data", exc=exc) def __str__(self): - return f"" + text = f"" @dataclass From 7c6185b581cbcd2ee2a16dc2357d92ba20a74395 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 21 May 2021 19:53:40 +0200 Subject: [PATCH 4/7] api: fix URL names for admin Authenticator Views Signed-off-by: Jens Langhammer --- authentik/api/v2/urls.py | 16 +++++++++++++--- authentik/lib/views.py | 22 ---------------------- authentik/stages/dummy/tests.py | 3 +-- 3 files changed, 14 insertions(+), 27 deletions(-) diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py index d46be9528..067c938de 100644 --- a/authentik/api/v2/urls.py +++ b/authentik/api/v2/urls.py @@ -169,9 +169,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/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/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", From d9a788aac8bc93d838f4677486dd807f56111162 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 21 May 2021 20:14:03 +0200 Subject: [PATCH 5/7] api: rename auth to authentication, add authorization for rest_framework permission class Signed-off-by: Jens Langhammer --- authentik/api/{auth.py => authentication.py} | 2 +- authentik/api/authorization.py | 24 ++++++++++++++++++++ authentik/api/tests/test_auth.py | 2 +- authentik/core/channels.py | 2 +- authentik/root/settings.py | 2 +- 5 files changed, 28 insertions(+), 4 deletions(-) rename authentik/api/{auth.py => authentication.py} (97%) create mode 100644 authentik/api/authorization.py diff --git a/authentik/api/auth.py b/authentik/api/authentication.py similarity index 97% rename from authentik/api/auth.py rename to authentik/api/authentication.py index 97f821b56..54d555d03 100644 --- a/authentik/api/auth.py +++ b/authentik/api/authentication.py @@ -42,7 +42,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]: diff --git a/authentik/api/authorization.py b/authentik/api/authorization.py new file mode 100644 index 000000000..eba573836 --- /dev/null +++ b/authentik/api/authorization.py @@ -0,0 +1,24 @@ +"""API Authorization""" +from django.db.models import Model +from rest_framework.permissions import BasePermission +from rest_framework.request import Request + + +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/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/root/settings.py b/authentik/root/settings.py index cbca48760..e8a219280 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -161,7 +161,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": [ From a603f42cc056dcfdef4ba9f45a05dc4329104605 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 21 May 2021 20:46:59 +0200 Subject: [PATCH 6/7] api: add OwnerFilter Signed-off-by: Jens Langhammer --- authentik/api/authorization.py | 11 +++++++++++ swagger.yaml | 1 + web/src/pages/sources/plex/PlexSourceForm.ts | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/authentik/api/authorization.py b/authentik/api/authorization.py index eba573836..765f94637 100644 --- a/authentik/api/authorization.py +++ b/authentik/api/authorization.py @@ -1,9 +1,20 @@ """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""" diff --git a/swagger.yaml b/swagger.yaml index b15aa7b98..f631bd9ac 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -18045,6 +18045,7 @@ definitions: required: - name - slug + - plex_token type: object properties: pk: diff --git a/web/src/pages/sources/plex/PlexSourceForm.ts b/web/src/pages/sources/plex/PlexSourceForm.ts index 404f83d44..14f7fbcc9 100644 --- a/web/src/pages/sources/plex/PlexSourceForm.ts +++ b/web/src/pages/sources/plex/PlexSourceForm.ts @@ -46,7 +46,7 @@ export class PlexSourceForm extends ModelForm { } send = (data: PlexSource): Promise => { - data.plexToken = this.plexToken; + data.plexToken = this.plexToken || ""; if (this.instance?.slug) { return new SourcesApi(DEFAULT_CONFIG).sourcesPlexUpdate({ slug: this.instance.slug, From a265dd54ccb504b39fcccb705ad5cae16b84126d Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 21 May 2021 21:04:56 +0200 Subject: [PATCH 7/7] 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: