diff --git a/authentik/flows/markers.py b/authentik/flows/markers.py index e545cf89b..7d3599c27 100644 --- a/authentik/flows/markers.py +++ b/authentik/flows/markers.py @@ -47,7 +47,8 @@ class ReevaluateMarker(StageMarker): from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER LOGGER.debug( - "f(plan_inst)[re-eval marker]: running re-evaluation", + "f(plan_inst): running re-evaluation", + marker="ReevaluateMarker", binding=binding, policy_binding=self.binding, ) @@ -56,13 +57,15 @@ class ReevaluateMarker(StageMarker): ) engine.use_cache = False 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() result = engine.result if result.passing: return binding LOGGER.warning( - "f(plan_inst)[re-eval marker]: binding failed re-evaluation", + "f(plan_inst): binding failed re-evaluation", + marker="ReevaluateMarker", binding=binding, messages=result.messages, ) diff --git a/authentik/flows/models.py b/authentik/flows/models.py index 45f89d4d2..b1645ce37 100644 --- a/authentik/flows/models.py +++ b/authentik/flows/models.py @@ -87,13 +87,15 @@ class Stage(SerializerModel): 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.""" stage = Stage() # Because we can't pickle a locally generated function, # we set the view as a separate property and reference a generic function # that returns that member setattr(stage, "__in_memory_type", view) + for key, value in kwargs.items(): + setattr(stage, key, value) return stage diff --git a/authentik/flows/planner.py b/authentik/flows/planner.py index 1c3528f81..e64681575 100644 --- a/authentik/flows/planner.py +++ b/authentik/flows/planner.py @@ -13,7 +13,7 @@ from authentik.events.models import cleanse_dict from authentik.flows.apps import HIST_FLOWS_PLAN_TIME from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException 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.policies.engine import PolicyEngine @@ -62,6 +62,12 @@ class FlowPlan: self.bindings.insert(1, FlowStageBinding(stage=stage, order=0)) 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]: """Return next pending stage from the bottom of the list""" if not self.has_stages: @@ -137,7 +143,7 @@ class FlowPlanner: engine = PolicyEngine(self.flow, user, request) if default_context: span.set_data("default_context", cleanse_dict(default_context)) - engine.request.context = default_context + engine.request.context.update(default_context) engine.build() result = engine.result if not result.passing: @@ -198,7 +204,8 @@ class FlowPlanner: stage=binding.stage, ) engine = PolicyEngine(binding, user, request) - engine.request.context = plan.context + engine.request.context["flow_plan"] = plan + engine.request.context.update(plan.context) engine.build() if engine.passing: self._logger.debug( diff --git a/authentik/flows/stage.py b/authentik/flows/stage.py index fd962ab67..a9d2ec1ef 100644 --- a/authentik/flows/stage.py +++ b/authentik/flows/stage.py @@ -19,6 +19,7 @@ from authentik.flows.challenge import ( ChallengeTypes, ContextualFlowInfo, HttpChallengeResponse, + RedirectChallenge, WithUserInfoChallenge, ) from authentik.flows.models import InvalidResponseAction @@ -219,3 +220,21 @@ class AccessDeniedChallengeView(ChallengeStageView): # .get() method is called def challenge_valid(self, response: ChallengeResponse) -> HttpResponse: # pragma: no cover 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()) diff --git a/website/docs/flow/examples.md b/website/docs/flow/examples/flows.md similarity index 100% rename from website/docs/flow/examples.md rename to website/docs/flow/examples/flows.md diff --git a/website/docs/flow/examples/snippets.md b/website/docs/flow/examples/snippets.md new file mode 100644 index 000000000..b5df0e7e9 --- /dev/null +++ b/website/docs/flow/examples/snippets.md @@ -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. diff --git a/website/docs/policies/expression.mdx b/website/docs/policies/expression.mdx index 1321e8b62..55863c187 100644 --- a/website/docs/policies/expression.mdx +++ b/website/docs/policies/expression.mdx @@ -94,6 +94,7 @@ Additionally, when the policy is executed from a flow, every variable from the f 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['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) diff --git a/website/sidebars.js b/website/sidebars.js index 28bc984ed..bf952888f 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -102,7 +102,11 @@ module.exports = { items: [ "flow/layouts", "flow/inspector", - "flow/examples", + { + type: "category", + label: "Examples", + items: ["flow/examples/flows", "flow/examples/snippets"], + }, { type: "category", label: "Executors",