diff --git a/authentik/events/signals.py b/authentik/events/signals.py index 3f93e5deaa..9f4680f6e7 100644 --- a/authentik/events/signals.py +++ b/authentik/events/signals.py @@ -13,7 +13,7 @@ from authentik.events.apps import SYSTEM_TASK_STATUS from authentik.events.models import Event, EventAction, SystemTask from authentik.events.tasks import event_notification_handler, gdpr_cleanup from authentik.flows.models import Stage -from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan +from authentik.flows.planner import PLAN_CONTEXT_OUTPOST, PLAN_CONTEXT_SOURCE, FlowPlan from authentik.flows.views.executor import SESSION_KEY_PLAN from authentik.root.monitoring import monitoring_set from authentik.stages.invitation.models import Invitation @@ -38,6 +38,9 @@ def on_user_logged_in(sender, request: HttpRequest, user: User, **_): # Save the login method used kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD] kwargs[PLAN_CONTEXT_METHOD_ARGS] = flow_plan.context.get(PLAN_CONTEXT_METHOD_ARGS, {}) + if PLAN_CONTEXT_OUTPOST in flow_plan.context: + # Save outpost context + kwargs[PLAN_CONTEXT_OUTPOST] = flow_plan.context[PLAN_CONTEXT_OUTPOST] event = Event.new(EventAction.LOGIN, **kwargs).from_http(request, user=user) request.session[SESSION_LOGIN_EVENT] = event diff --git a/authentik/flows/planner.py b/authentik/flows/planner.py index 2c7231669e..69e151af86 100644 --- a/authentik/flows/planner.py +++ b/authentik/flows/planner.py @@ -23,6 +23,7 @@ from authentik.flows.models import ( in_memory_stage, ) from authentik.lib.config import CONFIG +from authentik.outposts.models import Outpost from authentik.policies.engine import PolicyEngine from authentik.root.middleware import ClientIPMiddleware @@ -32,6 +33,7 @@ PLAN_CONTEXT_SSO = "is_sso" PLAN_CONTEXT_REDIRECT = "redirect" PLAN_CONTEXT_APPLICATION = "application" PLAN_CONTEXT_SOURCE = "source" +PLAN_CONTEXT_OUTPOST = "outpost" # Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan # was restored. PLAN_CONTEXT_IS_RESTORED = "is_restored" @@ -143,10 +145,23 @@ class FlowPlanner: and not request.user.is_superuser ): raise FlowNonApplicableException() + outpost_user = ClientIPMiddleware.get_outpost_user(request) if self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_OUTPOST: - outpost_user = ClientIPMiddleware.get_outpost_user(request) if not outpost_user: raise FlowNonApplicableException() + if outpost_user: + outpost = Outpost.objects.filter( + # TODO: Since Outpost and user are not directly connected, we have to look up a user + # like this. This should ideally by in authentik/outposts/models.py + pk=outpost_user.username.replace("ak-outpost-", "") + ).first() + if outpost: + return { + PLAN_CONTEXT_OUTPOST: { + "instance": outpost, + } + } + return {} def plan(self, request: HttpRequest, default_context: dict[str, Any] | None = None) -> FlowPlan: """Check each of the flows' policies, check policies for each stage with PolicyBinding @@ -159,11 +174,12 @@ class FlowPlanner: self._logger.debug( "f(plan): starting planning process", ) + context = default_context or {} # Bit of a workaround here, if there is a pending user set in the default context # we use that user for our cache key # to make sure they don't get the generic response - if default_context and PLAN_CONTEXT_PENDING_USER in default_context: - user = default_context[PLAN_CONTEXT_PENDING_USER] + if context and PLAN_CONTEXT_PENDING_USER in context: + user = context[PLAN_CONTEXT_PENDING_USER] else: user = request.user # We only need to check the flow authentication if it's planned without a user @@ -171,14 +187,13 @@ class FlowPlanner: # or if a flow is restarted due to `invalid_response_action` being set to # `restart_with_context`, which can only happen if the user was already authorized # to use the flow - self._check_authentication(request) + context.update(self._check_authentication(request)) # First off, check the flow's direct policy bindings # to make sure the user even has access to the flow engine = PolicyEngine(self.flow, user, request) engine.use_cache = self.use_cache - if default_context: - span.set_data("default_context", cleanse_dict(default_context)) - engine.request.context.update(default_context) + span.set_data("context", cleanse_dict(context)) + engine.request.context.update(context) engine.build() result = engine.result if not result.passing: @@ -195,12 +210,12 @@ class FlowPlanner: key=cached_plan_key, ) # Reset the context as this isn't factored into caching - cached_plan.context = default_context or {} + cached_plan.context = context return cached_plan self._logger.debug( "f(plan): building plan", ) - plan = self._build_plan(user, request, default_context) + plan = self._build_plan(user, request, context) if self.use_cache: cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT) if not plan.bindings and not self.allow_empty_flows: diff --git a/authentik/root/middleware.py b/authentik/root/middleware.py index c8e53bc88b..b66ccee847 100644 --- a/authentik/root/middleware.py +++ b/authentik/root/middleware.py @@ -221,9 +221,9 @@ class ClientIPMiddleware: ) return None # Update sentry scope to include correct IP - user = Scope.get_isolation_scope()._user or {} - user["ip_address"] = delegated_ip - Scope.get_isolation_scope().set_user(user) + sentry_user = Scope.get_isolation_scope()._user or {} + sentry_user["ip_address"] = delegated_ip + Scope.get_isolation_scope().set_user(sentry_user) # Set the outpost service account on the request setattr(request, self.request_attr_outpost_user, user) return delegated_ip diff --git a/website/docs/flow/context/index.md b/website/docs/flow/context/index.md index 9283501f72..5b705f3752 100644 --- a/website/docs/flow/context/index.md +++ b/website/docs/flow/context/index.md @@ -60,6 +60,10 @@ When an unauthenticated user attempts to access a secured resource, they are red When a user authenticates/enrolls via an external source, this will be set to the source they are using. +#### `outpost` (dictionary) authentik 2024.10+ + +When a flow is executed by an Outpost (for example the [LDAP](../../providers/ldap/index.md) or [RADIUS](../../providers/radius/index.mdx)), this will be set to a dictionary containing the Outpost instance under the key `"instance"`. + ### Scenario-specific keys #### `is_sso` (boolean)