This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
authentik/authentik/policies/engine.py

176 lines
7 KiB
Python
Raw Normal View History

2020-12-05 21:08:42 +00:00
"""authentik policy engine"""
from multiprocessing import Pipe, current_process
from multiprocessing.connection import Connection
from timeit import default_timer
from typing import Iterator, Optional
from django.core.cache import cache
from django.http import HttpRequest
from sentry_sdk.hub import Hub
from sentry_sdk.tracing import Span
2021-02-02 14:50:42 +00:00
from structlog.stdlib import BoundLogger, get_logger
2020-12-05 21:08:42 +00:00
from authentik.core.models import User
from authentik.lib.utils.reflection import class_to_path
from authentik.policies.apps import HIST_POLICIES_ENGINE_TOTAL_TIME, HIST_POLICIES_EXECUTION_TIME
from authentik.policies.exceptions import PolicyEngineException
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel, PolicyEngineMode
2020-12-05 21:08:42 +00:00
from authentik.policies.process import PolicyProcess, cache_key
from authentik.policies.types import PolicyRequest, PolicyResult
CURRENT_PROCESS = current_process()
2019-10-04 11:44:26 +00:00
class PolicyProcessInfo:
2019-10-04 11:49:27 +00:00
"""Dataclass to hold all information and communication channels to a process"""
2019-10-04 11:44:26 +00:00
process: PolicyProcess
connection: Connection
result: Optional[PolicyResult]
binding: PolicyBinding
2019-10-04 11:44:26 +00:00
def __init__(self, process: PolicyProcess, connection: Connection, binding: PolicyBinding):
2019-10-04 11:44:26 +00:00
self.process = process
self.connection = connection
self.binding = binding
self.result = None
2019-10-04 11:44:26 +00:00
2019-12-31 11:51:16 +00:00
class PolicyEngine:
"""Orchestrate policy checking, launch tasks and return result"""
use_cache: bool
2019-10-28 16:40:57 +00:00
request: PolicyRequest
2021-02-02 14:50:42 +00:00
logger: BoundLogger
mode: PolicyEngineMode
# Allow objects with no policies attached to pass
empty_result: bool
def __init__(self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None):
2021-02-02 14:50:42 +00:00
self.logger = get_logger().bind()
self.mode = pbm.policy_engine_mode
# For backwards compatibility, set empty_result to true
# objects with no policies attached will pass.
self.empty_result = True
if not isinstance(pbm, PolicyBindingModel): # pragma: no cover
raise PolicyEngineException(f"{pbm} is not instance of PolicyBindingModel")
if not user:
raise PolicyEngineException("User must be set")
self.__pbm = pbm
2019-10-28 16:40:57 +00:00
self.request = PolicyRequest(user)
2020-12-31 10:16:01 +00:00
self.request.obj = pbm
2019-10-28 16:40:57 +00:00
if request:
2021-02-12 08:47:37 +00:00
self.request.set_http_request(request)
self.__cached_policies: list[PolicyResult] = []
self.__processes: list[PolicyProcessInfo] = []
self.use_cache = True
self.__expected_result_count = 0
enterprise: initial enterprise (#5721) * initial Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add user type Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add external users Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add ui, add more logic, add public JWT validation key Signed-off-by: Jens Langhammer <jens@goauthentik.io> * revert to not use install_id as session jwt signing key Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * switch to PKI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add more licensing stuff Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add install ID to form Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix bugs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start adding tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fixes Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use x5c correctly Signed-off-by: Jens Langhammer <jens@goauthentik.io> * license checks Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use production CA Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more UI stuff Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rename to summary Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update locale, improve ui Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add direct button Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update link Signed-off-by: Jens Langhammer <jens@goauthentik.io> * format and such Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove old attributes from ldap Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove is_enterprise_licensed Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix admin interface styling issue Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Update authentik/core/models.py Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Jens L. <jens@beryju.org> * fix default case Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Signed-off-by: Jens L. <jens@beryju.org> Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2023-07-17 15:57:08 +00:00
def iterate_bindings(self) -> Iterator[PolicyBinding]:
"""Make sure all Policies are their respective classes"""
return (
PolicyBinding.objects.filter(target=self.__pbm, enabled=True)
.order_by("order")
.iterator()
2019-12-31 11:51:16 +00:00
)
def _check_policy_type(self, binding: PolicyBinding):
"""Check policy type, make sure it's not the root class as that has no logic implemented"""
if binding.policy is not None and binding.policy.__class__ == Policy:
raise PolicyEngineException(f"Policy '{binding.policy}' is root type")
def _check_cache(self, binding: PolicyBinding):
if not self.use_cache:
return False
before = default_timer()
key = cache_key(binding, self.request)
cached_policy = cache.get(key, None)
duration = max(default_timer() - before, 0)
if not cached_policy:
return False
self.logger.debug(
"P_ENG: Taking result from cache",
binding=binding,
cache_key=key,
request=self.request,
)
HIST_POLICIES_EXECUTION_TIME.labels(
binding_order=binding.order,
binding_target_type=binding.target_type,
binding_target_name=binding.target_name,
object_pk=str(self.request.obj.pk),
object_type=class_to_path(self.request.obj.__class__),
mode="cache_retrieve",
).observe(duration)
# It's a bit silly to time this, but
self.__cached_policies.append(cached_policy)
return True
2019-12-31 11:51:16 +00:00
def build(self) -> "PolicyEngine":
"""Build wrapper which monitors performance"""
with (
Hub.current.start_span(
op="authentik.policy.engine.build",
description=self.__pbm,
) as span,
HIST_POLICIES_ENGINE_TOTAL_TIME.labels(
obj_type=class_to_path(self.__pbm.__class__),
obj_pk=str(self.__pbm.pk),
).time(),
):
span: Span
span.set_data("pbm", self.__pbm)
span.set_data("request", self.request)
enterprise: initial enterprise (#5721) * initial Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add user type Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add external users Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add ui, add more logic, add public JWT validation key Signed-off-by: Jens Langhammer <jens@goauthentik.io> * revert to not use install_id as session jwt signing key Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * switch to PKI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add more licensing stuff Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add install ID to form Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix bugs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start adding tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fixes Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use x5c correctly Signed-off-by: Jens Langhammer <jens@goauthentik.io> * license checks Signed-off-by: Jens Langhammer <jens@goauthentik.io> * use production CA Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more UI stuff Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rename to summary Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update locale, improve ui Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add direct button Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update link Signed-off-by: Jens Langhammer <jens@goauthentik.io> * format and such Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove old attributes from ldap Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove is_enterprise_licensed Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix admin interface styling issue Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Update authentik/core/models.py Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Jens L. <jens@beryju.org> * fix default case Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Signed-off-by: Jens L. <jens@beryju.org> Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2023-07-17 15:57:08 +00:00
for binding in self.iterate_bindings():
self.__expected_result_count += 1
self._check_policy_type(binding)
if self._check_cache(binding):
continue
self.logger.debug("P_ENG: Evaluating policy", binding=binding, request=self.request)
our_end, task_end = Pipe(False)
task = PolicyProcess(binding, self.request, task_end)
task.daemon = False
self.logger.debug("P_ENG: Starting Process", binding=binding, request=self.request)
if not CURRENT_PROCESS._config.get("daemon"):
task.run()
else:
task.start()
self.__processes.append(
PolicyProcessInfo(process=task, connection=our_end, binding=binding)
)
# If all policies are cached, we have an empty list here.
for proc_info in self.__processes:
if proc_info.process.is_alive():
proc_info.process.join(proc_info.binding.timeout)
# Only call .recv() if no result is saved, otherwise we just deadlock here
if not proc_info.result:
proc_info.result = proc_info.connection.recv()
return self
@property
def result(self) -> PolicyResult:
"""Get policy-checking result"""
process_results: list[PolicyResult] = [x.result for x in self.__processes if x.result]
all_results = list(process_results + self.__cached_policies)
if len(all_results) < self.__expected_result_count: # pragma: no cover
raise AssertionError("Got less results than polices")
# No results, no policies attached -> passing
if len(all_results) == 0:
return PolicyResult(self.empty_result)
passing = False
if self.mode == PolicyEngineMode.MODE_ALL:
passing = all(x.passing for x in all_results)
if self.mode == PolicyEngineMode.MODE_ANY:
passing = any(x.passing for x in all_results)
result = PolicyResult(passing)
result.source_results = all_results
result.messages = tuple(y for x in all_results for y in x.messages)
return result
@property
def passing(self) -> bool:
"""Only get true/false if user passes"""
return self.result.passing