flows: add shortcut to redirect current flow (#3192)

This commit is contained in:
Jens L 2022-07-01 23:19:41 +02:00 committed by GitHub
parent 1c64616ebd
commit 5e3f44dd87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 61 additions and 8 deletions

View File

@ -47,7 +47,8 @@ class ReevaluateMarker(StageMarker):
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER 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): running re-evaluation",
marker="ReevaluateMarker",
binding=binding, binding=binding,
policy_binding=self.binding, policy_binding=self.binding,
) )
@ -56,13 +57,15 @@ class ReevaluateMarker(StageMarker):
) )
engine.use_cache = False engine.use_cache = False
engine.request.set_http_request(http_request) engine.request.set_http_request(http_request)
engine.request.context = plan.context engine.request.context["flow_plan"] = plan
engine.request.context.update(plan.context)
engine.build() engine.build()
result = engine.result result = engine.result
if result.passing: if result.passing:
return binding return binding
LOGGER.warning( LOGGER.warning(
"f(plan_inst)[re-eval marker]: binding failed re-evaluation", "f(plan_inst): binding failed re-evaluation",
marker="ReevaluateMarker",
binding=binding, binding=binding,
messages=result.messages, messages=result.messages,
) )

View File

@ -87,13 +87,15 @@ class Stage(SerializerModel):
return f"Stage {self.name}" return f"Stage {self.name}"
def in_memory_stage(view: type["StageView"]) -> Stage: def in_memory_stage(view: type["StageView"], **kwargs) -> Stage:
"""Creates an in-memory stage instance, based on a `view` as view.""" """Creates an in-memory stage instance, based on a `view` as view."""
stage = Stage() stage = Stage()
# Because we can't pickle a locally generated function, # Because we can't pickle a locally generated function,
# we set the view as a separate property and reference a generic function # we set the view as a separate property and reference a generic function
# that returns that member # that returns that member
setattr(stage, "__in_memory_type", view) setattr(stage, "__in_memory_type", view)
for key, value in kwargs.items():
setattr(stage, key, value)
return stage return stage

View File

@ -13,7 +13,7 @@ from authentik.events.models import cleanse_dict
from authentik.flows.apps import HIST_FLOWS_PLAN_TIME from authentik.flows.apps import HIST_FLOWS_PLAN_TIME
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
from authentik.flows.markers import ReevaluateMarker, StageMarker from authentik.flows.markers import ReevaluateMarker, StageMarker
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage, in_memory_stage
from authentik.lib.config import CONFIG from authentik.lib.config import CONFIG
from authentik.policies.engine import PolicyEngine from authentik.policies.engine import PolicyEngine
@ -62,6 +62,12 @@ class FlowPlan:
self.bindings.insert(1, FlowStageBinding(stage=stage, order=0)) self.bindings.insert(1, FlowStageBinding(stage=stage, order=0))
self.markers.insert(1, marker or StageMarker()) self.markers.insert(1, marker or StageMarker())
def redirect(self, destination: str):
"""Insert a redirect stage as next stage"""
from authentik.flows.stage import RedirectStage
self.insert_stage(in_memory_stage(RedirectStage, destination=destination))
def next(self, http_request: Optional[HttpRequest]) -> Optional[FlowStageBinding]: def next(self, http_request: Optional[HttpRequest]) -> Optional[FlowStageBinding]:
"""Return next pending stage from the bottom of the list""" """Return next pending stage from the bottom of the list"""
if not self.has_stages: if not self.has_stages:
@ -137,7 +143,7 @@ class FlowPlanner:
engine = PolicyEngine(self.flow, user, request) engine = PolicyEngine(self.flow, user, request)
if default_context: if default_context:
span.set_data("default_context", cleanse_dict(default_context)) span.set_data("default_context", cleanse_dict(default_context))
engine.request.context = default_context engine.request.context.update(default_context)
engine.build() engine.build()
result = engine.result result = engine.result
if not result.passing: if not result.passing:
@ -198,7 +204,8 @@ class FlowPlanner:
stage=binding.stage, stage=binding.stage,
) )
engine = PolicyEngine(binding, user, request) engine = PolicyEngine(binding, user, request)
engine.request.context = plan.context engine.request.context["flow_plan"] = plan
engine.request.context.update(plan.context)
engine.build() engine.build()
if engine.passing: if engine.passing:
self._logger.debug( self._logger.debug(

View File

@ -19,6 +19,7 @@ from authentik.flows.challenge import (
ChallengeTypes, ChallengeTypes,
ContextualFlowInfo, ContextualFlowInfo,
HttpChallengeResponse, HttpChallengeResponse,
RedirectChallenge,
WithUserInfoChallenge, WithUserInfoChallenge,
) )
from authentik.flows.models import InvalidResponseAction from authentik.flows.models import InvalidResponseAction
@ -219,3 +220,21 @@ class AccessDeniedChallengeView(ChallengeStageView):
# .get() method is called # .get() method is called
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: # pragma: no cover def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: # pragma: no cover
return self.executor.cancel() return self.executor.cancel()
class RedirectStage(ChallengeStageView):
"""Redirect to any URL"""
def get_challenge(self, *args, **kwargs) -> RedirectChallenge:
destination = getattr(
self.executor.current_stage, "destination", reverse("authentik_core:root-redirect")
)
return RedirectChallenge(
data={
"type": ChallengeTypes.REDIRECT.value,
"to": destination,
}
)
def challenge_valid(self, response: ChallengeResponse) -> HttpResponse:
return HttpChallengeResponse(self.get_challenge())

View File

@ -0,0 +1,17 @@
---
title: Example policy snippets for flows
---
### Redirect current flow to another URL
:::info
Requires authentik 2022.7
:::
```python
plan = request.context["flow_plan"]
plan.redirect("https://foo.bar")
return False
```
This policy should be bound to the stage after your redirect should happen. For example, if you have an identification and a password stage, and you want to redirect after identification, bind the policy to the password stage. Make sure the policy binding is set to re-evaluate policies.

View File

@ -94,6 +94,7 @@ Additionally, when the policy is executed from a flow, every variable from the f
This includes the following: This includes the following:
- `context['flow_plan']`: The actual flow plan itself, can be used to inject stages.
- `context['prompt_data']`: Data which has been saved from a prompt stage or an external source. - `context['prompt_data']`: Data which has been saved from a prompt stage or an external source.
- `context['application']`: The application the user is in the process of authorizing. - `context['application']`: The application the user is in the process of authorizing.
- `context['pending_user']`: The currently pending user, see [User](../user-group/user.md#object-attributes) - `context['pending_user']`: The currently pending user, see [User](../user-group/user.md#object-attributes)

View File

@ -102,7 +102,11 @@ module.exports = {
items: [ items: [
"flow/layouts", "flow/layouts",
"flow/inspector", "flow/inspector",
"flow/examples", {
type: "category",
label: "Examples",
items: ["flow/examples/flows", "flow/examples/snippets"],
},
{ {
type: "category", type: "category",
label: "Executors", label: "Executors",