From 2ccab75021c18fbaf675429ffcb469efc0a7c7eb Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Sat, 12 Feb 2022 16:55:50 +0100 Subject: [PATCH] stages/authenticator_validate: add ability to select multiple configuration stages which the user can choose closes #1843 Signed-off-by: Jens Langhammer --- .../stages/authenticator_validate/api.py | 8 +- ...idatestage_configuration_stage_and_more.py | 44 +++++++ .../stages/authenticator_validate/models.py | 6 +- .../stages/authenticator_validate/stage.py | 109 ++++++++++++++---- .../stages/authenticator_validate/tests.py | 2 +- schema.yml | 69 ++++++++--- .../AuthenticatorValidateStage.ts | 31 ++++- web/src/locales/de.po | 20 +++- web/src/locales/es.po | 20 +++- web/src/locales/pl.po | 20 +++- web/src/locales/tr.po | 20 +++- web/src/locales/zh-Hans.po | 20 +++- web/src/locales/zh-Hant.po | 20 +++- web/src/locales/zh_TW.po | 20 +++- .../AuthenticatorValidateStageForm.ts | 37 +++--- .../identification/IdentificationStageForm.ts | 2 +- 16 files changed, 346 insertions(+), 102 deletions(-) create mode 100644 authentik/stages/authenticator_validate/migrations/0010_remove_authenticatorvalidatestage_configuration_stage_and_more.py diff --git a/authentik/stages/authenticator_validate/api.py b/authentik/stages/authenticator_validate/api.py index 08b110e3e..20c016726 100644 --- a/authentik/stages/authenticator_validate/api.py +++ b/authentik/stages/authenticator_validate/api.py @@ -13,8 +13,8 @@ class AuthenticatorValidateStageSerializer(StageSerializer): def validate_not_configured_action(self, value): """Ensure that a configuration stage is set when not_configured_action is configure""" - configuration_stage = self.initial_data.get("configuration_stage") - if value == NotConfiguredAction.CONFIGURE and configuration_stage is None: + configuration_stages = self.initial_data.get("configuration_stages") + if value == NotConfiguredAction.CONFIGURE and configuration_stages is None: raise ValidationError( ( 'When "Not configured action" is set to "Configure", ' @@ -29,7 +29,7 @@ class AuthenticatorValidateStageSerializer(StageSerializer): fields = StageSerializer.Meta.fields + [ "not_configured_action", "device_classes", - "configuration_stage", + "configuration_stages", ] @@ -38,5 +38,5 @@ class AuthenticatorValidateStageViewSet(UsedByMixin, ModelViewSet): queryset = AuthenticatorValidateStage.objects.all() serializer_class = AuthenticatorValidateStageSerializer - filterset_fields = ["name", "not_configured_action", "configuration_stage"] + filterset_fields = ["name", "not_configured_action", "configuration_stages"] ordering = ["name"] diff --git a/authentik/stages/authenticator_validate/migrations/0010_remove_authenticatorvalidatestage_configuration_stage_and_more.py b/authentik/stages/authenticator_validate/migrations/0010_remove_authenticatorvalidatestage_configuration_stage_and_more.py new file mode 100644 index 000000000..be6377e6a --- /dev/null +++ b/authentik/stages/authenticator_validate/migrations/0010_remove_authenticatorvalidatestage_configuration_stage_and_more.py @@ -0,0 +1,44 @@ +# Generated by Django 4.0.1 on 2022-01-05 22:09 + +from django.apps.registry import Apps +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + + +def migrate_configuration_stage(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): + db_alias = schema_editor.connection.alias + AuthenticatorValidateStage = apps.get_model( + "authentik_stages_authenticator_validate", "AuthenticatorValidateStage" + ) + + for stage in AuthenticatorValidateStage.objects.using(db_alias).all(): + if stage.configuration_stage: + stage.configuration_stages.set([stage.configuration_stage]) + stage.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_flows", "0021_auto_20211227_2103"), + ("authentik_stages_authenticator_validate", "0009_default_stage"), + ] + + operations = [ + migrations.AddField( + model_name="authenticatorvalidatestage", + name="configuration_stages", + field=models.ManyToManyField( + blank=True, + default=None, + help_text="Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again.", + related_name="+", + to="authentik_flows.Stage", + ), + ), + migrations.RunPython(migrate_configuration_stage), + migrations.RemoveField( + model_name="authenticatorvalidatestage", + name="configuration_stage", + ), + ] diff --git a/authentik/stages/authenticator_validate/models.py b/authentik/stages/authenticator_validate/models.py index c9f9cd382..4f21b20d1 100644 --- a/authentik/stages/authenticator_validate/models.py +++ b/authentik/stages/authenticator_validate/models.py @@ -38,16 +38,14 @@ class AuthenticatorValidateStage(Stage): choices=NotConfiguredAction.choices, default=NotConfiguredAction.SKIP ) - configuration_stage = models.ForeignKey( + configuration_stages = models.ManyToManyField( Stage, - null=True, blank=True, default=None, - on_delete=models.SET_DEFAULT, related_name="+", help_text=_( ( - "Stage used to configure Authenticator when user doesn't have any compatible " + "Stages used to configure Authenticator when user doesn't have any compatible " "devices. After this configuration Stage passes, the user is not prompted again." ) ), diff --git a/authentik/stages/authenticator_validate/stage.py b/authentik/stages/authenticator_validate/stage.py index d30ff1ad2..622e0873c 100644 --- a/authentik/stages/authenticator_validate/stage.py +++ b/authentik/stages/authenticator_validate/stage.py @@ -1,10 +1,12 @@ """Authenticator Validation""" from django.http import HttpRequest, HttpResponse from django_otp import devices_for_user -from rest_framework.fields import CharField, IntegerField, JSONField, ListField +from rest_framework.fields import CharField, IntegerField, JSONField, ListField, UUIDField from rest_framework.serializers import ValidationError from structlog.stdlib import get_logger +from authentik.core.api.utils import PassiveSerializer +from authentik.core.models import User from authentik.events.models import Event, EventAction from authentik.events.utils import cleanse_dict, sanitize_dict from authentik.flows.challenge import ChallengeResponse, ChallengeTypes, WithUserInfoChallenge @@ -26,6 +28,18 @@ from authentik.stages.authenticator_webauthn.models import WebAuthnDevice from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS LOGGER = get_logger() +SESSION_STAGES = "goauthentik.io/stages/authenticator_validate/stages" +SESSION_SELECTED_STAGE = "goauthentik.io/stages/authenticator_validate/selected_stage" +SESSION_DEVICE_CHALLENGES = "goauthentik.io/stages/authenticator_validate/device_challenges" + + +class SelectableStageSerializer(PassiveSerializer): + """Serializer for stages which can be selected by users""" + + pk = UUIDField() + name = CharField() + verbose_name = CharField() + meta_model_name = CharField() class AuthenticatorValidationChallenge(WithUserInfoChallenge): @@ -33,12 +47,14 @@ class AuthenticatorValidationChallenge(WithUserInfoChallenge): device_challenges = ListField(child=DeviceChallenge()) component = CharField(default="ak-stage-authenticator-validate") + configuration_stages = ListField(child=SelectableStageSerializer()) class AuthenticatorValidationChallengeResponse(ChallengeResponse): """Challenge used for Code-based and WebAuthn authenticators""" selected_challenge = DeviceChallenge(required=False) + selected_stage = CharField(required=False) code = CharField(required=False) webauthn = JSONField(required=False) @@ -84,6 +100,15 @@ class AuthenticatorValidationChallengeResponse(ChallengeResponse): select_challenge(self.stage.request, devices.first()) return challenge + def validate_selected_stage(self, stage_pk: str) -> str: + """Check that the selected stage is valid""" + stages = self.stage.request.session.get(SESSION_STAGES, []) + if not any(str(stage.pk) == stage_pk for stage in stages): + raise ValidationError("Selected stage is invalid") + LOGGER.debug("Setting selected stage to ", stage=stage_pk) + self.stage.request.session[SESSION_SELECTED_STAGE] = stage_pk + return stage_pk + def validate(self, attrs: dict): # Checking if the given data is from a valid device class is done above # Here we only check if the any data was sent at all @@ -164,7 +189,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): else: LOGGER.debug("No pending user, continuing") return self.executor.stage_ok() - self.request.session["device_challenges"] = challenges + self.request.session[SESSION_DEVICE_CHALLENGES] = challenges # No allowed devices if len(challenges) < 1: @@ -175,35 +200,71 @@ class AuthenticatorValidateStageView(ChallengeStageView): LOGGER.debug("Authenticator not configured, denying") return self.executor.stage_invalid() if stage.not_configured_action == NotConfiguredAction.CONFIGURE: - if not stage.configuration_stage: - Event.new( - EventAction.CONFIGURATION_ERROR, - message=( - "Authenticator validation stage is set to configure user " - "but no configuration flow is set." - ), - stage=self, - ).from_http(self.request).set_user(user).save() - return self.executor.stage_invalid() - LOGGER.debug("Authenticator not configured, sending user to configure") - # Because the foreign key to stage.configuration_stage points to - # a base stage class, we need to do another lookup - stage = Stage.objects.get_subclass(pk=stage.configuration_stage.pk) - # plan.insert inserts at 1 index, so when stage_ok pops 0, - # the configuration stage is next - self.executor.plan.insert_stage(stage) - return self.executor.stage_ok() + LOGGER.debug("Authenticator not configured, forcing configure") + return self.prepare_stages(user) return super().get(request, *args, **kwargs) - def get_challenge(self) -> AuthenticatorValidationChallenge: - challenges = self.request.session.get("device_challenges") - if not challenges: - LOGGER.debug("Authenticator Validation stage ran without challenges") + def prepare_stages(self, user: User, *args, **kwargs) -> HttpResponse: + """Check how the user can configure themselves. If no stages are set, return an error. + If a single stage is set, insert that stage directly. If multiple are selected, include + them in the challenge.""" + stage: AuthenticatorValidateStage = self.executor.current_stage + if not stage.configuration_stages.exists(): + Event.new( + EventAction.CONFIGURATION_ERROR, + message=( + "Authenticator validation stage is set to configure user " + "but no configuration flow is set." + ), + stage=self, + ).from_http(self.request).set_user(user).save() return self.executor.stage_invalid() + if stage.configuration_stages.count() == 1: + self.request.session[SESSION_SELECTED_STAGE] = stage.configuration_stages.first() + LOGGER.debug( + "Single stage configured, auto-selecting", + stage=self.request.session[SESSION_SELECTED_STAGE], + ) + stages = Stage.objects.filter(pk__in=stage.configuration_stages.all()).select_subclasses() + self.request.session[SESSION_STAGES] = stages + return super().get(self.request, *args, **kwargs) + + def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + if ( + SESSION_SELECTED_STAGE in self.request.session + and self.executor.current_stage.not_configured_action == NotConfiguredAction.CONFIGURE + ): + LOGGER.debug("Got selected stage in session, running that") + stage_pk = self.request.session.get(SESSION_SELECTED_STAGE) + # Because the foreign key to stage.configuration_stage points to + # a base stage class, we need to do another lookup + stage = Stage.objects.get_subclass(pk=stage_pk) + # plan.insert inserts at 1 index, so when stage_ok pops 0, + # the configuration stage is next + self.executor.plan.insert_stage(stage) + return self.executor.stage_ok() + return super().post(request, *args, **kwargs) + + def get_challenge(self) -> AuthenticatorValidationChallenge: + challenges = self.request.session.get(SESSION_DEVICE_CHALLENGES, []) + stages = self.request.session.get(SESSION_STAGES, []) + stage_challenges = [] + for stage in stages: + serializer = SelectableStageSerializer( + data={ + "pk": stage.pk, + "name": stage.name, + "verbose_name": str(stage._meta.verbose_name), + "meta_model_name": f"{stage._meta.app_label}.{stage._meta.model_name}", + } + ) + serializer.is_valid() + stage_challenges.append(serializer.data) return AuthenticatorValidationChallenge( data={ "type": ChallengeTypes.NATIVE.value, "device_challenges": challenges, + "configuration_stages": stage_challenges, } ) diff --git a/authentik/stages/authenticator_validate/tests.py b/authentik/stages/authenticator_validate/tests.py index 3b9ee8879..80b4fd9e0 100644 --- a/authentik/stages/authenticator_validate/tests.py +++ b/authentik/stages/authenticator_validate/tests.py @@ -43,8 +43,8 @@ class AuthenticatorValidateStageTests(FlowTestCase): stage = AuthenticatorValidateStage.objects.create( name="foo", not_configured_action=NotConfiguredAction.CONFIGURE, - configuration_stage=conf_stage, ) + stage.configuration_stages.set([conf_stage]) flow = Flow.objects.create(name="test", slug="test", title="test") FlowStageBinding.objects.create(target=flow, stage=conf_stage, order=0) FlowStageBinding.objects.create(target=flow, stage=stage, order=1) diff --git a/schema.yml b/schema.yml index 7c4464e76..ab4112680 100644 --- a/schema.yml +++ b/schema.yml @@ -15045,10 +15045,14 @@ paths: description: AuthenticatorValidateStage Viewset parameters: - in: query - name: configuration_stage + name: configuration_stages schema: - type: string - format: uuid + type: array + items: + type: string + format: uuid + explode: true + style: form - in: query name: name schema: @@ -19826,11 +19830,12 @@ components: items: $ref: '#/components/schemas/DeviceClassesEnum' description: Device classes which can be used to authenticate - configuration_stage: - type: string - format: uuid - nullable: true - description: Stage used to configure Authenticator when user doesn't have + configuration_stages: + type: array + items: + type: string + format: uuid + description: Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again. required: @@ -19858,11 +19863,12 @@ components: items: $ref: '#/components/schemas/DeviceClassesEnum' description: Device classes which can be used to authenticate - configuration_stage: - type: string - format: uuid - nullable: true - description: Stage used to configure Authenticator when user doesn't have + configuration_stages: + type: array + items: + type: string + format: uuid + description: Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again. required: @@ -19892,7 +19898,12 @@ components: type: array items: $ref: '#/components/schemas/DeviceChallenge' + configuration_stages: + type: array + items: + $ref: '#/components/schemas/SelectableStage' required: + - configuration_stages - device_challenges - pending_user - pending_user_avatar @@ -19907,6 +19918,9 @@ components: default: ak-stage-authenticator-validate selected_challenge: $ref: '#/components/schemas/DeviceChallengeRequest' + selected_stage: + type: string + minLength: 1 code: type: string minLength: 1 @@ -26677,11 +26691,12 @@ components: items: $ref: '#/components/schemas/DeviceClassesEnum' description: Device classes which can be used to authenticate - configuration_stage: - type: string - format: uuid - nullable: true - description: Stage used to configure Authenticator when user doesn't have + configuration_stages: + type: array + items: + type: string + format: uuid + description: Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again. PatchedCaptchaStageRequest: @@ -30017,6 +30032,24 @@ components: - direct - cached type: string + SelectableStage: + type: object + description: Serializer for stages which can be selected by users + properties: + pk: + type: string + format: uuid + name: + type: string + verbose_name: + type: string + meta_model_name: + type: string + required: + - meta_model_name + - name + - pk + - verbose_name ServiceConnection: type: object description: ServiceConnection Serializer diff --git a/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts b/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts index 97aee14ef..368ed38c4 100644 --- a/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts +++ b/web/src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts @@ -67,7 +67,7 @@ export class AuthenticatorValidateStage return this._selectedDeviceChallenge; } - submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise { + submit(payload: AuthenticatorValidationChallengeResponseRequest): Promise { return this.host?.submit(payload) || Promise.resolve(); } @@ -140,7 +140,7 @@ export class AuthenticatorValidateStage } renderDevicePicker(): TemplateResult { - return html`
    + return html`
      ${this.challenge?.deviceChallenges.map((challenges) => { return html`
    • +
    • `; + })} +
    `; + } + renderDeviceChallenge(): TemplateResult { if (!this.selectedDeviceChallenge) { return html``; @@ -242,6 +266,9 @@ export class AuthenticatorValidateStage ${this.selectedDeviceChallenge ? "" : html`

    ${t`Select an authentication method.`}

    `} + ${this.challenge.configurationStages.length > 0 + ? this.renderStagePicker() + : html``} ${this.renderDevicePicker()} diff --git a/web/src/locales/de.po b/web/src/locales/de.po index eb32dbb76..1423333f0 100644 --- a/web/src/locales/de.po +++ b/web/src/locales/de.po @@ -959,8 +959,12 @@ msgid "Configuration flow" msgstr "Ablauf der Konfiguration" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Configuration stage" -msgstr "Konfiguration Stufe" +#~ msgid "Configuration stage" +#~ msgstr "Konfiguration Stufe" + +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Configuration stages" +msgstr "" #~ msgid "Configure WebAuthn" #~ msgstr "Konfiguriere WebAuthn" @@ -4410,8 +4414,8 @@ msgid "Stage type" msgstr "Phasen Typ" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." -msgstr "Phase zum Konfigurieren von Authenticator, wenn der Benutzer keine kompatiblen Geräte hat. Nach Ablauf dieser Konfigurationsphase wird der Benutzer nicht erneut aufgefordert." +#~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +#~ msgstr "Phase zum Konfigurieren von Authenticator, wenn der Benutzer keine kompatiblen Geräte hat. Nach Ablauf dieser Konfigurationsphase wird der Benutzer nicht erneut aufgefordert." #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." @@ -4470,6 +4474,10 @@ msgstr "Phasen" msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." msgstr "Phasen sind einzelne Schritte eines Flows, durch die ein Benutzer geführt wird. Eine Phase kann nur innerhalb eines Flows ausgeführt werden." +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +msgstr "" + #: src/pages/outposts/ServiceConnectionListPage.ts msgid "State" msgstr "Zustand" @@ -5935,6 +5943,10 @@ msgstr "Wenn diese Option aktiviert ist, wird die Einladung nach ihrer Benutzung msgid "When enabled, user fields are matched regardless of their casing." msgstr "Wenn diese Option aktiviert ist, werden Benutzerfelder unabhängig von ihrem Format abgeglichen." +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "When multiple stages are selected, the user can choose which one they want to enroll." +msgstr "" + #: src/pages/stages/identification/IdentificationStageForm.ts msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." msgstr "Wenn diese Option ausgewählt ist, wird ein Passwortfeld auf derselben Seite statt auf einer separaten Seite angezeigt. Dadurch werden Angriffe auf die Aufzählung von Benutzernamen verhindert." diff --git a/web/src/locales/es.po b/web/src/locales/es.po index 7abf1ba10..9fe9e5ee9 100644 --- a/web/src/locales/es.po +++ b/web/src/locales/es.po @@ -950,8 +950,12 @@ msgid "Configuration flow" msgstr "Flujo de configuración" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Configuration stage" -msgstr "Etapa de configuración" +#~ msgid "Configuration stage" +#~ msgstr "Etapa de configuración" + +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Configuration stages" +msgstr "" #~ msgid "Configure WebAuthn" #~ msgstr "Configurar WebAuthn" @@ -4403,8 +4407,8 @@ msgid "Stage type" msgstr "Tipo de escenario" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." -msgstr "Etapa utilizada para configurar Authenticator cuando el usuario no tiene ningún dispositivo compatible. Una vez superada esta etapa de configuración, no se volverá a preguntar al usuario." +#~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +#~ msgstr "Etapa utilizada para configurar Authenticator cuando el usuario no tiene ningún dispositivo compatible. Una vez superada esta etapa de configuración, no se volverá a preguntar al usuario." #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." @@ -4463,6 +4467,10 @@ msgstr "Etapas" msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." msgstr "Las etapas son pasos individuales de un flujo por los que se guía al usuario. Una etapa solo se puede ejecutar desde dentro de un flujo." +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +msgstr "" + #: src/pages/outposts/ServiceConnectionListPage.ts msgid "State" msgstr "Estado" @@ -5928,6 +5936,10 @@ msgstr "Cuando se habilita, la invitación se eliminará después de su uso." msgid "When enabled, user fields are matched regardless of their casing." msgstr "Cuando se habilita, los campos de usuario coinciden independientemente de su carcasa." +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "When multiple stages are selected, the user can choose which one they want to enroll." +msgstr "" + #: src/pages/stages/identification/IdentificationStageForm.ts msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." msgstr "Cuando se selecciona, se muestra un campo de contraseña en la misma página en lugar de en una página separada. Esto evita ataques de enumeración de nombres de usuario." diff --git a/web/src/locales/pl.po b/web/src/locales/pl.po index 52c22015c..43a751c5a 100644 --- a/web/src/locales/pl.po +++ b/web/src/locales/pl.po @@ -947,8 +947,12 @@ msgid "Configuration flow" msgstr "Przepływ konfiguracji" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Configuration stage" -msgstr "Etap konfiguracji" +#~ msgid "Configuration stage" +#~ msgstr "Etap konfiguracji" + +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Configuration stages" +msgstr "" #~ msgid "Configure WebAuthn" #~ msgstr "Skonfiguruj WebAuthn" @@ -4400,8 +4404,8 @@ msgid "Stage type" msgstr "Typ etapu" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." -msgstr "Etap używany do konfiguracji uwierzytelniacza, gdy użytkownik nie ma żadnych kompatybilnych urządzeń. Po zakończeniu tego etapu konfiguracji użytkownik nie jest ponownie pytany." +#~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +#~ msgstr "Etap używany do konfiguracji uwierzytelniacza, gdy użytkownik nie ma żadnych kompatybilnych urządzeń. Po zakończeniu tego etapu konfiguracji użytkownik nie jest ponownie pytany." #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." @@ -4460,6 +4464,10 @@ msgstr "Etapy" msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." msgstr "Etapy to pojedyncze kroki przepływu, przez które prowadzony jest użytkownik. Etap można wykonać tylko z przepływu." +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +msgstr "" + #: src/pages/outposts/ServiceConnectionListPage.ts msgid "State" msgstr "Stan" @@ -5925,6 +5933,10 @@ msgstr "Po włączeniu zaproszenie zostanie usunięte po użyciu." msgid "When enabled, user fields are matched regardless of their casing." msgstr "Po włączeniu pola użytkownika są dopasowywane niezależnie od wielkości liter." +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "When multiple stages are selected, the user can choose which one they want to enroll." +msgstr "" + #: src/pages/stages/identification/IdentificationStageForm.ts msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." msgstr "Po wybraniu pole hasła jest wyświetlane na tej samej stronie zamiast na osobnej stronie. Zapobiega to atakom polegającym na wyliczaniu nazw użytkowników." diff --git a/web/src/locales/tr.po b/web/src/locales/tr.po index 9bbac5cca..915d6c042 100644 --- a/web/src/locales/tr.po +++ b/web/src/locales/tr.po @@ -950,8 +950,12 @@ msgid "Configuration flow" msgstr "Yapılandırma akışı" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Configuration stage" -msgstr "Yapılandırma aşamasında" +#~ msgid "Configuration stage" +#~ msgstr "Yapılandırma aşamasında" + +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Configuration stages" +msgstr "" #~ msgid "Configure WebAuthn" #~ msgstr "WebAuthn'i Yapılandır" @@ -4405,8 +4409,8 @@ msgid "Stage type" msgstr "Aşama türü" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." -msgstr "Kullanıcının uyumlu bir aygıtı olmadığında Kimlik Doğrulayıcısı'nı yapılandırmak için kullanılan Aşama. Bu yapılandırma Stage geçtikten sonra kullanıcıya yeniden istenmez." +#~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +#~ msgstr "Kullanıcının uyumlu bir aygıtı olmadığında Kimlik Doğrulayıcısı'nı yapılandırmak için kullanılan Aşama. Bu yapılandırma Stage geçtikten sonra kullanıcıya yeniden istenmez." #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." @@ -4465,6 +4469,10 @@ msgstr "Aşamalar" msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." msgstr "Aşamalar, bir Akış'ın kullanıcının yönlendirildiği tek adımlardır. Bir aşama yalnızca bir akış içinden yürütülebilir." +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +msgstr "" + #: src/pages/outposts/ServiceConnectionListPage.ts msgid "State" msgstr "Eyalet" @@ -5930,6 +5938,10 @@ msgstr "Etkinleştirildiğinde, davetiye kullanımdan sonra silinir." msgid "When enabled, user fields are matched regardless of their casing." msgstr "Etkinleştirildiğinde, kullanıcı alanları muhafazası ne olursa olsun eşleştirilir." +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "When multiple stages are selected, the user can choose which one they want to enroll." +msgstr "" + #: src/pages/stages/identification/IdentificationStageForm.ts msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." msgstr "Seçildiğinde, ayrı bir sayfa yerine aynı sayfada bir parola alanı gösterilir. Bu, kullanıcı adı numaralandırma saldırılarını engeller." diff --git a/web/src/locales/zh-Hans.po b/web/src/locales/zh-Hans.po index 33166ebea..cd7bee7fe 100644 --- a/web/src/locales/zh-Hans.po +++ b/web/src/locales/zh-Hans.po @@ -948,8 +948,12 @@ msgid "Configuration flow" msgstr "配置流程" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Configuration stage" -msgstr "配置阶段" +#~ msgid "Configuration stage" +#~ msgstr "配置阶段" + +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Configuration stages" +msgstr "" #~ msgid "Configure WebAuthn" #~ msgstr "配置 WebAuthn" @@ -4401,8 +4405,8 @@ msgid "Stage type" msgstr "阶段类型" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." -msgstr "Stage 用于在用户没有任何兼容设备时配置身份验证器。此配置 Stage 通过后,不会再次提示用户。" +#~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +#~ msgstr "Stage 用于在用户没有任何兼容设备时配置身份验证器。此配置 Stage 通过后,不会再次提示用户。" #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." @@ -4461,6 +4465,10 @@ msgstr "阶段" msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." msgstr "阶段是引导用户完成的流程的单个步骤。阶段只能在流程内部执行。" +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +msgstr "" + #: src/pages/outposts/ServiceConnectionListPage.ts msgid "State" msgstr "状态" @@ -5926,6 +5934,10 @@ msgstr "启用后,邀请将在使用后被删除。" msgid "When enabled, user fields are matched regardless of their casing." msgstr "启用后,无论用户字段大小写如何,都将匹配用户字段。" +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "When multiple stages are selected, the user can choose which one they want to enroll." +msgstr "" + #: src/pages/stages/identification/IdentificationStageForm.ts msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." msgstr "选中后,密码字段将显示在同一页面上,而不是单独的页面上。这样可以防止用户名枚举攻击。" diff --git a/web/src/locales/zh-Hant.po b/web/src/locales/zh-Hant.po index 93d83ba4d..b39dcdebb 100644 --- a/web/src/locales/zh-Hant.po +++ b/web/src/locales/zh-Hant.po @@ -948,8 +948,12 @@ msgid "Configuration flow" msgstr "配置流程" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Configuration stage" -msgstr "配置阶段" +#~ msgid "Configuration stage" +#~ msgstr "配置阶段" + +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Configuration stages" +msgstr "" #~ msgid "Configure WebAuthn" #~ msgstr "配置 WebAuthn" @@ -4401,8 +4405,8 @@ msgid "Stage type" msgstr "阶段类型" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." -msgstr "Stage 用于在用户没有任何兼容设备时配置身份验证器。此配置 Stage 通过后,不会再次提示用户。" +#~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +#~ msgstr "Stage 用于在用户没有任何兼容设备时配置身份验证器。此配置 Stage 通过后,不会再次提示用户。" #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." @@ -4461,6 +4465,10 @@ msgstr "阶段" msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." msgstr "阶段是引导用户完成的流程的单个步骤。阶段只能在流程内部执行。" +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +msgstr "" + #: src/pages/outposts/ServiceConnectionListPage.ts msgid "State" msgstr "州" @@ -5926,6 +5934,10 @@ msgstr "启用后,邀请将在使用后被删除。" msgid "When enabled, user fields are matched regardless of their casing." msgstr "启用后,无论用户字段大小写如何,都将匹配用户字段。" +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "When multiple stages are selected, the user can choose which one they want to enroll." +msgstr "" + #: src/pages/stages/identification/IdentificationStageForm.ts msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." msgstr "选中后,密码字段将显示在同一页面上,而不是单独的页面上。这样可以防止用户名枚举攻击。" diff --git a/web/src/locales/zh_TW.po b/web/src/locales/zh_TW.po index 29cdd75aa..1ea8b3492 100644 --- a/web/src/locales/zh_TW.po +++ b/web/src/locales/zh_TW.po @@ -948,8 +948,12 @@ msgid "Configuration flow" msgstr "配置流程" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Configuration stage" -msgstr "配置阶段" +#~ msgid "Configuration stage" +#~ msgstr "配置阶段" + +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Configuration stages" +msgstr "" #~ msgid "Configure WebAuthn" #~ msgstr "配置 WebAuthn" @@ -4401,8 +4405,8 @@ msgid "Stage type" msgstr "阶段类型" #: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts -msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." -msgstr "Stage 用于在用户没有任何兼容设备时配置身份验证器。此配置 Stage 通过后,不会再次提示用户。" +#~ msgid "Stage used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +#~ msgstr "Stage 用于在用户没有任何兼容设备时配置身份验证器。此配置 Stage 通过后,不会再次提示用户。" #: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts msgid "Stage used to configure a TOTP authenticator (i.e. Authy/Google Authenticator)." @@ -4461,6 +4465,10 @@ msgstr "阶段" msgid "Stages are single steps of a Flow that a user is guided through. A stage can only be executed from within a flow." msgstr "阶段是引导用户完成的流程的单个步骤。阶段只能在流程内部执行。" +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "Stages used to configure Authenticator when user doesn't have any compatible devices. After this configuration Stage passes, the user is not prompted again." +msgstr "" + #: src/pages/outposts/ServiceConnectionListPage.ts msgid "State" msgstr "州" @@ -5926,6 +5934,10 @@ msgstr "启用后,邀请将在使用后被删除。" msgid "When enabled, user fields are matched regardless of their casing." msgstr "启用后,无论用户字段大小写如何,都将匹配用户字段。" +#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +msgid "When multiple stages are selected, the user can choose which one they want to enroll." +msgstr "" + #: src/pages/stages/identification/IdentificationStageForm.ts msgid "When selected, a password field is shown on the same page instead of a separate page. This prevents username enumeration attacks." msgstr "选中后,密码字段将显示在同一页面上,而不是单独的页面上。这样可以防止用户名枚举攻击。" diff --git a/web/src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts b/web/src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts index 4eb2cf995..94da42ba9 100644 --- a/web/src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts +++ b/web/src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts @@ -25,14 +25,14 @@ export class AuthenticatorValidateStageForm extends ModelForm { - this.showConfigurationStage = + this.showConfigurationStages = stage.notConfiguredAction === NotConfiguredActionEnum.Configure; return stage; }); } @property({ type: Boolean }) - showConfigurationStage = true; + showConfigurationStages = true; getSuccessMessage(): string { if (this.instance) { @@ -136,9 +136,9 @@ export class AuthenticatorValidateStageForm extends ModelForm @@ -165,21 +165,13 @@ export class AuthenticatorValidateStageForm extends ModelForm - ${this.showConfigurationStage + ${this.showConfigurationStages ? html` - ${until( new StagesApi(DEFAULT_CONFIG) .stagesAllList({ @@ -187,9 +179,11 @@ export class AuthenticatorValidateStageForm extends ModelForm { return stages.results.map((stage) => { - const selected = - this.instance?.configurationStage === - stage.pk; + const selected = Array.from( + this.instance?.configurationStages || [], + ).some((su) => { + return su == stage.pk; + }); return html` ` diff --git a/web/src/pages/stages/identification/IdentificationStageForm.ts b/web/src/pages/stages/identification/IdentificationStageForm.ts index 9e4f3da38..e4072c2f3 100644 --- a/web/src/pages/stages/identification/IdentificationStageForm.ts +++ b/web/src/pages/stages/identification/IdentificationStageForm.ts @@ -153,7 +153,7 @@ export class IdentificationStageForm extends ModelForm - ${until( new SourcesApi(DEFAULT_CONFIG) .sourcesAllList({})