"""passbook expression policy evaluator""" import re from typing import TYPE_CHECKING, Any, Dict from django.core.exceptions import ValidationError from jinja2 import Undefined 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.core.models import User LOGGER = get_logger() class Evaluator: """Validate and evaulate jinja2-based expressions""" _env: NativeEnvironment def __init__(self): self._env = NativeEnvironment() # update passbook/policies/expression/templates/policy/expression/form.html # update docs/policies/expression/index.md self._env.filters["regex_match"] = Evaluator.jinja2_filter_regex_match self._env.filters["regex_replace"] = Evaluator.jinja2_filter_regex_replace @staticmethod def jinja2_filter_regex_match(value: Any, regex: str) -> bool: """Jinja2 Filter to run re.search""" return re.search(regex, value) is None @staticmethod def jinja2_filter_regex_replace(value: Any, regex: str, repl: str) -> str: """Jinja2 Filter to run re.sub""" return re.sub(regex, repl, value) @staticmethod def jinja2_func_is_group_member(user: "User", group_name: str) -> bool: """Check if `user` is member of group with name `group_name`""" return user.groups.filter(name=group_name).exists() def _get_expression_context( self, request: PolicyRequest, **kwargs ) -> Dict[str, Any]: """Return dictionary with additional global variables passed to expression""" # update passbook/policies/expression/templates/policy/expression/form.html # update docs/policies/expression/index.md kwargs["pb_is_sso_flow"] = request.http_request.session.get( AuthenticationView.SESSION_IS_SSO_LOGIN, False ) kwargs["pb_is_group_member"] = Evaluator.jinja2_func_is_group_member 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, Undefined): LOGGER.warning( "Expression policy returned undefined", src=expression_source, req=request, ) return PolicyRequest(False) 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