diff --git a/changelogs/fragments/65198-ansibleundefined-is-not-unsafe.yml b/changelogs/fragments/65198-ansibleundefined-is-not-unsafe.yml new file mode 100644 index 00000000000..6ba2a1636a4 --- /dev/null +++ b/changelogs/fragments/65198-ansibleundefined-is-not-unsafe.yml @@ -0,0 +1,4 @@ +bugfixes: +- >- + ``AnsibleUnsafe``/``AnsibleContext``/``Templar`` - Do not treat ``AnsibleUndefined`` as being "unsafe" + (https://github.com/ansible/ansible/issues/65198) diff --git a/lib/ansible/template/__init__.py b/lib/ansible/template/__init__.py index 7189dbc6a2d..700a9bbd944 100644 --- a/lib/ansible/template/__init__.py +++ b/lib/ansible/template/__init__.py @@ -236,6 +236,10 @@ class AnsibleUndefined(StrictUndefined): rather than throwing an exception. ''' def __getattr__(self, name): + if name == '__UNSAFE__': + # AnsibleUndefined should never be assumed to be unsafe + # This prevents ``hasattr(val, '__UNSAFE__')`` from evaluating to ``True`` + raise AttributeError(name) # Return original Undefined object to preserve the first failure context return self @@ -273,7 +277,7 @@ class AnsibleContext(Context): for item in val: if self._is_unsafe(item): return True - elif hasattr(val, '__UNSAFE__'): + elif getattr(val, '__UNSAFE__', False) is True: return True return False diff --git a/test/units/template/test_templar.py b/test/units/template/test_templar.py index 6ba90e29271..dd6985ce3bd 100644 --- a/test/units/template/test_templar.py +++ b/test/units/template/test_templar.py @@ -27,7 +27,7 @@ from units.compat.mock import patch from ansible import constants as C from ansible.errors import AnsibleError, AnsibleUndefinedVariable from ansible.module_utils.six import string_types -from ansible.template import Templar, AnsibleContext, AnsibleEnvironment +from ansible.template import Templar, AnsibleContext, AnsibleEnvironment, AnsibleUndefined from ansible.utils.unsafe_proxy import AnsibleUnsafe, wrap_var from units.mock.loader import DictDataLoader @@ -56,31 +56,10 @@ class BaseTemplar(object): "/path/to/my_file.txt": "foo\n", }) self.templar = Templar(loader=self.fake_loader, variables=self.test_vars) + self._ansible_context = AnsibleContext(self.templar.environment, {}, {}, {}) def is_unsafe(self, obj): - if obj is None: - return False - - if hasattr(obj, '__UNSAFE__'): - return True - - if isinstance(obj, AnsibleUnsafe): - return True - - if isinstance(obj, dict): - for key in obj.keys(): - if self.is_unsafe(key) or self.is_unsafe(obj[key]): - return True - - if isinstance(obj, list): - for item in obj: - if self.is_unsafe(item): - return True - - if isinstance(obj, string_types) and hasattr(obj, '__UNSAFE__'): - return True - - return False + return self._ansible_context._is_unsafe(obj) # class used for testing arbitrary objects passed to template @@ -461,3 +440,7 @@ class TestAnsibleContext(BaseTemplar, unittest.TestCase): # self.assertNotIsInstance(res, AnsibleUnsafe) self.assertFalse(self.is_unsafe(res), 'return of AnsibleContext.resolve (%s) was not expected to be marked unsafe but was' % res) + + def test_is_unsafe(self): + context = self._context() + self.assertFalse(context._is_unsafe(AnsibleUndefined())) diff --git a/test/units/template/test_template_utilities.py b/test/units/template/test_template_utilities.py index e57a4aba94c..1044895f03d 100644 --- a/test/units/template/test_template_utilities.py +++ b/test/units/template/test_template_utilities.py @@ -22,7 +22,7 @@ __metaclass__ = type import jinja2 from units.compat import unittest -from ansible.template import _escape_backslashes, _count_newlines_from_end +from ansible.template import AnsibleUndefined, _escape_backslashes, _count_newlines_from_end # These are internal utility functions only needed for templating. They're # algorithmic so good candidates for unittesting by themselves @@ -106,3 +106,12 @@ class TestCountNewlines(unittest.TestCase): def test_mostly_newlines(self): self.assertEqual(_count_newlines_from_end(u'The quick brown fox jumped over the lazy dog' + u'\n' * 1000), 1000) + + +class TestAnsibleUndefined(unittest.TestCase): + def test_getattr(self): + val = AnsibleUndefined() + + self.assertIs(getattr(val, 'foo'), val) + + self.assertRaises(AttributeError, getattr, val, '__UNSAFE__')