Ansible: template injection
Description
A template injection flaw was found in Ansible where a user's controller internal templating operations may remove the unsafe designation from template data. This issue could allow an attacker to use a specially crafted file to introduce templating injection when supplying templating data.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A template injection flaw in Ansible allows specially crafted data to bypass unsafe designation, enabling template injection.
What is the vulnerability?
CVE-2023-5764 is a template injection flaw in Ansible where internal templating operations can remove the unsafe designation from template data. This occurs when user-supplied data is processed through templating, and the unsafe marker is lost, allowing the data to be interpreted as a template instead of safe content [1][2]. The root cause lies in the handling of unsafe objects during serialization, where the unsafe designation was not properly preserved [3][4].
How is it exploited?
An attacker can exploit this vulnerability by supplying a specially crafted file that contains template data. When Ansible processes this data through its templating engine, the unsafe designation may be stripped, causing it to be executed as a Jinja2 template. This requires the attacker to have the ability to provide template data to the Ansible controller, which could be through a playbook, variable file, or other templating inputs. No authentication is required to trigger the vulnerability if the attacker can inject data that is later processed by the templating engine [1][2].
Impact
If successfully exploited, an attacker can achieve template injection, potentially leading to arbitrary code execution within the Ansible controller context. This could allow the attacker to execute arbitrary commands, exfiltrate sensitive data, or perform other malicious actions depending on the privileges of the Ansible process. The vulnerability is rated as High severity with a CVSS score of 8.8, indicating significant risk [1][2].
Mitigation
Red Hat and the Ansible project have released patches for Ansible Automation Platform and Ansible Engine. The fix involves modifying the serialization code to properly strip unsafe markers when encoding objects, ensuring the unsafe designation is not inadvertently removed [3][4]. Users are advised to update to the latest versions as specified in the Red Hat errata RHSA-2023:7773 [1]. There are no known workarounds for this vulnerability, and it is not currently listed on the Known Exploited Vulnerabilities (KEV) catalog.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
ansible-corePyPI | >= 2.16.0, < 2.16.1 | 2.16.1 |
ansible-corePyPI | >= 2.15.0, < 2.15.8 | 2.15.8 |
ansible-corePyPI | < 2.14.12 | 2.14.12 |
Affected products
35- Red Hat/Red Hat Ansible Automation Platform 2.4 for RHEL 9v5cpe:/a:redhat:ansible_automation_platform_developer:2.4::el9Range: 1:2.15.8-1.el9ap
- ghsa-coords34 versionspkg:pypi/ansible-corepkg:rpm/opensuse/ansible&distro=openSUSE%20Leap%2015.5pkg:rpm/opensuse/dracut-saltboot&distro=openSUSE%20Leap%2015.5pkg:rpm/opensuse/golang-github-prometheus-promu&distro=openSUSE%20Leap%2015.5pkg:rpm/opensuse/POS_Image-Graphical7&distro=openSUSE%20Leap%2015.5pkg:rpm/opensuse/POS_Image-JeOS7&distro=openSUSE%20Leap%2015.5pkg:rpm/opensuse/spacecmd&distro=openSUSE%20Leap%2015.5pkg:rpm/suse/ansible&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/ansible&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/ansible&distro=SUSE%20Manager%20Proxy%20Module%204.3pkg:rpm/suse/dracut-saltboot&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/dracut-saltboot&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/dracut-saltboot&distro=SUSE%20Manager%20Client%20Tools%20for%20SLE%20Micro%205pkg:rpm/suse/golang-github-prometheus-node_exporter&distro=SUSE%20Manager%20Client%20Tools%20Beta%20for%20SLE%20Micro%205pkg:rpm/suse/golang-github-prometheus-promu&distro=SUSE%20Linux%20Enterprise%20Module%20for%20Package%20Hub%2015%20SP5pkg:rpm/suse/grafana&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/grafana&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/mgr-daemon&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/POS_Image-Graphical7&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/POS_Image-Graphical7&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/POS_Image-JeOS7&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/POS_Image-JeOS7&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/spacecmd&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/spacecmd&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/spacewalk-client-tools&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/spacewalk-client-tools&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/spacewalk-koan&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/supportutils-plugin-susemanager-client&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/uyuni-common-libs&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/uyuni-proxy-systemd-services&distro=SUSE%20Manager%20Client%20Tools%2015pkg:rpm/suse/uyuni-proxy-systemd-services&distro=SUSE%20Manager%20Client%20Tools%20for%20SLE%20Micro%205pkg:rpm/suse/uyuni-proxy-systemd-services&distro=SUSE%20Manager%20Proxy%20Module%204.3pkg:rpm/suse/uyuni-tools&distro=SUSE%20Manager%20Client%20Tools%2015-BETApkg:rpm/suse/uyuni-tools&distro=SUSE%20Manager%20Client%20Tools%20Beta%20for%20SLE%20Micro%205
>= 2.16.0, < 2.16.1+ 33 more
- (no CPE)range: >= 2.16.0, < 2.16.1
- (no CPE)range: < 2.9.27-150000.1.17.2
- (no CPE)range: < 0.1.1710765237.46af599-150000.1.53.2
- (no CPE)range: < 0.14.0-150000.3.18.2
- (no CPE)range: < 0.1.1710765237.46af599-150000.1.21.2
- (no CPE)range: < 0.1.1710765237.46af599-150000.1.21.2
- (no CPE)range: < 4.3.27-150000.3.116.2
- (no CPE)range: < 2.9.27-150000.1.17.2
- (no CPE)range: < 2.9.27-159000.3.12.2
- (no CPE)range: < 2.9.27-150000.1.17.2
- (no CPE)range: < 0.1.1710765237.46af599-150000.1.53.2
- (no CPE)range: < 0.1.1710765237.46af599-159000.3.33.2
- (no CPE)range: < 0.1.1710765237.46af599-150000.1.53.2
- (no CPE)range: < 1.5.0-159000.6.2.1
- (no CPE)range: < 0.14.0-150000.3.18.2
- (no CPE)range: < 9.5.18-150000.1.63.2
- (no CPE)range: < 9.5.16-159000.4.30.2
- (no CPE)range: < 4.3.9-150000.1.47.2
- (no CPE)range: < 0.1.1710765237.46af599-150000.1.21.2
- (no CPE)range: < 0.1.1710765237.46af599-159000.3.24.2
- (no CPE)range: < 0.1.1710765237.46af599-150000.1.21.2
- (no CPE)range: < 0.1.1710765237.46af599-159000.3.24.2
- (no CPE)range: < 4.3.27-150000.3.116.2
- (no CPE)range: < 5.0.5-159000.6.48.2
- (no CPE)range: < 4.3.19-150000.3.89.2
- (no CPE)range: < 5.0.4-159000.6.54.2
- (no CPE)range: < 4.3.6-150000.3.33.2
- (no CPE)range: < 5.0.3-159000.6.21.2
- (no CPE)range: < 4.3.10-150000.1.39.2
- (no CPE)range: < 4.3.12-150000.1.21.2
- (no CPE)range: < 4.3.12-150000.1.21.2
- (no CPE)range: < 4.3.12-150000.1.21.2
- (no CPE)range: < 0.1.7-159000.3.8.1
- (no CPE)range: < 0.1.7-159000.3.8.1
Patches
37239d2d371bcEnsure that unsafe is more difficult to lose [stable-2.14] (#82295)
57 files changed · +519 −143
changelogs/fragments/cve-2023-5764.yml+6 −0 added@@ -0,0 +1,6 @@ +security_fixes: +- templating - Address issues where internal templating can cause unsafe + variables to lose their unsafe designation (CVE-2023-5764) +breaking_changes: +- assert - Nested templating may result in an inability for the conditional + to be evaluated. See the porting guide for more information.
lib/ansible/module_utils/common/json.py+2 −2 modified@@ -30,7 +30,7 @@ def _preprocess_unsafe_encode(value): Used in ``AnsibleJSONEncoder.iterencode`` """ if _is_unsafe(value): - value = {'__ansible_unsafe': to_text(value, errors='surrogate_or_strict', nonstring='strict')} + value = {'__ansible_unsafe': to_text(value._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')} elif is_sequence(value): value = [_preprocess_unsafe_encode(v) for v in value] elif isinstance(value, Mapping): @@ -63,7 +63,7 @@ def default(self, o): value = {'__ansible_vault': to_text(o._ciphertext, errors='surrogate_or_strict', nonstring='strict')} elif getattr(o, '__UNSAFE__', False): # unsafe object, this will never be triggered, see ``AnsibleJSONEncoder.iterencode`` - value = {'__ansible_unsafe': to_text(o, errors='surrogate_or_strict', nonstring='strict')} + value = {'__ansible_unsafe': to_text(o._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')} elif isinstance(o, Mapping): # hostvars and other objects value = dict(o)
lib/ansible/parsing/yaml/dumper.py+5 −1 modified@@ -24,7 +24,7 @@ from ansible.module_utils.six import text_type, binary_type from ansible.module_utils.common.yaml import SafeDumper from ansible.parsing.yaml.objects import AnsibleUnicode, AnsibleSequence, AnsibleMapping, AnsibleVaultEncryptedUnicode -from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText +from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText, _is_unsafe from ansible.template import AnsibleUndefined from ansible.vars.hostvars import HostVars, HostVarsVars from ansible.vars.manager import VarsWithSources @@ -47,10 +47,14 @@ def represent_vault_encrypted_unicode(self, data): def represent_unicode(self, data): + if _is_unsafe(data): + data = data._strip_unsafe() return yaml.representer.SafeRepresenter.represent_str(self, text_type(data)) def represent_binary(self, data): + if _is_unsafe(data): + data = data._strip_unsafe() return yaml.representer.SafeRepresenter.represent_binary(self, binary_type(data))
lib/ansible/playbook/conditional.py+5 −4 modified@@ -26,7 +26,7 @@ from jinja2.exceptions import UndefinedError from ansible import constants as C -from ansible.errors import AnsibleError, AnsibleUndefinedVariable +from ansible.errors import AnsibleError, AnsibleUndefinedVariable, AnsibleTemplateError from ansible.module_utils.six import text_type from ansible.module_utils._text import to_native, to_text from ansible.playbook.attribute import FieldAttribute @@ -138,9 +138,10 @@ def _check_conditional(self, conditional, templar, all_vars): if not isinstance(conditional, text_type) or conditional == "": return conditional - # update the lookups flag, as the string returned above may now be unsafe - # and we don't want future templating calls to do unsafe things - disable_lookups |= hasattr(conditional, '__UNSAFE__') + # If the result of the first-pass template render (to resolve inline templates) is marked unsafe, + # explicitly fail since the next templating operation would never evaluate + if hasattr(conditional, '__UNSAFE__'): + raise AnsibleTemplateError('Conditional is marked as unsafe, and cannot be evaluated.') # First, we do some low-level jinja2 parsing involving the AST format of the # statement to ensure we don't do anything unsafe (using the disable_lookup flag above)
lib/ansible/playbook/task.py+24 −0 modified@@ -290,6 +290,30 @@ def post_validate(self, templar): super(Task, self).post_validate(templar) + def _post_validate_args(self, attr, value, templar): + # smuggle an untemplated copy of the task args for actions that need more control over the templating of their + # input (eg, debug's var/msg, assert's "that" conditional expressions) + self.untemplated_args = value + + # now recursively template the args dict + args = templar.template(value) + + # FIXME: could we just nuke this entirely and/or wrap it up in ModuleArgsParser or something? + if '_variable_params' in args: + variable_params = args.pop('_variable_params') + if isinstance(variable_params, dict): + if C.INJECT_FACTS_AS_VARS: + display.warning("Using a variable for a task's 'args' is unsafe in some situations " + "(see https://docs.ansible.com/ansible/devel/reference_appendices/faq.html#argsplat-unsafe)") + variable_params.update(args) + args = variable_params + else: + # if we didn't get a dict, it means there's garbage remaining after k=v parsing, just give up + # see https://github.com/ansible/ansible/issues/79862 + raise AnsibleError(f"invalid or malformed argument: '{variable_params}'") + + return args + def _post_validate_loop(self, attr, value, templar): ''' Override post validation for the loop field, which is templated
lib/ansible/plugins/action/assert.py+22 −1 modified@@ -63,8 +63,29 @@ def run(self, tmp=None, task_vars=None): quiet = boolean(self._task.args.get('quiet', False), strict=False) + # directly access 'that' via untemplated args from the task so we can intelligently trust embedded + # templates and preserve the original inputs/locations for better messaging on assert failures and + # errors. + # FIXME: even in devel, things like `that: item` don't always work properly (truthy string value + # is not really an embedded expression) + # we could fix that by doing direct var lookups on the inputs + # FIXME: some form of this code should probably be shared between debug, assert, and + # Task.post_validate, since they + # have a lot of overlapping needs + try: + thats = self._task.untemplated_args['that'] + except KeyError: + # in the case of "we got our entire args dict from a template", we can just consult the + # post-templated dict (the damage has likely already been done for embedded templates anyway) + thats = self._task.args['that'] + + # FIXME: this is a case where we only want to resolve indirections, NOT recurse containers + # (and even then, the leaf-most expression being wrapped is at least suboptimal + # (since its expression will be "eaten"). + if isinstance(thats, str): + thats = self._templar.template(thats) + # make sure the 'that' items are a list - thats = self._task.args['that'] if not isinstance(thats, list): thats = [thats]
lib/ansible/plugins/callback/__init__.py+3 −1 modified@@ -38,7 +38,7 @@ from ansible.plugins import AnsiblePlugin from ansible.utils.color import stringc from ansible.utils.display import Display -from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText +from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText, _is_unsafe from ansible.vars.clean import strip_internal_keys, module_response_deepcopy import yaml @@ -113,6 +113,8 @@ def _munge_data_for_lossy_yaml(scalar): def _pretty_represent_str(self, data): """Uses block style for multi-line strings""" + if _is_unsafe(data): + data = data._strip_unsafe() data = text_type(data) if _should_use_block(data): style = '|'
lib/ansible/plugins/filter/core.py+5 −0 modified@@ -37,6 +37,7 @@ from ansible.utils.encrypt import passlib_or_crypt from ansible.utils.hashing import md5s, checksum_s from ansible.utils.unicode import unicode_wrap +from ansible.utils.unsafe_proxy import _is_unsafe from ansible.utils.vars import merge_hash display = Display() @@ -215,6 +216,8 @@ def from_yaml(data): # The ``text_type`` call here strips any custom # string wrapper class, so that CSafeLoader can # read the data + if _is_unsafe(data): + data = data._strip_unsafe() return yaml_load(text_type(to_text(data, errors='surrogate_or_strict'))) return data @@ -224,6 +227,8 @@ def from_yaml_all(data): # The ``text_type`` call here strips any custom # string wrapper class, so that CSafeLoader can # read the data + if _is_unsafe(data): + data = data._strip_unsafe() return yaml_load_all(text_type(to_text(data, errors='surrogate_or_strict'))) return data
lib/ansible/plugins/lookup/first_found.py+13 −2 modified@@ -136,7 +136,6 @@ elements: path """ import os -import re from collections.abc import Mapping, Sequence @@ -147,10 +146,22 @@ from ansible.plugins.lookup import LookupBase +def _splitter(value, chars): + chars = set(chars) + v = '' + for c in value: + if c in chars: + yield v + v = '' + continue + v += c + yield v + + def _split_on(terms, spliters=','): termlist = [] if isinstance(terms, string_types): - termlist = re.split(r'[%s]' % ''.join(map(re.escape, spliters)), terms) + termlist = list(_splitter(terms, spliters)) else: # added since options will already listify for t in terms:
lib/ansible/template/__init__.py+13 −2 modified@@ -31,7 +31,7 @@ from numbers import Number from traceback import format_exc -from jinja2.exceptions import TemplateSyntaxError, UndefinedError +from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError from jinja2.loaders import FileSystemLoader from jinja2.nativetypes import NativeEnvironment from jinja2.runtime import Context, StrictUndefined @@ -55,7 +55,7 @@ from ansible.utils.display import Display from ansible.utils.listify import listify_lookup_plugin_terms from ansible.utils.native_jinja import NativeJinjaText -from ansible.utils.unsafe_proxy import wrap_var +from ansible.utils.unsafe_proxy import wrap_var, AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText display = Display() @@ -332,10 +332,21 @@ class AnsibleContext(Context): flag is checked post-templating, and (when set) will result in the final templated result being wrapped in AnsibleUnsafe. ''' + _disallowed_callables = frozenset({ + AnsibleUnsafeText._strip_unsafe.__qualname__, + AnsibleUnsafeBytes._strip_unsafe.__qualname__, + NativeJinjaUnsafeText._strip_unsafe.__qualname__, + }) + def __init__(self, *args, **kwargs): super(AnsibleContext, self).__init__(*args, **kwargs) self.unsafe = False + def call(self, obj, *args, **kwargs): + if getattr(obj, '__qualname__', None) in self._disallowed_callables or obj in self._disallowed_callables: + raise SecurityError(f"{obj!r} is not safely callable") + return super().call(obj, *args, **kwargs) + def _is_unsafe(self, val): ''' Our helper function, which will also recursively check dict and
lib/ansible/utils/unsafe_proxy.py+259 −6 modified@@ -69,15 +69,264 @@ class AnsibleUnsafe(object): class AnsibleUnsafeBytes(binary_type, AnsibleUnsafe): - def decode(self, *args, **kwargs): - """Wrapper method to ensure type conversions maintain unsafe context""" - return AnsibleUnsafeText(super(AnsibleUnsafeBytes, self).decode(*args, **kwargs)) + def _strip_unsafe(self): + return super().__bytes__() + + def __str__(self, /): # pylint: disable=invalid-str-returned + return self.encode() + + def __bytes__(self, /): # pylint: disable=invalid-bytes-returned + return self + + def __repr__(self, /): # pylint: disable=invalid-repr-returned + return AnsibleUnsafeText(super().__repr__()) + + def __format__(self, format_spec, /): # pylint: disable=invalid-format-returned + return self.__class__(super().__format__(format_spec)) + + def __getitem__(self, key, /): + return self.__class__(super().__getitem__(key)) + + def __iter__(self, /): + cls = self.__class__ + return (cls(c) for c in super().__iter__()) + + def __reversed__(self, /): + return self[::-1] + + def __add__(self, value, /): + return self.__class__(super().__add__(value)) + + def __radd__(self, value, /): + return self.__class__(value.__add__(self)) + + def __mul__(self, value, /): + return self.__class__(super().__mul__(value)) + + __rmul__ = __mul__ + + def __mod__(self, value, /): + return self.__class__(super().__mod__(value)) + + def __rmod__(self, value, /): + return self.__class__(super().__rmod__(value)) + + def capitalize(self, /): + return self.__class__(super().capitalize()) + + def casefold(self, /): + return self.__class__(super().casefold()) + + def center(self, width, fillchar=b' ', /): + return self.__class__(super().center(width, fillchar)) + + def decode(self, /, encoding='utf-8', errors='strict'): + return AnsibleUnsafeText(super().decode(encoding=encoding, errors=errors)) + + def removeprefix(self, prefix, /): + return self.__class__(super().removeprefix(prefix)) + + def removesuffix(self, suffix, /): + return self.__class__(super().removesuffix(suffix)) + + def expandtabs(self, /, tabsize=8): + return self.__class__(super().expandtabs(tabsize)) + + def format(self, /, *args, **kwargs): + return self.__class__(super().format(*args, **kwargs)) + + def format_map(self, mapping, /): + return self.__class__(super().format_map(mapping)) + + def join(self, iterable_of_bytes, /): + return self.__class__(super().join(iterable_of_bytes)) + + def ljust(self, width, fillchar=b' ', /): + return self.__class__(super().ljust(width, fillchar)) + + def lower(self, /): + return self.__class__(super().lower()) + + def lstrip(self, bytes=None, /): + return self.__class__(super().lstrip(bytes)) + + def partition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().partition(sep)) + + def replace(self, old, new, count=-1, /): + return self.__class__(super().replace(old, new, count)) + + def rjust(self, width, fillchar=b' ', /): + return self.__class__(super().rjust(width, fillchar)) + + def rpartition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().rpartition(sep)) + + def rstrip(self, bytes=None, /): + return self.__class__(super().rstrip(bytes)) + + def split(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)] + + def rsplit(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)] + + def splitlines(self, /, keepends=False): + cls = self.__class__ + return [cls(e) for e in super().splitlines(keepends=keepends)] + + def strip(self, bytes=None, /): + return self.__class__(super().strip(bytes)) + + def swapcase(self, /): + return self.__class__(super().swapcase()) + + def title(self, /): + return self.__class__(super().title()) + + def translate(self, table, /, delete=b''): + return self.__class__(super().translate(table, delete=delete)) + + def upper(self, /): + return self.__class__(super().upper()) + + def zfill(self, width, /): + return self.__class__(super().zfill(width)) class AnsibleUnsafeText(text_type, AnsibleUnsafe): - def encode(self, *args, **kwargs): - """Wrapper method to ensure type conversions maintain unsafe context""" - return AnsibleUnsafeBytes(super(AnsibleUnsafeText, self).encode(*args, **kwargs)) + # def __getattribute__(self, name): + # print(f'attr: {name}') + # return object.__getattribute__(self, name) + + def _strip_unsafe(self, /): + return super().__str__() + + def __str__(self, /): # pylint: disable=invalid-str-returned + return self + + def __repr__(self, /): # pylint: disable=invalid-repr-returned + return self.__class__(super().__repr__()) + + def __format__(self, format_spec, /): # pylint: disable=invalid-format-returned + return self.__class__(super().__format__(format_spec)) + + def __getitem__(self, key, /): + return self.__class__(super().__getitem__(key)) + + def __iter__(self, /): + cls = self.__class__ + return (cls(c) for c in super().__iter__()) + + def __reversed__(self, /): + return self[::-1] + + def __add__(self, value, /): + return self.__class__(super().__add__(value)) + + def __radd__(self, value, /): + return self.__class__(value.__add__(self)) + + def __mul__(self, value, /): + return self.__class__(super().__mul__(value)) + + __rmul__ = __mul__ + + def __mod__(self, value, /): + return self.__class__(super().__mod__(value)) + + def __rmod__(self, value, /): + return self.__class__(super().__rmod__(value)) + + def capitalize(self, /): + return self.__class__(super().capitalize()) + + def casefold(self, /): + return self.__class__(super().casefold()) + + def center(self, width, fillchar=' ', /): + return self.__class__(super().center(width, fillchar)) + + def encode(self, /, encoding='utf-8', errors='strict'): + return AnsibleUnsafeBytes(super().encode(encoding=encoding, errors=errors)) + + def removeprefix(self, prefix, /): + return self.__class__(super().removeprefix(prefix)) + + def removesuffix(self, suffix, /): + return self.__class__(super().removesuffix(suffix)) + + def expandtabs(self, /, tabsize=8): + return self.__class__(super().expandtabs(tabsize)) + + def format(self, /, *args, **kwargs): + return self.__class__(super().format(*args, **kwargs)) + + def format_map(self, mapping, /): + return self.__class__(super().format_map(mapping)) + + def join(self, iterable, /): + return self.__class__(super().join(iterable)) + + def ljust(self, width, fillchar=' ', /): + return self.__class__(super().ljust(width, fillchar)) + + def lower(self, /): + return self.__class__(super().lower()) + + def lstrip(self, chars=None, /): + return self.__class__(super().lstrip(chars)) + + def partition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().partition(sep)) + + def replace(self, old, new, count=-1, /): + return self.__class__(super().replace(old, new, count)) + + def rjust(self, width, fillchar=' ', /): + return self.__class__(super().rjust(width, fillchar)) + + def rpartition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().rpartition(sep)) + + def rstrip(self, chars=None, /): + return self.__class__(super().rstrip(chars)) + + def split(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)] + + def rsplit(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)] + + def splitlines(self, /, keepends=False): + cls = self.__class__ + return [cls(e) for e in super().splitlines(keepends=keepends)] + + def strip(self, chars=None, /): + return self.__class__(super().strip(chars)) + + def swapcase(self, /): + return self.__class__(super().swapcase()) + + def title(self, /): + return self.__class__(super().title()) + + def translate(self, table, /): + return self.__class__(super().translate(table)) + + def upper(self, /): + return self.__class__(super().upper()) + + def zfill(self, width, /): + return self.__class__(super().zfill(width)) class NativeJinjaUnsafeText(NativeJinjaText, AnsibleUnsafeText): @@ -126,3 +375,7 @@ def to_unsafe_bytes(*args, **kwargs): def to_unsafe_text(*args, **kwargs): return wrap_var(to_text(*args, **kwargs)) + + +def _is_unsafe(obj): + return getattr(obj, '__UNSAFE__', False) is True
test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml+8 −8 modified@@ -13,22 +13,22 @@ in command.stdout_lines - >- "Installing 'namespace_1.collection_1:1.0.0' to - '{{ install_path }}/namespace_1/collection_1'" + '" ~ install_path ~ "/namespace_1/collection_1'" in command.stdout_lines - >- 'Created collection for namespace_1.collection_1:1.0.0 at - {{ install_path }}/namespace_1/collection_1' + ' ~ install_path ~ '/namespace_1/collection_1' in command.stdout_lines - >- 'namespace_1.collection_1:1.0.0 was installed successfully' in command.stdout_lines - >- "Installing 'namespace_2.collection_2:1.0.0' to - '{{ install_path }}/namespace_2/collection_2'" + '" ~ install_path ~ "/namespace_2/collection_2'" in command.stdout_lines - >- 'Created collection for namespace_2.collection_2:1.0.0 at - {{ install_path }}/namespace_2/collection_2' + ' ~ install_path ~ '/namespace_2/collection_2' in command.stdout_lines - >- 'namespace_2.collection_2:1.0.0 was installed successfully' @@ -58,22 +58,22 @@ in command.stdout_lines - >- "Installing 'namespace_1.collection_1:1.0.0' to - '{{ install_path }}/namespace_1/collection_1'" + '" ~ install_path ~ "/namespace_1/collection_1'" in command.stdout_lines - >- 'Created collection for namespace_1.collection_1:1.0.0 at - {{ install_path }}/namespace_1/collection_1' + ' ~ install_path ~ '/namespace_1/collection_1' in command.stdout_lines - >- 'namespace_1.collection_1:1.0.0 was installed successfully' in command.stdout_lines - >- "Installing 'namespace_2.collection_2:1.0.0' to - '{{ install_path }}/namespace_2/collection_2'" + '" ~ install_path ~ "/namespace_2/collection_2'" in command.stdout_lines - >- 'Created collection for namespace_2.collection_2:1.0.0 at - {{ install_path }}/namespace_2/collection_2' + ' ~ install_path ~ '/namespace_2/collection_2' in command.stdout_lines - >- 'namespace_2.collection_2:1.0.0 was installed successfully'
test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml+1 −1 modified@@ -2,7 +2,7 @@ - name: Assert that a embedded vault of a string with no newline works assert: that: - - '"{{ vault_encrypted_one_line_var }}" == "Setec Astronomy"' + - 'vault_encrypted_one_line_var == "Setec Astronomy"' - name: Assert that a multi line embedded vault works, including new line assert:
test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml+1 −1 modified@@ -2,7 +2,7 @@ - name: Assert that a vault encrypted file with embedded vault of a string with no newline works assert: that: - - '"{{ vault_file_encrypted_with_encrypted_one_line_var }}" == "Setec Astronomy"' + - 'vault_file_encrypted_with_encrypted_one_line_var == "Setec Astronomy"' - name: Assert that a vault encrypted file with multi line embedded vault works, including new line assert:
test/integration/targets/apt_repository/tasks/apt.yml+5 −5 modified@@ -50,7 +50,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_name}}"' + - 'result.repo == test_ppa_name' - name: 'examine apt cache mtime' stat: path='/var/cache/apt/pkgcache.bin' @@ -81,7 +81,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_name}}"' + - 'result.repo == test_ppa_name' - name: 'examine apt cache mtime' stat: path='/var/cache/apt/pkgcache.bin' @@ -112,7 +112,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_name}}"' + - 'result.repo == test_ppa_name' - name: 'examine apt cache mtime' stat: path='/var/cache/apt/pkgcache.bin' @@ -151,7 +151,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_spec}}"' + - 'result.repo == test_ppa_spec' - result_cache is not changed - name: 'examine apt cache mtime' @@ -191,7 +191,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_spec}}"' + - 'result.repo == test_ppa_spec' - name: 'examine source file' stat: path='/etc/apt/sources.list.d/{{test_ppa_filename}}.list'
test/integration/targets/assert/assert.out.nested_tmpl.stderr+4 −0 added@@ -0,0 +1,4 @@ ++ ansible-playbook -i localhost, -c local nested_tmpl.yml +++ set +x +[WARNING]: conditional statements should not include jinja2 templating +delimiters such as {{ }} or {% %}. Found: "{{ foo }}" == "bar"
test/integration/targets/assert/assert.out.nested_tmpl.stdout+12 −0 added@@ -0,0 +1,12 @@ + +PLAY [localhost] *************************************************************** + +TASK [assert] ****************************************************************** +ok: [localhost] => { + "changed": false, + "msg": "All assertions passed" +} + +PLAY RECAP ********************************************************************* +localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +
test/integration/targets/assert/assert.out.quiet.stderr+0 −0 renamedtest/integration/targets/assert/assert.out.quiet.stdout+0 −0 renamedtest/integration/targets/assert/nested_tmpl.yml+9 −0 added@@ -0,0 +1,9 @@ +- hosts: localhost + gather_facts: False + tasks: + - assert: + that: + - '"{{ foo }}" == "bar"' + - foo == "bar" + vars: + foo: bar
test/integration/targets/assert/quiet.yml+2 −2 modified@@ -5,12 +5,12 @@ item_A: yes tasks: - assert: - that: "{{ item }} is defined" + that: "item is defined" quiet: True with_items: - item_A - assert: - that: "{{ item }} is defined" + that: "item is defined" quiet: False with_items: - item_A
test/integration/targets/assert/runme.sh+2 −1 modified@@ -45,7 +45,7 @@ cleanup() { fi } -BASEFILE=assert_quiet.out +BASEFILE=assert.out ORIGFILE="${BASEFILE}" OUTFILE="${BASEFILE}.new" @@ -69,3 +69,4 @@ export ANSIBLE_NOCOLOR=1 export ANSIBLE_RETRY_FILES_ENABLED=0 run_test quiet +run_test nested_tmpl
test/integration/targets/command_shell/tasks/main.yml+1 −1 modified@@ -296,7 +296,7 @@ assert: that: - shell_result0 is changed - - shell_result0.cmd == '{{ remote_tmp_dir_test }}/test.sh' + - shell_result0.cmd == remote_tmp_dir_test ~ '/test.sh' - shell_result0.rc == 0 - shell_result0.stderr == '' - shell_result0.stdout == 'win'
test/integration/targets/copy/tasks/tests.yml+21 −21 modified@@ -1176,7 +1176,7 @@ assert: that: - "copy_result6.changed" - - "copy_result6.dest == '{{remote_dir_expanded}}/multiline.txt'" + - "copy_result6.dest == remote_dir_expanded ~ '/multiline.txt'" - "copy_result6.checksum == '9cd0697c6a9ff6689f0afb9136fa62e0b3fee903'" # test overwriting a file as an unprivileged user (pull request #8624) @@ -2079,26 +2079,26 @@ assert: that: - testcase5 is changed - - "stat_new_dir_with_chown.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown.stat.gr_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_file1.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown_file1.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown_file1.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_file1.stat.gr_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_subdir.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown_subdir.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown_subdir.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_subdir.stat.gr_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_subdir_file12.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown_subdir_file12.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_link_file12.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown_link_file12.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown_link_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_link_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'" + - "stat_new_dir_with_chown.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown.stat.gr_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_file1.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown_file1.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown_file1.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_file1.stat.gr_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_subdir.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown_subdir.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown_subdir.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_subdir.stat.gr_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_subdir_file12.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown_subdir_file12.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_link_file12.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown_link_file12.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown_link_file12.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_link_file12.stat.gr_name == ansible_copy_test_user_name" always: - name: execute - remove the user for test
test/integration/targets/debug/runme.sh+2 −0 modified@@ -18,3 +18,5 @@ done # ensure debug does not set top level vars when looking at ansible_facts ansible-playbook nosetfacts.yml "$@" + +ansible-playbook unsafe.yml "$@"
test/integration/targets/debug/unsafe.yml+13 −0 added@@ -0,0 +1,13 @@ +- hosts: localhost + gather_facts: false + vars: + unsafe_var: !unsafe undef()|mandatory + tasks: + - debug: + var: '{{ unsafe_var }}' + ignore_errors: true + register: result + + - assert: + that: + - result is successful
test/integration/targets/dnf/tasks/test_sos_removal.yml+1 −1 modified@@ -15,5 +15,5 @@ that: - sos_rm is successful - sos_rm is changed - - "'Removed: sos-{{ sos_version }}-{{ sos_release }}' in sos_rm.results[0]" + - "'Removed: sos-' ~ sos_version ~ '-' ~ sos_release in sos_rm.results[0]" - sos_rm.results|length == 1
test/integration/targets/expect/tasks/main.yml+1 −1 modified@@ -117,7 +117,7 @@ - name: assert chdir works assert: that: - - "'{{chdir_result.stdout | trim}}' == '{{remote_tmp_dir_real_path.stdout | trim}}'" + - "chdir_result.stdout | trim == remote_tmp_dir_real_path.stdout | trim" - name: test timeout option expect:
test/integration/targets/file/tasks/main.yml+1 −1 modified@@ -927,7 +927,7 @@ that: - "file_error3 is failed" - "file_error3.msg == 'src does not exist'" - - "file_error3.dest == '{{ remote_tmp_dir_test }}/hard.txt' | expanduser" + - "file_error3.dest == remote_tmp_dir_test | expanduser ~ '/hard.txt'" - "file_error3.src == 'non-existing-file-that-does-not-exist.txt'" - block:
test/integration/targets/file/tasks/state_link.yml+1 −1 modified@@ -199,7 +199,7 @@ - "missing_dst_no_follow_enable_force_use_mode2 is changed" - "missing_dst_no_follow_enable_force_use_mode3 is not changed" - "soft3_result['stat'].islnk" - - "soft3_result['stat'].lnk_target == '{{ user.home }}/nonexistent'" + - "soft3_result['stat'].lnk_target == user.home ~ '/nonexistent'" # # Test creating a link to a directory https://github.com/ansible/ansible/issues/1369
test/integration/targets/find/tasks/main.yml+6 −6 modified@@ -267,7 +267,7 @@ - name: assert we skipped the ogg file assert: that: - - '"{{ remote_tmp_dir_test }}/e/f/g/h/8.ogg" not in find_test3_list' + - 'remote_tmp_dir_test ~ "/e/f/g/h/8.ogg" not in find_test3_list' - name: patterns with regex find: @@ -317,7 +317,7 @@ assert: that: - result.matched == 1 - - '"{{ remote_tmp_dir_test }}/astest/old.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/old.txt" in astest_list' - name: find files newer than 1 week find: @@ -332,7 +332,7 @@ assert: that: - result.matched == 1 - - '"{{ remote_tmp_dir_test }}/astest/new.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/new.txt" in astest_list' - name: add some content to the new file shell: "echo hello world > {{ remote_tmp_dir_test }}/astest/new.txt" @@ -352,7 +352,7 @@ assert: that: - result.matched == 1 - - '"{{ remote_tmp_dir_test }}/astest/new.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/new.txt" in astest_list' - '"checksum" in result.files[0]' - name: find ANY item with LESS than 5 bytes, also get checksums @@ -371,6 +371,6 @@ assert: that: - result.matched == 2 - - '"{{ remote_tmp_dir_test }}/astest/old.txt" in astest_list' - - '"{{ remote_tmp_dir_test }}/astest/.hidden.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/old.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/.hidden.txt" in astest_list' - '"checksum" in result.files[0]'
test/integration/targets/gathering_facts/test_gathering_facts.yml+2 −2 modified@@ -433,7 +433,7 @@ - name: Test reading facts from default fact_path assert: that: - - '"{{ ansible_local.testfact.fact_dir }}" == "default"' + - 'ansible_local.testfact.fact_dir == "default"' - hosts: facthost9 tags: [ 'fact_local'] @@ -444,7 +444,7 @@ - name: Test reading facts from custom fact_path assert: that: - - '"{{ ansible_local.testfact.fact_dir }}" == "custom"' + - 'ansible_local.testfact.fact_dir == "custom"' - hosts: facthost20 tags: [ 'fact_facter_ohai' ]
test/integration/targets/git/tasks/depth.yml+1 −1 modified@@ -169,7 +169,7 @@ - name: DEPTH | check update arrived assert: that: - - "{{ a_file.content | b64decode | trim }} == 3" + - a_file.content | b64decode | trim == "3" - git_fetch is changed - name: DEPTH | clear checkout_dir
test/integration/targets/git/tasks/localmods.yml+2 −2 modified@@ -47,7 +47,7 @@ - name: LOCALMODS | check update arrived assert: that: - - "{{ a_file.content | b64decode | trim }} == 2" + - a_file.content | b64decode | trim == "2" - git_fetch_force is changed - name: LOCALMODS | clear checkout_dir @@ -105,7 +105,7 @@ - name: LOCALMODS | check update arrived assert: that: - - "{{ a_file.content | b64decode | trim }} == 2" + - a_file.content | b64decode | trim == "2" - git_fetch_force is changed - name: LOCALMODS | clear checkout_dir
test/integration/targets/git/tasks/submodules.yml+7 −7 modified@@ -32,7 +32,7 @@ - name: SUBMODULES | Ensure submodu1 is at the appropriate commit assert: - that: '{{ submodule1.stdout_lines | length }} == 2' + that: 'submodule1.stdout_lines | length == 2' - name: SUBMODULES | clear checkout_dir file: @@ -53,7 +53,7 @@ - name: SUBMODULES | Ensure submodule1 is at the appropriate commit assert: - that: '{{ submodule1.stdout_lines | length }} == 4' + that: 'submodule1.stdout_lines | length == 4' - name: SUBMODULES | Copy the checkout so we can run several different tests on it command: 'cp -pr {{ checkout_dir }} {{ checkout_dir }}.bak' @@ -84,8 +84,8 @@ - name: SUBMODULES | Ensure both submodules are at the appropriate commit assert: that: - - '{{ submodule1.stdout_lines|length }} == 4' - - '{{ submodule2.stdout_lines|length }} == 2' + - 'submodule1.stdout_lines|length == 4' + - 'submodule2.stdout_lines|length == 2' - name: SUBMODULES | Remove checkout dir @@ -112,7 +112,7 @@ - name: SUBMODULES | Ensure submodule1 is at the appropriate commit assert: - that: '{{ submodule1.stdout_lines | length }} == 5' + that: 'submodule1.stdout_lines | length == 5' - name: SUBMODULES | Test that update with recursive found new submodules @@ -121,7 +121,7 @@ - name: SUBMODULES | Enusre submodule2 is at the appropriate commit assert: - that: '{{ submodule2.stdout_lines | length }} == 4' + that: 'submodule2.stdout_lines | length == 4' - name: SUBMODULES | clear checkout_dir file: @@ -147,4 +147,4 @@ - name: SUBMODULES | Ensure submodule1 is at the appropriate commit assert: - that: '{{ submodule1.stdout_lines | length }} == 4' + that: 'submodule1.stdout_lines | length == 4'
test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml+2 −2 modified@@ -22,7 +22,7 @@ - name: Check that multiple duplicate lines collapse into a single commands assert: that: - - "{{ result.commands|length }} == 1" + - "result.commands|length == 1" - name: Check that set is correctly prepended assert: @@ -58,6 +58,6 @@ - assert: that: - - "{{ result.filtered|length }} == 2" + - "result.filtered|length == 2" - debug: msg="END cli/config_check.yaml on connection={{ ansible_connection }}"
test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml+4 −4 modified@@ -16,17 +16,17 @@ - name: Assert that the before dicts were correctly generated assert: that: - - "{{ populate | symmetric_difference(result['before']) |length == 0 }}" + - "populate | symmetric_difference(result['before']) |length == 0" - name: Assert that the correct set of commands were generated assert: that: - - "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + - "deleted['commands'] | symmetric_difference(result['commands']) |length == 0" - name: Assert that the after dicts were correctly generated assert: that: - - "{{ deleted['after'] | symmetric_difference(result['after']) |length == 0 }}" + - "deleted['after'] | symmetric_difference(result['after']) |length == 0" - name: Delete attributes of given interfaces (IDEMPOTENT) vyos.vyos.vyos_lldp_interfaces: *deleted @@ -41,6 +41,6 @@ - name: Assert that the before dicts were correctly generated assert: that: - - "{{ deleted['after'] | symmetric_difference(result['before']) |length == 0 }}" + - "deleted['after'] | symmetric_difference(result['before']) |length == 0" always: - include_tasks: _remove_config.yaml
test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml+4 −4 modified@@ -28,17 +28,17 @@ - name: Assert that before dicts were correctly generated assert: - that: "{{ merged['before'] | symmetric_difference(result['before']) |length == 0 }}" + that: "merged['before'] | symmetric_difference(result['before']) |length == 0" - name: Assert that correct set of commands were generated assert: that: - - "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + - "merged['commands'] | symmetric_difference(result['commands']) |length == 0" - name: Assert that after dicts was correctly generated assert: that: - - "{{ merged['after'] | symmetric_difference(result['after']) |length == 0 }}" + - "merged['after'] | symmetric_difference(result['after']) |length == 0" - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT) vyos.vyos.vyos_lldp_interfaces: *merged @@ -52,7 +52,7 @@ - name: Assert that before dicts were correctly generated assert: that: - - "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}" + - "merged['after'] | symmetric_difference(result['before']) |length == 0" always: - include_tasks: _remove_config.yaml
test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml+4 −4 modified@@ -19,17 +19,17 @@ - name: Assert that before dicts were correctly generated assert: that: - - "{{ populate_intf | symmetric_difference(result['before']) |length == 0 }}" + - "populate_intf | symmetric_difference(result['before']) |length == 0" - name: Assert that correct commands were generated assert: that: - - "{{ overridden['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + - "overridden['commands'] | symmetric_difference(result['commands']) |length == 0" - name: Assert that after dicts were correctly generated assert: that: - - "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}" + - "overridden['after'] | symmetric_difference(result['after']) |length == 0" - name: Overrides all device configuration with provided configurations (IDEMPOTENT) vyos.vyos.vyos_lldp_interfaces: *overridden @@ -43,7 +43,7 @@ - name: Assert that before dicts were correctly generated assert: that: - - "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}" + - "overridden['after'] | symmetric_difference(result['before']) |length == 0" always: - include_tasks: _remove_config.yaml
test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml+4 −4 modified@@ -33,17 +33,17 @@ - name: Assert that correct set of commands were generated assert: that: - - "{{ replaced['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + - "replaced['commands'] | symmetric_difference(result['commands']) |length == 0" - name: Assert that before dicts are correctly generated assert: that: - - "{{ populate | symmetric_difference(result['before']) |length == 0 }}" + - "populate | symmetric_difference(result['before']) |length == 0" - name: Assert that after dict is correctly generated assert: that: - - "{{ replaced['after'] | symmetric_difference(result['after']) |length == 0 }}" + - "replaced['after'] | symmetric_difference(result['after']) |length == 0" - name: Replace device configurations of listed LLDP interfaces with provided configurarions (IDEMPOTENT) vyos.vyos.vyos_lldp_interfaces: *replaced @@ -57,7 +57,7 @@ - name: Assert that before dict is correctly generated assert: that: - - "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}" + - "replaced['after'] | symmetric_difference(result['before']) |length == 0" always: - include_tasks: _remove_config.yaml
test/integration/targets/include_vars/tasks/main.yml+14 −14 modified@@ -15,7 +15,7 @@ that: - "testing == 789" - "base_dir == 'environments/development'" - - "{{ included_one_file.ansible_included_var_files | length }} == 1" + - "included_one_file.ansible_included_var_files | length == 1" - "'vars/environments/development/all.yml' in included_one_file.ansible_included_var_files[0]" - name: include the vars/environments/development/all.yml and save results in all @@ -51,7 +51,7 @@ assert: that: - webapp_version is defined - - "'file_without_extension' in '{{ include_without_file_extension.ansible_included_var_files | join(' ') }}'" + - "'file_without_extension' in include_without_file_extension.ansible_included_var_files | join(' ')" - name: include every directory in vars include_vars: @@ -67,7 +67,7 @@ - "testing == 456" - "base_dir == 'services'" - "webapp_containers == 10" - - "{{ include_every_dir.ansible_included_var_files | length }} == 7" + - "include_every_dir.ansible_included_var_files | length == 7" - "'vars/all/all.yml' in include_every_dir.ansible_included_var_files[0]" - "'vars/environments/development/all.yml' in include_every_dir.ansible_included_var_files[1]" - "'vars/environments/development/services/webapp.yml' in include_every_dir.ansible_included_var_files[2]" @@ -88,9 +88,9 @@ that: - "testing == 789" - "base_dir == 'environments/development'" - - "{{ include_without_webapp.ansible_included_var_files | length }} == 4" - - "'webapp.yml' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'" - - "'file_without_extension' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'" + - "include_without_webapp.ansible_included_var_files | length == 4" + - "'webapp.yml' not in include_without_webapp.ansible_included_var_files | join(' ')" + - "'file_without_extension' not in include_without_webapp.ansible_included_var_files | join(' ')" - name: include only files matching webapp.yml include_vars: @@ -104,9 +104,9 @@ - "testing == 101112" - "base_dir == 'development/services'" - "webapp_containers == 20" - - "{{ include_match_webapp.ansible_included_var_files | length }} == 1" + - "include_match_webapp.ansible_included_var_files | length == 1" - "'vars/environments/development/services/webapp.yml' in include_match_webapp.ansible_included_var_files[0]" - - "'all.yml' not in '{{ include_match_webapp.ansible_included_var_files | join(' ') }}'" + - "'all.yml' not in include_match_webapp.ansible_included_var_files | join(' ')" - name: include only files matching webapp.yml and store results in webapp include_vars: @@ -173,10 +173,10 @@ - name: Verify the hash variable assert: that: - - "{{ config | length }} == 3" + - "config | length == 3" - "config.key0 == 0" - "config.key1 == 0" - - "{{ config.key2 | length }} == 1" + - "config.key2 | length == 1" - "config.key2.a == 21" - name: Include the second file to merge the hash variable @@ -187,10 +187,10 @@ - name: Verify that the hash is merged assert: that: - - "{{ config | length }} == 4" + - "config | length == 4" - "config.key0 == 0" - "config.key1 == 1" - - "{{ config.key2 | length }} == 2" + - "config.key2 | length == 2" - "config.key2.a == 21" - "config.key2.b == 22" - "config.key3 == 3" @@ -202,9 +202,9 @@ - name: Verify that the properties from the first file is cleared assert: that: - - "{{ config | length }} == 3" + - "config | length == 3" - "config.key1 == 1" - - "{{ config.key2 | length }} == 1" + - "config.key2 | length == 1" - "config.key2.b == 22" - "config.key3 == 3"
test/integration/targets/lookup_ini/test_lookup_properties.yml+1 −1 modified@@ -10,7 +10,7 @@ field_with_space: "{{lookup('ini', 'field.with.space type=properties file=lookup.properties')}}" - assert: - that: "{{item}} is defined" + that: "item is defined" with_items: [ 'test1', 'test2', 'test_dot', 'field_with_space' ] - name: "read ini value"
test/integration/targets/lookup_subelements/tasks/main.yml+3 −3 modified@@ -133,7 +133,7 @@ - assert: that: - - "'{{ item.0.name }}' != 'carol'" + - "item.0.name != 'carol'" with_subelements: - "{{ users }}" - mysql.privs @@ -220,5 +220,5 @@ - assert: that: - - "'{{ user_alice }}' == 'localhost'" - - "'{{ user_bob }}' == 'db1'" + - "user_alice == 'localhost'" + - "user_bob == 'db1'"
test/integration/targets/loop_control/inner.yml+2 −2 modified@@ -3,7 +3,7 @@ that: - ansible_loop.index == ansible_loop.index0 + 1 - ansible_loop.revindex == ansible_loop.revindex0 + 1 - - ansible_loop.first == {{ ansible_loop.index == 1 }} - - ansible_loop.last == {{ ansible_loop.index == ansible_loop.length }} + - ansible_loop.first == (ansible_loop.index == 1) + - ansible_loop.last == (ansible_loop.index == ansible_loop.length) - ansible_loop.length == 3 - ansible_loop.allitems|join(',') == 'first,second,third'
test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml+1 −1 modified@@ -13,4 +13,4 @@ - assert: that: - '"location" in result' - - 'result["location"] == "{{ expected_location}}"' + - 'result["location"] == expected_location'
test/integration/targets/module_precedence/modules_test_multiple_roles.yml+1 −1 modified@@ -14,4 +14,4 @@ - assert: that: - '"location" in result' - - 'result["location"] == "{{ expected_location}}"' + - 'result["location"] == expected_location'
test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml+1 −1 modified@@ -7,4 +7,4 @@ assert: that: - '"location" in result' - - 'result["location"] == "{{ expected_location }}"' + - 'result["location"] == expected_location'
test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml+1 −1 modified@@ -7,4 +7,4 @@ assert: that: - '"location" in result' - - 'result["location"] == "{{ expected_location }}"' + - 'result["location"] == expected_location'
test/integration/targets/script/tasks/main.yml+2 −2 modified@@ -198,7 +198,7 @@ assert: that: - _check_mode_test2 is skipped - - '_check_mode_test2.msg == "{{ remote_tmp_dir_test | expanduser }}/afile2.txt exists, matching creates option"' + - '_check_mode_test2.msg == remote_tmp_dir_test | expanduser ~ "/afile2.txt exists, matching creates option"' - name: Remove afile2.txt file: @@ -220,7 +220,7 @@ assert: that: - _check_mode_test3 is skipped - - '_check_mode_test3.msg == "{{ remote_tmp_dir_test | expanduser }}/afile2.txt does not exist, matching removes option"' + - '_check_mode_test3.msg == remote_tmp_dir_test | expanduser ~ "/afile2.txt does not exist, matching removes option"' # executable
test/integration/targets/slurp/tasks/main.yml+1 −1 modified@@ -33,7 +33,7 @@ - 'slurp_existing.encoding == "base64"' - 'slurp_existing is not changed' - 'slurp_existing is not failed' - - '"{{ slurp_existing.content | b64decode }}" == "We are at the café"' + - 'slurp_existing.content | b64decode == "We are at the café"' - name: Create a binary file to test with copy:
test/integration/targets/template/tasks/main.yml+1 −1 modified@@ -357,7 +357,7 @@ - assert: that: - "\"foo t'e~m\\plated\" in unusual_results.stdout_lines" - - "{{unusual_results.stdout_lines| length}} == 1" + - "unusual_results.stdout_lines| length == 1" - name: check that the unusual filename can be checked for changes template:
test/integration/targets/unarchive/tasks/test_missing_binaries.yml+1 −1 modified@@ -66,7 +66,7 @@ - zip_success.changed # Verify that file list is generated - "'files' in zip_success" - - "{{zip_success['files']| length}} == 3" + - "zip_success['files']| length == 3" - "'foo-unarchive.txt' in zip_success['files']" - "'foo-unarchive-777.txt' in zip_success['files']" - "'FOO-UNAR.TXT' in zip_success['files']"
test/integration/targets/unarchive/tasks/test_mode.yml+4 −4 modified@@ -24,7 +24,7 @@ - "unarchive06_stat.stat.mode == '0600'" # Verify that file list is generated - "'files' in unarchive06" - - "{{unarchive06['files']| length}} == 1" + - "unarchive06['files']| length == 1" - "'foo-unarchive.txt' in unarchive06['files']" - name: remove our tar.gz unarchive destination @@ -74,7 +74,7 @@ - "unarchive07.changed == false" # Verify that file list is generated - "'files' in unarchive07" - - "{{unarchive07['files']| length}} == 1" + - "unarchive07['files']| length == 1" - "'foo-unarchive.txt' in unarchive07['files']" - name: remove our tar.gz unarchive destination @@ -108,7 +108,7 @@ - "unarchive08_stat.stat.mode == '0601'" # Verify that file list is generated - "'files' in unarchive08" - - "{{unarchive08['files']| length}} == 3" + - "unarchive08['files']| length == 3" - "'foo-unarchive.txt' in unarchive08['files']" - "'foo-unarchive-777.txt' in unarchive08['files']" - "'FOO-UNAR.TXT' in unarchive08['files']" @@ -140,7 +140,7 @@ - "unarchive08_stat.stat.mode == '0601'" # Verify that file list is generated - "'files' in unarchive08" - - "{{unarchive08['files']| length}} == 3" + - "unarchive08['files']| length == 3" - "'foo-unarchive.txt' in unarchive08['files']" - "'foo-unarchive-777.txt' in unarchive08['files']" - "'FOO-UNAR.TXT' in unarchive08['files']"
test/integration/targets/unarchive/tasks/test_unprivileged_user.yml+1 −1 modified@@ -40,7 +40,7 @@ - unarchive10 is changed # Verify that file list is generated - "'files' in unarchive10" - - "{{unarchive10['files']| length}} == 1" + - "unarchive10['files']| length == 1" - "'foo-unarchive.txt' in unarchive10['files']" - archive_path.stat.exists
test/integration/targets/unarchive/tasks/test_zip.yml+1 −1 modified@@ -17,7 +17,7 @@ - "unarchive03.changed == true" # Verify that file list is generated - "'files' in unarchive03" - - "{{unarchive03['files']| length}} == 3" + - "unarchive03['files']| length == 3" - "'foo-unarchive.txt' in unarchive03['files']" - "'foo-unarchive-777.txt' in unarchive03['files']" - "'FOO-UNAR.TXT' in unarchive03['files']"
test/integration/targets/wait_for/tasks/main.yml+4 −4 modified@@ -40,7 +40,7 @@ assert: that: - waitfor is successful - - waitfor.path == "{{ remote_tmp_dir | expanduser }}/wait_for_file" + - waitfor.path == remote_tmp_dir | expanduser ~ "/wait_for_file" - waitfor.elapsed >= 2 - waitfor.elapsed <= 15 @@ -58,7 +58,7 @@ assert: that: - waitfor is successful - - waitfor.path == "{{ remote_tmp_dir | expanduser }}/wait_for_file" + - waitfor.path == remote_tmp_dir | expanduser ~ "/wait_for_file" - waitfor.elapsed >= 2 - waitfor.elapsed <= 15 @@ -156,7 +156,7 @@ that: - waitfor is successful - waitfor is not changed - - "waitfor.port == {{ http_port }}" + - "waitfor.port == http_port" - name: install psutil using pip (non-Linux only) pip: @@ -184,7 +184,7 @@ that: - waitfor is successful - waitfor is not changed - - "waitfor.port == {{ http_port }}" + - "waitfor.port == http_port" - name: test wait_for with delay wait_for:
test/units/parsing/yaml/test_dumper.py+2 −5 modified@@ -29,7 +29,6 @@ from ansible.parsing.yaml.loader import AnsibleLoader from ansible.module_utils.six import PY2 from ansible.template import AnsibleUndefined -from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes from units.mock.yaml_helper import YamlTestUtils from units.mock.vault_helper import TextVaultSecret @@ -69,8 +68,7 @@ def test_ansible_vault_encrypted_unicode(self): def test_bytes(self): b_text = u'tréma'.encode('utf-8') - unsafe_object = AnsibleUnsafeBytes(b_text) - yaml_out = self._dump_string(unsafe_object, dumper=self.dumper) + yaml_out = self._dump_string(b_text, dumper=self.dumper) stream = self._build_stream(yaml_out) loader = self._loader(stream) @@ -97,8 +95,7 @@ def test_bytes(self): def test_unicode(self): u_text = u'nöel' - unsafe_object = AnsibleUnsafeText(u_text) - yaml_out = self._dump_string(unsafe_object, dumper=self.dumper) + yaml_out = self._dump_string(u_text, dumper=self.dumper) stream = self._build_stream(yaml_out) loader = self._loader(stream)
fea130480d26Ensure that unsafe is more difficult to lose [stable-2.15] (#82294)
56 files changed · +522 −146
changelogs/fragments/cve-2023-5764.yml+6 −0 added@@ -0,0 +1,6 @@ +security_fixes: +- templating - Address issues where internal templating can cause unsafe + variables to lose their unsafe designation (CVE-2023-5764) +breaking_changes: +- assert - Nested templating may result in an inability for the conditional + to be evaluated. See the porting guide for more information.
lib/ansible/module_utils/common/json.py+2 −2 modified@@ -30,7 +30,7 @@ def _preprocess_unsafe_encode(value): Used in ``AnsibleJSONEncoder.iterencode`` """ if _is_unsafe(value): - value = {'__ansible_unsafe': to_text(value, errors='surrogate_or_strict', nonstring='strict')} + value = {'__ansible_unsafe': to_text(value._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')} elif is_sequence(value): value = [_preprocess_unsafe_encode(v) for v in value] elif isinstance(value, Mapping): @@ -63,7 +63,7 @@ def default(self, o): value = {'__ansible_vault': to_text(o._ciphertext, errors='surrogate_or_strict', nonstring='strict')} elif getattr(o, '__UNSAFE__', False): # unsafe object, this will never be triggered, see ``AnsibleJSONEncoder.iterencode`` - value = {'__ansible_unsafe': to_text(o, errors='surrogate_or_strict', nonstring='strict')} + value = {'__ansible_unsafe': to_text(o._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')} elif isinstance(o, Mapping): # hostvars and other objects value = dict(o)
lib/ansible/parsing/yaml/dumper.py+5 −1 modified@@ -24,7 +24,7 @@ from ansible.module_utils.six import text_type, binary_type from ansible.module_utils.common.yaml import SafeDumper from ansible.parsing.yaml.objects import AnsibleUnicode, AnsibleSequence, AnsibleMapping, AnsibleVaultEncryptedUnicode -from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText +from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText, _is_unsafe from ansible.template import AnsibleUndefined from ansible.vars.hostvars import HostVars, HostVarsVars from ansible.vars.manager import VarsWithSources @@ -47,10 +47,14 @@ def represent_vault_encrypted_unicode(self, data): def represent_unicode(self, data): + if _is_unsafe(data): + data = data._strip_unsafe() return yaml.representer.SafeRepresenter.represent_str(self, text_type(data)) def represent_binary(self, data): + if _is_unsafe(data): + data = data._strip_unsafe() return yaml.representer.SafeRepresenter.represent_binary(self, binary_type(data))
lib/ansible/playbook/conditional.py+6 −5 modified@@ -25,7 +25,7 @@ from jinja2.compiler import generate from jinja2.exceptions import UndefinedError -from ansible.errors import AnsibleError, AnsibleUndefinedVariable +from ansible.errors import AnsibleError, AnsibleUndefinedVariable, AnsibleTemplateError from ansible.module_utils.six import text_type from ansible.module_utils._text import to_native from ansible.playbook.attribute import FieldAttribute @@ -132,9 +132,10 @@ def _check_conditional(self, conditional, templar, all_vars): if not isinstance(conditional, text_type) or conditional == "": return conditional - # update the lookups flag, as the string returned above may now be unsafe - # and we don't want future templating calls to do unsafe things - disable_lookups |= hasattr(conditional, '__UNSAFE__') + # If the result of the first-pass template render (to resolve inline templates) is marked unsafe, + # explicitly fail since the next templating operation would never evaluate + if hasattr(conditional, '__UNSAFE__'): + raise AnsibleTemplateError('Conditional is marked as unsafe, and cannot be evaluated.') # First, we do some low-level jinja2 parsing involving the AST format of the # statement to ensure we don't do anything unsafe (using the disable_lookup flag above) @@ -178,7 +179,7 @@ def generic_visit(self, node, inside_call=False, inside_yield=False): # NOTE The spaces around True and False are intentional to short-circuit literal_eval for # jinja2_native=False and avoid its expensive calls. presented = "{%% if %s %%} True {%% else %%} False {%% endif %%}" % conditional - val = templar.template(presented, disable_lookups=disable_lookups).strip() + val = templar.template(presented).strip() if val == "True": return True elif val == "False":
lib/ansible/playbook/task.py+24 −0 modified@@ -289,6 +289,30 @@ def post_validate(self, templar): super(Task, self).post_validate(templar) + def _post_validate_args(self, attr, value, templar): + # smuggle an untemplated copy of the task args for actions that need more control over the templating of their + # input (eg, debug's var/msg, assert's "that" conditional expressions) + self.untemplated_args = value + + # now recursively template the args dict + args = templar.template(value) + + # FIXME: could we just nuke this entirely and/or wrap it up in ModuleArgsParser or something? + if '_variable_params' in args: + variable_params = args.pop('_variable_params') + if isinstance(variable_params, dict): + if C.INJECT_FACTS_AS_VARS: + display.warning("Using a variable for a task's 'args' is unsafe in some situations " + "(see https://docs.ansible.com/ansible/devel/reference_appendices/faq.html#argsplat-unsafe)") + variable_params.update(args) + args = variable_params + else: + # if we didn't get a dict, it means there's garbage remaining after k=v parsing, just give up + # see https://github.com/ansible/ansible/issues/79862 + raise AnsibleError(f"invalid or malformed argument: '{variable_params}'") + + return args + def _post_validate_loop(self, attr, value, templar): ''' Override post validation for the loop field, which is templated
lib/ansible/plugins/action/assert.py+22 −1 modified@@ -63,8 +63,29 @@ def run(self, tmp=None, task_vars=None): quiet = boolean(self._task.args.get('quiet', False), strict=False) + # directly access 'that' via untemplated args from the task so we can intelligently trust embedded + # templates and preserve the original inputs/locations for better messaging on assert failures and + # errors. + # FIXME: even in devel, things like `that: item` don't always work properly (truthy string value + # is not really an embedded expression) + # we could fix that by doing direct var lookups on the inputs + # FIXME: some form of this code should probably be shared between debug, assert, and + # Task.post_validate, since they + # have a lot of overlapping needs + try: + thats = self._task.untemplated_args['that'] + except KeyError: + # in the case of "we got our entire args dict from a template", we can just consult the + # post-templated dict (the damage has likely already been done for embedded templates anyway) + thats = self._task.args['that'] + + # FIXME: this is a case where we only want to resolve indirections, NOT recurse containers + # (and even then, the leaf-most expression being wrapped is at least suboptimal + # (since its expression will be "eaten"). + if isinstance(thats, str): + thats = self._templar.template(thats) + # make sure the 'that' items are a list - thats = self._task.args['that'] if not isinstance(thats, list): thats = [thats]
lib/ansible/plugins/callback/__init__.py+3 −1 modified@@ -38,7 +38,7 @@ from ansible.plugins import AnsiblePlugin from ansible.utils.color import stringc from ansible.utils.display import Display -from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText +from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText, _is_unsafe from ansible.vars.clean import strip_internal_keys, module_response_deepcopy import yaml @@ -113,6 +113,8 @@ def _munge_data_for_lossy_yaml(scalar): def _pretty_represent_str(self, data): """Uses block style for multi-line strings""" + if _is_unsafe(data): + data = data._strip_unsafe() data = text_type(data) if _should_use_block(data): style = '|'
lib/ansible/plugins/filter/core.py+5 −0 modified@@ -37,6 +37,7 @@ from ansible.utils.encrypt import passlib_or_crypt, PASSLIB_AVAILABLE from ansible.utils.hashing import md5s, checksum_s from ansible.utils.unicode import unicode_wrap +from ansible.utils.unsafe_proxy import _is_unsafe from ansible.utils.vars import merge_hash display = Display() @@ -215,6 +216,8 @@ def from_yaml(data): # The ``text_type`` call here strips any custom # string wrapper class, so that CSafeLoader can # read the data + if _is_unsafe(data): + data = data._strip_unsafe() return yaml_load(text_type(to_text(data, errors='surrogate_or_strict'))) return data @@ -224,6 +227,8 @@ def from_yaml_all(data): # The ``text_type`` call here strips any custom # string wrapper class, so that CSafeLoader can # read the data + if _is_unsafe(data): + data = data._strip_unsafe() return yaml_load_all(text_type(to_text(data, errors='surrogate_or_strict'))) return data
lib/ansible/plugins/lookup/first_found.py+13 −2 modified@@ -136,7 +136,6 @@ elements: path """ import os -import re from collections.abc import Mapping, Sequence @@ -147,10 +146,22 @@ from ansible.plugins.lookup import LookupBase +def _splitter(value, chars): + chars = set(chars) + v = '' + for c in value: + if c in chars: + yield v + v = '' + continue + v += c + yield v + + def _split_on(terms, spliters=','): termlist = [] if isinstance(terms, string_types): - termlist = re.split(r'[%s]' % ''.join(map(re.escape, spliters)), terms) + termlist = list(_splitter(terms, spliters)) else: # added since options will already listify for t in terms:
lib/ansible/template/__init__.py+13 −2 modified@@ -31,7 +31,7 @@ from numbers import Number from traceback import format_exc -from jinja2.exceptions import TemplateSyntaxError, UndefinedError +from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError from jinja2.loaders import FileSystemLoader from jinja2.nativetypes import NativeEnvironment from jinja2.runtime import Context, StrictUndefined @@ -55,7 +55,7 @@ from ansible.utils.display import Display from ansible.utils.listify import listify_lookup_plugin_terms from ansible.utils.native_jinja import NativeJinjaText -from ansible.utils.unsafe_proxy import wrap_var +from ansible.utils.unsafe_proxy import wrap_var, AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText display = Display() @@ -365,10 +365,21 @@ class AnsibleContext(Context): flag is checked post-templating, and (when set) will result in the final templated result being wrapped in AnsibleUnsafe. ''' + _disallowed_callables = frozenset({ + AnsibleUnsafeText._strip_unsafe.__qualname__, + AnsibleUnsafeBytes._strip_unsafe.__qualname__, + NativeJinjaUnsafeText._strip_unsafe.__qualname__, + }) + def __init__(self, *args, **kwargs): super(AnsibleContext, self).__init__(*args, **kwargs) self.unsafe = False + def call(self, obj, *args, **kwargs): + if getattr(obj, '__qualname__', None) in self._disallowed_callables or obj in self._disallowed_callables: + raise SecurityError(f"{obj!r} is not safely callable") + return super().call(obj, *args, **kwargs) + def _is_unsafe(self, val): ''' Our helper function, which will also recursively check dict and
lib/ansible/utils/unsafe_proxy.py+259 −6 modified@@ -69,15 +69,264 @@ class AnsibleUnsafe(object): class AnsibleUnsafeBytes(binary_type, AnsibleUnsafe): - def decode(self, *args, **kwargs): - """Wrapper method to ensure type conversions maintain unsafe context""" - return AnsibleUnsafeText(super(AnsibleUnsafeBytes, self).decode(*args, **kwargs)) + def _strip_unsafe(self): + return super().__bytes__() + + def __str__(self, /): # pylint: disable=invalid-str-returned + return self.encode() + + def __bytes__(self, /): # pylint: disable=invalid-bytes-returned + return self + + def __repr__(self, /): # pylint: disable=invalid-repr-returned + return AnsibleUnsafeText(super().__repr__()) + + def __format__(self, format_spec, /): # pylint: disable=invalid-format-returned + return self.__class__(super().__format__(format_spec)) + + def __getitem__(self, key, /): + return self.__class__(super().__getitem__(key)) + + def __iter__(self, /): + cls = self.__class__ + return (cls(c) for c in super().__iter__()) + + def __reversed__(self, /): + return self[::-1] + + def __add__(self, value, /): + return self.__class__(super().__add__(value)) + + def __radd__(self, value, /): + return self.__class__(value.__add__(self)) + + def __mul__(self, value, /): + return self.__class__(super().__mul__(value)) + + __rmul__ = __mul__ + + def __mod__(self, value, /): + return self.__class__(super().__mod__(value)) + + def __rmod__(self, value, /): + return self.__class__(super().__rmod__(value)) + + def capitalize(self, /): + return self.__class__(super().capitalize()) + + def casefold(self, /): + return self.__class__(super().casefold()) + + def center(self, width, fillchar=b' ', /): + return self.__class__(super().center(width, fillchar)) + + def decode(self, /, encoding='utf-8', errors='strict'): + return AnsibleUnsafeText(super().decode(encoding=encoding, errors=errors)) + + def removeprefix(self, prefix, /): + return self.__class__(super().removeprefix(prefix)) + + def removesuffix(self, suffix, /): + return self.__class__(super().removesuffix(suffix)) + + def expandtabs(self, /, tabsize=8): + return self.__class__(super().expandtabs(tabsize)) + + def format(self, /, *args, **kwargs): + return self.__class__(super().format(*args, **kwargs)) + + def format_map(self, mapping, /): + return self.__class__(super().format_map(mapping)) + + def join(self, iterable_of_bytes, /): + return self.__class__(super().join(iterable_of_bytes)) + + def ljust(self, width, fillchar=b' ', /): + return self.__class__(super().ljust(width, fillchar)) + + def lower(self, /): + return self.__class__(super().lower()) + + def lstrip(self, bytes=None, /): + return self.__class__(super().lstrip(bytes)) + + def partition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().partition(sep)) + + def replace(self, old, new, count=-1, /): + return self.__class__(super().replace(old, new, count)) + + def rjust(self, width, fillchar=b' ', /): + return self.__class__(super().rjust(width, fillchar)) + + def rpartition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().rpartition(sep)) + + def rstrip(self, bytes=None, /): + return self.__class__(super().rstrip(bytes)) + + def split(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)] + + def rsplit(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)] + + def splitlines(self, /, keepends=False): + cls = self.__class__ + return [cls(e) for e in super().splitlines(keepends=keepends)] + + def strip(self, bytes=None, /): + return self.__class__(super().strip(bytes)) + + def swapcase(self, /): + return self.__class__(super().swapcase()) + + def title(self, /): + return self.__class__(super().title()) + + def translate(self, table, /, delete=b''): + return self.__class__(super().translate(table, delete=delete)) + + def upper(self, /): + return self.__class__(super().upper()) + + def zfill(self, width, /): + return self.__class__(super().zfill(width)) class AnsibleUnsafeText(text_type, AnsibleUnsafe): - def encode(self, *args, **kwargs): - """Wrapper method to ensure type conversions maintain unsafe context""" - return AnsibleUnsafeBytes(super(AnsibleUnsafeText, self).encode(*args, **kwargs)) + # def __getattribute__(self, name): + # print(f'attr: {name}') + # return object.__getattribute__(self, name) + + def _strip_unsafe(self, /): + return super().__str__() + + def __str__(self, /): # pylint: disable=invalid-str-returned + return self + + def __repr__(self, /): # pylint: disable=invalid-repr-returned + return self.__class__(super().__repr__()) + + def __format__(self, format_spec, /): # pylint: disable=invalid-format-returned + return self.__class__(super().__format__(format_spec)) + + def __getitem__(self, key, /): + return self.__class__(super().__getitem__(key)) + + def __iter__(self, /): + cls = self.__class__ + return (cls(c) for c in super().__iter__()) + + def __reversed__(self, /): + return self[::-1] + + def __add__(self, value, /): + return self.__class__(super().__add__(value)) + + def __radd__(self, value, /): + return self.__class__(value.__add__(self)) + + def __mul__(self, value, /): + return self.__class__(super().__mul__(value)) + + __rmul__ = __mul__ + + def __mod__(self, value, /): + return self.__class__(super().__mod__(value)) + + def __rmod__(self, value, /): + return self.__class__(super().__rmod__(value)) + + def capitalize(self, /): + return self.__class__(super().capitalize()) + + def casefold(self, /): + return self.__class__(super().casefold()) + + def center(self, width, fillchar=' ', /): + return self.__class__(super().center(width, fillchar)) + + def encode(self, /, encoding='utf-8', errors='strict'): + return AnsibleUnsafeBytes(super().encode(encoding=encoding, errors=errors)) + + def removeprefix(self, prefix, /): + return self.__class__(super().removeprefix(prefix)) + + def removesuffix(self, suffix, /): + return self.__class__(super().removesuffix(suffix)) + + def expandtabs(self, /, tabsize=8): + return self.__class__(super().expandtabs(tabsize)) + + def format(self, /, *args, **kwargs): + return self.__class__(super().format(*args, **kwargs)) + + def format_map(self, mapping, /): + return self.__class__(super().format_map(mapping)) + + def join(self, iterable, /): + return self.__class__(super().join(iterable)) + + def ljust(self, width, fillchar=' ', /): + return self.__class__(super().ljust(width, fillchar)) + + def lower(self, /): + return self.__class__(super().lower()) + + def lstrip(self, chars=None, /): + return self.__class__(super().lstrip(chars)) + + def partition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().partition(sep)) + + def replace(self, old, new, count=-1, /): + return self.__class__(super().replace(old, new, count)) + + def rjust(self, width, fillchar=' ', /): + return self.__class__(super().rjust(width, fillchar)) + + def rpartition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().rpartition(sep)) + + def rstrip(self, chars=None, /): + return self.__class__(super().rstrip(chars)) + + def split(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)] + + def rsplit(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)] + + def splitlines(self, /, keepends=False): + cls = self.__class__ + return [cls(e) for e in super().splitlines(keepends=keepends)] + + def strip(self, chars=None, /): + return self.__class__(super().strip(chars)) + + def swapcase(self, /): + return self.__class__(super().swapcase()) + + def title(self, /): + return self.__class__(super().title()) + + def translate(self, table, /): + return self.__class__(super().translate(table)) + + def upper(self, /): + return self.__class__(super().upper()) + + def zfill(self, width, /): + return self.__class__(super().zfill(width)) class NativeJinjaUnsafeText(NativeJinjaText, AnsibleUnsafeText): @@ -126,3 +375,7 @@ def to_unsafe_bytes(*args, **kwargs): def to_unsafe_text(*args, **kwargs): return wrap_var(to_text(*args, **kwargs)) + + +def _is_unsafe(obj): + return getattr(obj, '__UNSAFE__', False) is True
test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml+8 −8 modified@@ -13,22 +13,22 @@ in command.stdout_lines - >- "Installing 'namespace_1.collection_1:1.0.0' to - '{{ install_path }}/namespace_1/collection_1'" + '" ~ install_path ~ "/namespace_1/collection_1'" in command.stdout_lines - >- 'Created collection for namespace_1.collection_1:1.0.0 at - {{ install_path }}/namespace_1/collection_1' + ' ~ install_path ~ '/namespace_1/collection_1' in command.stdout_lines - >- 'namespace_1.collection_1:1.0.0 was installed successfully' in command.stdout_lines - >- "Installing 'namespace_2.collection_2:1.0.0' to - '{{ install_path }}/namespace_2/collection_2'" + '" ~ install_path ~ "/namespace_2/collection_2'" in command.stdout_lines - >- 'Created collection for namespace_2.collection_2:1.0.0 at - {{ install_path }}/namespace_2/collection_2' + ' ~ install_path ~ '/namespace_2/collection_2' in command.stdout_lines - >- 'namespace_2.collection_2:1.0.0 was installed successfully' @@ -58,22 +58,22 @@ in command.stdout_lines - >- "Installing 'namespace_1.collection_1:1.0.0' to - '{{ install_path }}/namespace_1/collection_1'" + '" ~ install_path ~ "/namespace_1/collection_1'" in command.stdout_lines - >- 'Created collection for namespace_1.collection_1:1.0.0 at - {{ install_path }}/namespace_1/collection_1' + ' ~ install_path ~ '/namespace_1/collection_1' in command.stdout_lines - >- 'namespace_1.collection_1:1.0.0 was installed successfully' in command.stdout_lines - >- "Installing 'namespace_2.collection_2:1.0.0' to - '{{ install_path }}/namespace_2/collection_2'" + '" ~ install_path ~ "/namespace_2/collection_2'" in command.stdout_lines - >- 'Created collection for namespace_2.collection_2:1.0.0 at - {{ install_path }}/namespace_2/collection_2' + ' ~ install_path ~ '/namespace_2/collection_2' in command.stdout_lines - >- 'namespace_2.collection_2:1.0.0 was installed successfully'
test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml+1 −1 modified@@ -2,7 +2,7 @@ - name: Assert that a embedded vault of a string with no newline works assert: that: - - '"{{ vault_encrypted_one_line_var }}" == "Setec Astronomy"' + - 'vault_encrypted_one_line_var == "Setec Astronomy"' - name: Assert that a multi line embedded vault works, including new line assert:
test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml+1 −1 modified@@ -2,7 +2,7 @@ - name: Assert that a vault encrypted file with embedded vault of a string with no newline works assert: that: - - '"{{ vault_file_encrypted_with_encrypted_one_line_var }}" == "Setec Astronomy"' + - 'vault_file_encrypted_with_encrypted_one_line_var == "Setec Astronomy"' - name: Assert that a vault encrypted file with multi line embedded vault works, including new line assert:
test/integration/targets/apt_repository/tasks/apt.yml+6 −6 modified@@ -50,7 +50,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_name}}"' + - 'result.repo == test_ppa_name' - name: 'examine apt cache mtime' stat: path='/var/cache/apt/pkgcache.bin' @@ -81,7 +81,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_name}}"' + - 'result.repo == test_ppa_name' - name: 'examine apt cache mtime' stat: path='/var/cache/apt/pkgcache.bin' @@ -112,7 +112,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_name}}"' + - 'result.repo == test_ppa_name' - name: 'examine apt cache mtime' stat: path='/var/cache/apt/pkgcache.bin' @@ -151,7 +151,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_spec}}"' + - 'result.repo == test_ppa_spec' - '"sources_added" in result' - 'result.sources_added | length == 1' - '"git" in result.sources_added[0]' @@ -176,7 +176,7 @@ that: - 'result.changed' - 'result.state == "absent"' - - 'result.repo == "{{test_ppa_spec}}"' + - 'result.repo == test_ppa_spec' - '"sources_added" in result' - 'result.sources_added | length == 0' - '"sources_removed" in result' @@ -207,7 +207,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_spec}}"' + - 'result.repo == test_ppa_spec' - name: 'examine source file' stat: path='/etc/apt/sources.list.d/{{test_ppa_filename}}.list'
test/integration/targets/assert/assert.out.nested_tmpl.stderr+4 −0 added@@ -0,0 +1,4 @@ ++ ansible-playbook -i localhost, -c local nested_tmpl.yml +++ set +x +[WARNING]: conditional statements should not include jinja2 templating +delimiters such as {{ }} or {% %}. Found: "{{ foo }}" == "bar"
test/integration/targets/assert/assert.out.nested_tmpl.stdout+12 −0 added@@ -0,0 +1,12 @@ + +PLAY [localhost] *************************************************************** + +TASK [assert] ****************************************************************** +ok: [localhost] => { + "changed": false, + "msg": "All assertions passed" +} + +PLAY RECAP ********************************************************************* +localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +
test/integration/targets/assert/assert.out.quiet.stderr+0 −0 renamedtest/integration/targets/assert/assert.out.quiet.stdout+0 −0 renamedtest/integration/targets/assert/nested_tmpl.yml+9 −0 added@@ -0,0 +1,9 @@ +- hosts: localhost + gather_facts: False + tasks: + - assert: + that: + - '"{{ foo }}" == "bar"' + - foo == "bar" + vars: + foo: bar
test/integration/targets/assert/quiet.yml+2 −2 modified@@ -5,12 +5,12 @@ item_A: yes tasks: - assert: - that: "{{ item }} is defined" + that: "item is defined" quiet: True with_items: - item_A - assert: - that: "{{ item }} is defined" + that: "item is defined" quiet: False with_items: - item_A
test/integration/targets/assert/runme.sh+2 −1 modified@@ -45,7 +45,7 @@ cleanup() { fi } -BASEFILE=assert_quiet.out +BASEFILE=assert.out ORIGFILE="${BASEFILE}" OUTFILE="${BASEFILE}.new" @@ -69,3 +69,4 @@ export ANSIBLE_NOCOLOR=1 export ANSIBLE_RETRY_FILES_ENABLED=0 run_test quiet +run_test nested_tmpl
test/integration/targets/command_shell/tasks/main.yml+1 −1 modified@@ -296,7 +296,7 @@ assert: that: - shell_result0 is changed - - shell_result0.cmd == '{{ remote_tmp_dir_test }}/test.sh' + - shell_result0.cmd == remote_tmp_dir_test ~ '/test.sh' - shell_result0.rc == 0 - shell_result0.stderr == '' - shell_result0.stdout == 'win'
test/integration/targets/copy/tasks/tests.yml+21 −21 modified@@ -1250,7 +1250,7 @@ assert: that: - "copy_result6.changed" - - "copy_result6.dest == '{{remote_dir_expanded}}/multiline.txt'" + - "copy_result6.dest == remote_dir_expanded ~ '/multiline.txt'" - "copy_result6.checksum == '9cd0697c6a9ff6689f0afb9136fa62e0b3fee903'" # test overwriting a file as an unprivileged user (pull request #8624) @@ -2153,26 +2153,26 @@ assert: that: - testcase5 is changed - - "stat_new_dir_with_chown.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown.stat.gr_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_file1.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown_file1.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown_file1.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_file1.stat.gr_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_subdir.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown_subdir.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown_subdir.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_subdir.stat.gr_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_subdir_file12.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown_subdir_file12.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_link_file12.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown_link_file12.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown_link_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_link_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'" + - "stat_new_dir_with_chown.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown.stat.gr_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_file1.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown_file1.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown_file1.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_file1.stat.gr_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_subdir.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown_subdir.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown_subdir.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_subdir.stat.gr_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_subdir_file12.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown_subdir_file12.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_link_file12.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown_link_file12.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown_link_file12.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_link_file12.stat.gr_name == ansible_copy_test_user_name" always: - name: execute - remove the user for test
test/integration/targets/debug/runme.sh+2 −0 modified@@ -18,3 +18,5 @@ done # ensure debug does not set top level vars when looking at ansible_facts ansible-playbook nosetfacts.yml "$@" + +ansible-playbook unsafe.yml "$@"
test/integration/targets/debug/unsafe.yml+13 −0 added@@ -0,0 +1,13 @@ +- hosts: localhost + gather_facts: false + vars: + unsafe_var: !unsafe undef()|mandatory + tasks: + - debug: + var: '{{ unsafe_var }}' + ignore_errors: true + register: result + + - assert: + that: + - result is successful
test/integration/targets/expect/tasks/main.yml+1 −1 modified@@ -117,7 +117,7 @@ - name: assert chdir works assert: that: - - "'{{chdir_result.stdout | trim}}' == '{{remote_tmp_dir_real_path.stdout | trim}}'" + - "chdir_result.stdout | trim == remote_tmp_dir_real_path.stdout | trim" - name: test timeout option expect:
test/integration/targets/file/tasks/main.yml+1 −1 modified@@ -927,7 +927,7 @@ that: - "file_error3 is failed" - "file_error3.msg == 'src does not exist'" - - "file_error3.dest == '{{ remote_tmp_dir_test }}/hard.txt' | expanduser" + - "file_error3.dest == remote_tmp_dir_test | expanduser ~ '/hard.txt'" - "file_error3.src == 'non-existing-file-that-does-not-exist.txt'" - block:
test/integration/targets/file/tasks/state_link.yml+1 −1 modified@@ -199,7 +199,7 @@ - "missing_dst_no_follow_enable_force_use_mode2 is changed" - "missing_dst_no_follow_enable_force_use_mode3 is not changed" - "soft3_result['stat'].islnk" - - "soft3_result['stat'].lnk_target == '{{ user.home }}/nonexistent'" + - "soft3_result['stat'].lnk_target == user.home ~ '/nonexistent'" # # Test creating a link to a directory https://github.com/ansible/ansible/issues/1369
test/integration/targets/find/tasks/main.yml+6 −6 modified@@ -267,7 +267,7 @@ - name: assert we skipped the ogg file assert: that: - - '"{{ remote_tmp_dir_test }}/e/f/g/h/8.ogg" not in find_test3_list' + - 'remote_tmp_dir_test ~ "/e/f/g/h/8.ogg" not in find_test3_list' - name: patterns with regex find: @@ -317,7 +317,7 @@ assert: that: - result.matched == 1 - - '"{{ remote_tmp_dir_test }}/astest/old.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/old.txt" in astest_list' - name: find files newer than 1 week find: @@ -332,7 +332,7 @@ assert: that: - result.matched == 1 - - '"{{ remote_tmp_dir_test }}/astest/new.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/new.txt" in astest_list' - name: add some content to the new file shell: "echo hello world > {{ remote_tmp_dir_test }}/astest/new.txt" @@ -352,7 +352,7 @@ assert: that: - result.matched == 1 - - '"{{ remote_tmp_dir_test }}/astest/new.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/new.txt" in astest_list' - '"checksum" in result.files[0]' - name: find ANY item with LESS than 5 bytes, also get checksums @@ -371,6 +371,6 @@ assert: that: - result.matched == 2 - - '"{{ remote_tmp_dir_test }}/astest/old.txt" in astest_list' - - '"{{ remote_tmp_dir_test }}/astest/.hidden.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/old.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/.hidden.txt" in astest_list' - '"checksum" in result.files[0]'
test/integration/targets/gathering_facts/test_gathering_facts.yml+2 −2 modified@@ -433,7 +433,7 @@ - name: Test reading facts from default fact_path assert: that: - - '"{{ ansible_local.testfact.fact_dir }}" == "default"' + - 'ansible_local.testfact.fact_dir == "default"' - hosts: facthost9 tags: [ 'fact_local'] @@ -444,7 +444,7 @@ - name: Test reading facts from custom fact_path assert: that: - - '"{{ ansible_local.testfact.fact_dir }}" == "custom"' + - 'ansible_local.testfact.fact_dir == "custom"' - hosts: facthost20 tags: [ 'fact_facter_ohai' ]
test/integration/targets/git/tasks/depth.yml+1 −1 modified@@ -172,7 +172,7 @@ - name: DEPTH | check update arrived assert: that: - - "{{ a_file.content | b64decode | trim }} == 3" + - a_file.content | b64decode | trim == "3" - git_fetch is changed - name: DEPTH | clear checkout_dir
test/integration/targets/git/tasks/localmods.yml+2 −2 modified@@ -58,7 +58,7 @@ - name: LOCALMODS | check update arrived assert: that: - - "{{ a_file.content | b64decode | trim }} == 2" + - a_file.content | b64decode | trim == "2" - git_fetch_force is changed - name: LOCALMODS | clear checkout_dir @@ -127,7 +127,7 @@ - name: LOCALMODS | check update arrived assert: that: - - "{{ a_file.content | b64decode | trim }} == 2" + - a_file.content | b64decode | trim == "2" - git_fetch_force is changed - name: LOCALMODS | clear checkout_dir
test/integration/targets/git/tasks/submodules.yml+7 −7 modified@@ -32,7 +32,7 @@ - name: SUBMODULES | Ensure submodu1 is at the appropriate commit assert: - that: '{{ submodule1.stdout_lines | length }} == 2' + that: 'submodule1.stdout_lines | length == 2' - name: SUBMODULES | clear checkout_dir file: @@ -53,7 +53,7 @@ - name: SUBMODULES | Ensure submodule1 is at the appropriate commit assert: - that: '{{ submodule1.stdout_lines | length }} == 4' + that: 'submodule1.stdout_lines | length == 4' - name: SUBMODULES | Copy the checkout so we can run several different tests on it command: 'cp -pr {{ checkout_dir }} {{ checkout_dir }}.bak' @@ -84,8 +84,8 @@ - name: SUBMODULES | Ensure both submodules are at the appropriate commit assert: that: - - '{{ submodule1.stdout_lines|length }} == 4' - - '{{ submodule2.stdout_lines|length }} == 2' + - 'submodule1.stdout_lines|length == 4' + - 'submodule2.stdout_lines|length == 2' - name: SUBMODULES | Remove checkout dir @@ -112,7 +112,7 @@ - name: SUBMODULES | Ensure submodule1 is at the appropriate commit assert: - that: '{{ submodule1.stdout_lines | length }} == 5' + that: 'submodule1.stdout_lines | length == 5' - name: SUBMODULES | Test that update with recursive found new submodules @@ -121,7 +121,7 @@ - name: SUBMODULES | Enusre submodule2 is at the appropriate commit assert: - that: '{{ submodule2.stdout_lines | length }} == 4' + that: 'submodule2.stdout_lines | length == 4' - name: SUBMODULES | clear checkout_dir file: @@ -147,4 +147,4 @@ - name: SUBMODULES | Ensure submodule1 is at the appropriate commit assert: - that: '{{ submodule1.stdout_lines | length }} == 4' + that: 'submodule1.stdout_lines | length == 4'
test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml+2 −2 modified@@ -22,7 +22,7 @@ - name: Check that multiple duplicate lines collapse into a single commands assert: that: - - "{{ result.commands|length }} == 1" + - "result.commands|length == 1" - name: Check that set is correctly prepended assert: @@ -58,6 +58,6 @@ - assert: that: - - "{{ result.filtered|length }} == 2" + - "result.filtered|length == 2" - debug: msg="END cli/config_check.yaml on connection={{ ansible_connection }}"
test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml+4 −4 modified@@ -16,17 +16,17 @@ - name: Assert that the before dicts were correctly generated assert: that: - - "{{ populate | symmetric_difference(result['before']) |length == 0 }}" + - "populate | symmetric_difference(result['before']) |length == 0" - name: Assert that the correct set of commands were generated assert: that: - - "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + - "deleted['commands'] | symmetric_difference(result['commands']) |length == 0" - name: Assert that the after dicts were correctly generated assert: that: - - "{{ deleted['after'] | symmetric_difference(result['after']) |length == 0 }}" + - "deleted['after'] | symmetric_difference(result['after']) |length == 0" - name: Delete attributes of given interfaces (IDEMPOTENT) vyos.vyos.vyos_lldp_interfaces: *deleted @@ -41,6 +41,6 @@ - name: Assert that the before dicts were correctly generated assert: that: - - "{{ deleted['after'] | symmetric_difference(result['before']) |length == 0 }}" + - "deleted['after'] | symmetric_difference(result['before']) |length == 0" always: - include_tasks: _remove_config.yaml
test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml+4 −4 modified@@ -28,17 +28,17 @@ - name: Assert that before dicts were correctly generated assert: - that: "{{ merged['before'] | symmetric_difference(result['before']) |length == 0 }}" + that: "merged['before'] | symmetric_difference(result['before']) |length == 0" - name: Assert that correct set of commands were generated assert: that: - - "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + - "merged['commands'] | symmetric_difference(result['commands']) |length == 0" - name: Assert that after dicts was correctly generated assert: that: - - "{{ merged['after'] | symmetric_difference(result['after']) |length == 0 }}" + - "merged['after'] | symmetric_difference(result['after']) |length == 0" - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT) vyos.vyos.vyos_lldp_interfaces: *merged @@ -52,7 +52,7 @@ - name: Assert that before dicts were correctly generated assert: that: - - "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}" + - "merged['after'] | symmetric_difference(result['before']) |length == 0" always: - include_tasks: _remove_config.yaml
test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml+4 −4 modified@@ -19,17 +19,17 @@ - name: Assert that before dicts were correctly generated assert: that: - - "{{ populate_intf | symmetric_difference(result['before']) |length == 0 }}" + - "populate_intf | symmetric_difference(result['before']) |length == 0" - name: Assert that correct commands were generated assert: that: - - "{{ overridden['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + - "overridden['commands'] | symmetric_difference(result['commands']) |length == 0" - name: Assert that after dicts were correctly generated assert: that: - - "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}" + - "overridden['after'] | symmetric_difference(result['after']) |length == 0" - name: Overrides all device configuration with provided configurations (IDEMPOTENT) vyos.vyos.vyos_lldp_interfaces: *overridden @@ -43,7 +43,7 @@ - name: Assert that before dicts were correctly generated assert: that: - - "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}" + - "overridden['after'] | symmetric_difference(result['before']) |length == 0" always: - include_tasks: _remove_config.yaml
test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml+4 −4 modified@@ -33,17 +33,17 @@ - name: Assert that correct set of commands were generated assert: that: - - "{{ replaced['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + - "replaced['commands'] | symmetric_difference(result['commands']) |length == 0" - name: Assert that before dicts are correctly generated assert: that: - - "{{ populate | symmetric_difference(result['before']) |length == 0 }}" + - "populate | symmetric_difference(result['before']) |length == 0" - name: Assert that after dict is correctly generated assert: that: - - "{{ replaced['after'] | symmetric_difference(result['after']) |length == 0 }}" + - "replaced['after'] | symmetric_difference(result['after']) |length == 0" - name: Replace device configurations of listed LLDP interfaces with provided configurarions (IDEMPOTENT) vyos.vyos.vyos_lldp_interfaces: *replaced @@ -57,7 +57,7 @@ - name: Assert that before dict is correctly generated assert: that: - - "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}" + - "replaced['after'] | symmetric_difference(result['before']) |length == 0" always: - include_tasks: _remove_config.yaml
test/integration/targets/include_vars/tasks/main.yml+16 −16 modified@@ -15,7 +15,7 @@ that: - "testing == 789" - "base_dir == 'environments/development'" - - "{{ included_one_file.ansible_included_var_files | length }} == 1" + - "included_one_file.ansible_included_var_files | length == 1" - "'vars/environments/development/all.yml' in included_one_file.ansible_included_var_files[0]" - name: include the vars/environments/development/all.yml and save results in all @@ -51,7 +51,7 @@ assert: that: - webapp_version is defined - - "'file_without_extension' in '{{ include_without_file_extension.ansible_included_var_files | join(' ') }}'" + - "'file_without_extension' in include_without_file_extension.ansible_included_var_files | join(' ')" - name: include every directory in vars include_vars: @@ -67,7 +67,7 @@ - "testing == 456" - "base_dir == 'services'" - "webapp_containers == 10" - - "{{ include_every_dir.ansible_included_var_files | length }} == 7" + - "include_every_dir.ansible_included_var_files | length == 7" - "'vars/all/all.yml' in include_every_dir.ansible_included_var_files[0]" - "'vars/environments/development/all.yml' in include_every_dir.ansible_included_var_files[1]" - "'vars/environments/development/services/webapp.yml' in include_every_dir.ansible_included_var_files[2]" @@ -88,9 +88,9 @@ that: - "testing == 789" - "base_dir == 'environments/development'" - - "{{ include_without_webapp.ansible_included_var_files | length }} == 4" - - "'webapp.yml' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'" - - "'file_without_extension' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'" + - "include_without_webapp.ansible_included_var_files | length == 4" + - "'webapp.yml' not in include_without_webapp.ansible_included_var_files | join(' ')" + - "'file_without_extension' not in include_without_webapp.ansible_included_var_files | join(' ')" - name: include only files matching webapp.yml include_vars: @@ -104,9 +104,9 @@ - "testing == 101112" - "base_dir == 'development/services'" - "webapp_containers == 20" - - "{{ include_match_webapp.ansible_included_var_files | length }} == 1" + - "include_match_webapp.ansible_included_var_files | length == 1" - "'vars/environments/development/services/webapp.yml' in include_match_webapp.ansible_included_var_files[0]" - - "'all.yml' not in '{{ include_match_webapp.ansible_included_var_files | join(' ') }}'" + - "'all.yml' not in include_match_webapp.ansible_included_var_files | join(' ')" - name: include only files matching webapp.yml and store results in webapp include_vars: @@ -173,10 +173,10 @@ - name: Verify the hash variable assert: that: - - "{{ config | length }} == 3" + - "config | length == 3" - "config.key0 == 0" - "config.key1 == 0" - - "{{ config.key2 | length }} == 1" + - "config.key2 | length == 1" - "config.key2.a == 21" - name: Include the second file to merge the hash variable @@ -187,10 +187,10 @@ - name: Verify that the hash is merged assert: that: - - "{{ config | length }} == 4" + - "config | length == 4" - "config.key0 == 0" - "config.key1 == 1" - - "{{ config.key2 | length }} == 2" + - "config.key2 | length == 2" - "config.key2.a == 21" - "config.key2.b == 22" - "config.key3 == 3" @@ -202,9 +202,9 @@ - name: Verify that the properties from the first file is cleared assert: that: - - "{{ config | length }} == 3" + - "config | length == 3" - "config.key1 == 1" - - "{{ config.key2 | length }} == 1" + - "config.key2 | length == 1" - "config.key2.b == 22" - "config.key3 == 3" @@ -216,10 +216,10 @@ - name: Verify that the hash is merged after vars files are accumulated assert: that: - - "{{ config | length }} == 3" + - "config | length == 3" - "config.key0 is undefined" - "config.key1 == 1" - - "{{ config.key2 | length }} == 1" + - "config.key2 | length == 1" - "config.key2.b == 22" - "config.key3 == 3"
test/integration/targets/lookup_ini/test_lookup_properties.yml+1 −1 modified@@ -10,7 +10,7 @@ field_with_space: "{{lookup('ini', 'field.with.space type=properties file=lookup.properties')}}" - assert: - that: "{{item}} is defined" + that: "item is defined" with_items: [ 'test1', 'test2', 'test_dot', 'field_with_space' ] - name: "read ini value"
test/integration/targets/lookup_subelements/tasks/main.yml+3 −3 modified@@ -133,7 +133,7 @@ - assert: that: - - "'{{ item.0.name }}' != 'carol'" + - "item.0.name != 'carol'" with_subelements: - "{{ users }}" - mysql.privs @@ -220,5 +220,5 @@ - assert: that: - - "'{{ user_alice }}' == 'localhost'" - - "'{{ user_bob }}' == 'db1'" + - "user_alice == 'localhost'" + - "user_bob == 'db1'"
test/integration/targets/loop_control/inner.yml+2 −2 modified@@ -3,7 +3,7 @@ that: - ansible_loop.index == ansible_loop.index0 + 1 - ansible_loop.revindex == ansible_loop.revindex0 + 1 - - ansible_loop.first == {{ ansible_loop.index == 1 }} - - ansible_loop.last == {{ ansible_loop.index == ansible_loop.length }} + - ansible_loop.first == (ansible_loop.index == 1) + - ansible_loop.last == (ansible_loop.index == ansible_loop.length) - ansible_loop.length == 3 - ansible_loop.allitems|join(',') == 'first,second,third'
test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml+1 −1 modified@@ -13,4 +13,4 @@ - assert: that: - '"location" in result' - - 'result["location"] == "{{ expected_location}}"' + - 'result["location"] == expected_location'
test/integration/targets/module_precedence/modules_test_multiple_roles.yml+1 −1 modified@@ -14,4 +14,4 @@ - assert: that: - '"location" in result' - - 'result["location"] == "{{ expected_location}}"' + - 'result["location"] == expected_location'
test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml+1 −1 modified@@ -7,4 +7,4 @@ assert: that: - '"location" in result' - - 'result["location"] == "{{ expected_location }}"' + - 'result["location"] == expected_location'
test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml+1 −1 modified@@ -7,4 +7,4 @@ assert: that: - '"location" in result' - - 'result["location"] == "{{ expected_location }}"' + - 'result["location"] == expected_location'
test/integration/targets/script/tasks/main.yml+2 −2 modified@@ -198,7 +198,7 @@ assert: that: - _check_mode_test2 is skipped - - '_check_mode_test2.msg == "{{ remote_tmp_dir_test | expanduser }}/afile2.txt exists, matching creates option"' + - '_check_mode_test2.msg == remote_tmp_dir_test | expanduser ~ "/afile2.txt exists, matching creates option"' - name: Remove afile2.txt file: @@ -220,7 +220,7 @@ assert: that: - _check_mode_test3 is skipped - - '_check_mode_test3.msg == "{{ remote_tmp_dir_test | expanduser }}/afile2.txt does not exist, matching removes option"' + - '_check_mode_test3.msg == remote_tmp_dir_test | expanduser ~ "/afile2.txt does not exist, matching removes option"' # executable
test/integration/targets/slurp/tasks/main.yml+1 −1 modified@@ -33,7 +33,7 @@ - 'slurp_existing.encoding == "base64"' - 'slurp_existing is not changed' - 'slurp_existing is not failed' - - '"{{ slurp_existing.content | b64decode }}" == "We are at the café"' + - 'slurp_existing.content | b64decode == "We are at the café"' - name: Create a binary file to test with copy:
test/integration/targets/template/tasks/main.yml+1 −1 modified@@ -357,7 +357,7 @@ - assert: that: - "\"foo t'e~m\\plated\" in unusual_results.stdout_lines" - - "{{unusual_results.stdout_lines| length}} == 1" + - "unusual_results.stdout_lines| length == 1" - name: check that the unusual filename can be checked for changes template:
test/integration/targets/unarchive/tasks/test_missing_binaries.yml+1 −1 modified@@ -66,7 +66,7 @@ - zip_success.changed # Verify that file list is generated - "'files' in zip_success" - - "{{zip_success['files']| length}} == 3" + - "zip_success['files']| length == 3" - "'foo-unarchive.txt' in zip_success['files']" - "'foo-unarchive-777.txt' in zip_success['files']" - "'FOO-UNAR.TXT' in zip_success['files']"
test/integration/targets/unarchive/tasks/test_mode.yml+4 −4 modified@@ -24,7 +24,7 @@ - "unarchive06_stat.stat.mode == '0600'" # Verify that file list is generated - "'files' in unarchive06" - - "{{unarchive06['files']| length}} == 1" + - "unarchive06['files']| length == 1" - "'foo-unarchive.txt' in unarchive06['files']" - name: remove our tar.gz unarchive destination @@ -74,7 +74,7 @@ - "unarchive07.changed == false" # Verify that file list is generated - "'files' in unarchive07" - - "{{unarchive07['files']| length}} == 1" + - "unarchive07['files']| length == 1" - "'foo-unarchive.txt' in unarchive07['files']" - name: remove our tar.gz unarchive destination @@ -108,7 +108,7 @@ - "unarchive08_stat.stat.mode == '0601'" # Verify that file list is generated - "'files' in unarchive08" - - "{{unarchive08['files']| length}} == 3" + - "unarchive08['files']| length == 3" - "'foo-unarchive.txt' in unarchive08['files']" - "'foo-unarchive-777.txt' in unarchive08['files']" - "'FOO-UNAR.TXT' in unarchive08['files']" @@ -140,7 +140,7 @@ - "unarchive08_stat.stat.mode == '0601'" # Verify that file list is generated - "'files' in unarchive08" - - "{{unarchive08['files']| length}} == 3" + - "unarchive08['files']| length == 3" - "'foo-unarchive.txt' in unarchive08['files']" - "'foo-unarchive-777.txt' in unarchive08['files']" - "'FOO-UNAR.TXT' in unarchive08['files']"
test/integration/targets/unarchive/tasks/test_unprivileged_user.yml+1 −1 modified@@ -40,7 +40,7 @@ - unarchive10 is changed # Verify that file list is generated - "'files' in unarchive10" - - "{{unarchive10['files']| length}} == 1" + - "unarchive10['files']| length == 1" - "'foo-unarchive.txt' in unarchive10['files']" - archive_path.stat.exists
test/integration/targets/unarchive/tasks/test_zip.yml+1 −1 modified@@ -17,7 +17,7 @@ - "unarchive03.changed == true" # Verify that file list is generated - "'files' in unarchive03" - - "{{unarchive03['files']| length}} == 3" + - "unarchive03['files']| length == 3" - "'foo-unarchive.txt' in unarchive03['files']" - "'foo-unarchive-777.txt' in unarchive03['files']" - "'FOO-UNAR.TXT' in unarchive03['files']"
test/integration/targets/wait_for/tasks/main.yml+4 −4 modified@@ -40,7 +40,7 @@ assert: that: - waitfor is successful - - waitfor.path == "{{ remote_tmp_dir | expanduser }}/wait_for_file" + - waitfor.path == remote_tmp_dir | expanduser ~ "/wait_for_file" - waitfor.elapsed >= 2 - waitfor.elapsed <= 15 @@ -58,7 +58,7 @@ assert: that: - waitfor is successful - - waitfor.path == "{{ remote_tmp_dir | expanduser }}/wait_for_file" + - waitfor.path == remote_tmp_dir | expanduser ~ "/wait_for_file" - waitfor.elapsed >= 2 - waitfor.elapsed <= 15 @@ -165,7 +165,7 @@ that: - waitfor is successful - waitfor is not changed - - "waitfor.port == {{ http_port }}" + - "waitfor.port == http_port" - name: install psutil using pip (non-Linux only) pip: @@ -193,7 +193,7 @@ that: - waitfor is successful - waitfor is not changed - - "waitfor.port == {{ http_port }}" + - "waitfor.port == http_port" - name: test wait_for with delay wait_for:
test/units/parsing/yaml/test_dumper.py+2 −5 modified@@ -29,7 +29,6 @@ from ansible.parsing.yaml.loader import AnsibleLoader from ansible.module_utils.six import PY2 from ansible.template import AnsibleUndefined -from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes from units.mock.yaml_helper import YamlTestUtils from units.mock.vault_helper import TextVaultSecret @@ -69,8 +68,7 @@ def test_ansible_vault_encrypted_unicode(self): def test_bytes(self): b_text = u'tréma'.encode('utf-8') - unsafe_object = AnsibleUnsafeBytes(b_text) - yaml_out = self._dump_string(unsafe_object, dumper=self.dumper) + yaml_out = self._dump_string(b_text, dumper=self.dumper) stream = self._build_stream(yaml_out) loader = self._loader(stream) @@ -97,8 +95,7 @@ def test_bytes(self): def test_unicode(self): u_text = u'nöel' - unsafe_object = AnsibleUnsafeText(u_text) - yaml_out = self._dump_string(unsafe_object, dumper=self.dumper) + yaml_out = self._dump_string(u_text, dumper=self.dumper) stream = self._build_stream(yaml_out) loader = self._loader(stream)
270b39f6ff02Ensure that unsafe is more difficult to lose [stable-2.16] (#82293)
58 files changed · +526 −151
changelogs/fragments/cve-2023-5764.yml+6 −0 added@@ -0,0 +1,6 @@ +security_fixes: +- templating - Address issues where internal templating can cause unsafe + variables to lose their unsafe designation (CVE-2023-5764) +breaking_changes: +- assert - Nested templating may result in an inability for the conditional + to be evaluated. See the porting guide for more information.
lib/ansible/module_utils/common/json.py+2 −2 modified@@ -30,7 +30,7 @@ def _preprocess_unsafe_encode(value): Used in ``AnsibleJSONEncoder.iterencode`` """ if _is_unsafe(value): - value = {'__ansible_unsafe': to_text(value, errors='surrogate_or_strict', nonstring='strict')} + value = {'__ansible_unsafe': to_text(value._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')} elif is_sequence(value): value = [_preprocess_unsafe_encode(v) for v in value] elif isinstance(value, Mapping): @@ -63,7 +63,7 @@ def default(self, o): value = {'__ansible_vault': to_text(o._ciphertext, errors='surrogate_or_strict', nonstring='strict')} elif getattr(o, '__UNSAFE__', False): # unsafe object, this will never be triggered, see ``AnsibleJSONEncoder.iterencode`` - value = {'__ansible_unsafe': to_text(o, errors='surrogate_or_strict', nonstring='strict')} + value = {'__ansible_unsafe': to_text(o._strip_unsafe(), errors='surrogate_or_strict', nonstring='strict')} elif isinstance(o, Mapping): # hostvars and other objects value = dict(o)
lib/ansible/parsing/yaml/dumper.py+5 −1 modified@@ -24,7 +24,7 @@ from ansible.module_utils.six import text_type, binary_type from ansible.module_utils.common.yaml import SafeDumper from ansible.parsing.yaml.objects import AnsibleUnicode, AnsibleSequence, AnsibleMapping, AnsibleVaultEncryptedUnicode -from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText +from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText, NativeJinjaText, _is_unsafe from ansible.template import AnsibleUndefined from ansible.vars.hostvars import HostVars, HostVarsVars from ansible.vars.manager import VarsWithSources @@ -47,10 +47,14 @@ def represent_vault_encrypted_unicode(self, data): def represent_unicode(self, data): + if _is_unsafe(data): + data = data._strip_unsafe() return yaml.representer.SafeRepresenter.represent_str(self, text_type(data)) def represent_binary(self, data): + if _is_unsafe(data): + data = data._strip_unsafe() return yaml.representer.SafeRepresenter.represent_binary(self, binary_type(data))
lib/ansible/playbook/conditional.py+5 −5 modified@@ -21,7 +21,7 @@ import typing as t -from ansible.errors import AnsibleError, AnsibleUndefinedVariable +from ansible.errors import AnsibleError, AnsibleUndefinedVariable, AnsibleTemplateError from ansible.module_utils.common.text.converters import to_native from ansible.playbook.attribute import FieldAttribute from ansible.template import Templar @@ -102,14 +102,14 @@ def _check_conditional(self, conditional: str, templar: Templar, all_vars: dict[ return False # If the result of the first-pass template render (to resolve inline templates) is marked unsafe, - # explicitly disable lookups on the final pass to prevent evaluation of untrusted content in the - # constructed template. - disable_lookups = hasattr(conditional, '__UNSAFE__') + # explicitly fail since the next templating operation would never evaluate + if hasattr(conditional, '__UNSAFE__'): + raise AnsibleTemplateError('Conditional is marked as unsafe, and cannot be evaluated.') # NOTE The spaces around True and False are intentional to short-circuit literal_eval for # jinja2_native=False and avoid its expensive calls. return templar.template( "{%% if %s %%} True {%% else %%} False {%% endif %%}" % conditional, - disable_lookups=disable_lookups).strip() == "True" + ).strip() == "True" except AnsibleUndefinedVariable as e: raise AnsibleUndefinedVariable("error while evaluating conditional (%s): %s" % (original, e))
lib/ansible/playbook/task.py+24 −0 modified@@ -289,6 +289,30 @@ def post_validate(self, templar): super(Task, self).post_validate(templar) + def _post_validate_args(self, attr, value, templar): + # smuggle an untemplated copy of the task args for actions that need more control over the templating of their + # input (eg, debug's var/msg, assert's "that" conditional expressions) + self.untemplated_args = value + + # now recursively template the args dict + args = templar.template(value) + + # FIXME: could we just nuke this entirely and/or wrap it up in ModuleArgsParser or something? + if '_variable_params' in args: + variable_params = args.pop('_variable_params') + if isinstance(variable_params, dict): + if C.INJECT_FACTS_AS_VARS: + display.warning("Using a variable for a task's 'args' is unsafe in some situations " + "(see https://docs.ansible.com/ansible/devel/reference_appendices/faq.html#argsplat-unsafe)") + variable_params.update(args) + args = variable_params + else: + # if we didn't get a dict, it means there's garbage remaining after k=v parsing, just give up + # see https://github.com/ansible/ansible/issues/79862 + raise AnsibleError(f"invalid or malformed argument: '{variable_params}'") + + return args + def _post_validate_loop(self, attr, value, templar): ''' Override post validation for the loop field, which is templated
lib/ansible/plugins/action/assert.py+22 −1 modified@@ -64,8 +64,29 @@ def run(self, tmp=None, task_vars=None): quiet = boolean(self._task.args.get('quiet', False), strict=False) + # directly access 'that' via untemplated args from the task so we can intelligently trust embedded + # templates and preserve the original inputs/locations for better messaging on assert failures and + # errors. + # FIXME: even in devel, things like `that: item` don't always work properly (truthy string value + # is not really an embedded expression) + # we could fix that by doing direct var lookups on the inputs + # FIXME: some form of this code should probably be shared between debug, assert, and + # Task.post_validate, since they + # have a lot of overlapping needs + try: + thats = self._task.untemplated_args['that'] + except KeyError: + # in the case of "we got our entire args dict from a template", we can just consult the + # post-templated dict (the damage has likely already been done for embedded templates anyway) + thats = self._task.args['that'] + + # FIXME: this is a case where we only want to resolve indirections, NOT recurse containers + # (and even then, the leaf-most expression being wrapped is at least suboptimal + # (since its expression will be "eaten"). + if isinstance(thats, str): + thats = self._templar.template(thats) + # make sure the 'that' items are a list - thats = self._task.args['that'] if not isinstance(thats, list): thats = [thats]
lib/ansible/plugins/callback/__init__.py+3 −1 modified@@ -38,7 +38,7 @@ from ansible.plugins import AnsiblePlugin from ansible.utils.color import stringc from ansible.utils.display import Display -from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText +from ansible.utils.unsafe_proxy import AnsibleUnsafeText, NativeJinjaUnsafeText, _is_unsafe from ansible.vars.clean import strip_internal_keys, module_response_deepcopy import yaml @@ -113,6 +113,8 @@ def _munge_data_for_lossy_yaml(scalar): def _pretty_represent_str(self, data): """Uses block style for multi-line strings""" + if _is_unsafe(data): + data = data._strip_unsafe() data = text_type(data) if _should_use_block(data): style = '|'
lib/ansible/plugins/filter/core.py+5 −0 modified@@ -37,6 +37,7 @@ from ansible.utils.encrypt import do_encrypt, PASSLIB_AVAILABLE from ansible.utils.hashing import md5s, checksum_s from ansible.utils.unicode import unicode_wrap +from ansible.utils.unsafe_proxy import _is_unsafe from ansible.utils.vars import merge_hash display = Display() @@ -215,6 +216,8 @@ def from_yaml(data): # The ``text_type`` call here strips any custom # string wrapper class, so that CSafeLoader can # read the data + if _is_unsafe(data): + data = data._strip_unsafe() return yaml_load(text_type(to_text(data, errors='surrogate_or_strict'))) return data @@ -224,6 +227,8 @@ def from_yaml_all(data): # The ``text_type`` call here strips any custom # string wrapper class, so that CSafeLoader can # read the data + if _is_unsafe(data): + data = data._strip_unsafe() return yaml_load_all(text_type(to_text(data, errors='surrogate_or_strict'))) return data
lib/ansible/plugins/lookup/first_found.py+13 −2 modified@@ -139,7 +139,6 @@ elements: path """ import os -import re from collections.abc import Mapping, Sequence @@ -150,10 +149,22 @@ from ansible.plugins.lookup import LookupBase +def _splitter(value, chars): + chars = set(chars) + v = '' + for c in value: + if c in chars: + yield v + v = '' + continue + v += c + yield v + + def _split_on(terms, spliters=','): termlist = [] if isinstance(terms, string_types): - termlist = re.split(r'[%s]' % ''.join(map(re.escape, spliters)), terms) + termlist = list(_splitter(terms, spliters)) else: # added since options will already listify for t in terms:
lib/ansible/template/__init__.py+13 −2 modified@@ -31,7 +31,7 @@ from numbers import Number from traceback import format_exc -from jinja2.exceptions import TemplateSyntaxError, UndefinedError +from jinja2.exceptions import TemplateSyntaxError, UndefinedError, SecurityError from jinja2.loaders import FileSystemLoader from jinja2.nativetypes import NativeEnvironment from jinja2.runtime import Context, StrictUndefined @@ -55,7 +55,7 @@ from ansible.utils.display import Display from ansible.utils.listify import listify_lookup_plugin_terms from ansible.utils.native_jinja import NativeJinjaText -from ansible.utils.unsafe_proxy import to_unsafe_text, wrap_var +from ansible.utils.unsafe_proxy import to_unsafe_text, wrap_var, AnsibleUnsafeText, AnsibleUnsafeBytes, NativeJinjaUnsafeText display = Display() @@ -365,10 +365,21 @@ class AnsibleContext(Context): flag is checked post-templating, and (when set) will result in the final templated result being wrapped in AnsibleUnsafe. ''' + _disallowed_callables = frozenset({ + AnsibleUnsafeText._strip_unsafe.__qualname__, + AnsibleUnsafeBytes._strip_unsafe.__qualname__, + NativeJinjaUnsafeText._strip_unsafe.__qualname__, + }) + def __init__(self, *args, **kwargs): super(AnsibleContext, self).__init__(*args, **kwargs) self.unsafe = False + def call(self, obj, *args, **kwargs): + if getattr(obj, '__qualname__', None) in self._disallowed_callables or obj in self._disallowed_callables: + raise SecurityError(f"{obj!r} is not safely callable") + return super().call(obj, *args, **kwargs) + def _is_unsafe(self, val): ''' Our helper function, which will also recursively check dict and
lib/ansible/utils/unsafe_proxy.py+259 −6 modified@@ -69,15 +69,264 @@ class AnsibleUnsafe(object): class AnsibleUnsafeBytes(binary_type, AnsibleUnsafe): - def decode(self, *args, **kwargs): - """Wrapper method to ensure type conversions maintain unsafe context""" - return AnsibleUnsafeText(super(AnsibleUnsafeBytes, self).decode(*args, **kwargs)) + def _strip_unsafe(self): + return super().__bytes__() + + def __str__(self, /): # pylint: disable=invalid-str-returned + return self.encode() + + def __bytes__(self, /): # pylint: disable=invalid-bytes-returned + return self + + def __repr__(self, /): # pylint: disable=invalid-repr-returned + return AnsibleUnsafeText(super().__repr__()) + + def __format__(self, format_spec, /): # pylint: disable=invalid-format-returned + return self.__class__(super().__format__(format_spec)) + + def __getitem__(self, key, /): + return self.__class__(super().__getitem__(key)) + + def __iter__(self, /): + cls = self.__class__ + return (cls(c) for c in super().__iter__()) + + def __reversed__(self, /): + return self[::-1] + + def __add__(self, value, /): + return self.__class__(super().__add__(value)) + + def __radd__(self, value, /): + return self.__class__(value.__add__(self)) + + def __mul__(self, value, /): + return self.__class__(super().__mul__(value)) + + __rmul__ = __mul__ + + def __mod__(self, value, /): + return self.__class__(super().__mod__(value)) + + def __rmod__(self, value, /): + return self.__class__(super().__rmod__(value)) + + def capitalize(self, /): + return self.__class__(super().capitalize()) + + def casefold(self, /): + return self.__class__(super().casefold()) + + def center(self, width, fillchar=b' ', /): + return self.__class__(super().center(width, fillchar)) + + def decode(self, /, encoding='utf-8', errors='strict'): + return AnsibleUnsafeText(super().decode(encoding=encoding, errors=errors)) + + def removeprefix(self, prefix, /): + return self.__class__(super().removeprefix(prefix)) + + def removesuffix(self, suffix, /): + return self.__class__(super().removesuffix(suffix)) + + def expandtabs(self, /, tabsize=8): + return self.__class__(super().expandtabs(tabsize)) + + def format(self, /, *args, **kwargs): + return self.__class__(super().format(*args, **kwargs)) + + def format_map(self, mapping, /): + return self.__class__(super().format_map(mapping)) + + def join(self, iterable_of_bytes, /): + return self.__class__(super().join(iterable_of_bytes)) + + def ljust(self, width, fillchar=b' ', /): + return self.__class__(super().ljust(width, fillchar)) + + def lower(self, /): + return self.__class__(super().lower()) + + def lstrip(self, bytes=None, /): + return self.__class__(super().lstrip(bytes)) + + def partition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().partition(sep)) + + def replace(self, old, new, count=-1, /): + return self.__class__(super().replace(old, new, count)) + + def rjust(self, width, fillchar=b' ', /): + return self.__class__(super().rjust(width, fillchar)) + + def rpartition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().rpartition(sep)) + + def rstrip(self, bytes=None, /): + return self.__class__(super().rstrip(bytes)) + + def split(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)] + + def rsplit(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)] + + def splitlines(self, /, keepends=False): + cls = self.__class__ + return [cls(e) for e in super().splitlines(keepends=keepends)] + + def strip(self, bytes=None, /): + return self.__class__(super().strip(bytes)) + + def swapcase(self, /): + return self.__class__(super().swapcase()) + + def title(self, /): + return self.__class__(super().title()) + + def translate(self, table, /, delete=b''): + return self.__class__(super().translate(table, delete=delete)) + + def upper(self, /): + return self.__class__(super().upper()) + + def zfill(self, width, /): + return self.__class__(super().zfill(width)) class AnsibleUnsafeText(text_type, AnsibleUnsafe): - def encode(self, *args, **kwargs): - """Wrapper method to ensure type conversions maintain unsafe context""" - return AnsibleUnsafeBytes(super(AnsibleUnsafeText, self).encode(*args, **kwargs)) + # def __getattribute__(self, name): + # print(f'attr: {name}') + # return object.__getattribute__(self, name) + + def _strip_unsafe(self, /): + return super().__str__() + + def __str__(self, /): # pylint: disable=invalid-str-returned + return self + + def __repr__(self, /): # pylint: disable=invalid-repr-returned + return self.__class__(super().__repr__()) + + def __format__(self, format_spec, /): # pylint: disable=invalid-format-returned + return self.__class__(super().__format__(format_spec)) + + def __getitem__(self, key, /): + return self.__class__(super().__getitem__(key)) + + def __iter__(self, /): + cls = self.__class__ + return (cls(c) for c in super().__iter__()) + + def __reversed__(self, /): + return self[::-1] + + def __add__(self, value, /): + return self.__class__(super().__add__(value)) + + def __radd__(self, value, /): + return self.__class__(value.__add__(self)) + + def __mul__(self, value, /): + return self.__class__(super().__mul__(value)) + + __rmul__ = __mul__ + + def __mod__(self, value, /): + return self.__class__(super().__mod__(value)) + + def __rmod__(self, value, /): + return self.__class__(super().__rmod__(value)) + + def capitalize(self, /): + return self.__class__(super().capitalize()) + + def casefold(self, /): + return self.__class__(super().casefold()) + + def center(self, width, fillchar=' ', /): + return self.__class__(super().center(width, fillchar)) + + def encode(self, /, encoding='utf-8', errors='strict'): + return AnsibleUnsafeBytes(super().encode(encoding=encoding, errors=errors)) + + def removeprefix(self, prefix, /): + return self.__class__(super().removeprefix(prefix)) + + def removesuffix(self, suffix, /): + return self.__class__(super().removesuffix(suffix)) + + def expandtabs(self, /, tabsize=8): + return self.__class__(super().expandtabs(tabsize)) + + def format(self, /, *args, **kwargs): + return self.__class__(super().format(*args, **kwargs)) + + def format_map(self, mapping, /): + return self.__class__(super().format_map(mapping)) + + def join(self, iterable, /): + return self.__class__(super().join(iterable)) + + def ljust(self, width, fillchar=' ', /): + return self.__class__(super().ljust(width, fillchar)) + + def lower(self, /): + return self.__class__(super().lower()) + + def lstrip(self, chars=None, /): + return self.__class__(super().lstrip(chars)) + + def partition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().partition(sep)) + + def replace(self, old, new, count=-1, /): + return self.__class__(super().replace(old, new, count)) + + def rjust(self, width, fillchar=' ', /): + return self.__class__(super().rjust(width, fillchar)) + + def rpartition(self, sep, /): + cls = self.__class__ + return tuple(cls(e) for e in super().rpartition(sep)) + + def rstrip(self, chars=None, /): + return self.__class__(super().rstrip(chars)) + + def split(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().split(sep=sep, maxsplit=maxsplit)] + + def rsplit(self, /, sep=None, maxsplit=-1): + cls = self.__class__ + return [cls(e) for e in super().rsplit(sep=sep, maxsplit=maxsplit)] + + def splitlines(self, /, keepends=False): + cls = self.__class__ + return [cls(e) for e in super().splitlines(keepends=keepends)] + + def strip(self, chars=None, /): + return self.__class__(super().strip(chars)) + + def swapcase(self, /): + return self.__class__(super().swapcase()) + + def title(self, /): + return self.__class__(super().title()) + + def translate(self, table, /): + return self.__class__(super().translate(table)) + + def upper(self, /): + return self.__class__(super().upper()) + + def zfill(self, width, /): + return self.__class__(super().zfill(width)) class NativeJinjaUnsafeText(NativeJinjaText, AnsibleUnsafeText): @@ -126,3 +375,7 @@ def to_unsafe_bytes(*args, **kwargs): def to_unsafe_text(*args, **kwargs): return wrap_var(to_text(*args, **kwargs)) + + +def _is_unsafe(obj): + return getattr(obj, '__UNSAFE__', False) is True
test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml+8 −8 modified@@ -13,22 +13,22 @@ in command.stdout_lines - >- "Installing 'namespace_1.collection_1:1.0.0' to - '{{ install_path }}/namespace_1/collection_1'" + '" ~ install_path ~ "/namespace_1/collection_1'" in command.stdout_lines - >- 'Created collection for namespace_1.collection_1:1.0.0 at - {{ install_path }}/namespace_1/collection_1' + ' ~ install_path ~ '/namespace_1/collection_1' in command.stdout_lines - >- 'namespace_1.collection_1:1.0.0 was installed successfully' in command.stdout_lines - >- "Installing 'namespace_2.collection_2:1.0.0' to - '{{ install_path }}/namespace_2/collection_2'" + '" ~ install_path ~ "/namespace_2/collection_2'" in command.stdout_lines - >- 'Created collection for namespace_2.collection_2:1.0.0 at - {{ install_path }}/namespace_2/collection_2' + ' ~ install_path ~ '/namespace_2/collection_2' in command.stdout_lines - >- 'namespace_2.collection_2:1.0.0 was installed successfully' @@ -58,22 +58,22 @@ in command.stdout_lines - >- "Installing 'namespace_1.collection_1:1.0.0' to - '{{ install_path }}/namespace_1/collection_1'" + '" ~ install_path ~ "/namespace_1/collection_1'" in command.stdout_lines - >- 'Created collection for namespace_1.collection_1:1.0.0 at - {{ install_path }}/namespace_1/collection_1' + ' ~ install_path ~ '/namespace_1/collection_1' in command.stdout_lines - >- 'namespace_1.collection_1:1.0.0 was installed successfully' in command.stdout_lines - >- "Installing 'namespace_2.collection_2:1.0.0' to - '{{ install_path }}/namespace_2/collection_2'" + '" ~ install_path ~ "/namespace_2/collection_2'" in command.stdout_lines - >- 'Created collection for namespace_2.collection_2:1.0.0 at - {{ install_path }}/namespace_2/collection_2' + ' ~ install_path ~ '/namespace_2/collection_2' in command.stdout_lines - >- 'namespace_2.collection_2:1.0.0 was installed successfully'
test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml+1 −1 modified@@ -2,7 +2,7 @@ - name: Assert that a embedded vault of a string with no newline works assert: that: - - '"{{ vault_encrypted_one_line_var }}" == "Setec Astronomy"' + - 'vault_encrypted_one_line_var == "Setec Astronomy"' - name: Assert that a multi line embedded vault works, including new line assert:
test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml+1 −1 modified@@ -2,7 +2,7 @@ - name: Assert that a vault encrypted file with embedded vault of a string with no newline works assert: that: - - '"{{ vault_file_encrypted_with_encrypted_one_line_var }}" == "Setec Astronomy"' + - 'vault_file_encrypted_with_encrypted_one_line_var == "Setec Astronomy"' - name: Assert that a vault encrypted file with multi line embedded vault works, including new line assert:
test/integration/targets/apt_repository/tasks/apt.yml+6 −6 modified@@ -50,7 +50,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_name}}"' + - 'result.repo == test_ppa_name' - name: 'examine apt cache mtime' stat: path='/var/cache/apt/pkgcache.bin' @@ -81,7 +81,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_name}}"' + - 'result.repo == test_ppa_name' - name: 'examine apt cache mtime' stat: path='/var/cache/apt/pkgcache.bin' @@ -112,7 +112,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_name}}"' + - 'result.repo == test_ppa_name' - name: 'examine apt cache mtime' stat: path='/var/cache/apt/pkgcache.bin' @@ -151,7 +151,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_spec}}"' + - 'result.repo == test_ppa_spec' - '"sources_added" in result' - 'result.sources_added | length == 1' - '"git" in result.sources_added[0]' @@ -176,7 +176,7 @@ that: - 'result.changed' - 'result.state == "absent"' - - 'result.repo == "{{test_ppa_spec}}"' + - 'result.repo == test_ppa_spec' - '"sources_added" in result' - 'result.sources_added | length == 0' - '"sources_removed" in result' @@ -207,7 +207,7 @@ that: - 'result.changed' - 'result.state == "present"' - - 'result.repo == "{{test_ppa_spec}}"' + - 'result.repo == test_ppa_spec' - name: 'examine source file' stat: path='/etc/apt/sources.list.d/{{test_ppa_filename}}.list'
test/integration/targets/assert/assert.out.nested_tmpl.stderr+4 −0 added@@ -0,0 +1,4 @@ ++ ansible-playbook -i localhost, -c local nested_tmpl.yml +++ set +x +[WARNING]: conditional statements should not include jinja2 templating +delimiters such as {{ }} or {% %}. Found: "{{ foo }}" == "bar"
test/integration/targets/assert/assert.out.nested_tmpl.stdout+12 −0 added@@ -0,0 +1,12 @@ + +PLAY [localhost] *************************************************************** + +TASK [assert] ****************************************************************** +ok: [localhost] => { + "changed": false, + "msg": "All assertions passed" +} + +PLAY RECAP ********************************************************************* +localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +
test/integration/targets/assert/assert.out.quiet.stderr+0 −0 renamedtest/integration/targets/assert/assert.out.quiet.stdout+0 −0 renamedtest/integration/targets/assert/nested_tmpl.yml+9 −0 added@@ -0,0 +1,9 @@ +- hosts: localhost + gather_facts: False + tasks: + - assert: + that: + - '"{{ foo }}" == "bar"' + - foo == "bar" + vars: + foo: bar
test/integration/targets/assert/quiet.yml+2 −2 modified@@ -5,12 +5,12 @@ item_A: yes tasks: - assert: - that: "{{ item }} is defined" + that: "item is defined" quiet: True with_items: - item_A - assert: - that: "{{ item }} is defined" + that: "item is defined" quiet: False with_items: - item_A
test/integration/targets/assert/runme.sh+2 −1 modified@@ -45,7 +45,7 @@ cleanup() { fi } -BASEFILE=assert_quiet.out +BASEFILE=assert.out ORIGFILE="${BASEFILE}" OUTFILE="${BASEFILE}.new" @@ -69,3 +69,4 @@ export ANSIBLE_NOCOLOR=1 export ANSIBLE_RETRY_FILES_ENABLED=0 run_test quiet +run_test nested_tmpl
test/integration/targets/command_shell/tasks/main.yml+1 −1 modified@@ -320,7 +320,7 @@ assert: that: - shell_result0 is changed - - shell_result0.cmd == '{{ remote_tmp_dir_test }}/test.sh' + - shell_result0.cmd == remote_tmp_dir_test ~ '/test.sh' - shell_result0.rc == 0 - shell_result0.stderr == '' - shell_result0.stdout == 'win'
test/integration/targets/copy/tasks/tests.yml+21 −21 modified@@ -1250,7 +1250,7 @@ assert: that: - "copy_result6.changed" - - "copy_result6.dest == '{{remote_dir_expanded}}/multiline.txt'" + - "copy_result6.dest == remote_dir_expanded ~ '/multiline.txt'" - "copy_result6.checksum == '9cd0697c6a9ff6689f0afb9136fa62e0b3fee903'" # test overwriting a file as an unprivileged user (pull request #8624) @@ -2153,26 +2153,26 @@ assert: that: - testcase5 is changed - - "stat_new_dir_with_chown.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown.stat.gr_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_file1.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown_file1.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown_file1.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_file1.stat.gr_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_subdir.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown_subdir.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown_subdir.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_subdir.stat.gr_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_subdir_file12.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown_subdir_file12.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_link_file12.stat.uid == {{ ansible_copy_test_user.uid }}" - - "stat_new_dir_with_chown_link_file12.stat.gid == {{ ansible_copy_test_group.gid }}" - - "stat_new_dir_with_chown_link_file12.stat.pw_name == '{{ ansible_copy_test_user_name }}'" - - "stat_new_dir_with_chown_link_file12.stat.gr_name == '{{ ansible_copy_test_user_name }}'" + - "stat_new_dir_with_chown.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown.stat.gr_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_file1.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown_file1.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown_file1.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_file1.stat.gr_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_subdir.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown_subdir.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown_subdir.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_subdir.stat.gr_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_subdir_file12.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown_subdir_file12.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown_subdir_file12.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_subdir_file12.stat.gr_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_link_file12.stat.uid == ansible_copy_test_user.uid" + - "stat_new_dir_with_chown_link_file12.stat.gid == ansible_copy_test_group.gid" + - "stat_new_dir_with_chown_link_file12.stat.pw_name == ansible_copy_test_user_name" + - "stat_new_dir_with_chown_link_file12.stat.gr_name == ansible_copy_test_user_name" always: - name: execute - remove the user for test
test/integration/targets/debug/runme.sh+2 −0 modified@@ -18,3 +18,5 @@ done # ensure debug does not set top level vars when looking at ansible_facts ansible-playbook nosetfacts.yml "$@" + +ansible-playbook unsafe.yml "$@"
test/integration/targets/debug/unsafe.yml+13 −0 added@@ -0,0 +1,13 @@ +- hosts: localhost + gather_facts: false + vars: + unsafe_var: !unsafe undef()|mandatory + tasks: + - debug: + var: '{{ unsafe_var }}' + ignore_errors: true + register: result + + - assert: + that: + - result is successful
test/integration/targets/expect/tasks/main.yml+1 −1 modified@@ -117,7 +117,7 @@ - name: assert chdir works assert: that: - - "'{{chdir_result.stdout | trim}}' == '{{remote_tmp_dir_real_path.stdout | trim}}'" + - "chdir_result.stdout | trim == remote_tmp_dir_real_path.stdout | trim" - name: test timeout option expect:
test/integration/targets/file/tasks/main.yml+1 −1 modified@@ -927,7 +927,7 @@ that: - "file_error3 is failed" - "file_error3.msg == 'src does not exist'" - - "file_error3.dest == '{{ remote_tmp_dir_test }}/hard.txt' | expanduser" + - "file_error3.dest == remote_tmp_dir_test | expanduser ~ '/hard.txt'" - "file_error3.src == 'non-existing-file-that-does-not-exist.txt'" - block:
test/integration/targets/file/tasks/state_link.yml+1 −1 modified@@ -199,7 +199,7 @@ - "missing_dst_no_follow_enable_force_use_mode2 is changed" - "missing_dst_no_follow_enable_force_use_mode3 is not changed" - "soft3_result['stat'].islnk" - - "soft3_result['stat'].lnk_target == '{{ user.home }}/nonexistent'" + - "soft3_result['stat'].lnk_target == user.home ~ '/nonexistent'" # # Test creating a link to a directory https://github.com/ansible/ansible/issues/1369
test/integration/targets/find/tasks/main.yml+6 −6 modified@@ -267,7 +267,7 @@ - name: assert we skipped the ogg file assert: that: - - '"{{ remote_tmp_dir_test }}/e/f/g/h/8.ogg" not in find_test3_list' + - 'remote_tmp_dir_test ~ "/e/f/g/h/8.ogg" not in find_test3_list' - name: patterns with regex find: @@ -317,7 +317,7 @@ assert: that: - result.matched == 1 - - '"{{ remote_tmp_dir_test }}/astest/old.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/old.txt" in astest_list' - name: find files newer than 1 week find: @@ -332,7 +332,7 @@ assert: that: - result.matched == 1 - - '"{{ remote_tmp_dir_test }}/astest/new.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/new.txt" in astest_list' - name: add some content to the new file shell: "echo hello world > {{ remote_tmp_dir_test }}/astest/new.txt" @@ -352,7 +352,7 @@ assert: that: - result.matched == 1 - - '"{{ remote_tmp_dir_test }}/astest/new.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/new.txt" in astest_list' - '"checksum" in result.files[0]' - name: find ANY item with LESS than 5 bytes, also get checksums @@ -371,8 +371,8 @@ assert: that: - result.matched == 2 - - '"{{ remote_tmp_dir_test }}/astest/old.txt" in astest_list' - - '"{{ remote_tmp_dir_test }}/astest/.hidden.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/old.txt" in astest_list' + - 'remote_tmp_dir_test ~ "/astest/.hidden.txt" in astest_list' - '"checksum" in result.files[0]' - name: Run mode tests
test/integration/targets/find/tasks/mode.yml+1 −1 modified@@ -61,7 +61,7 @@ - assert: that: - exact_mode_0644.files == exact_mode_0644_symbolic.files - - exact_mode_0644.files[0].path == '{{ remote_tmp_dir_test }}/mode_0644' + - exact_mode_0644.files[0].path == remote_tmp_dir_test ~ '/mode_0644' - user_readable_octal.files == user_readable_symbolic.files - user_readable_octal.files|map(attribute='path')|map('basename')|sort == ['mode_0400', 'mode_0444', 'mode_0644', 'mode_0666', 'mode_0700'] - other_readable_octal.files == other_readable_symbolic.files
test/integration/targets/gathering_facts/test_gathering_facts.yml+2 −2 modified@@ -433,7 +433,7 @@ - name: Test reading facts from default fact_path assert: that: - - '"{{ ansible_local.testfact.fact_dir }}" == "default"' + - 'ansible_local.testfact.fact_dir == "default"' - hosts: facthost9 tags: [ 'fact_local'] @@ -444,7 +444,7 @@ - name: Test reading facts from custom fact_path assert: that: - - '"{{ ansible_local.testfact.fact_dir }}" == "custom"' + - 'ansible_local.testfact.fact_dir == "custom"' - hosts: facthost20 tags: [ 'fact_facter_ohai' ]
test/integration/targets/git/tasks/depth.yml+1 −1 modified@@ -172,7 +172,7 @@ - name: DEPTH | check update arrived assert: that: - - "{{ a_file.content | b64decode | trim }} == 3" + - a_file.content | b64decode | trim == "3" - git_fetch is changed - name: DEPTH | clear checkout_dir
test/integration/targets/git/tasks/localmods.yml+2 −2 modified@@ -58,7 +58,7 @@ - name: LOCALMODS | check update arrived assert: that: - - "{{ a_file.content | b64decode | trim }} == 2" + - a_file.content | b64decode | trim == "2" - git_fetch_force is changed - name: LOCALMODS | clear checkout_dir @@ -127,7 +127,7 @@ - name: LOCALMODS | check update arrived assert: that: - - "{{ a_file.content | b64decode | trim }} == 2" + - a_file.content | b64decode | trim == "2" - git_fetch_force is changed - name: LOCALMODS | clear checkout_dir
test/integration/targets/git/tasks/submodules.yml+7 −7 modified@@ -32,7 +32,7 @@ - name: SUBMODULES | Ensure submodu1 is at the appropriate commit assert: - that: '{{ submodule1.stdout_lines | length }} == 2' + that: 'submodule1.stdout_lines | length == 2' - name: SUBMODULES | clear checkout_dir file: @@ -53,7 +53,7 @@ - name: SUBMODULES | Ensure submodule1 is at the appropriate commit assert: - that: '{{ submodule1.stdout_lines | length }} == 4' + that: 'submodule1.stdout_lines | length == 4' - name: SUBMODULES | Copy the checkout so we can run several different tests on it command: 'cp -pr {{ checkout_dir }} {{ checkout_dir }}.bak' @@ -84,8 +84,8 @@ - name: SUBMODULES | Ensure both submodules are at the appropriate commit assert: that: - - '{{ submodule1.stdout_lines|length }} == 4' - - '{{ submodule2.stdout_lines|length }} == 2' + - 'submodule1.stdout_lines|length == 4' + - 'submodule2.stdout_lines|length == 2' - name: SUBMODULES | Remove checkout dir @@ -112,7 +112,7 @@ - name: SUBMODULES | Ensure submodule1 is at the appropriate commit assert: - that: '{{ submodule1.stdout_lines | length }} == 5' + that: 'submodule1.stdout_lines | length == 5' - name: SUBMODULES | Test that update with recursive found new submodules @@ -121,7 +121,7 @@ - name: SUBMODULES | Enusre submodule2 is at the appropriate commit assert: - that: '{{ submodule2.stdout_lines | length }} == 4' + that: 'submodule2.stdout_lines | length == 4' - name: SUBMODULES | clear checkout_dir file: @@ -147,4 +147,4 @@ - name: SUBMODULES | Ensure submodule1 is at the appropriate commit assert: - that: '{{ submodule1.stdout_lines | length }} == 4' + that: 'submodule1.stdout_lines | length == 4'
test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml+2 −2 modified@@ -22,7 +22,7 @@ - name: Check that multiple duplicate lines collapse into a single commands assert: that: - - "{{ result.commands|length }} == 1" + - "result.commands|length == 1" - name: Check that set is correctly prepended assert: @@ -58,6 +58,6 @@ - assert: that: - - "{{ result.filtered|length }} == 2" + - "result.filtered|length == 2" - debug: msg="END cli/config_check.yaml on connection={{ ansible_connection }}"
test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml+4 −4 modified@@ -16,17 +16,17 @@ - name: Assert that the before dicts were correctly generated assert: that: - - "{{ populate | symmetric_difference(result['before']) |length == 0 }}" + - "populate | symmetric_difference(result['before']) |length == 0" - name: Assert that the correct set of commands were generated assert: that: - - "{{ deleted['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + - "deleted['commands'] | symmetric_difference(result['commands']) |length == 0" - name: Assert that the after dicts were correctly generated assert: that: - - "{{ deleted['after'] | symmetric_difference(result['after']) |length == 0 }}" + - "deleted['after'] | symmetric_difference(result['after']) |length == 0" - name: Delete attributes of given interfaces (IDEMPOTENT) vyos.vyos.vyos_lldp_interfaces: *deleted @@ -41,6 +41,6 @@ - name: Assert that the before dicts were correctly generated assert: that: - - "{{ deleted['after'] | symmetric_difference(result['before']) |length == 0 }}" + - "deleted['after'] | symmetric_difference(result['before']) |length == 0" always: - include_tasks: _remove_config.yaml
test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml+4 −4 modified@@ -28,17 +28,17 @@ - name: Assert that before dicts were correctly generated assert: - that: "{{ merged['before'] | symmetric_difference(result['before']) |length == 0 }}" + that: "merged['before'] | symmetric_difference(result['before']) |length == 0" - name: Assert that correct set of commands were generated assert: that: - - "{{ merged['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + - "merged['commands'] | symmetric_difference(result['commands']) |length == 0" - name: Assert that after dicts was correctly generated assert: that: - - "{{ merged['after'] | symmetric_difference(result['after']) |length == 0 }}" + - "merged['after'] | symmetric_difference(result['after']) |length == 0" - name: Merge the provided configuration with the existing running configuration (IDEMPOTENT) vyos.vyos.vyos_lldp_interfaces: *merged @@ -52,7 +52,7 @@ - name: Assert that before dicts were correctly generated assert: that: - - "{{ merged['after'] | symmetric_difference(result['before']) |length == 0 }}" + - "merged['after'] | symmetric_difference(result['before']) |length == 0" always: - include_tasks: _remove_config.yaml
test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml+4 −4 modified@@ -19,17 +19,17 @@ - name: Assert that before dicts were correctly generated assert: that: - - "{{ populate_intf | symmetric_difference(result['before']) |length == 0 }}" + - "populate_intf | symmetric_difference(result['before']) |length == 0" - name: Assert that correct commands were generated assert: that: - - "{{ overridden['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + - "overridden['commands'] | symmetric_difference(result['commands']) |length == 0" - name: Assert that after dicts were correctly generated assert: that: - - "{{ overridden['after'] | symmetric_difference(result['after']) |length == 0 }}" + - "overridden['after'] | symmetric_difference(result['after']) |length == 0" - name: Overrides all device configuration with provided configurations (IDEMPOTENT) vyos.vyos.vyos_lldp_interfaces: *overridden @@ -43,7 +43,7 @@ - name: Assert that before dicts were correctly generated assert: that: - - "{{ overridden['after'] | symmetric_difference(result['before']) |length == 0 }}" + - "overridden['after'] | symmetric_difference(result['before']) |length == 0" always: - include_tasks: _remove_config.yaml
test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml+4 −4 modified@@ -33,17 +33,17 @@ - name: Assert that correct set of commands were generated assert: that: - - "{{ replaced['commands'] | symmetric_difference(result['commands']) |length == 0 }}" + - "replaced['commands'] | symmetric_difference(result['commands']) |length == 0" - name: Assert that before dicts are correctly generated assert: that: - - "{{ populate | symmetric_difference(result['before']) |length == 0 }}" + - "populate | symmetric_difference(result['before']) |length == 0" - name: Assert that after dict is correctly generated assert: that: - - "{{ replaced['after'] | symmetric_difference(result['after']) |length == 0 }}" + - "replaced['after'] | symmetric_difference(result['after']) |length == 0" - name: Replace device configurations of listed LLDP interfaces with provided configurarions (IDEMPOTENT) vyos.vyos.vyos_lldp_interfaces: *replaced @@ -57,7 +57,7 @@ - name: Assert that before dict is correctly generated assert: that: - - "{{ replaced['after'] | symmetric_difference(result['before']) |length == 0 }}" + - "replaced['after'] | symmetric_difference(result['before']) |length == 0" always: - include_tasks: _remove_config.yaml
test/integration/targets/include_vars/tasks/main.yml+16 −16 modified@@ -15,7 +15,7 @@ that: - "testing == 789" - "base_dir == 'environments/development'" - - "{{ included_one_file.ansible_included_var_files | length }} == 1" + - "included_one_file.ansible_included_var_files | length == 1" - "'vars/environments/development/all.yml' in included_one_file.ansible_included_var_files[0]" - name: include the vars/environments/development/all.yml and save results in all @@ -51,7 +51,7 @@ assert: that: - webapp_version is defined - - "'file_without_extension' in '{{ include_without_file_extension.ansible_included_var_files | join(' ') }}'" + - "'file_without_extension' in include_without_file_extension.ansible_included_var_files | join(' ')" - name: include every directory in vars include_vars: @@ -67,7 +67,7 @@ - "testing == 456" - "base_dir == 'services'" - "webapp_containers == 10" - - "{{ include_every_dir.ansible_included_var_files | length }} == 7" + - "include_every_dir.ansible_included_var_files | length == 7" - "'vars/all/all.yml' in include_every_dir.ansible_included_var_files[0]" - "'vars/environments/development/all.yml' in include_every_dir.ansible_included_var_files[1]" - "'vars/environments/development/services/webapp.yml' in include_every_dir.ansible_included_var_files[2]" @@ -88,9 +88,9 @@ that: - "testing == 789" - "base_dir == 'environments/development'" - - "{{ include_without_webapp.ansible_included_var_files | length }} == 4" - - "'webapp.yml' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'" - - "'file_without_extension' not in '{{ include_without_webapp.ansible_included_var_files | join(' ') }}'" + - "include_without_webapp.ansible_included_var_files | length == 4" + - "'webapp.yml' not in include_without_webapp.ansible_included_var_files | join(' ')" + - "'file_without_extension' not in include_without_webapp.ansible_included_var_files | join(' ')" - name: include only files matching webapp.yml include_vars: @@ -104,9 +104,9 @@ - "testing == 101112" - "base_dir == 'development/services'" - "webapp_containers == 20" - - "{{ include_match_webapp.ansible_included_var_files | length }} == 1" + - "include_match_webapp.ansible_included_var_files | length == 1" - "'vars/environments/development/services/webapp.yml' in include_match_webapp.ansible_included_var_files[0]" - - "'all.yml' not in '{{ include_match_webapp.ansible_included_var_files | join(' ') }}'" + - "'all.yml' not in include_match_webapp.ansible_included_var_files | join(' ')" - name: include only files matching webapp.yml and store results in webapp include_vars: @@ -173,10 +173,10 @@ - name: Verify the hash variable assert: that: - - "{{ config | length }} == 3" + - "config | length == 3" - "config.key0 == 0" - "config.key1 == 0" - - "{{ config.key2 | length }} == 1" + - "config.key2 | length == 1" - "config.key2.a == 21" - name: Include the second file to merge the hash variable @@ -187,10 +187,10 @@ - name: Verify that the hash is merged assert: that: - - "{{ config | length }} == 4" + - "config | length == 4" - "config.key0 == 0" - "config.key1 == 1" - - "{{ config.key2 | length }} == 2" + - "config.key2 | length == 2" - "config.key2.a == 21" - "config.key2.b == 22" - "config.key3 == 3" @@ -202,9 +202,9 @@ - name: Verify that the properties from the first file is cleared assert: that: - - "{{ config | length }} == 3" + - "config | length == 3" - "config.key1 == 1" - - "{{ config.key2 | length }} == 1" + - "config.key2 | length == 1" - "config.key2.b == 22" - "config.key3 == 3" @@ -216,10 +216,10 @@ - name: Verify that the hash is merged after vars files are accumulated assert: that: - - "{{ config | length }} == 3" + - "config | length == 3" - "config.key0 is undefined" - "config.key1 == 1" - - "{{ config.key2 | length }} == 1" + - "config.key2 | length == 1" - "config.key2.b == 22" - "config.key3 == 3"
test/integration/targets/lookup_first_found/tasks/main.yml+4 −4 modified@@ -109,8 +109,8 @@ - name: Load variables specific for OS family assert: that: - - "{{item|quote}} is file" - - "{{item|basename == 'itworks.yml'}}" + - "item is file" + - "item|basename == 'itworks.yml'" with_first_found: - files: - "{{ansible_id}}-{{ansible_lsb.major_release}}.yml" # invalid var, should be skipped @@ -124,8 +124,8 @@ - name: Load variables specific for OS family, but now as list of dicts, same options as above assert: that: - - "{{item|quote}} is file" - - "{{item|basename == 'itworks.yml'}}" + - "item is file" + - "item|basename == 'itworks.yml'" with_first_found: - files: - "{{ansible_id}}-{{ansible_lsb.major_release}}.yml"
test/integration/targets/lookup_ini/test_lookup_properties.yml+1 −1 modified@@ -10,7 +10,7 @@ field_with_space: "{{lookup('ini', 'field.with.space type=properties file=lookup.properties')}}" - assert: - that: "{{item}} is defined" + that: "item is defined" with_items: [ 'test1', 'test2', 'test_dot', 'field_with_space' ] - name: "read ini value"
test/integration/targets/lookup_subelements/tasks/main.yml+3 −3 modified@@ -133,7 +133,7 @@ - assert: that: - - "'{{ item.0.name }}' != 'carol'" + - "item.0.name != 'carol'" with_subelements: - "{{ users }}" - mysql.privs @@ -220,5 +220,5 @@ - assert: that: - - "'{{ user_alice }}' == 'localhost'" - - "'{{ user_bob }}' == 'db1'" + - "user_alice == 'localhost'" + - "user_bob == 'db1'"
test/integration/targets/loop_control/inner.yml+2 −2 modified@@ -3,7 +3,7 @@ that: - ansible_loop.index == ansible_loop.index0 + 1 - ansible_loop.revindex == ansible_loop.revindex0 + 1 - - ansible_loop.first == {{ ansible_loop.index == 1 }} - - ansible_loop.last == {{ ansible_loop.index == ansible_loop.length }} + - ansible_loop.first == (ansible_loop.index == 1) + - ansible_loop.last == (ansible_loop.index == ansible_loop.length) - ansible_loop.length == 3 - ansible_loop.allitems|join(',') == 'first,second,third'
test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml+1 −1 modified@@ -13,4 +13,4 @@ - assert: that: - '"location" in result' - - 'result["location"] == "{{ expected_location}}"' + - 'result["location"] == expected_location'
test/integration/targets/module_precedence/modules_test_multiple_roles.yml+1 −1 modified@@ -14,4 +14,4 @@ - assert: that: - '"location" in result' - - 'result["location"] == "{{ expected_location}}"' + - 'result["location"] == expected_location'
test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml+1 −1 modified@@ -7,4 +7,4 @@ assert: that: - '"location" in result' - - 'result["location"] == "{{ expected_location }}"' + - 'result["location"] == expected_location'
test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml+1 −1 modified@@ -7,4 +7,4 @@ assert: that: - '"location" in result' - - 'result["location"] == "{{ expected_location }}"' + - 'result["location"] == expected_location'
test/integration/targets/script/tasks/main.yml+2 −2 modified@@ -209,7 +209,7 @@ assert: that: - _check_mode_test2 is skipped - - '_check_mode_test2.msg == "{{ remote_tmp_dir_test | expanduser }}/afile2.txt exists, matching creates option"' + - '_check_mode_test2.msg == remote_tmp_dir_test | expanduser ~ "/afile2.txt exists, matching creates option"' - name: Remove afile2.txt file: @@ -231,7 +231,7 @@ assert: that: - _check_mode_test3 is skipped - - '_check_mode_test3.msg == "{{ remote_tmp_dir_test | expanduser }}/afile2.txt does not exist, matching removes option"' + - '_check_mode_test3.msg == remote_tmp_dir_test | expanduser ~ "/afile2.txt does not exist, matching removes option"' # executable
test/integration/targets/slurp/tasks/main.yml+1 −1 modified@@ -33,7 +33,7 @@ - 'slurp_existing.encoding == "base64"' - 'slurp_existing is not changed' - 'slurp_existing is not failed' - - '"{{ slurp_existing.content | b64decode }}" == "We are at the café"' + - 'slurp_existing.content | b64decode == "We are at the café"' - name: Create a binary file to test with copy:
test/integration/targets/template/tasks/main.yml+1 −1 modified@@ -357,7 +357,7 @@ - assert: that: - "\"foo t'e~m\\plated\" in unusual_results.stdout_lines" - - "{{unusual_results.stdout_lines| length}} == 1" + - "unusual_results.stdout_lines| length == 1" - name: check that the unusual filename can be checked for changes template:
test/integration/targets/unarchive/tasks/test_missing_binaries.yml+1 −1 modified@@ -66,7 +66,7 @@ - zip_success.changed # Verify that file list is generated - "'files' in zip_success" - - "{{zip_success['files']| length}} == 3" + - "zip_success['files']| length == 3" - "'foo-unarchive.txt' in zip_success['files']" - "'foo-unarchive-777.txt' in zip_success['files']" - "'FOO-UNAR.TXT' in zip_success['files']"
test/integration/targets/unarchive/tasks/test_mode.yml+4 −4 modified@@ -47,7 +47,7 @@ - "unarchive06_stat.stat.mode == '0600'" # Verify that file list is generated - "'files' in unarchive06" - - "{{unarchive06['files']| length}} == 1" + - "unarchive06['files']| length == 1" - "'foo-unarchive.txt' in unarchive06['files']" - name: remove our tar.gz unarchive destination @@ -97,7 +97,7 @@ - "unarchive07.changed == false" # Verify that file list is generated - "'files' in unarchive07" - - "{{unarchive07['files']| length}} == 1" + - "unarchive07['files']| length == 1" - "'foo-unarchive.txt' in unarchive07['files']" - name: remove our tar.gz unarchive destination @@ -131,7 +131,7 @@ - "unarchive08_stat.stat.mode == '0601'" # Verify that file list is generated - "'files' in unarchive08" - - "{{unarchive08['files']| length}} == 3" + - "unarchive08['files']| length == 3" - "'foo-unarchive.txt' in unarchive08['files']" - "'foo-unarchive-777.txt' in unarchive08['files']" - "'FOO-UNAR.TXT' in unarchive08['files']" @@ -163,7 +163,7 @@ - "unarchive08_stat.stat.mode == '0601'" # Verify that file list is generated - "'files' in unarchive08" - - "{{unarchive08['files']| length}} == 3" + - "unarchive08['files']| length == 3" - "'foo-unarchive.txt' in unarchive08['files']" - "'foo-unarchive-777.txt' in unarchive08['files']" - "'FOO-UNAR.TXT' in unarchive08['files']"
test/integration/targets/unarchive/tasks/test_unprivileged_user.yml+1 −1 modified@@ -40,7 +40,7 @@ - unarchive10 is changed # Verify that file list is generated - "'files' in unarchive10" - - "{{unarchive10['files']| length}} == 1" + - "unarchive10['files']| length == 1" - "'foo-unarchive.txt' in unarchive10['files']" - archive_path.stat.exists
test/integration/targets/unarchive/tasks/test_zip.yml+1 −1 modified@@ -17,7 +17,7 @@ - "unarchive03.changed == true" # Verify that file list is generated - "'files' in unarchive03" - - "{{unarchive03['files']| length}} == 3" + - "unarchive03['files']| length == 3" - "'foo-unarchive.txt' in unarchive03['files']" - "'foo-unarchive-777.txt' in unarchive03['files']" - "'FOO-UNAR.TXT' in unarchive03['files']"
test/integration/targets/wait_for/tasks/main.yml+4 −4 modified@@ -40,7 +40,7 @@ assert: that: - waitfor is successful - - waitfor.path == "{{ remote_tmp_dir | expanduser }}/wait_for_file" + - waitfor.path == remote_tmp_dir | expanduser ~ "/wait_for_file" - waitfor.elapsed >= 2 - waitfor.elapsed <= 15 @@ -58,7 +58,7 @@ assert: that: - waitfor is successful - - waitfor.path == "{{ remote_tmp_dir | expanduser }}/wait_for_file" + - waitfor.path == remote_tmp_dir | expanduser ~ "/wait_for_file" - waitfor.elapsed >= 2 - waitfor.elapsed <= 15 @@ -165,7 +165,7 @@ that: - waitfor is successful - waitfor is not changed - - "waitfor.port == {{ http_port }}" + - "waitfor.port == http_port" - name: install psutil using pip (non-Linux only) pip: @@ -193,7 +193,7 @@ that: - waitfor is successful - waitfor is not changed - - "waitfor.port == {{ http_port }}" + - "waitfor.port == http_port" - name: test wait_for with delay wait_for:
test/units/parsing/yaml/test_dumper.py+2 −5 modified@@ -27,7 +27,6 @@ from ansible.parsing.yaml import dumper, objects from ansible.parsing.yaml.loader import AnsibleLoader from ansible.template import AnsibleUndefined -from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes from units.mock.yaml_helper import YamlTestUtils from units.mock.vault_helper import TextVaultSecret @@ -67,8 +66,7 @@ def test_ansible_vault_encrypted_unicode(self): def test_bytes(self): b_text = u'tréma'.encode('utf-8') - unsafe_object = AnsibleUnsafeBytes(b_text) - yaml_out = self._dump_string(unsafe_object, dumper=self.dumper) + yaml_out = self._dump_string(b_text, dumper=self.dumper) stream = self._build_stream(yaml_out) loader = self._loader(stream) @@ -81,8 +79,7 @@ def test_bytes(self): def test_unicode(self): u_text = u'nöel' - unsafe_object = AnsibleUnsafeText(u_text) - yaml_out = self._dump_string(unsafe_object, dumper=self.dumper) + yaml_out = self._dump_string(u_text, dumper=self.dumper) stream = self._build_stream(yaml_out) loader = self._loader(stream)
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- access.redhat.com/errata/RHSA-2023:7773ghsavendor-advisoryx_refsource_REDHATWEB
- github.com/advisories/GHSA-7j69-qfc3-2fq9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-5764ghsaADVISORY
- access.redhat.com/security/cve/CVE-2023-5764ghsavdb-entryx_refsource_REDHATWEB
- bugzilla.redhat.com/show_bug.cgighsaissue-trackingx_refsource_REDHATWEB
- github.com/ansible/ansible/commit/270b39f6ff02511a2199505161218cbd1a5ae34fghsaWEB
- github.com/ansible/ansible/commit/7239d2d371bc6e274cbb7314e01431adce6ae25aghsaWEB
- github.com/ansible/ansible/commit/fea130480d261ea5bf6fcd5cf19a348f1686ceb1ghsaWEB
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/X7Q6CHPVCHMZS5M7V22GOKFSXZAQ24EUghsaWEB
News mentions
0No linked articles in our index yet.