flows: add invalid_response_action to configure how the FlowExecutor should handle invalid responses

closes #1079

Default value of `retry` behaves like previous version.

`restart` and `restart_with_context` restart the flow upon an invalid response. `restart_with_context` keeps the same context of the Flow, allowing users to bind policies that maybe aren't valid on the first execution, but are after a retry, like a reputation policy with a deny stage.

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-06-27 23:57:42 +02:00
parent ba9edd6c44
commit 2b1356bb91
14 changed files with 291 additions and 16 deletions

View File

@ -25,6 +25,7 @@ class FlowStageBindingSerializer(ModelSerializer):
"re_evaluate_policies", "re_evaluate_policies",
"order", "order",
"policy_engine_mode", "policy_engine_mode",
"invalid_response_action",
] ]

View File

@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Optional
from django.http.request import HttpRequest from django.http.request import HttpRequest
from structlog.stdlib import get_logger from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.flows.models import FlowStageBinding from authentik.flows.models import FlowStageBinding
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding
@ -25,7 +24,7 @@ class StageMarker:
self, self,
plan: "FlowPlan", plan: "FlowPlan",
binding: FlowStageBinding, binding: FlowStageBinding,
http_request: Optional[HttpRequest], http_request: HttpRequest,
) -> Optional[FlowStageBinding]: ) -> Optional[FlowStageBinding]:
"""Process callback for this marker. This should be overridden by sub-classes. """Process callback for this marker. This should be overridden by sub-classes.
If a stage should be removed, return None.""" If a stage should be removed, return None."""
@ -37,23 +36,25 @@ class ReevaluateMarker(StageMarker):
"""Reevaluate Marker, forces stage's policies to be evaluated again.""" """Reevaluate Marker, forces stage's policies to be evaluated again."""
binding: PolicyBinding binding: PolicyBinding
user: User
def process( def process(
self, self,
plan: "FlowPlan", plan: "FlowPlan",
binding: FlowStageBinding, binding: FlowStageBinding,
http_request: Optional[HttpRequest], http_request: HttpRequest,
) -> Optional[FlowStageBinding]: ) -> Optional[FlowStageBinding]:
"""Re-evaluate policies bound to stage, and if they fail, remove from plan""" """Re-evaluate policies bound to stage, and if they fail, remove from plan"""
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
LOGGER.debug( LOGGER.debug(
"f(plan_inst)[re-eval marker]: running re-evaluation", "f(plan_inst)[re-eval marker]: running re-evaluation",
binding=binding, binding=binding,
policy_binding=self.binding, policy_binding=self.binding,
) )
engine = PolicyEngine(self.binding, self.user) engine = PolicyEngine(
self.binding, plan.context.get(PLAN_CONTEXT_PENDING_USER, http_request.user)
)
engine.use_cache = False engine.use_cache = False
if http_request:
engine.request.set_http_request(http_request) engine.request.set_http_request(http_request)
engine.request.context = plan.context engine.request.context = plan.context
engine.build() engine.build()

View File

@ -0,0 +1,22 @@
# Generated by Django 3.2.4 on 2021-06-27 16:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_flows", "0020_flow_compatibility_mode"),
]
operations = [
migrations.AddField(
model_name="flowstagebinding",
name="invalid_response_action",
field=models.TextField(
choices=[("retry", "Retry"), ("continue", "Continue")],
default="retry",
help_text="Configure how the flow executor should handle an invalid response to a challenge. RETRY returns the error message and a similar challenge to the executor while CONTINUE continues with the next stage.",
),
),
]

View File

@ -27,6 +27,14 @@ class NotConfiguredAction(models.TextChoices):
CONFIGURE = "configure" CONFIGURE = "configure"
class InvalidResponseAction(models.TextChoices):
"""Configure how the flow executor should handle invalid responses to challenges"""
RETRY = "retry"
RESTART = "restart"
RESTART_WITH_CONTEXT = "restart_with_context"
class FlowDesignation(models.TextChoices): class FlowDesignation(models.TextChoices):
"""Designation of what a Flow should be used for. At a later point, this """Designation of what a Flow should be used for. At a later point, this
should be replaced by a database entry.""" should be replaced by a database entry."""
@ -201,6 +209,17 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
help_text=_("Evaluate policies when the Stage is present to the user."), help_text=_("Evaluate policies when the Stage is present to the user."),
) )
invalid_response_action = models.TextField(
choices=InvalidResponseAction.choices,
default=InvalidResponseAction.RETRY,
help_text=_(
"Configure how the flow executor should handle an invalid response to a "
"challenge. RETRY returns the error message and a similar challenge to the "
"executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT "
"restarts the flow while keeping the current context."
),
)
order = models.IntegerField() order = models.IntegerField()
objects = InheritanceManager() objects = InheritanceManager()

View File

@ -224,7 +224,7 @@ class FlowPlanner:
"f(plan): stage has re-evaluate marker", "f(plan): stage has re-evaluate marker",
stage=binding.stage, stage=binding.stage,
) )
marker = ReevaluateMarker(binding=binding, user=user) marker = ReevaluateMarker(binding=binding)
if stage: if stage:
plan.append(binding, marker) plan.append(binding, marker)
HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug) HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug)

View File

@ -16,6 +16,7 @@ from authentik.flows.challenge import (
HttpChallengeResponse, HttpChallengeResponse,
WithUserInfoChallenge, WithUserInfoChallenge,
) )
from authentik.flows.models import InvalidResponseAction
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.views import FlowExecutorView from authentik.flows.views import FlowExecutorView
@ -69,7 +70,13 @@ class ChallengeStageView(StageView):
"""Return a challenge for the frontend to solve""" """Return a challenge for the frontend to solve"""
challenge = self._get_challenge(*args, **kwargs) challenge = self._get_challenge(*args, **kwargs)
if not challenge.is_valid(): if not challenge.is_valid():
LOGGER.warning(challenge.errors, stage_view=self, challenge=challenge) LOGGER.warning(
"f(ch): Invalid challenge",
binding=self.executor.current_binding,
errors=challenge.errors,
stage_view=self,
challenge=challenge,
)
return HttpChallengeResponse(challenge) return HttpChallengeResponse(challenge)
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -77,6 +84,21 @@ class ChallengeStageView(StageView):
"""Handle challenge response""" """Handle challenge response"""
challenge: ChallengeResponse = self.get_response_instance(data=request.data) challenge: ChallengeResponse = self.get_response_instance(data=request.data)
if not challenge.is_valid(): if not challenge.is_valid():
if self.executor.current_binding.invalid_response_action in [
InvalidResponseAction.RESTART,
InvalidResponseAction.RESTART_WITH_CONTEXT,
]:
keep_context = (
self.executor.current_binding.invalid_response_action
== InvalidResponseAction.RESTART_WITH_CONTEXT
)
LOGGER.debug(
"f(ch): Invalid response, restarting flow",
binding=self.executor.current_binding,
stage_view=self,
keep_context=keep_context,
)
return self.executor.restart_flow(keep_context)
return self.challenge_invalid(challenge) return self.challenge_invalid(challenge)
return self.challenge_valid(challenge) return self.challenge_valid(challenge)
@ -126,5 +148,10 @@ class ChallengeStageView(StageView):
) )
challenge_response.initial_data["response_errors"] = full_errors challenge_response.initial_data["response_errors"] = full_errors
if not challenge_response.is_valid(): if not challenge_response.is_valid():
LOGGER.warning(challenge_response.errors) LOGGER.warning(
"f(ch): invalid challenge response",
binding=self.executor.current_binding,
errors=challenge_response.errors,
stage_view=self,
)
return HttpChallengeResponse(challenge_response) return HttpChallengeResponse(challenge_response)

View File

@ -11,15 +11,23 @@ from authentik.core.models import User
from authentik.flows.challenge import ChallengeTypes from authentik.flows.challenge import ChallengeTypes
from authentik.flows.exceptions import FlowNonApplicableException from authentik.flows.exceptions import FlowNonApplicableException
from authentik.flows.markers import ReevaluateMarker, StageMarker from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding from authentik.flows.models import (
Flow,
FlowDesignation,
FlowStageBinding,
InvalidResponseAction,
)
from authentik.flows.planner import FlowPlan, FlowPlanner from authentik.flows.planner import FlowPlan, FlowPlanner
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, StageView
from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView from authentik.flows.views import NEXT_ARG_NAME, SESSION_KEY_PLAN, FlowExecutorView
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.policies.dummy.models import DummyPolicy from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding from authentik.policies.models import PolicyBinding
from authentik.policies.reputation.models import ReputationPolicy
from authentik.policies.types import PolicyResult from authentik.policies.types import PolicyResult
from authentik.stages.deny.models import DenyStage
from authentik.stages.dummy.models import DummyStage from authentik.stages.dummy.models import DummyStage
from authentik.stages.identification.models import IdentificationStage, UserFields
POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False)) POLICY_RETURN_FALSE = PropertyMock(return_value=PolicyResult(False))
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True)) POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
@ -513,3 +521,78 @@ class TestFlowExecutor(TestCase):
stage_view = StageView(executor) stage_view = StageView(executor)
self.assertEqual(ident, stage_view.get_pending_user(for_display=True).username) self.assertEqual(ident, stage_view.get_pending_user(for_display=True).username)
def test_invalid_restart(self):
"""Test flow that restarts on invalid entry"""
flow = Flow.objects.create(
name="restart-on-invalid",
slug="restart-on-invalid",
designation=FlowDesignation.AUTHENTICATION,
)
# Stage 0 is a deny stage that is added dynamically
# when the reputation policy says so
deny_stage = DenyStage.objects.create(name="deny")
reputation_policy = ReputationPolicy.objects.create(
name="reputation", threshold=-1, check_ip=False
)
deny_binding = FlowStageBinding.objects.create(
target=flow,
stage=deny_stage,
order=0,
evaluate_on_plan=False,
re_evaluate_policies=True,
)
PolicyBinding.objects.create(
policy=reputation_policy, target=deny_binding, order=0
)
# Stage 1 is an identification stage
ident_stage = IdentificationStage.objects.create(
name="ident",
user_fields=[UserFields.E_MAIL],
)
FlowStageBinding.objects.create(
target=flow,
stage=ident_stage,
order=1,
invalid_response_action=InvalidResponseAction.RESTART_WITH_CONTEXT,
)
exec_url = reverse(
"authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}
)
# First request, run the planner
response = self.client.get(exec_url)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{
"type": ChallengeTypes.NATIVE.value,
"component": "ak-stage-identification",
"flow_info": {
"background": flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
"title": "",
},
"password_fields": False,
"primary_action": "Log in",
"sources": [],
"user_fields": [UserFields.E_MAIL],
},
)
response = self.client.post(
exec_url, {"uid_field": "invalid-string"}, follow=True
)
self.assertEqual(response.status_code, 200)
self.assertJSONEqual(
force_str(response.content),
{
"component": "ak-stage-access-denied",
"error_message": None,
"flow_info": {
"background": flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
"title": "",
},
"type": ChallengeTypes.NATIVE.value,
},
)

View File

@ -288,6 +288,20 @@ class FlowExecutorView(APIView):
return self._initiate_plan() return self._initiate_plan()
return plan return plan
def restart_flow(self, keep_context=False) -> HttpResponse:
"""Restart the currently active flow, optionally keeping the current context"""
planner = FlowPlanner(self.flow)
default_context = None
if keep_context:
default_context = self.plan.context
plan = planner.plan(self.request, default_context)
self.request.session[SESSION_KEY_PLAN] = plan
kwargs = self.kwargs
kwargs.update({"flow_slug": self.flow.slug})
return redirect_with_qs(
"authentik_api:flow-executor", self.request.GET, **kwargs
)
def _flow_done(self) -> HttpResponse: def _flow_done(self) -> HttpResponse:
"""User Successfully passed all stages""" """User Successfully passed all stages"""
# Since this is wrapped by the ExecutorShell, the next argument is saved in the session # Since this is wrapped by the ExecutorShell, the next argument is saved in the session

View File

@ -33,21 +33,21 @@ class ReputationPolicy(Policy):
def passes(self, request: PolicyRequest) -> PolicyResult: def passes(self, request: PolicyRequest) -> PolicyResult:
remote_ip = get_client_ip(request.http_request) remote_ip = get_client_ip(request.http_request)
passing = True passing = False
if self.check_ip: if self.check_ip:
score = cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0) score = cache.get_or_set(CACHE_KEY_IP_PREFIX + remote_ip, 0)
passing = passing and score <= self.threshold passing += passing or score <= self.threshold
LOGGER.debug("Score for IP", ip=remote_ip, score=score, passing=passing) LOGGER.debug("Score for IP", ip=remote_ip, score=score, passing=passing)
if self.check_username: if self.check_username:
score = cache.get_or_set(CACHE_KEY_USER_PREFIX + request.user.username, 0) score = cache.get_or_set(CACHE_KEY_USER_PREFIX + request.user.username, 0)
passing = passing and score <= self.threshold passing += passing or score <= self.threshold
LOGGER.debug( LOGGER.debug(
"Score for Username", "Score for Username",
username=request.user.username, username=request.user.username,
score=score, score=score,
passing=passing, passing=passing,
) )
return PolicyResult(passing) return PolicyResult(bool(passing))
class Meta: class Meta:

View File

@ -85,6 +85,18 @@ class IdentificationChallengeResponse(ChallengeResponse):
identification_failed.send( identification_failed.send(
sender=self, request=self.stage.request, uid_field=uid_field sender=self, request=self.stage.request, uid_field=uid_field
) )
# We set the pending_user even on failure so it's part of the context, even
# when the input is invalid
# This is so its part of the current flow plan, and on flow restart can be kept, and
# policies can be applied.
self.stage.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
username=uid_field,
email=uid_field,
)
if not current_stage.show_matched_user:
self.stage.executor.plan.context[
PLAN_CONTEXT_PENDING_USER_IDENTIFIER
] = uid_field
raise ValidationError("Failed to authenticate.") raise ValidationError("Failed to authenticate.")
self.pre_user = pre_user self.pre_user = pre_user
if not current_stage.password_stage: if not current_stage.password_stage:

View File

@ -4572,6 +4572,18 @@ paths:
schema: schema:
type: string type: string
format: uuid format: uuid
- in: query
name: invalid_response_action
schema:
type: string
enum:
- restart
- restart_with_context
- retry
description: Configure how the flow executor should handle an invalid response
to a challenge. RETRY returns the error message and a similar challenge
to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT
restarts the flow while keeping the current context.
- in: query - in: query
name: order name: order
schema: schema:
@ -19810,6 +19822,13 @@ components:
minimum: -2147483648 minimum: -2147483648
policy_engine_mode: policy_engine_mode:
$ref: '#/components/schemas/PolicyEngineMode' $ref: '#/components/schemas/PolicyEngineMode'
invalid_response_action:
allOf:
- $ref: '#/components/schemas/InvalidResponseActionEnum'
description: Configure how the flow executor should handle an invalid response
to a challenge. RETRY returns the error message and a similar challenge
to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT
restarts the flow while keeping the current context.
required: required:
- order - order
- pk - pk
@ -19840,6 +19859,13 @@ components:
minimum: -2147483648 minimum: -2147483648
policy_engine_mode: policy_engine_mode:
$ref: '#/components/schemas/PolicyEngineMode' $ref: '#/components/schemas/PolicyEngineMode'
invalid_response_action:
allOf:
- $ref: '#/components/schemas/InvalidResponseActionEnum'
description: Configure how the flow executor should handle an invalid response
to a challenge. RETRY returns the error message and a similar challenge
to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT
restarts the flow while keeping the current context.
required: required:
- order - order
- stage - stage
@ -20185,6 +20211,12 @@ components:
- api - api
- recovery - recovery
type: string type: string
InvalidResponseActionEnum:
enum:
- retry
- restart
- restart_with_context
type: string
Invitation: Invitation:
type: object type: object
description: Invitation Serializer description: Invitation Serializer
@ -24662,6 +24694,13 @@ components:
minimum: -2147483648 minimum: -2147483648
policy_engine_mode: policy_engine_mode:
$ref: '#/components/schemas/PolicyEngineMode' $ref: '#/components/schemas/PolicyEngineMode'
invalid_response_action:
allOf:
- $ref: '#/components/schemas/InvalidResponseActionEnum'
description: Configure how the flow executor should handle an invalid response
to a challenge. RETRY returns the error message and a similar challenge
to the executor. RESTART restarts the flow from the beginning, and RESTART_WITH_CONTEXT
restarts the flow while keeping the current context.
PatchedGroupRequest: PatchedGroupRequest:
type: object type: object
description: Group Serializer description: Group Serializer

View File

@ -698,6 +698,10 @@ msgstr "Configure how long refresh tokens and their id_tokens are valid for."
msgid "Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected." msgid "Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected."
msgstr "Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected." msgstr "Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected."
#: src/pages/flows/StageBindingForm.ts
msgid "Configure how the flow executor should handle an invalid response to a challenge."
msgstr "Configure how the flow executor should handle an invalid response to a challenge."
#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts
msgid "Configure how the issuer field of the ID Token should be filled." msgid "Configure how the issuer field of the ID Token should be filled."
msgstr "Configure how the issuer field of the ID Token should be filled." msgstr "Configure how the issuer field of the ID Token should be filled."
@ -1881,6 +1885,10 @@ msgstr "Internal host"
msgid "Internal host SSL Validation" msgid "Internal host SSL Validation"
msgstr "Internal host SSL Validation" msgstr "Internal host SSL Validation"
#: src/pages/flows/StageBindingForm.ts
msgid "Invalid response action"
msgstr "Invalid response action"
#: src/pages/flows/FlowForm.ts #: src/pages/flows/FlowForm.ts
msgid "Invalidation" msgid "Invalidation"
msgstr "Invalidation" msgstr "Invalidation"
@ -2847,6 +2855,18 @@ msgstr "Public key, acquired from https://www.google.com/recaptcha/intro/v3.html
msgid "Publisher" msgid "Publisher"
msgstr "Publisher" msgstr "Publisher"
#: src/pages/flows/StageBindingForm.ts
msgid "RESTART restarts the flow from the beginning, while keeping the flow context."
msgstr "RESTART restarts the flow from the beginning, while keeping the flow context."
#: src/pages/flows/StageBindingForm.ts
msgid "RESTART restarts the flow from the beginning."
msgstr "RESTART restarts the flow from the beginning."
#: src/pages/flows/StageBindingForm.ts
msgid "RETRY returns the error message and a similar challenge to the executor."
msgstr "RETRY returns the error message and a similar challenge to the executor."
#: src/pages/providers/oauth2/OAuth2ProviderForm.ts #: src/pages/providers/oauth2/OAuth2ProviderForm.ts
msgid "RS256 (Asymmetric Encryption)" msgid "RS256 (Asymmetric Encryption)"
msgstr "RS256 (Asymmetric Encryption)" msgstr "RS256 (Asymmetric Encryption)"

View File

@ -692,6 +692,10 @@ msgstr ""
msgid "Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected." msgid "Configure how the NameID value will be created. When left empty, the NameIDPolicy of the incoming request will be respected."
msgstr "" msgstr ""
#:
msgid "Configure how the flow executor should handle an invalid response to a challenge."
msgstr ""
#: #:
msgid "Configure how the issuer field of the ID Token should be filled." msgid "Configure how the issuer field of the ID Token should be filled."
msgstr "" msgstr ""
@ -1873,6 +1877,10 @@ msgstr ""
msgid "Internal host SSL Validation" msgid "Internal host SSL Validation"
msgstr "" msgstr ""
#:
msgid "Invalid response action"
msgstr ""
#: #:
msgid "Invalidation" msgid "Invalidation"
msgstr "" msgstr ""
@ -2839,6 +2847,18 @@ msgstr ""
msgid "Publisher" msgid "Publisher"
msgstr "" msgstr ""
#:
msgid "RESTART restarts the flow from the beginning, while keeping the flow context."
msgstr ""
#:
msgid "RESTART restarts the flow from the beginning."
msgstr ""
#:
msgid "RETRY returns the error message and a similar challenge to the executor."
msgstr ""
#: #:
msgid "RS256 (Asymmetric Encryption)" msgid "RS256 (Asymmetric Encryption)"
msgstr "" msgstr ""

View File

@ -1,4 +1,4 @@
import { FlowsApi, FlowStageBinding, PolicyEngineMode, Stage, StagesApi } from "authentik-api"; import { FlowsApi, FlowStageBinding, InvalidResponseActionEnum, PolicyEngineMode, Stage, StagesApi } from "authentik-api";
import { t } from "@lingui/macro"; import { t } from "@lingui/macro";
import { customElement, property } from "lit-element"; import { customElement, property } from "lit-element";
import { html, TemplateResult } from "lit-html"; import { html, TemplateResult } from "lit-html";
@ -135,6 +135,23 @@ export class StageBindingForm extends ModelForm<FlowStageBinding, string> {
</div> </div>
<p class="pf-c-form__helper-text">${t`Evaluate policies before the Stage is present to the user.`}</p> <p class="pf-c-form__helper-text">${t`Evaluate policies before the Stage is present to the user.`}</p>
</ak-form-element-horizontal> </ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Invalid response action`}
?required=${true}
name="invalidResponseAction">
<select class="pf-c-form-control">
<option value=${InvalidResponseActionEnum.Retry} ?selected=${this.instance?.invalidResponseAction === InvalidResponseActionEnum.Retry}>
${t`RETRY returns the error message and a similar challenge to the executor.`}
</option>
<option value=${InvalidResponseActionEnum.Restart} ?selected=${this.instance?.invalidResponseAction === InvalidResponseActionEnum.Restart}>
${t`RESTART restarts the flow from the beginning.`}
</option>
<option value=${InvalidResponseActionEnum.RestartWithContext} ?selected=${this.instance?.invalidResponseAction === InvalidResponseActionEnum.RestartWithContext}>
${t`RESTART restarts the flow from the beginning, while keeping the flow context.`}
</option>
</select>
<p class="pf-c-form__helper-text">${t`Configure how the flow executor should handle an invalid response to a challenge.`}</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal <ak-form-element-horizontal
label=${t`Policy engine mode`} label=${t`Policy engine mode`}
?required=${true} ?required=${true}