From b942ae78490a81fd357a83311f1250ba0cc3a2c3 Mon Sep 17 00:00:00 2001 From: Marc 'risson' Schmitt Date: Mon, 5 Aug 2024 16:23:00 +0200 Subject: [PATCH] core: applications api: properly select provider (#10373) * core: applications api: properly select provider Signed-off-by: Marc 'risson' Schmitt * fixup get_launch_url Signed-off-by: Marc 'risson' Schmitt * Reapply "core: applications api: add option to only list apps with launch url (#10336)" (#10370) This reverts commit 763a19b9148aa458b77998df62951c4277da91ec. * wip Signed-off-by: Marc 'risson' Schmitt * more fixes Signed-off-by: Marc 'risson' Schmitt * make website Signed-off-by: Marc 'risson' Schmitt * remove serializer change Signed-off-by: Marc 'risson' Schmitt --------- Signed-off-by: Marc 'risson' Schmitt --- authentik/core/api/applications.py | 29 +++++++++++++++++++- authentik/core/models.py | 32 ++++++++++++++++------ authentik/providers/saml/views/metadata.py | 6 ++-- schema.yml | 4 +++ web/src/user/LibraryPage/ak-library.ts | 1 + 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index 6e6db93df5..54c4e3f3c2 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -103,7 +103,12 @@ class ApplicationSerializer(ModelSerializer): class ApplicationViewSet(UsedByMixin, ModelViewSet): """Application Viewset""" - queryset = Application.objects.all().prefetch_related("provider").prefetch_related("policies") + queryset = ( + Application.objects.all() + .with_provider() + .prefetch_related("policies") + .prefetch_related("backchannel_providers") + ) serializer_class = ApplicationSerializer search_fields = [ "name", @@ -147,6 +152,15 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet): applications.append(application) return applications + def _filter_applications_with_launch_url( + self, pagined_apps: Iterator[Application] + ) -> list[Application]: + applications = [] + for app in pagined_apps: + if app.get_launch_url(): + applications.append(app) + return applications + @extend_schema( parameters=[ OpenApiParameter( @@ -204,6 +218,11 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet): location=OpenApiParameter.QUERY, type=OpenApiTypes.INT, ), + OpenApiParameter( + name="only_with_launch_url", + location=OpenApiParameter.QUERY, + type=OpenApiTypes.BOOL, + ), ] ) def list(self, request: Request) -> Response: @@ -216,6 +235,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet): if superuser_full_list and request.user.is_superuser: return super().list(request) + only_with_launch_url = str( + request.query_params.get("only_with_launch_url", "false") + ).lower() + queryset = self._filter_queryset_for_list(self.get_queryset()) paginator: Pagination = self.paginator paginated_apps = paginator.paginate_queryset(queryset, request) @@ -251,6 +274,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet): allowed_applications, timeout=86400, ) + + if only_with_launch_url == "true": + allowed_applications = self._filter_applications_with_launch_url(allowed_applications) + serializer = self.get_serializer(allowed_applications, many=True) return self.get_paginated_response(serializer.data) diff --git a/authentik/core/models.py b/authentik/core/models.py index 908bb614b6..deaa9df923 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -11,6 +11,7 @@ from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import UserManager as DjangoUserManager from django.db import models from django.db.models import Q, QuerySet, options +from django.db.models.constants import LOOKUP_SEP from django.http import HttpRequest from django.utils.functional import SimpleLazyObject, cached_property from django.utils.timezone import now @@ -461,6 +462,16 @@ class BackchannelProvider(Provider): abstract = True +class ApplicationQuerySet(QuerySet): + def with_provider(self) -> "QuerySet[Application]": + qs = self.select_related("provider") + for subclass in Provider.objects.get_queryset()._get_subclasses_recurse(Provider): + if LOOKUP_SEP in subclass: + continue + qs = qs.select_related(f"provider__{subclass}") + return qs + + class Application(SerializerModel, PolicyBindingModel): """Every Application which uses authentik for authentication/identification/authorization needs an Application record. Other authentication types can subclass this Model to @@ -492,6 +503,8 @@ class Application(SerializerModel, PolicyBindingModel): meta_description = models.TextField(default="", blank=True) meta_publisher = models.TextField(default="", blank=True) + objects = ApplicationQuerySet.as_manager() + @property def serializer(self) -> Serializer: from authentik.core.api.applications import ApplicationSerializer @@ -528,16 +541,19 @@ class Application(SerializerModel, PolicyBindingModel): return url def get_provider(self) -> Provider | None: - """Get casted provider instance""" + """Get casted provider instance. Needs Application queryset with_provider""" if not self.provider: return None - # if the Application class has been cache, self.provider is set - # but doing a direct query lookup will fail. - # In that case, just return None - try: - return Provider.objects.get_subclass(pk=self.provider.pk) - except Provider.DoesNotExist: - return None + + for subclass in Provider.objects.get_queryset()._get_subclasses_recurse(Provider): + # We don't care about recursion, skip nested models + if LOOKUP_SEP in subclass: + continue + try: + return getattr(self.provider, subclass) + except AttributeError: + pass + return None def __str__(self): return str(self.name) diff --git a/authentik/providers/saml/views/metadata.py b/authentik/providers/saml/views/metadata.py index e76b997804..73437df090 100644 --- a/authentik/providers/saml/views/metadata.py +++ b/authentik/providers/saml/views/metadata.py @@ -1,7 +1,7 @@ """metadata redirect""" from django.http import Http404, HttpRequest, HttpResponse -from django.shortcuts import get_object_or_404, redirect +from django.shortcuts import redirect from django.urls import reverse from django.views import View @@ -12,7 +12,9 @@ class MetadataDownload(View): """Redirect to metadata download""" def dispatch(self, request: HttpRequest, application_slug: str) -> HttpResponse: - app: Application = get_object_or_404(Application, slug=application_slug) + app = Application.objects.filter(slug=application_slug).with_provider().first() + if not app: + raise Http404 provider = app.get_provider() if not provider: raise Http404 diff --git a/schema.yml b/schema.yml index abe058e533..c7639c7359 100644 --- a/schema.yml +++ b/schema.yml @@ -2682,6 +2682,10 @@ paths: name: name schema: type: string + - in: query + name: only_with_launch_url + schema: + type: boolean - name: ordering required: false in: query diff --git a/web/src/user/LibraryPage/ak-library.ts b/web/src/user/LibraryPage/ak-library.ts index e6edea0ac0..c783156637 100644 --- a/web/src/user/LibraryPage/ak-library.ts +++ b/web/src/user/LibraryPage/ak-library.ts @@ -70,6 +70,7 @@ export class LibraryPage extends AKElement { ordering: "name", page, pageSize: 100, + onlyWithLaunchUrl: true, }); const applicationListFetch = await coreApi().coreApplicationsList(applicationListParams(1));