diff --git a/authentik/core/api/property_mappings.py b/authentik/core/api/property_mappings.py index bf15742635..992a600a8b 100644 --- a/authentik/core/api/property_mappings.py +++ b/authentik/core/api/property_mappings.py @@ -30,8 +30,10 @@ from authentik.core.api.utils import ( PassiveSerializer, ) from authentik.core.expression.evaluator import PropertyMappingEvaluator +from authentik.core.expression.exceptions import PropertyMappingExpressionException from authentik.core.models import Group, PropertyMapping, User from authentik.events.utils import sanitize_item +from authentik.lib.utils.errors import exception_to_string from authentik.policies.api.exec import PolicyTestSerializer from authentik.rbac.decorators import permission_required @@ -162,12 +164,15 @@ class PropertyMappingViewSet( response_data = {"successful": True, "result": ""} try: - result = mapping.evaluate(**context) + result = mapping.evaluate(dry_run=True, **context) response_data["result"] = dumps( sanitize_item(result), indent=(4 if format_result else None) ) + except PropertyMappingExpressionException as exc: + response_data["result"] = exception_to_string(exc.exc) + response_data["successful"] = False except Exception as exc: - response_data["result"] = str(exc) + response_data["result"] = exception_to_string(exc) response_data["successful"] = False response = PropertyMappingTestResultSerializer(response_data) return Response(response.data) diff --git a/authentik/core/models.py b/authentik/core/models.py index 1bcbe64746..85ee1fd925 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -901,7 +901,7 @@ class PropertyMapping(SerializerModel, ManagedModel): except ControlFlowException as exc: raise exc except Exception as exc: - raise PropertyMappingExpressionException(self, exc) from exc + raise PropertyMappingExpressionException(exc, self) from exc def __str__(self): return f"Property Mapping {self.name}" diff --git a/authentik/lib/expression/evaluator.py b/authentik/lib/expression/evaluator.py index 13d33db31e..5dbf1b763d 100644 --- a/authentik/lib/expression/evaluator.py +++ b/authentik/lib/expression/evaluator.py @@ -2,7 +2,6 @@ import re import socket -from collections.abc import Iterable from ipaddress import ip_address, ip_network from textwrap import indent from types import CodeType @@ -28,6 +27,12 @@ from authentik.stages.authenticator import devices_for_user LOGGER = get_logger() +ARG_SANITIZE = re.compile(r"[:.-]") + + +def sanitize_arg(arg_name: str) -> str: + return re.sub(ARG_SANITIZE, "_", arg_name) + class BaseEvaluator: """Validate and evaluate python-based expressions""" @@ -177,9 +182,9 @@ class BaseEvaluator: proc = PolicyProcess(PolicyBinding(policy=policy), request=req, connection=None) return proc.profiling_wrapper() - def wrap_expression(self, expression: str, params: Iterable[str]) -> str: + def wrap_expression(self, expression: str) -> str: """Wrap expression in a function, call it, and save the result as `result`""" - handler_signature = ",".join(params) + handler_signature = ",".join(sanitize_arg(x) for x in self._context.keys()) full_expression = "" full_expression += f"def handler({handler_signature}):\n" full_expression += indent(expression, " ") @@ -188,8 +193,8 @@ class BaseEvaluator: def compile(self, expression: str) -> CodeType: """Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect.""" - param_keys = self._context.keys() - return compile(self.wrap_expression(expression, param_keys), self._filename, "exec") + expression = self.wrap_expression(expression) + return compile(expression, self._filename, "exec") def evaluate(self, expression_source: str) -> Any: """Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised. @@ -205,7 +210,7 @@ class BaseEvaluator: self.handle_error(exc, expression_source) raise exc try: - _locals = self._context + _locals = {sanitize_arg(x): y for x, y in self._context.items()} # Yes this is an exec, yes it is potentially bad. Since we limit what variables are # available here, and these policies can only be edited by admins, this is a risk # we're willing to take. diff --git a/web/src/admin/property-mappings/PropertyMappingTestForm.ts b/web/src/admin/property-mappings/PropertyMappingTestForm.ts index 0a0372132d..ae847ad4f3 100644 --- a/web/src/admin/property-mappings/PropertyMappingTestForm.ts +++ b/web/src/admin/property-mappings/PropertyMappingTestForm.ts @@ -61,7 +61,9 @@ export class PolicyTestForm extends Form { ` : html`
- ${this.result?.result} + +
${this.result?.result}
+
`} `; diff --git a/website/docs/sources/scim/index.md b/website/docs/sources/scim/index.md index ba0bc1e2c7..da61988ced 100644 --- a/website/docs/sources/scim/index.md +++ b/website/docs/sources/scim/index.md @@ -34,6 +34,43 @@ See the [overview](../property-mappings/index.md) for information on how propert ### Expression data -The following variables are available to SCIM source property mappings: +Each top level SCIM attribute is available as a variable in the expression. For example given an SCIM request with the payload of -- `data`: A Python dictionary containing data from the SCIM source. +```json +{ + "schemas": [ + "urn:scim:schemas:core:2.0", + "urn:scim:schemas:extension:enterprise:2.0" + ], + "userName": "foo.bar", + "name": { + "familyName": "bar", + "givenName": "foo", + "formatted": "foo.bar" + }, + "emails": [ + { + "value": "foo.bar@authentik.company", + "type": "work", + "primary": true + } + ], + "title": "", + "urn:scim:schemas:extension:enterprise:2.0": { + "department": "" + } +} +``` + +The following variables are available in the expression: + +- `schemas` as a list of strings +- `userName` as a string +- `name` as a dictionary +- `emails` as a dictionary +- `title` as a string +- `urn_scim_schemas_extension_enterprise_2_0` as a dictionary + + :::info + Top-level keys which include symbols not allowed in python syntax are converted to `_`. + :::