flows: add shortcut to redirect current flow (#3192)
This commit is contained in:
parent
1c64616ebd
commit
5e3f44dd87
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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.
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Reference in New Issue