diff --git a/authentik/core/models.py b/authentik/core/models.py index e76ac8c62..a262fe2b4 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -24,8 +24,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.core.types import UILoginButton, UserSettingSerializer from authentik.flows.models import Flow from authentik.lib.config import CONFIG from authentik.lib.models import CreatedUpdatedModel, SerializerModel @@ -342,9 +341,9 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel): return None @property - def ui_user_settings(self) -> Optional[Challenge]: + def ui_user_settings(self) -> Optional[UserSettingSerializer]: """Entrypoint to integrate with User settings. Can either return None if no - user settings are available, or a challenge.""" + user settings are available, or UserSettingSerializer.""" return None def __str__(self): diff --git a/authentik/core/types.py b/authentik/core/types.py index d8ba7cadf..4773e826a 100644 --- a/authentik/core/types.py +++ b/authentik/core/types.py @@ -36,3 +36,4 @@ class UserSettingSerializer(PassiveSerializer): object_uid = CharField() component = CharField() title = CharField() + configure_url = CharField() diff --git a/authentik/flows/api/stages.py b/authentik/flows/api/stages.py index d14acf700..0b83dbf8b 100644 --- a/authentik/flows/api/stages.py +++ b/authentik/flows/api/stages.py @@ -5,7 +5,6 @@ from django.urls.base import reverse from drf_spectacular.utils import extend_schema from rest_framework import mixins from rest_framework.decorators import action -from rest_framework.fields import BooleanField, CharField from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ModelSerializer, SerializerMethodField @@ -21,12 +20,6 @@ from authentik.lib.utils.reflection import all_subclasses LOGGER = get_logger() -class StageUserSettingSerializer(UserSettingSerializer): - """User settings but can include a configure flow""" - - configure_flow = CharField(required=False) - - class StageSerializer(ModelSerializer, MetaNameSerializer): """Stage Serializer""" @@ -87,7 +80,7 @@ class StageViewSet( data = sorted(data, key=lambda x: x["name"]) return Response(TypeCreateSerializer(data, many=True).data) - @extend_schema(responses={200: StageUserSettingSerializer(many=True)}) + @extend_schema(responses={200: UserSettingSerializer(many=True)}) @action(detail=False, pagination_class=None, filter_backends=[]) def user_settings(self, request: Request) -> Response: """Get all stages the user can configure""" @@ -98,8 +91,8 @@ class StageViewSet( if not user_settings: continue user_settings.initial_data["object_uid"] = str(stage.pk) - if hasattr(stage, "configure_flow"): - user_settings.initial_data["configure_flow"] = reverse( + if hasattr(stage, "configure_url"): + user_settings.initial_data["configure_url"] = reverse( "authentik_flows:configure", kwargs={"stage_uuid": stage.uuid.hex}, ) diff --git a/authentik/flows/challenge.py b/authentik/flows/challenge.py index cc7401b9c..2a9237234 100644 --- a/authentik/flows/challenge.py +++ b/authentik/flows/challenge.py @@ -43,7 +43,7 @@ class Challenge(PassiveSerializer): type = ChoiceField( choices=[(x.value, x.name) for x in ChallengeTypes], ) - flow_info = ContextualFlowInfo() + flow_info = ContextualFlowInfo(required=False) component = CharField(default="") response_errors = DictField( diff --git a/authentik/flows/tests/test_views.py b/authentik/flows/tests/test_views.py index 82dbda8c3..16823d3f5 100644 --- a/authentik/flows/tests/test_views.py +++ b/authentik/flows/tests/test_views.py @@ -93,7 +93,11 @@ class TestFlowExecutor(TestCase): { "component": "ak-stage-access-denied", "error_message": FlowNonApplicableException.__doc__, - "title": "", + "flow_info": { + "background": flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": "", + }, "type": ChallengeTypes.NATIVE.value, }, ) @@ -422,10 +426,13 @@ class TestFlowExecutor(TestCase): self.assertJSONEqual( force_str(response.content), { - "background": flow.background_url, "type": ChallengeTypes.NATIVE.value, "component": "ak-stage-dummy", - "title": binding.stage.name, + "flow_info": { + "background": flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": "", + }, }, ) @@ -453,10 +460,13 @@ class TestFlowExecutor(TestCase): self.assertJSONEqual( force_str(response.content), { - "background": flow.background_url, "type": ChallengeTypes.NATIVE.value, "component": "ak-stage-dummy", - "title": binding4.stage.name, + "flow_info": { + "background": flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": "", + }, }, ) diff --git a/authentik/flows/views.py b/authentik/flows/views.py index e0dfc7328..826211ff7 100644 --- a/authentik/flows/views.py +++ b/authentik/flows/views.py @@ -8,6 +8,7 @@ from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect from django.http.request import QueryDict from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse +from django.urls.base import reverse from django.utils.decorators import method_decorator from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.generic import View @@ -309,9 +310,13 @@ class FlowExecutorView(APIView): AccessDeniedChallenge( { "error_message": error_message, - "title": self.flow.title, "type": ChallengeTypes.NATIVE.value, "component": "ak-stage-access-denied", + "flow_info": { + "title": self.flow.title, + "background": self.flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + }, } ) ) @@ -413,7 +418,10 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons ) return HttpChallengeResponse( RedirectChallenge( - {"type": ChallengeTypes.REDIRECT, "to": str(redirect_url)} + { + "type": ChallengeTypes.REDIRECT, + "to": str(redirect_url), + } ) ) if isinstance(source, TemplateResponse): @@ -425,7 +433,7 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons } ) ) - # Check for actual HttpResponse (without isinstance as we dont want to check inheritance) + # Check for actual HttpResponse (without isinstance as we don't want to check inheritance) if source.__class__ == HttpResponse: return HttpChallengeResponse( ShellChallenge( diff --git a/authentik/sources/oauth/models.py b/authentik/sources/oauth/models.py index b44ff57f1..13b0bf364 100644 --- a/authentik/sources/oauth/models.py +++ b/authentik/sources/oauth/models.py @@ -87,6 +87,10 @@ class OAuthSource(Source): data={ "title": f"OAuth {self.name}", "component": "ak-user-settings-source-oauth", + "configure_url": reverse( + "authentik_sources_oauth:oauth-client-login", + kwargs={"source_slug": self.slug}, + ), } ) diff --git a/authentik/stages/deny/tests.py b/authentik/stages/deny/tests.py index f87d4601a..0df3d9bfd 100644 --- a/authentik/stages/deny/tests.py +++ b/authentik/stages/deny/tests.py @@ -47,7 +47,11 @@ class TestUserDenyStage(TestCase): { "component": "ak-stage-access-denied", "error_message": None, - "title": "", + "flow_info": { + "background": self.flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": "", + }, "type": ChallengeTypes.NATIVE.value, }, ) diff --git a/authentik/stages/identification/tests.py b/authentik/stages/identification/tests.py index c0fa530f0..b8bcfe7c2 100644 --- a/authentik/stages/identification/tests.py +++ b/authentik/stages/identification/tests.py @@ -108,7 +108,6 @@ class TestIdentificationStage(TestCase): self.assertJSONEqual( force_str(response.content), { - "background": self.flow.background_url, "type": ChallengeTypes.NATIVE.value, "component": "ak-stage-identification", "password_fields": True, @@ -118,6 +117,11 @@ class TestIdentificationStage(TestCase): {"code": "invalid", "string": "Failed to " "authenticate."} ] }, + "flow_info": { + "background": self.flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": "", + }, "sources": [ { "challenge": { @@ -129,7 +133,6 @@ class TestIdentificationStage(TestCase): "name": "test", } ], - "title": "", "user_fields": ["email"], }, ) @@ -181,7 +184,6 @@ class TestIdentificationStage(TestCase): self.assertJSONEqual( force_str(response.content), { - "background": flow.background_url, "type": ChallengeTypes.NATIVE.value, "component": "ak-stage-identification", "user_fields": ["email"], @@ -191,7 +193,11 @@ class TestIdentificationStage(TestCase): kwargs={"flow_slug": "unique-enrollment-string"}, ), "primary_action": "Log in", - "title": self.flow.title, + "flow_info": { + "background": flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": self.flow.title, + }, "sources": [ { "icon_url": "/static/authentik/sources/.svg", @@ -229,7 +235,6 @@ class TestIdentificationStage(TestCase): self.assertJSONEqual( force_str(response.content), { - "background": flow.background_url, "type": ChallengeTypes.NATIVE.value, "component": "ak-stage-identification", "user_fields": ["email"], @@ -239,7 +244,11 @@ class TestIdentificationStage(TestCase): kwargs={"flow_slug": "unique-recovery-string"}, ), "primary_action": "Log in", - "title": self.flow.title, + "flow_info": { + "background": flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": self.flow.title, + }, "sources": [ { "challenge": { diff --git a/authentik/stages/invitation/tests.py b/authentik/stages/invitation/tests.py index 8d3cc3afe..3a30742ce 100644 --- a/authentik/stages/invitation/tests.py +++ b/authentik/stages/invitation/tests.py @@ -62,8 +62,12 @@ class TestUserLoginStage(TestCase): { "component": "ak-stage-access-denied", "error_message": None, - "title": "", "type": ChallengeTypes.NATIVE.value, + "flow_info": { + "background": self.flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": "", + }, }, ) diff --git a/authentik/stages/password/tests.py b/authentik/stages/password/tests.py index af6026eda..1707eef83 100644 --- a/authentik/stages/password/tests.py +++ b/authentik/stages/password/tests.py @@ -67,8 +67,12 @@ class TestPasswordStage(TestCase): { "component": "ak-stage-access-denied", "error_message": None, - "title": "", "type": ChallengeTypes.NATIVE.value, + "flow_info": { + "background": self.flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": "", + }, }, ) @@ -205,7 +209,11 @@ class TestPasswordStage(TestCase): { "component": "ak-stage-access-denied", "error_message": None, - "title": "", + "flow_info": { + "background": self.flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": "", + }, "type": ChallengeTypes.NATIVE.value, }, ) diff --git a/authentik/stages/user_delete/tests.py b/authentik/stages/user_delete/tests.py index b3d1bcf14..897194398 100644 --- a/authentik/stages/user_delete/tests.py +++ b/authentik/stages/user_delete/tests.py @@ -54,8 +54,12 @@ class TestUserDeleteStage(TestCase): { "component": "ak-stage-access-denied", "error_message": None, - "title": "", "type": ChallengeTypes.NATIVE.value, + "flow_info": { + "background": self.flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": "", + }, }, ) diff --git a/authentik/stages/user_login/tests.py b/authentik/stages/user_login/tests.py index 003f42c40..f5538e9b7 100644 --- a/authentik/stages/user_login/tests.py +++ b/authentik/stages/user_login/tests.py @@ -108,7 +108,11 @@ class TestUserLoginStage(TestCase): { "component": "ak-stage-access-denied", "error_message": None, - "title": "", "type": ChallengeTypes.NATIVE.value, + "flow_info": { + "background": self.flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": "", + }, }, ) diff --git a/authentik/stages/user_write/tests.py b/authentik/stages/user_write/tests.py index b78bb2fe1..4d3b2767d 100644 --- a/authentik/stages/user_write/tests.py +++ b/authentik/stages/user_write/tests.py @@ -151,8 +151,12 @@ class TestUserWriteStage(TestCase): { "component": "ak-stage-access-denied", "error_message": None, - "title": "", "type": ChallengeTypes.NATIVE.value, + "flow_info": { + "background": self.flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": "", + }, }, ) @@ -184,8 +188,12 @@ class TestUserWriteStage(TestCase): { "component": "ak-stage-access-denied", "error_message": None, - "title": "", "type": ChallengeTypes.NATIVE.value, + "flow_info": { + "background": self.flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": "", + }, }, ) @@ -217,7 +225,11 @@ class TestUserWriteStage(TestCase): { "component": "ak-stage-access-denied", "error_message": None, - "title": "", "type": ChallengeTypes.NATIVE.value, + "flow_info": { + "background": self.flow.background_url, + "cancel_url": reverse("authentik_flows:cancel"), + "title": "", + }, }, ) diff --git a/schema.yml b/schema.yml index 5d001d51b..c731fb2ab 100644 --- a/schema.yml +++ b/schema.yml @@ -11170,7 +11170,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/StageUserSetting' + $ref: '#/components/schemas/UserSetting' description: '' '400': $ref: '#/components/schemas/ValidationError' @@ -15361,7 +15361,6 @@ components: error_message: type: string required: - - flow_info - type ActionEnum: enum: @@ -15692,7 +15691,6 @@ components: required: - activation_barcode - activation_code - - flow_info - pending_user - pending_user_avatar - stage_uuid @@ -15801,7 +15799,6 @@ components: type: string required: - codes - - flow_info - pending_user - pending_user_avatar - type @@ -15899,7 +15896,6 @@ components: type: string required: - config_url - - flow_info - pending_user - pending_user_avatar - type @@ -16077,7 +16073,6 @@ components: $ref: '#/components/schemas/DeviceChallenge' required: - device_challenges - - flow_info - pending_user - pending_user_avatar - type @@ -16120,7 +16115,6 @@ components: type: object additionalProperties: {} required: - - flow_info - pending_user - pending_user_avatar - registration @@ -16169,7 +16163,6 @@ components: type: string required: - attrs - - flow_info - type - url BackendsEnum: @@ -16222,7 +16215,6 @@ components: site_key: type: string required: - - flow_info - pending_user - pending_user_avatar - site_key @@ -16430,7 +16422,6 @@ components: items: $ref: '#/components/schemas/Permission' required: - - flow_info - header_text - pending_user - pending_user_avatar @@ -16730,7 +16721,6 @@ components: items: $ref: '#/components/schemas/ErrorDetail' required: - - flow_info - type DummyChallengeResponseRequest: type: object @@ -16889,7 +16879,6 @@ components: items: $ref: '#/components/schemas/ErrorDetail' required: - - flow_info - type EmailChallengeResponseRequest: type: object @@ -17700,7 +17689,6 @@ components: items: $ref: '#/components/schemas/UILoginButton' required: - - flow_info - password_fields - primary_action - type @@ -21644,7 +21632,6 @@ components: recovery_url: type: string required: - - flow_info - pending_user - pending_user_avatar - type @@ -23394,7 +23381,6 @@ components: type: string required: - client_id - - flow_info - slug - type PlexAuthenticationChallengeResponseRequest: @@ -23754,7 +23740,6 @@ components: $ref: '#/components/schemas/StagePrompt' required: - fields - - flow_info - type PromptChallengeResponseRequest: type: object @@ -24210,7 +24195,6 @@ components: to: type: string required: - - flow_info - to - type RefreshTokenModel: @@ -24927,7 +24911,6 @@ components: type: string required: - body - - flow_info - type SignatureAlgorithmEnum: enum: @@ -25093,22 +25076,6 @@ components: $ref: '#/components/schemas/FlowRequest' required: - name - StageUserSetting: - type: object - description: User settings but can include a configure flow - properties: - object_uid: - type: string - component: - type: string - title: - type: string - configure_flow: - type: string - required: - - component - - object_uid - - title StaticDevice: type: object description: Serializer for static authenticator devices @@ -25788,8 +25755,11 @@ components: type: string title: type: string + configure_url: + type: string required: - component + - configure_url - object_uid - title UserWriteStage: diff --git a/web/src/api/legacy.ts b/web/src/api/legacy.ts deleted file mode 100644 index bcddb5da1..000000000 --- a/web/src/api/legacy.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class AppURLManager { - - static sourceOAuth(slug: string, action: string): string { - return `/source/oauth/${action}/${slug}/`; - } - -} diff --git a/web/src/flows/FlowExecutor.ts b/web/src/flows/FlowExecutor.ts index f191e96c4..5f87ac2a2 100644 --- a/web/src/flows/FlowExecutor.ts +++ b/web/src/flows/FlowExecutor.ts @@ -86,8 +86,8 @@ export class FlowExecutor extends LitElement implements StageHost { private postUpdate(): void { tenant().then(tenant => { - if (this.challenge?.flowInfo.title) { - document.title = `${this.challenge.flowInfo.title} - ${tenant.brandingTitle}`; + if (this.challenge?.flowInfo?.title) { + document.title = `${this.challenge.flowInfo?.title} - ${tenant.brandingTitle}`; } else { document.title = tenant.brandingTitle || TITLE_DEFAULT; } @@ -124,7 +124,7 @@ export class FlowExecutor extends LitElement implements StageHost { }).then((challenge) => { this.challenge = challenge; // Only set background on first update, flow won't change throughout execution - if (this.challenge?.flowInfo.background) { + if (this.challenge?.flowInfo?.background) { this.setBackground(this.challenge.flowInfo.background); } this.postUpdate(); @@ -271,7 +271,7 @@ export class FlowExecutor extends LitElement implements StageHost { ${this.tenant?.brandingTitle != "authentik" ? html`
${t`Select an identification method.`}
diff --git a/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts b/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
index 11124a402..b3371079e 100644
--- a/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
+++ b/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStageCode.ts
@@ -15,6 +15,7 @@ import { PasswordManagerPrefill } from "../identification/IdentificationStage";
import "../../FormStatic";
import { AuthenticatorValidationChallenge } from "authentik-api/dist/models/AuthenticatorValidationChallenge";
import { AuthenticatorValidationChallengeResponseRequest, DeviceChallenge } from "authentik-api";
+import { ifDefined } from "lit-html/directives/if-defined";
@customElement("ak-stage-authenticator-validate-code")
export class AuthenticatorValidateStageWebCode extends BaseStage
- ${this.challenge?.flowInfo.title}
+ ${this.challenge?.flowInfo?.title}
- ${this.challenge.flowInfo.title}
+ ${this.challenge.flowInfo?.title}
- ${this.challenge.flowInfo.title}
+ ${this.challenge.flowInfo?.title}
- ${this.challenge.flowInfo.title}
+ ${this.challenge.flowInfo?.title}
- ${this.challenge.flowInfo.title}
+ ${this.challenge.flowInfo?.title}
- ${this.challenge.flowInfo.title}
+ ${this.challenge.flowInfo?.title}
- ${this.challenge.flowInfo.title}
+ ${this.challenge.flowInfo?.title}
- ${this.challenge.flowInfo.title}
+ ${this.challenge.flowInfo?.title}
- ${this.challenge.flowInfo.title}
+ ${this.challenge.flowInfo?.title}