core: add helper function to create events from expressions, move ak_user_has_authenticator to base evaluator
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
ab28370f20
commit
9f5c019daa
|
@ -17,7 +17,7 @@ from authentik.api.decorators import permission_required
|
||||||
from authentik.blueprints.api import ManagedSerializer
|
from authentik.blueprints.api import ManagedSerializer
|
||||||
from authentik.core.api.used_by import UsedByMixin
|
from authentik.core.api.used_by import UsedByMixin
|
||||||
from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer
|
from authentik.core.api.utils import MetaNameSerializer, PassiveSerializer, TypeCreateSerializer
|
||||||
from authentik.core.expression import PropertyMappingEvaluator
|
from authentik.core.expression.evaluator import PropertyMappingEvaluator
|
||||||
from authentik.core.models import PropertyMapping
|
from authentik.core.models import PropertyMapping
|
||||||
from authentik.lib.utils.reflection import all_subclasses
|
from authentik.lib.utils.reflection import all_subclasses
|
||||||
from authentik.policies.api.exec import PolicyTestSerializer
|
from authentik.policies.api.exec import PolicyTestSerializer
|
||||||
|
@ -41,7 +41,9 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
|
||||||
|
|
||||||
def validate_expression(self, expression: str) -> str:
|
def validate_expression(self, expression: str) -> str:
|
||||||
"""Test Syntax"""
|
"""Test Syntax"""
|
||||||
evaluator = PropertyMappingEvaluator()
|
evaluator = PropertyMappingEvaluator(
|
||||||
|
self.instance,
|
||||||
|
)
|
||||||
evaluator.validate(expression)
|
evaluator.validate(expression)
|
||||||
return expression
|
return expression
|
||||||
|
|
||||||
|
|
|
@ -2,28 +2,33 @@
|
||||||
from traceback import format_tb
|
from traceback import format_tb
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
from django.db.models import Model
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from guardian.utils import get_anonymous_user
|
from guardian.utils import get_anonymous_user
|
||||||
|
|
||||||
from authentik.core.models import PropertyMapping, User
|
from authentik.core.models import User
|
||||||
from authentik.events.models import Event, EventAction
|
from authentik.events.models import Event, EventAction
|
||||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||||
from authentik.policies.types import PolicyRequest
|
from authentik.policies.types import PolicyRequest
|
||||||
|
|
||||||
|
|
||||||
class PropertyMappingEvaluator(BaseEvaluator):
|
class PropertyMappingEvaluator(BaseEvaluator):
|
||||||
"""Custom Evalautor that adds some different context variables."""
|
"""Custom Evaluator that adds some different context variables."""
|
||||||
|
|
||||||
def set_context(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
user: Optional[User],
|
model: Model,
|
||||||
request: Optional[HttpRequest],
|
user: Optional[User] = None,
|
||||||
mapping: PropertyMapping,
|
request: Optional[HttpRequest] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Update context with context from PropertyMapping's evaluate"""
|
if hasattr(model, "name"):
|
||||||
|
_filename = model.name
|
||||||
|
else:
|
||||||
|
_filename = str(model)
|
||||||
|
super().__init__(filename=_filename)
|
||||||
req = PolicyRequest(user=get_anonymous_user())
|
req = PolicyRequest(user=get_anonymous_user())
|
||||||
req.obj = mapping
|
req.obj = model
|
||||||
if user:
|
if user:
|
||||||
req.user = user
|
req.user = user
|
||||||
self._context["user"] = user
|
self._context["user"] = user
|
|
@ -617,10 +617,9 @@ class PropertyMapping(SerializerModel, ManagedModel):
|
||||||
|
|
||||||
def evaluate(self, user: Optional[User], request: Optional[HttpRequest], **kwargs) -> Any:
|
def evaluate(self, user: Optional[User], request: Optional[HttpRequest], **kwargs) -> Any:
|
||||||
"""Evaluate `self.expression` using `**kwargs` as Context."""
|
"""Evaluate `self.expression` using `**kwargs` as Context."""
|
||||||
from authentik.core.expression import PropertyMappingEvaluator
|
from authentik.core.expression.evaluator import PropertyMappingEvaluator
|
||||||
|
|
||||||
evaluator = PropertyMappingEvaluator()
|
evaluator = PropertyMappingEvaluator(self, user, request, **kwargs)
|
||||||
evaluator.set_context(user, request, self, **kwargs)
|
|
||||||
try:
|
try:
|
||||||
return evaluator.evaluate(self.expression)
|
return evaluator.evaluate(self.expression)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
"""authentik expression policy evaluator"""
|
"""authentik expression policy evaluator"""
|
||||||
import re
|
import re
|
||||||
|
from ipaddress import ip_address, ip_network
|
||||||
from textwrap import indent
|
from textwrap import indent
|
||||||
from typing import Any, Iterable, Optional
|
from typing import Any, Iterable, Optional
|
||||||
|
|
||||||
from django.core.exceptions import FieldError
|
from django.core.exceptions import FieldError
|
||||||
|
from django_otp import devices_for_user
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
from sentry_sdk.hub import Hub
|
from sentry_sdk.hub import Hub
|
||||||
from sentry_sdk.tracing import Span
|
from sentry_sdk.tracing import Span
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
|
from authentik.events.models import Event
|
||||||
from authentik.lib.utils.http import get_http_session
|
from authentik.lib.utils.http import get_http_session
|
||||||
|
from authentik.policies.types import PolicyRequest
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
@ -26,7 +30,8 @@ class BaseEvaluator:
|
||||||
# Filename used for exec
|
# Filename used for exec
|
||||||
_filename: str
|
_filename: str
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, filename: Optional[str] = None):
|
||||||
|
self._filename = filename if filename else "BaseEvaluator"
|
||||||
# update website/docs/expressions/_objects.md
|
# update website/docs/expressions/_objects.md
|
||||||
# update website/docs/expressions/_functions.md
|
# update website/docs/expressions/_functions.md
|
||||||
self._globals = {
|
self._globals = {
|
||||||
|
@ -35,11 +40,14 @@ class BaseEvaluator:
|
||||||
"list_flatten": BaseEvaluator.expr_flatten,
|
"list_flatten": BaseEvaluator.expr_flatten,
|
||||||
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
|
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
|
||||||
"ak_user_by": BaseEvaluator.expr_user_by,
|
"ak_user_by": BaseEvaluator.expr_user_by,
|
||||||
"ak_logger": get_logger(),
|
"ak_user_has_authenticator": BaseEvaluator.expr_func_user_has_authenticator,
|
||||||
|
"ak_create_event": self.expr_event_create,
|
||||||
|
"ak_logger": get_logger(self._filename),
|
||||||
"requests": get_http_session(),
|
"requests": get_http_session(),
|
||||||
|
"ip_address": ip_address,
|
||||||
|
"ip_network": ip_network,
|
||||||
}
|
}
|
||||||
self._context = {}
|
self._context = {}
|
||||||
self._filename = "BaseEvalautor"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def expr_flatten(value: list[Any] | Any) -> Optional[Any]:
|
def expr_flatten(value: list[Any] | Any) -> Optional[Any]:
|
||||||
|
@ -60,6 +68,11 @@ class BaseEvaluator:
|
||||||
"""Expression Filter to run re.sub"""
|
"""Expression Filter to run re.sub"""
|
||||||
return re.sub(regex, repl, value)
|
return re.sub(regex, repl, value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def expr_is_group_member(user: User, **group_filters) -> bool:
|
||||||
|
"""Check if `user` is member of group with name `group_name`"""
|
||||||
|
return user.ak_groups.filter(**group_filters).exists()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def expr_user_by(**filters) -> Optional[User]:
|
def expr_user_by(**filters) -> Optional[User]:
|
||||||
"""Get user by filters"""
|
"""Get user by filters"""
|
||||||
|
@ -72,15 +85,37 @@ class BaseEvaluator:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def expr_is_group_member(user: User, **group_filters) -> bool:
|
def expr_func_user_has_authenticator(user: User, device_type: Optional[str] = None) -> bool:
|
||||||
"""Check if `user` is member of group with name `group_name`"""
|
"""Check if a user has any authenticator devices, optionally matching *device_type*"""
|
||||||
return user.ak_groups.filter(**group_filters).exists()
|
user_devices = devices_for_user(user)
|
||||||
|
if device_type:
|
||||||
|
for device in user_devices:
|
||||||
|
device_class = device.__class__.__name__.lower().replace("device", "")
|
||||||
|
if device_class == device_type:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
return len(list(user_devices)) > 0
|
||||||
|
|
||||||
|
def expr_event_create(self, action: str, **kwargs):
|
||||||
|
"""Create event with supplied data and try to extract as much relevant data
|
||||||
|
from the context"""
|
||||||
|
kwargs["context"] = self._context
|
||||||
|
event = Event.new(
|
||||||
|
action,
|
||||||
|
app=self._filename,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
if "request" in self._context and isinstance(PolicyRequest, self._context["request"]):
|
||||||
|
policy_request: PolicyRequest = self._context["request"]
|
||||||
|
if policy_request.http_request:
|
||||||
|
event.from_http(policy_request)
|
||||||
|
return
|
||||||
|
event.save()
|
||||||
|
|
||||||
def wrap_expression(self, expression: str, params: Iterable[str]) -> str:
|
def wrap_expression(self, expression: str, params: Iterable[str]) -> str:
|
||||||
"""Wrap expression in a function, call it, and save the result as `result`"""
|
"""Wrap expression in a function, call it, and save the result as `result`"""
|
||||||
handler_signature = ",".join(params)
|
handler_signature = ",".join(params)
|
||||||
full_expression = ""
|
full_expression = ""
|
||||||
full_expression += "from ipaddress import ip_address, ip_network\n"
|
|
||||||
full_expression += f"def handler({handler_signature}):\n"
|
full_expression += f"def handler({handler_signature}):\n"
|
||||||
full_expression += indent(expression, " ")
|
full_expression += indent(expression, " ")
|
||||||
full_expression += f"\nresult = handler({handler_signature})"
|
full_expression += f"\nresult = handler({handler_signature})"
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
"""authentik expression policy evaluator"""
|
"""authentik expression policy evaluator"""
|
||||||
from ipaddress import ip_address, ip_network
|
from ipaddress import ip_address
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django_otp import devices_for_user
|
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.models import User
|
|
||||||
from authentik.flows.planner import PLAN_CONTEXT_SSO
|
from authentik.flows.planner import PLAN_CONTEXT_SSO
|
||||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||||
from authentik.lib.utils.http import get_client_ip
|
from authentik.lib.utils.http import get_client_ip
|
||||||
|
@ -27,16 +25,14 @@ class PolicyEvaluator(BaseEvaluator):
|
||||||
|
|
||||||
policy: Optional["ExpressionPolicy"] = None
|
policy: Optional["ExpressionPolicy"] = None
|
||||||
|
|
||||||
def __init__(self, policy_name: str):
|
def __init__(self, policy_name: Optional[str] = None):
|
||||||
super().__init__()
|
super().__init__(policy_name or "PolicyEvaluator")
|
||||||
self._messages = []
|
self._messages = []
|
||||||
self._context["ak_logger"] = get_logger(policy_name)
|
# update website/docs/expressions/_objects.md
|
||||||
|
# update website/docs/expressions/_functions.md
|
||||||
self._context["ak_message"] = self.expr_func_message
|
self._context["ak_message"] = self.expr_func_message
|
||||||
self._context["ak_user_has_authenticator"] = self.expr_func_user_has_authenticator
|
self._context["ak_user_has_authenticator"] = self.expr_func_user_has_authenticator
|
||||||
self._context["ak_call_policy"] = self.expr_func_call_policy
|
self._context["ak_call_policy"] = self.expr_func_call_policy
|
||||||
self._context["ip_address"] = ip_address
|
|
||||||
self._context["ip_network"] = ip_network
|
|
||||||
self._filename = policy_name or "PolicyEvaluator"
|
|
||||||
|
|
||||||
def expr_func_message(self, message: str):
|
def expr_func_message(self, message: str):
|
||||||
"""Wrapper to append to messages list, which is returned with PolicyResult"""
|
"""Wrapper to append to messages list, which is returned with PolicyResult"""
|
||||||
|
@ -52,19 +48,6 @@ class PolicyEvaluator(BaseEvaluator):
|
||||||
proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
|
proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None)
|
||||||
return proc.profiling_wrapper()
|
return proc.profiling_wrapper()
|
||||||
|
|
||||||
def expr_func_user_has_authenticator(
|
|
||||||
self, user: User, device_type: Optional[str] = None
|
|
||||||
) -> bool:
|
|
||||||
"""Check if a user has any authenticator devices, optionally matching *device_type*"""
|
|
||||||
user_devices = devices_for_user(user)
|
|
||||||
if device_type:
|
|
||||||
for device in user_devices:
|
|
||||||
device_class = device.__class__.__name__.lower().replace("device", "")
|
|
||||||
if device_class == device_type:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
return len(list(user_devices)) > 0
|
|
||||||
|
|
||||||
def set_policy_request(self, request: PolicyRequest):
|
def set_policy_request(self, request: PolicyRequest):
|
||||||
"""Update context based on policy request (if http request is given, update that too)"""
|
"""Update context based on policy request (if http request is given, update that too)"""
|
||||||
# update website/docs/expressions/_objects.md
|
# update website/docs/expressions/_objects.md
|
||||||
|
|
|
@ -22,7 +22,7 @@ from rest_framework.serializers import BaseSerializer
|
||||||
from structlog.stdlib import get_logger
|
from structlog.stdlib import get_logger
|
||||||
|
|
||||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||||
from authentik.core.expression import PropertyMappingEvaluator
|
from authentik.core.expression.evaluator import PropertyMappingEvaluator
|
||||||
from authentik.core.models import User
|
from authentik.core.models import User
|
||||||
from authentik.flows.models import Stage
|
from authentik.flows.models import Stage
|
||||||
from authentik.lib.models import SerializerModel
|
from authentik.lib.models import SerializerModel
|
||||||
|
@ -124,8 +124,7 @@ class Prompt(SerializerModel):
|
||||||
return prompt_context[self.field_key]
|
return prompt_context[self.field_key]
|
||||||
|
|
||||||
if self.placeholder_expression:
|
if self.placeholder_expression:
|
||||||
evaluator = PropertyMappingEvaluator()
|
evaluator = PropertyMappingEvaluator(self, user, request, prompt_context=prompt_context)
|
||||||
evaluator.set_context(user, request, self, prompt_context=prompt_context)
|
|
||||||
try:
|
try:
|
||||||
return evaluator.evaluate(self.placeholder)
|
return evaluator.evaluate(self.placeholder)
|
||||||
except Exception as exc: # pylint:disable=broad-except
|
except Exception as exc: # pylint:disable=broad-except
|
||||||
|
|
|
@ -51,6 +51,45 @@ Example:
|
||||||
other_user = ak_user_by(username="other_user")
|
other_user = ak_user_by(username="other_user")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `ak_user_has_authenticator(user: User, device_type: Optional[str] = None) -> bool` (2021.9+)
|
||||||
|
|
||||||
|
:::info
|
||||||
|
Only available in property mappings with authentik 2022.9 and newer
|
||||||
|
:::
|
||||||
|
|
||||||
|
Check if a user has any authenticator devices. Only fully validated devices are counted.
|
||||||
|
|
||||||
|
Optionally, you can filter a specific device type. The following options are valid:
|
||||||
|
|
||||||
|
- `totp`
|
||||||
|
- `duo`
|
||||||
|
- `static`
|
||||||
|
- `webauthn`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return ak_user_has_authenticator(request.user)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `ak_create_event(action: str, **kwargs) -> None`
|
||||||
|
|
||||||
|
:::info
|
||||||
|
Requires authentik 2022.9
|
||||||
|
:::
|
||||||
|
|
||||||
|
Create a new event with the action set to `action`. Any additional key-word parameters will be saved in the event context. Additionally, `context` will be set to the context in which this function is called.
|
||||||
|
|
||||||
|
Before saving, any data-structure which are not representable in JSON are flattened, and credentials are removed.
|
||||||
|
|
||||||
|
The event is saved automatically
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ak_create_event("my_custom_event", foo=request.user)
|
||||||
|
```
|
||||||
|
|
||||||
## Comparing IP Addresses
|
## Comparing IP Addresses
|
||||||
|
|
||||||
To compare IP Addresses or check if an IP Address is within a given subnet, you can use the functions `ip_address('192.0.2.1')` and `ip_network('192.0.2.0/24')`. With these objects you can do [arithmetic operations](https://docs.python.org/3/library/ipaddress.html#operators).
|
To compare IP Addresses or check if an IP Address is within a given subnet, you can use the functions `ip_address('192.0.2.1')` and `ip_network('192.0.2.0/24')`. With these objects you can do [arithmetic operations](https://docs.python.org/3/library/ipaddress.html#operators).
|
||||||
|
|
|
@ -29,23 +29,6 @@ ak_message("Access denied")
|
||||||
return False
|
return False
|
||||||
```
|
```
|
||||||
|
|
||||||
### `ak_user_has_authenticator(user: User, device_type: Optional[str] = None) -> bool` (2021.9+)
|
|
||||||
|
|
||||||
Check if a user has any authenticator devices. Only fully validated devices are counted.
|
|
||||||
|
|
||||||
Optionally, you can filter a specific device type. The following options are valid:
|
|
||||||
|
|
||||||
- `totp`
|
|
||||||
- `duo`
|
|
||||||
- `static`
|
|
||||||
- `webauthn`
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
return ak_user_has_authenticator(request.user)
|
|
||||||
```
|
|
||||||
|
|
||||||
### `ak_call_policy(name: str, **kwargs) -> PolicyResult` (2021.12+)
|
### `ak_call_policy(name: str, **kwargs) -> PolicyResult` (2021.12+)
|
||||||
|
|
||||||
Call another policy with the name _name_. Current request is passed to policy. Key-word arguments
|
Call another policy with the name _name_. Current request is passed to policy. Key-word arguments
|
||||||
|
|
Reference in New Issue