diff --git a/authentik/core/api/sources.py b/authentik/core/api/sources.py index 66cf1f087..509f86504 100644 --- a/authentik/core/api/sources.py +++ b/authentik/core/api/sources.py @@ -1,4 +1,6 @@ """Source API Views""" +from typing import Iterable + from django.urls import reverse from drf_yasg2.utils import swagger_auto_schema from rest_framework.decorators import action @@ -6,11 +8,16 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ModelSerializer, SerializerMethodField from rest_framework.viewsets import ReadOnlyModelViewSet +from structlog.stdlib import get_logger from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer from authentik.core.models import Source +from authentik.flows.challenge import Challenge from authentik.lib.templatetags.authentik_utils import verbose_name from authentik.lib.utils.reflection import all_subclasses +from authentik.policies.engine import PolicyEngine + +LOGGER = get_logger() class SourceSerializer(ModelSerializer, MetaNameSerializer): @@ -63,3 +70,25 @@ class SourceViewSet(ReadOnlyModelViewSet): } ) return Response(TypeCreateSerializer(data, many=True).data) + + @swagger_auto_schema(responses={200: Challenge(many=True)}) + @action(detail=False) + def user_settings(self, request: Request) -> Response: + """Get all sources the user can configure""" + _all_sources: Iterable[Source] = Source.objects.filter( + enabled=True + ).select_subclasses() + matching_sources: list[Challenge] = [] + for source in _all_sources: + user_settings = source.ui_user_settings + if not user_settings: + continue + policy_engine = PolicyEngine(source, request.user, request) + policy_engine.build() + if not policy_engine.passing: + continue + source_settings = source.ui_user_settings + if not source_settings.is_valid(): + LOGGER.warning(source_settings.errors) + matching_sources.append(source_settings.validated_data) + return Response(matching_sources) diff --git a/authentik/core/models.py b/authentik/core/models.py index 1e556734d..1393c7bd4 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -25,6 +25,7 @@ from structlog.stdlib import get_logger from authentik.core.exceptions import PropertyMappingExpressionException from authentik.core.signals import password_changed from authentik.core.types import UILoginButton +from authentik.flows.challenge import Challenge from authentik.flows.models import Flow from authentik.lib.config import CONFIG from authentik.lib.models import CreatedUpdatedModel, SerializerModel @@ -286,9 +287,9 @@ class Source(SerializerModel, PolicyBindingModel): return None @property - def ui_user_settings(self) -> Optional[str]: + def ui_user_settings(self) -> Optional[Challenge]: """Entrypoint to integrate with User settings. Can either return None if no - user settings are available, or a string with the URL to fetch.""" + user settings are available, or a challenge.""" return None def __str__(self): diff --git a/authentik/sources/oauth/models.py b/authentik/sources/oauth/models.py index af232e976..234af4776 100644 --- a/authentik/sources/oauth/models.py +++ b/authentik/sources/oauth/models.py @@ -10,6 +10,7 @@ from rest_framework.serializers import Serializer from authentik.core.models import Source, UserSourceConnection from authentik.core.types import UILoginButton +from authentik.flows.challenge import Challenge, ChallengeTypes class OAuthSource(Source): @@ -66,9 +67,15 @@ class OAuthSource(Source): ) @property - def ui_user_settings(self) -> Optional[str]: + def ui_user_settings(self) -> Optional[Challenge]: view_name = "authentik_sources_oauth:oauth-client-user" - return reverse(view_name, kwargs={"source_slug": self.slug}) + return Challenge( + data={ + "type": ChallengeTypes.shell.value, + "title": self.name, + "component": reverse(view_name, kwargs={"source_slug": self.slug}), + } + ) def __str__(self) -> str: return f"OAuth Source {self.name}" diff --git a/swagger.yaml b/swagger.yaml index 3580e81ef..1544348f3 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -6688,6 +6688,43 @@ paths: tags: - sources parameters: [] + /sources/all/user_settings/: + get: + operationId: sources_all_user_settings + description: Get all sources the user can configure + parameters: + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: Page Index + required: false + type: integer + - name: page_size + in: query + description: Page Size + required: false + type: integer + responses: + '200': + description: Challenge that gets sent to the client based on which stage + is currently active + schema: + description: '' + type: array + items: + $ref: '#/definitions/Challenge' + tags: + - sources + parameters: [] /sources/all/{slug}/: get: operationId: sources_all_read