diff --git a/passbook/policies/expression/evaluator.py b/passbook/policies/expression/evaluator.py new file mode 100644 index 000000000..4a75057fc --- /dev/null +++ b/passbook/policies/expression/evaluator.py @@ -0,0 +1,77 @@ +"""passbook expression policy evaluator""" +import re +from typing import TYPE_CHECKING, Any, Dict + +from django.core.exceptions import ValidationError +from jinja2.exceptions import TemplateSyntaxError, UndefinedError +from jinja2.nativetypes import NativeEnvironment +from structlog import get_logger + +from passbook.factors.view import AuthenticationView +from passbook.policies.struct import PolicyRequest, PolicyResult + +if TYPE_CHECKING: + from passbook.policies.expression.models import ExpressionPolicy + + +class Evaluator: + """Validate and evaulate jinja2-based expressions""" + + _env: NativeEnvironment + + def __init__(self): + self._env = NativeEnvironment() + self._env.filters["regex_match"] = Evaluator.jinja2_regex_match + self._env.filters["regex_replace"] = Evaluator.jinja2_regex_replace + + @staticmethod + def jinja2_regex_match(value: Any, regex: str) -> bool: + """Jinja2 Filter to run re.search""" + return re.search(regex, value) is None + + @staticmethod + def jinja2_regex_replace(value: Any, regex: str, repl: str) -> str: + """Jinja2 Filter to run re.sub""" + return re.sub(regex, repl, value) + + def _get_expression_context( + self, request: PolicyRequest, **kwargs + ) -> Dict[str, Any]: + """Return dictionary with additional global variables passed to expression""" + kwargs["pb_is_sso_flow"] = request.user.session.get( + AuthenticationView.SESSION_IS_SSO_LOGIN, False + ) + kwargs["pb_is_group_member"] = lambda user, group: group.user_set.filter( + pk=user.pk + ).exists() + kwargs["pb_logger"] = get_logger() + return kwargs + + def evaluate(self, expression_source: str, request: PolicyRequest) -> PolicyResult: + """Parse and evaluate expression. + If the Expression evaluates to a list with 2 items, the first is used as passing bool and + the second as messages. + If the Expression evaluates to a truthy-object, it is used as passing bool.""" + try: + expression = self._env.from_string(expression_source) + except TemplateSyntaxError as exc: + return PolicyResult(False, str(exc)) + try: + result = expression.render( + request=request, **self._get_expression_context(request) + ) + if isinstance(result, list) and len(result) == 2: + return PolicyResult(*result) + if result: + return PolicyResult(result) + return PolicyResult(False) + except UndefinedError as exc: + return PolicyResult(False, str(exc)) + + def validate(self, expression: str): + """Validate expression's syntax, raise ValidationError if Syntax is invalid""" + try: + self._env.from_string(expression) + return True + except TemplateSyntaxError as exc: + raise ValidationError("Expression Syntax Error") from exc diff --git a/passbook/policies/expression/models.py b/passbook/policies/expression/models.py index 479519f9f..8ec07211f 100644 --- a/passbook/policies/expression/models.py +++ b/passbook/policies/expression/models.py @@ -1,17 +1,11 @@ """passbook expression Policy Models""" -from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import gettext as _ -from jinja2.exceptions import TemplateSyntaxError, UndefinedError -from jinja2.nativetypes import NativeEnvironment -from structlog import get_logger from passbook.core.models import Policy +from passbook.policies.expression.evaluator import Evaluator from passbook.policies.struct import PolicyRequest, PolicyResult -LOGGER = get_logger() -NATIVE_ENVIRONMENT = NativeEnvironment() - class ExpressionPolicy(Policy): """Jinja2-based Expression policy that allows Admins to write their own logic""" @@ -22,25 +16,10 @@ class ExpressionPolicy(Policy): def passes(self, request: PolicyRequest) -> PolicyResult: """Evaluate and render expression. Returns PolicyResult(false) on error.""" - try: - expression = NATIVE_ENVIRONMENT.from_string(self.expression) - except TemplateSyntaxError as exc: - return PolicyResult(False, str(exc)) - try: - result = expression.render(request=request) - if isinstance(result, list) and len(result) == 2: - return PolicyResult(*result) - if result: - return PolicyResult(result) - return PolicyResult(False) - except UndefinedError as exc: - return PolicyResult(False, str(exc)) + return Evaluator().evaluate(self.expression, request) def save(self, *args, **kwargs): - try: - NATIVE_ENVIRONMENT.from_string(self.expression) - except TemplateSyntaxError as exc: - raise ValidationError("Expression Syntax Error") from exc + Evaluator().validate(self.expression) return super().save(*args, **kwargs) class Meta: diff --git a/passbook/policies/expression/templates/policy/expression/form.html b/passbook/policies/expression/templates/policy/expression/form.html index 6fc3637c6..18c23b321 100644 --- a/passbook/policies/expression/templates/policy/expression/form.html +++ b/passbook/policies/expression/templates/policy/expression/form.html @@ -9,12 +9,18 @@

Expression using Jinja. Following variables are available: -

+ +

Custom Filters:

+
{% endblock %}