stages/prompt: add dry-run
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
parent
17d069dd45
commit
4a6efc338e
|
@ -19,11 +19,12 @@ from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
from authentik.core.api.utils import PassiveSerializer
|
from authentik.core.api.utils import PassiveSerializer
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes, HttpChallengeResponse
|
||||||
from authentik.flows.planner import FlowPlan
|
from authentik.flows.planner import FlowPlan
|
||||||
from authentik.flows.stage import ChallengeStageView
|
from authentik.flows.stage import ChallengeStageView
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.policies.models import PolicyBinding, PolicyBindingModel, PolicyEngineMode
|
from authentik.policies.models import PolicyBinding, PolicyBindingModel, PolicyEngineMode
|
||||||
|
from authentik.policies.types import PolicyResult
|
||||||
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
from authentik.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||||
from authentik.stages.prompt.signals import password_validate
|
from authentik.stages.prompt.signals import password_validate
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ PLAN_CONTEXT_PROMPT = "prompt_data"
|
||||||
class StagePromptSerializer(PassiveSerializer):
|
class StagePromptSerializer(PassiveSerializer):
|
||||||
"""Serializer for a single Prompt field"""
|
"""Serializer for a single Prompt field"""
|
||||||
|
|
||||||
field_key = CharField()
|
field_key = CharField(required=True)
|
||||||
label = CharField(allow_blank=True)
|
label = CharField(allow_blank=True)
|
||||||
type = ChoiceField(choices=FieldTypes.choices)
|
type = ChoiceField(choices=FieldTypes.choices)
|
||||||
required = BooleanField()
|
required = BooleanField()
|
||||||
|
@ -44,21 +45,39 @@ class StagePromptSerializer(PassiveSerializer):
|
||||||
choices = ListField(child=CharField(allow_blank=True), allow_empty=True, allow_null=True)
|
choices = ListField(child=CharField(allow_blank=True), allow_empty=True, allow_null=True)
|
||||||
|
|
||||||
|
|
||||||
|
class PromptChallengeMeta(PassiveSerializer):
|
||||||
|
"""Additional context sent with the initial challenge, which might contain
|
||||||
|
info when doing dry-runs or other validation fails"""
|
||||||
|
|
||||||
|
field_key = CharField(required=True)
|
||||||
|
|
||||||
|
|
||||||
class PromptChallenge(Challenge):
|
class PromptChallenge(Challenge):
|
||||||
"""Initial challenge being sent, define fields"""
|
"""Initial challenge being sent, define fields"""
|
||||||
|
|
||||||
fields = StagePromptSerializer(many=True)
|
fields = StagePromptSerializer(many=True)
|
||||||
|
meta = PromptChallengeMeta(many=True)
|
||||||
component = CharField(default="ak-stage-prompt")
|
component = CharField(default="ak-stage-prompt")
|
||||||
|
|
||||||
|
|
||||||
|
class PromptChallengeResponseMeta(PassiveSerializer):
|
||||||
|
"""Additional context sent back by the flow executor when submitting
|
||||||
|
the prompt stage"""
|
||||||
|
|
||||||
|
dry_run = BooleanField(required=True)
|
||||||
|
|
||||||
|
|
||||||
class PromptChallengeResponse(ChallengeResponse):
|
class PromptChallengeResponse(ChallengeResponse):
|
||||||
"""Validate response, fields are dynamically created based
|
"""Validate response, fields are dynamically created based
|
||||||
on the stage"""
|
on the stage"""
|
||||||
|
|
||||||
stage_instance: PromptStage
|
stage_instance: PromptStage
|
||||||
|
validation_result: PolicyResult
|
||||||
|
|
||||||
component = CharField(default="ak-stage-prompt")
|
component = CharField(default="ak-stage-prompt")
|
||||||
|
|
||||||
|
meta = PromptChallengeResponseMeta()
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
stage: PromptStage = kwargs.pop("stage_instance", None)
|
stage: PromptStage = kwargs.pop("stage_instance", None)
|
||||||
plan: FlowPlan = kwargs.pop("plan", None)
|
plan: FlowPlan = kwargs.pop("plan", None)
|
||||||
|
@ -142,9 +161,9 @@ class PromptChallengeResponse(ChallengeResponse):
|
||||||
engine.request.context[PLAN_CONTEXT_PROMPT] = attrs
|
engine.request.context[PLAN_CONTEXT_PROMPT] = attrs
|
||||||
engine.use_cache = False
|
engine.use_cache = False
|
||||||
engine.build()
|
engine.build()
|
||||||
result = engine.result
|
self.validation_result = engine.result
|
||||||
if not result.passing:
|
if not self.validation_result.passing:
|
||||||
raise ValidationError(list(result.messages))
|
raise ValidationError(list(self.validation_result.messages))
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
@ -240,8 +259,17 @@ class PromptStageView(ChallengeStageView):
|
||||||
user=self.get_pending_user(),
|
user=self.get_pending_user(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
|
def challenge_dry_run(self, response: PromptChallengeResponse, result: PolicyResult) -> HttpResponse:
|
||||||
if PLAN_CONTEXT_PROMPT not in self.executor.plan.context:
|
challenge = self.get_challenge()
|
||||||
self.executor.plan.context[PLAN_CONTEXT_PROMPT] = {}
|
# TODO update challenge.meta
|
||||||
|
return HttpChallengeResponse(challenge)
|
||||||
|
|
||||||
|
def challenge_valid(self, response: PromptChallengeResponse) -> HttpResponse:
|
||||||
|
if response.validated_data["meta"]["dry_run"]:
|
||||||
|
# If we get to this point, the serializer must have a .validation_result attribute
|
||||||
|
# as if any other validation fails, it would've raised a validation error
|
||||||
|
# which is handled in the challenge stage base class
|
||||||
|
return self.challenge_dry_run(response, response.validation_result)
|
||||||
|
self.executor.plan.context.setdefault(PLAN_CONTEXT_PROMPT, {})
|
||||||
self.executor.plan.context[PLAN_CONTEXT_PROMPT].update(response.validated_data)
|
self.executor.plan.context[PLAN_CONTEXT_PROMPT].update(response.validated_data)
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
|
|
29
schema.yml
29
schema.yml
|
@ -38593,9 +38593,34 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/StagePrompt'
|
$ref: '#/components/schemas/StagePrompt'
|
||||||
|
meta:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/PromptChallengeMeta'
|
||||||
required:
|
required:
|
||||||
- fields
|
- fields
|
||||||
|
- meta
|
||||||
- type
|
- type
|
||||||
|
PromptChallengeMeta:
|
||||||
|
type: object
|
||||||
|
description: |-
|
||||||
|
Additional context sent with the initial challenge, which might contain
|
||||||
|
info when doing dry-runs or other validation fails
|
||||||
|
properties:
|
||||||
|
field_key:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- field_key
|
||||||
|
PromptChallengeResponseMetaRequest:
|
||||||
|
type: object
|
||||||
|
description: |-
|
||||||
|
Additional context sent back by the flow executor when submitting
|
||||||
|
the prompt stage
|
||||||
|
properties:
|
||||||
|
dry_run:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- dry_run
|
||||||
PromptChallengeResponseRequest:
|
PromptChallengeResponseRequest:
|
||||||
type: object
|
type: object
|
||||||
description: |-
|
description: |-
|
||||||
|
@ -38606,6 +38631,10 @@ components:
|
||||||
type: string
|
type: string
|
||||||
minLength: 1
|
minLength: 1
|
||||||
default: ak-stage-prompt
|
default: ak-stage-prompt
|
||||||
|
meta:
|
||||||
|
$ref: '#/components/schemas/PromptChallengeResponseMetaRequest'
|
||||||
|
required:
|
||||||
|
- meta
|
||||||
additionalProperties: {}
|
additionalProperties: {}
|
||||||
PromptRequest:
|
PromptRequest:
|
||||||
type: object
|
type: object
|
||||||
|
|
Reference in New Issue