stages/prompt: add dry-run

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens Langhammer 2023-06-14 11:07:16 +02:00
parent 17d069dd45
commit 4a6efc338e
No known key found for this signature in database
2 changed files with 65 additions and 8 deletions

View File

@ -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()

View File

@ -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