flows: add shortcut to redirect current flow (#3192)
This commit is contained in:
		| @ -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()) | ||||||
|  | |||||||
							
								
								
									
										17
									
								
								website/docs/flow/examples/snippets.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								website/docs/flow/examples/snippets.md
									
									
									
									
									
										Normal 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. | ||||||
| @ -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
	
	Block a user
	 Jens L
					Jens L