2020-12-05 21:08:42 +00:00
|
|
|
"""authentik policy engine"""
|
2021-01-17 18:59:58 +00:00
|
|
|
from multiprocessing import Pipe, current_process
|
2019-10-01 08:17:39 +00:00
|
|
|
from multiprocessing.connection import Connection
|
2021-02-18 12:45:46 +00:00
|
|
|
from typing import Iterator, Optional
|
2019-10-01 08:17:39 +00:00
|
|
|
|
|
|
|
from django.core.cache import cache
|
|
|
|
from django.http import HttpRequest
|
2020-09-06 14:12:17 +00:00
|
|
|
from sentry_sdk.hub import Hub
|
2020-08-20 18:39:21 +00:00
|
|
|
from sentry_sdk.tracing import Span
|
2021-02-02 14:50:42 +00:00
|
|
|
from structlog.stdlib import BoundLogger, get_logger
|
2019-10-01 08:17:39 +00:00
|
|
|
|
2020-12-05 21:08:42 +00:00
|
|
|
from authentik.core.models import User
|
2022-07-01 14:49:24 +00:00
|
|
|
from authentik.policies.apps import HIST_POLICIES_BUILD_TIME
|
2021-08-03 15:45:16 +00:00
|
|
|
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
|
2019-10-01 08:17:39 +00:00
|
|
|
|
2021-01-17 18:59:58 +00:00
|
|
|
CURRENT_PROCESS = current_process()
|
2019-10-14 13:00:20 +00:00
|
|
|
|
2020-02-17 14:37:51 +00:00
|
|
|
|
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
|
2019-10-14 14:08:24 +00:00
|
|
|
result: Optional[PolicyResult]
|
2020-05-28 19:45:54 +00:00
|
|
|
binding: PolicyBinding
|
2019-10-04 11:44:26 +00:00
|
|
|
|
2021-08-03 15:45:16 +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
|
2020-05-28 19:45:54 +00:00
|
|
|
self.binding = binding
|
2019-10-14 14:08:24 +00:00
|
|
|
self.result = None
|
2019-10-04 11:44:26 +00:00
|
|
|
|
2019-12-31 11:51:16 +00:00
|
|
|
|
2019-10-01 08:17:39 +00:00
|
|
|
class PolicyEngine:
|
|
|
|
"""Orchestrate policy checking, launch tasks and return result"""
|
|
|
|
|
2020-09-09 15:20:37 +00:00
|
|
|
use_cache: bool
|
2019-10-28 16:40:57 +00:00
|
|
|
request: PolicyRequest
|
2019-10-01 08:17:39 +00:00
|
|
|
|
2021-02-02 14:50:42 +00:00
|
|
|
logger: BoundLogger
|
2021-01-12 17:22:25 +00:00
|
|
|
mode: PolicyEngineMode
|
|
|
|
# Allow objects with no policies attached to pass
|
|
|
|
empty_result: bool
|
|
|
|
|
2021-08-03 15:45:16 +00:00
|
|
|
def __init__(self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None):
|
2021-02-02 14:50:42 +00:00
|
|
|
self.logger = get_logger().bind()
|
2021-03-31 12:14:56 +00:00
|
|
|
self.mode = pbm.policy_engine_mode
|
2021-01-12 17:22:25 +00:00
|
|
|
# For backwards compatibility, set empty_result to true
|
|
|
|
# objects with no policies attached will pass.
|
|
|
|
self.empty_result = True
|
2020-09-29 08:32:41 +00:00
|
|
|
if not isinstance(pbm, PolicyBindingModel): # pragma: no cover
|
2020-05-28 19:45:54 +00:00
|
|
|
raise ValueError(f"{pbm} is not instance of PolicyBindingModel")
|
|
|
|
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)
|
2021-06-30 08:37:28 +00:00
|
|
|
self.__cached_policies: list[PolicyResult] = []
|
|
|
|
self.__processes: list[PolicyProcessInfo] = []
|
2020-09-09 15:20:37 +00:00
|
|
|
self.use_cache = True
|
2020-12-19 22:40:25 +00:00
|
|
|
self.__expected_result_count = 0
|
2019-10-01 08:17:39 +00:00
|
|
|
|
2020-09-09 15:20:37 +00:00
|
|
|
def _iter_bindings(self) -> Iterator[PolicyBinding]:
|
2019-10-04 08:22:06 +00:00
|
|
|
"""Make sure all Policies are their respective classes"""
|
2021-01-14 15:52:22 +00:00
|
|
|
return (
|
|
|
|
PolicyBinding.objects.filter(target=self.__pbm, enabled=True)
|
|
|
|
.order_by("order")
|
|
|
|
.iterator()
|
2019-12-31 11:51:16 +00:00
|
|
|
)
|
2019-10-04 08:22:06 +00:00
|
|
|
|
2021-09-16 09:04:31 +00:00
|
|
|
def _check_policy_type(self, binding: PolicyBinding):
|
2020-05-28 19:45:54 +00:00
|
|
|
"""Check policy type, make sure it's not the root class as that has no logic implemented"""
|
2020-09-13 22:28:23 +00:00
|
|
|
# pyright: reportGeneralTypeIssues=false
|
2021-09-16 09:04:31 +00:00
|
|
|
if binding.policy is not None and binding.policy.__class__ == Policy:
|
|
|
|
raise TypeError(f"Policy '{binding.policy}' is root type")
|
2020-05-28 19:45:54 +00:00
|
|
|
|
2019-12-31 11:51:16 +00:00
|
|
|
def build(self) -> "PolicyEngine":
|
2020-08-20 18:39:21 +00:00
|
|
|
"""Build wrapper which monitors performance"""
|
2021-05-23 18:29:34 +00:00
|
|
|
with Hub.current.start_span(
|
2021-12-13 15:18:26 +00:00
|
|
|
op="authentik.policy.engine.build",
|
2021-12-12 20:35:33 +00:00
|
|
|
description=self.__pbm,
|
2021-05-23 18:29:34 +00:00
|
|
|
) as span, HIST_POLICIES_BUILD_TIME.labels(
|
2022-05-29 19:45:25 +00:00
|
|
|
object_pk=str(self.__pbm.pk),
|
2021-05-23 18:29:34 +00:00
|
|
|
object_type=f"{self.__pbm._meta.app_label}.{self.__pbm._meta.model_name}",
|
|
|
|
).time():
|
2020-08-20 18:39:21 +00:00
|
|
|
span: Span
|
|
|
|
span.set_data("pbm", self.__pbm)
|
|
|
|
span.set_data("request", self.request)
|
|
|
|
for binding in self._iter_bindings():
|
2020-12-19 22:40:25 +00:00
|
|
|
self.__expected_result_count += 1
|
|
|
|
|
2021-09-16 09:04:31 +00:00
|
|
|
self._check_policy_type(binding)
|
2020-08-20 18:39:21 +00:00
|
|
|
key = cache_key(binding, self.request)
|
|
|
|
cached_policy = cache.get(key, None)
|
|
|
|
if cached_policy and self.use_cache:
|
2021-02-02 14:50:42 +00:00
|
|
|
self.logger.debug(
|
2020-08-20 18:39:21 +00:00
|
|
|
"P_ENG: Taking result from cache",
|
2021-05-21 17:10:15 +00:00
|
|
|
binding=binding,
|
2020-08-20 18:39:21 +00:00
|
|
|
cache_key=key,
|
2021-05-21 17:10:15 +00:00
|
|
|
request=self.request,
|
2020-08-20 18:39:21 +00:00
|
|
|
)
|
|
|
|
self.__cached_policies.append(cached_policy)
|
|
|
|
continue
|
2021-08-03 15:45:16 +00:00
|
|
|
self.logger.debug("P_ENG: Evaluating policy", binding=binding, request=self.request)
|
2020-08-20 18:39:21 +00:00
|
|
|
our_end, task_end = Pipe(False)
|
|
|
|
task = PolicyProcess(binding, self.request, task_end)
|
2021-01-17 18:59:58 +00:00
|
|
|
task.daemon = False
|
2021-08-03 15:45:16 +00:00
|
|
|
self.logger.debug("P_ENG: Starting Process", binding=binding, request=self.request)
|
2021-01-17 22:31:34 +00:00
|
|
|
if not CURRENT_PROCESS._config.get("daemon"):
|
2021-01-17 18:59:58 +00:00
|
|
|
task.run()
|
|
|
|
else:
|
|
|
|
task.start()
|
2020-08-20 18:39:21 +00:00
|
|
|
self.__processes.append(
|
|
|
|
PolicyProcessInfo(process=task, connection=our_end, binding=binding)
|
2020-05-23 20:01:38 +00:00
|
|
|
)
|
2020-08-20 18:39:21 +00:00
|
|
|
# If all policies are cached, we have an empty list here.
|
|
|
|
for proc_info in self.__processes:
|
2021-01-17 18:59:58 +00:00
|
|
|
if proc_info.process.is_alive():
|
|
|
|
proc_info.process.join(proc_info.binding.timeout)
|
2020-08-20 18:39:21 +00:00
|
|
|
# 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
|
2019-10-01 08:17:39 +00:00
|
|
|
|
|
|
|
@property
|
2020-05-28 19:45:54 +00:00
|
|
|
def result(self) -> PolicyResult:
|
2019-10-01 08:17:39 +00:00
|
|
|
"""Get policy-checking result"""
|
2021-08-03 15:45:16 +00:00
|
|
|
process_results: list[PolicyResult] = [x.result for x in self.__processes if x.result]
|
2020-12-19 22:40:25 +00:00
|
|
|
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")
|
2021-01-12 17:22:25 +00:00
|
|
|
# No results, no policies attached -> passing
|
|
|
|
if len(all_results) == 0:
|
|
|
|
return PolicyResult(self.empty_result)
|
|
|
|
passing = False
|
2021-03-31 12:14:56 +00:00
|
|
|
if self.mode == PolicyEngineMode.MODE_ALL:
|
2021-03-22 18:58:56 +00:00
|
|
|
passing = all(x.passing for x in all_results)
|
2021-03-31 12:14:56 +00:00
|
|
|
if self.mode == PolicyEngineMode.MODE_ANY:
|
2021-03-22 18:58:56 +00:00
|
|
|
passing = any(x.passing for x in all_results)
|
2021-01-12 17:22:25 +00:00
|
|
|
result = PolicyResult(passing)
|
2021-04-04 12:04:41 +00:00
|
|
|
result.source_results = all_results
|
2021-03-22 18:58:56 +00:00
|
|
|
result.messages = tuple(y for x in all_results for y in x.messages)
|
2021-01-12 17:22:25 +00:00
|
|
|
return result
|
2019-10-01 08:17:39 +00:00
|
|
|
|
|
|
|
@property
|
2019-10-04 07:27:29 +00:00
|
|
|
def passing(self) -> bool:
|
2019-10-01 08:17:39 +00:00
|
|
|
"""Only get true/false if user passes"""
|
2020-05-28 19:45:54 +00:00
|
|
|
return self.result.passing
|