flows: include Outpost instance in flow context and save in login event (#11318)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -13,7 +13,7 @@ from authentik.events.apps import SYSTEM_TASK_STATUS
|
|||||||
from authentik.events.models import Event, EventAction, SystemTask
|
from authentik.events.models import Event, EventAction, SystemTask
|
||||||
from authentik.events.tasks import event_notification_handler, gdpr_cleanup
|
from authentik.events.tasks import event_notification_handler, gdpr_cleanup
|
||||||
from authentik.flows.models import Stage
|
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.flows.views.executor import SESSION_KEY_PLAN
|
||||||
from authentik.root.monitoring import monitoring_set
|
from authentik.root.monitoring import monitoring_set
|
||||||
from authentik.stages.invitation.models import Invitation
|
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
|
# Save the login method used
|
||||||
kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD]
|
kwargs[PLAN_CONTEXT_METHOD] = flow_plan.context[PLAN_CONTEXT_METHOD]
|
||||||
kwargs[PLAN_CONTEXT_METHOD_ARGS] = flow_plan.context.get(PLAN_CONTEXT_METHOD_ARGS, {})
|
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)
|
event = Event.new(EventAction.LOGIN, **kwargs).from_http(request, user=user)
|
||||||
request.session[SESSION_LOGIN_EVENT] = event
|
request.session[SESSION_LOGIN_EVENT] = event
|
||||||
|
|
||||||
|
|||||||
@ -23,6 +23,7 @@ from authentik.flows.models import (
|
|||||||
in_memory_stage,
|
in_memory_stage,
|
||||||
)
|
)
|
||||||
from authentik.lib.config import CONFIG
|
from authentik.lib.config import CONFIG
|
||||||
|
from authentik.outposts.models import Outpost
|
||||||
from authentik.policies.engine import PolicyEngine
|
from authentik.policies.engine import PolicyEngine
|
||||||
from authentik.root.middleware import ClientIPMiddleware
|
from authentik.root.middleware import ClientIPMiddleware
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ PLAN_CONTEXT_SSO = "is_sso"
|
|||||||
PLAN_CONTEXT_REDIRECT = "redirect"
|
PLAN_CONTEXT_REDIRECT = "redirect"
|
||||||
PLAN_CONTEXT_APPLICATION = "application"
|
PLAN_CONTEXT_APPLICATION = "application"
|
||||||
PLAN_CONTEXT_SOURCE = "source"
|
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
|
# Is set by the Flow Planner when a FlowToken was used, and the currently active flow plan
|
||||||
# was restored.
|
# was restored.
|
||||||
PLAN_CONTEXT_IS_RESTORED = "is_restored"
|
PLAN_CONTEXT_IS_RESTORED = "is_restored"
|
||||||
@ -143,10 +145,23 @@ class FlowPlanner:
|
|||||||
and not request.user.is_superuser
|
and not request.user.is_superuser
|
||||||
):
|
):
|
||||||
raise FlowNonApplicableException()
|
raise FlowNonApplicableException()
|
||||||
|
outpost_user = ClientIPMiddleware.get_outpost_user(request)
|
||||||
if self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_OUTPOST:
|
if self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_OUTPOST:
|
||||||
outpost_user = ClientIPMiddleware.get_outpost_user(request)
|
|
||||||
if not outpost_user:
|
if not outpost_user:
|
||||||
raise FlowNonApplicableException()
|
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:
|
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
|
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
|
||||||
@ -159,11 +174,12 @@ class FlowPlanner:
|
|||||||
self._logger.debug(
|
self._logger.debug(
|
||||||
"f(plan): starting planning process",
|
"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
|
# Bit of a workaround here, if there is a pending user set in the default context
|
||||||
# we use that user for our cache key
|
# we use that user for our cache key
|
||||||
# to make sure they don't get the generic response
|
# to make sure they don't get the generic response
|
||||||
if default_context and PLAN_CONTEXT_PENDING_USER in default_context:
|
if context and PLAN_CONTEXT_PENDING_USER in context:
|
||||||
user = default_context[PLAN_CONTEXT_PENDING_USER]
|
user = context[PLAN_CONTEXT_PENDING_USER]
|
||||||
else:
|
else:
|
||||||
user = request.user
|
user = request.user
|
||||||
# We only need to check the flow authentication if it's planned without a 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
|
# 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
|
# `restart_with_context`, which can only happen if the user was already authorized
|
||||||
# to use the flow
|
# to use the flow
|
||||||
self._check_authentication(request)
|
context.update(self._check_authentication(request))
|
||||||
# First off, check the flow's direct policy bindings
|
# First off, check the flow's direct policy bindings
|
||||||
# to make sure the user even has access to the flow
|
# to make sure the user even has access to the flow
|
||||||
engine = PolicyEngine(self.flow, user, request)
|
engine = PolicyEngine(self.flow, user, request)
|
||||||
engine.use_cache = self.use_cache
|
engine.use_cache = self.use_cache
|
||||||
if default_context:
|
span.set_data("context", cleanse_dict(context))
|
||||||
span.set_data("default_context", cleanse_dict(default_context))
|
engine.request.context.update(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:
|
||||||
@ -195,12 +210,12 @@ class FlowPlanner:
|
|||||||
key=cached_plan_key,
|
key=cached_plan_key,
|
||||||
)
|
)
|
||||||
# Reset the context as this isn't factored into caching
|
# Reset the context as this isn't factored into caching
|
||||||
cached_plan.context = default_context or {}
|
cached_plan.context = context
|
||||||
return cached_plan
|
return cached_plan
|
||||||
self._logger.debug(
|
self._logger.debug(
|
||||||
"f(plan): building plan",
|
"f(plan): building plan",
|
||||||
)
|
)
|
||||||
plan = self._build_plan(user, request, default_context)
|
plan = self._build_plan(user, request, context)
|
||||||
if self.use_cache:
|
if self.use_cache:
|
||||||
cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
|
cache.set(cache_key(self.flow, user), plan, CACHE_TIMEOUT)
|
||||||
if not plan.bindings and not self.allow_empty_flows:
|
if not plan.bindings and not self.allow_empty_flows:
|
||||||
|
|||||||
@ -221,9 +221,9 @@ class ClientIPMiddleware:
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
# Update sentry scope to include correct IP
|
# Update sentry scope to include correct IP
|
||||||
user = Scope.get_isolation_scope()._user or {}
|
sentry_user = Scope.get_isolation_scope()._user or {}
|
||||||
user["ip_address"] = delegated_ip
|
sentry_user["ip_address"] = delegated_ip
|
||||||
Scope.get_isolation_scope().set_user(user)
|
Scope.get_isolation_scope().set_user(sentry_user)
|
||||||
# Set the outpost service account on the request
|
# Set the outpost service account on the request
|
||||||
setattr(request, self.request_attr_outpost_user, user)
|
setattr(request, self.request_attr_outpost_user, user)
|
||||||
return delegated_ip
|
return delegated_ip
|
||||||
|
|||||||
@ -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.
|
When a user authenticates/enrolls via an external source, this will be set to the source they are using.
|
||||||
|
|
||||||
|
#### `outpost` (dictionary) <span class="badge badge--info">authentik 2024.10+</span>
|
||||||
|
|
||||||
|
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
|
### Scenario-specific keys
|
||||||
|
|
||||||
#### `is_sso` (boolean)
|
#### `is_sso` (boolean)
|
||||||
|
|||||||
Reference in New Issue
Block a user