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.core.api.used_by import UsedByMixin
|
||||
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.lib.utils.reflection import all_subclasses
|
||||
from authentik.policies.api.exec import PolicyTestSerializer
|
||||
|
@ -41,7 +41,9 @@ class PropertyMappingSerializer(ManagedSerializer, ModelSerializer, MetaNameSeri
|
|||
|
||||
def validate_expression(self, expression: str) -> str:
|
||||
"""Test Syntax"""
|
||||
evaluator = PropertyMappingEvaluator()
|
||||
evaluator = PropertyMappingEvaluator(
|
||||
self.instance,
|
||||
)
|
||||
evaluator.validate(expression)
|
||||
return expression
|
||||
|
||||
|
|
|
@ -2,28 +2,33 @@
|
|||
from traceback import format_tb
|
||||
from typing import Optional
|
||||
|
||||
from django.db.models import Model
|
||||
from django.http import HttpRequest
|
||||
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.lib.expression.evaluator import BaseEvaluator
|
||||
from authentik.policies.types import PolicyRequest
|
||||
|
||||
|
||||
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,
|
||||
user: Optional[User],
|
||||
request: Optional[HttpRequest],
|
||||
mapping: PropertyMapping,
|
||||
model: Model,
|
||||
user: Optional[User] = None,
|
||||
request: Optional[HttpRequest] = None,
|
||||
**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.obj = mapping
|
||||
req.obj = model
|
||||
if user:
|
||||
req.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:
|
||||
"""Evaluate `self.expression` using `**kwargs` as Context."""
|
||||
from authentik.core.expression import PropertyMappingEvaluator
|
||||
from authentik.core.expression.evaluator import PropertyMappingEvaluator
|
||||
|
||||
evaluator = PropertyMappingEvaluator()
|
||||
evaluator.set_context(user, request, self, **kwargs)
|
||||
evaluator = PropertyMappingEvaluator(self, user, request, **kwargs)
|
||||
try:
|
||||
return evaluator.evaluate(self.expression)
|
||||
except Exception as exc:
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
"""authentik expression policy evaluator"""
|
||||
import re
|
||||
from ipaddress import ip_address, ip_network
|
||||
from textwrap import indent
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
from django.core.exceptions import FieldError
|
||||
from django_otp import devices_for_user
|
||||
from rest_framework.serializers import ValidationError
|
||||
from sentry_sdk.hub import Hub
|
||||
from sentry_sdk.tracing import Span
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.events.models import Event
|
||||
from authentik.lib.utils.http import get_http_session
|
||||
from authentik.policies.types import PolicyRequest
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
@ -26,7 +30,8 @@ class BaseEvaluator:
|
|||
# Filename used for exec
|
||||
_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/_functions.md
|
||||
self._globals = {
|
||||
|
@ -35,11 +40,14 @@ class BaseEvaluator:
|
|||
"list_flatten": BaseEvaluator.expr_flatten,
|
||||
"ak_is_group_member": BaseEvaluator.expr_is_group_member,
|
||||
"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(),
|
||||
"ip_address": ip_address,
|
||||
"ip_network": ip_network,
|
||||
}
|
||||
self._context = {}
|
||||
self._filename = "BaseEvalautor"
|
||||
|
||||
@staticmethod
|
||||
def expr_flatten(value: list[Any] | Any) -> Optional[Any]:
|
||||
|
@ -60,6 +68,11 @@ class BaseEvaluator:
|
|||
"""Expression Filter to run re.sub"""
|
||||
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
|
||||
def expr_user_by(**filters) -> Optional[User]:
|
||||
"""Get user by filters"""
|
||||
|
@ -72,15 +85,37 @@ class BaseEvaluator:
|
|||
return None
|
||||
|
||||
@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()
|
||||
def expr_func_user_has_authenticator(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 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:
|
||||
"""Wrap expression in a function, call it, and save the result as `result`"""
|
||||
handler_signature = ",".join(params)
|
||||
full_expression = ""
|
||||
full_expression += "from ipaddress import ip_address, ip_network\n"
|
||||
full_expression += f"def handler({handler_signature}):\n"
|
||||
full_expression += indent(expression, " ")
|
||||
full_expression += f"\nresult = handler({handler_signature})"
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
"""authentik expression policy evaluator"""
|
||||
from ipaddress import ip_address, ip_network
|
||||
from ipaddress import ip_address
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from django.http import HttpRequest
|
||||
from django_otp import devices_for_user
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.planner import PLAN_CONTEXT_SSO
|
||||
from authentik.lib.expression.evaluator import BaseEvaluator
|
||||
from authentik.lib.utils.http import get_client_ip
|
||||
|
@ -27,16 +25,14 @@ class PolicyEvaluator(BaseEvaluator):
|
|||
|
||||
policy: Optional["ExpressionPolicy"] = None
|
||||
|
||||
def __init__(self, policy_name: str):
|
||||
super().__init__()
|
||||
def __init__(self, policy_name: Optional[str] = None):
|
||||
super().__init__(policy_name or "PolicyEvaluator")
|
||||
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_user_has_authenticator"] = self.expr_func_user_has_authenticator
|
||||
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):
|
||||
"""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)
|
||||
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):
|
||||
"""Update context based on policy request (if http request is given, update that too)"""
|
||||
# update website/docs/expressions/_objects.md
|
||||
|
|
|
@ -22,7 +22,7 @@ from rest_framework.serializers import BaseSerializer
|
|||
from structlog.stdlib import get_logger
|
||||
|
||||
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.flows.models import Stage
|
||||
from authentik.lib.models import SerializerModel
|
||||
|
@ -124,8 +124,7 @@ class Prompt(SerializerModel):
|
|||
return prompt_context[self.field_key]
|
||||
|
||||
if self.placeholder_expression:
|
||||
evaluator = PropertyMappingEvaluator()
|
||||
evaluator.set_context(user, request, self, prompt_context=prompt_context)
|
||||
evaluator = PropertyMappingEvaluator(self, user, request, prompt_context=prompt_context)
|
||||
try:
|
||||
return evaluator.evaluate(self.placeholder)
|
||||
except Exception as exc: # pylint:disable=broad-except
|
||||
|
|
|
@ -51,6 +51,45 @@ Example:
|
|||
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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### `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+)
|
||||
|
||||
Call another policy with the name _name_. Current request is passed to policy. Key-word arguments
|
||||
|
|
Reference in New Issue