Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ability to disable recursively evaluation of jinja and yaql expressions. #250

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
Changelog
=========

-----
Added
~~~~~

* Ability to disable the recursively evaluation of jinja and yaql expressions, by setting the environment variable ``ENABLE_RECURSIVELY_EVALUATION`` to ``false``.
Contributed by @moradf90


Unreleased
----------

Expand Down Expand Up @@ -38,6 +46,7 @@ Fixed
* Update jsonschema requirements to allow 3.2 (security fix)
Contributed by @james-bellamy


1.5.0
-----

Expand Down
1 change: 1 addition & 0 deletions orquesta/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
# limitations under the License.

__version__ = "1.6.0"

15 changes: 13 additions & 2 deletions orquesta/expressions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import abc
import inspect
import logging
import os
import re
import six
import threading
Expand All @@ -37,6 +38,11 @@ class Evaluator(object):
_type = "unspecified"
_delimiter = None

@classmethod
def enable_recursively_evaluation(cls):
env_value = os.environ.get("ENABLE_RECURSIVELY_EVALUATION")
return not (env_value is not None and str(env_value).lower() == "false")

@classmethod
def get_type(cls):
return cls._type
Expand Down Expand Up @@ -131,7 +137,10 @@ def validate(statement):

def evaluate(statement, data=None):
if isinstance(statement, dict):
return {evaluate(k, data=data): evaluate(v, data=data) for k, v in six.iteritems(statement)}
return {
evaluate(k, data=data): evaluate(v, data=data)
for k, v in six.iteritems(statement)
}

elif isinstance(statement, list):
return [evaluate(item, data=data) for item in statement]
Expand Down Expand Up @@ -171,7 +180,9 @@ def extract_vars(statement):

def func_has_ctx_arg(func):
getargspec = (
inspect.getargspec if six.PY2 else inspect.getfullargspec # pylint: disable=no-member
inspect.getargspec
if six.PY2
else inspect.getfullargspec # pylint: disable=no-member
)

return "context" in getargspec(func).args
36 changes: 27 additions & 9 deletions orquesta/expressions/jinja.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ class JinjaEvaluator(expr_base.Evaluator):
# word boundary ctx().*
# word boundary ctx(*)*
# word boundary ctx(*).*
_regex_ctx_pattern = r'\bctx\([\'"]?{0}[\'"]?\)\.?{0}'.format(_regex_ctx_ref_pattern)
_regex_ctx_pattern = r'\bctx\([\'"]?{0}[\'"]?\)\.?{0}'.format(
_regex_ctx_ref_pattern
)
_regex_ctx_var_parser = re.compile(_regex_ctx_pattern)

_regex_var = r"[a-zA-Z0-9_-]+"
Expand Down Expand Up @@ -96,7 +98,11 @@ def contextualize(cls, data):
ctx["__current_item"] = ctx["__vars"].get("__current_item")

for name, func in six.iteritems(cls._custom_functions):
ctx[name] = functools.partial(func, ctx) if expr_base.func_has_ctx_arg(func) else func
ctx[name] = (
functools.partial(func, ctx)
if expr_base.func_has_ctx_arg(func)
else func
)

return ctx

Expand Down Expand Up @@ -136,7 +142,9 @@ def validate(cls, text):

try:
parser = jinja2.parser.Parser(
cls._jinja_env.overlay(), cls.strip_delimiter(expr), state="variable"
cls._jinja_env.overlay(),
cls.strip_delimiter(expr),
state="variable",
)

parser.parse_expression()
Expand All @@ -161,7 +169,8 @@ def _evaluate_and_expand(cls, text, data=None):

# Traverse and evaulate again in case additional inline epxressions are
# introduced after the jinja block is evaluated.
output = cls._evaluate_and_expand(output, data)
if cls.enable_recursively_evaluation():
output = cls._evaluate_and_expand(output, data)
else:
# The output will first be the original text and the expressions
# will be substituted by the evaluated value.
Expand All @@ -176,7 +185,10 @@ def _evaluate_and_expand(cls, text, data=None):
if inspect.isgenerator(result):
result = list(result)

if isinstance(result, six.string_types):
if (
isinstance(result, six.string_types)
and cls.enable_recursively_evaluation()
):
result = cls._evaluate_and_expand(result, data)

# For StrictUndefined values, UndefinedError only gets raised when the value is
Expand All @@ -185,7 +197,9 @@ def _evaluate_and_expand(cls, text, data=None):
# raise an exception with error description.
if not isinstance(result, jinja2.runtime.StrictUndefined):
if len(exprs) > 1 or block_exprs or len(output) > len(expr):
output = output.replace(expr, str_util.unicode(result, force=True))
output = output.replace(
expr, str_util.unicode(result, force=True)
)
else:
output = str_util.unicode(result)

Expand Down Expand Up @@ -216,17 +230,21 @@ def evaluate(cls, text, data=None):
output = cls._evaluate_and_expand(text, data=data)

if isinstance(output, six.string_types):
exprs = [cls.strip_delimiter(expr) for expr in cls._regex_parser.findall(output)]
exprs = [
cls.strip_delimiter(expr) for expr in cls._regex_parser.findall(output)
]

if exprs:
if exprs and cls.enable_recursively_evaluation():
raise JinjaEvaluationException(
"There are unresolved variables: %s" % ", ".join(exprs)
)

if isinstance(output, six.string_types) and raw_blocks:
# Put raw blocks back into the expression.
for i in range(0, len(raw_blocks)):
output = output.replace("{%s}" % str(i), raw_blocks[i]) # pylint: disable=E1101
output = output.replace(
"{%s}" % str(i), raw_blocks[i]
) # pylint: disable=E1101

# Evaluate the raw blocks.
ctx = cls.contextualize(data)
Expand Down
17 changes: 13 additions & 4 deletions orquesta/expressions/yql.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ class YAQLEvaluator(expr_base.Evaluator):
# word boundary ctx().*
# word boundary ctx(*)*
# word boundary ctx(*).*
_regex_ctx_pattern = r'\bctx\([\'"]?{0}[\'"]?\)\.?{0}'.format(_regex_ctx_ref_pattern)
_regex_ctx_pattern = r'\bctx\([\'"]?{0}[\'"]?\)\.?{0}'.format(
_regex_ctx_ref_pattern
)
_regex_ctx_var_parser = re.compile(_regex_ctx_pattern)

_regex_var = r"[a-zA-Z0-9_-]+"
Expand All @@ -84,7 +86,9 @@ def contextualize(cls, data):
# Some yaql expressions (e.g. distinct()) refer to hash value of variable.
# But some built-in Python type values (e.g. list and dict) don't have __hash__() method.
# The convert_input_data method parses specified variable and convert it to hashable one.
if isinstance(data, yaql_utils.SequenceType) or isinstance(data, yaql_utils.MappingType):
if isinstance(data, yaql_utils.SequenceType) or isinstance(
data, yaql_utils.MappingType
):
ctx["__vars"] = yaql_utils.convert_input_data(data)
else:
ctx["__vars"] = data or {}
Expand Down Expand Up @@ -144,7 +148,10 @@ def evaluate(cls, text, data=None):
if inspect.isgenerator(result):
result = list(result)

if isinstance(result, six.string_types):
if (
isinstance(result, six.string_types)
and cls.enable_recursively_evaluation()
):
result = cls.evaluate(result, data)

if len(exprs) > 1 or len(output) > len(expr):
Expand All @@ -158,7 +165,9 @@ def evaluate(cls, text, data=None):
raise YaqlEvaluationException(msg % (error, expr))
except (yaql_exc.YaqlException, ValueError, TypeError) as e:
msg = "Unable to evaluate expression '%s'. %s: %s"
raise YaqlEvaluationException(msg % (expr, e.__class__.__name__, str(e).strip("'")))
raise YaqlEvaluationException(
msg % (expr, e.__class__.__name__, str(e).strip("'"))
)
except Exception as e:
msg = "Unable to evaluate expression '%s'. %s: %s"
raise YaqlEvaluationException(msg % (expr, e.__class__.__name__, str(e)))
Expand Down
49 changes: 42 additions & 7 deletions orquesta/tests/unit/expressions/test_facade_jinja_evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
import unittest

from orquesta import exceptions as exc
Expand All @@ -30,7 +30,9 @@ def test_basic_eval_undefined(self):

data = {}

self.assertRaises(exc.ExpressionEvaluationException, expr_base.evaluate, expr, data)
self.assertRaises(
exc.ExpressionEvaluationException, expr_base.evaluate, expr, data
)

def test_dict_eval(self):
expr = "{{ ctx().nested.foo }}"
Expand Down Expand Up @@ -58,12 +60,28 @@ def test_eval_recursive(self):

self.assertEqual("fee-fi-fo-fum", expr_base.evaluate(expr, data))

# when the recursively evaluation is disable
os.environ.setdefault("ENABLE_RECURSIVELY_EVALUATION", "false")

self.assertEqual("{{ ctx().fi }}", expr_base.evaluate(expr, data))

def test_eval_recursive_undefined(self):
expr = "{{ ctx().fee }}"

data = {"fee": "{{ ctx().fi }}", "fi": "{{ ctx().fo }}", "fo": "{{ ctx().fum }}"}
data = {
"fee": "{{ ctx().fi }}",
"fi": "{{ ctx().fo }}",
"fo": "{{ ctx().fum }}",
}

self.assertRaises(
exc.ExpressionEvaluationException, expr_base.evaluate, expr, data
)

# when the recursively evaluation is disable
os.environ.setdefault("ENABLE_RECURSIVELY_EVALUATION", "false")

self.assertRaises(exc.ExpressionEvaluationException, expr_base.evaluate, expr, data)
self.assertEqual("{{ ctx().fi }}", expr_base.evaluate(expr, data))

def test_multi_eval_recursive(self):
expr = "{{ ctx().fee }} {{ ctx().im }}"
Expand All @@ -79,6 +97,13 @@ def test_multi_eval_recursive(self):

self.assertEqual("fee-fi-fo-fum! i'm hungry!", expr_base.evaluate(expr, data))

# when the recursively evaluation is disable
os.environ.setdefault("ENABLE_RECURSIVELY_EVALUATION", "false")

self.assertEqual(
"{{ ctx().fi }} {{ ctx().hungry }}", expr_base.evaluate(expr, data)
)

def test_eval_list(self):
expr = ["{{ ctx().foo }}", "{{ ctx().marco }}", "foo{{ ctx().foo }}"]

Expand Down Expand Up @@ -136,7 +161,14 @@ def test_eval_dict_of_list(self):
self.assertDictEqual(expected, expr_base.evaluate(expr, data))

def test_type_preservation(self):
data = {"k1": 101, "k2": 1.999, "k3": True, "k4": [1, 2], "k5": {"k": "v"}, "k6": None}
data = {
"k1": 101,
"k2": 1.999,
"k3": True,
"k4": [1, 2],
"k5": {"k": "v"},
"k6": None,
}

self.assertEqual(data["k1"], expr_base.evaluate("{{ ctx().k1 }}", data))

Expand Down Expand Up @@ -179,7 +211,9 @@ def test_block_eval_undefined(self):

data = {"x": ["a", "b", "c"]}

self.assertRaises(exc.ExpressionEvaluationException, expr_base.evaluate, expr, data)
self.assertRaises(
exc.ExpressionEvaluationException, expr_base.evaluate, expr, data
)

def test_block_eval_recursive(self):
expr = "{% for i in ctx().x %}{{ i }}{% endfor %}"
Expand All @@ -200,7 +234,8 @@ def test_block_eval_recursive(self):

def test_multi_block_eval(self):
expr = (
"{% for i in ctx().x %}{{ i }}{% endfor %}" "{% for i in ctx().y %}{{ i }}{% endfor %}"
"{% for i in ctx().x %}{{ i }}{% endfor %}"
"{% for i in ctx().y %}{{ i }}{% endfor %}"
)

data = {"x": ["a", "b", "c"], "y": ["d", "e", "f"]}
Expand Down
Loading