Restore 2.18 vault tag YAML dump behavior (#85275)

* Doing conditional redaction/formatting needs other bits that aren't ready for 2.19.

Co-authored-by: Matt Clay <matt@mystile.com>
pull/85276/head
Matt Davis 6 months ago committed by GitHub
parent ea7ad90c31
commit 2b7204527b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -140,8 +140,6 @@ deprecated_features:
- conditionals - Conditionals using Jinja templating delimiters (e.g., ``{{``, ``{%``) should be rewritten as expressions without delimiters, unless the entire conditional value is a single template that resolves to a trusted string expression. - conditionals - Conditionals using Jinja templating delimiters (e.g., ``{{``, ``{%``) should be rewritten as expressions without delimiters, unless the entire conditional value is a single template that resolves to a trusted string expression.
This is useful for dynamic indirection of conditional expressions, but is limited to trusted literal string expressions. This is useful for dynamic indirection of conditional expressions, but is limited to trusted literal string expressions.
- templating - The ``disable_lookups`` option has no effect, since plugins must be updated to apply trust before any templating can be performed. - templating - The ``disable_lookups`` option has no effect, since plugins must be updated to apply trust before any templating can be performed.
- to_yaml/to_nice_yaml filters - Implicit YAML dumping of vaulted value ciphertext is deprecated.
Set `dump_vault_tags` to explicitly specify the desired behavior.
- plugin error handling - The ``AnsibleError`` constructor arg ``suppress_extended_error`` is deprecated. - plugin error handling - The ``AnsibleError`` constructor arg ``suppress_extended_error`` is deprecated.
Using ``suppress_extended_error=True`` has the same effect as ``show_content=False``. Using ``suppress_extended_error=True`` has the same effect as ``show_content=False``.
- config - The ``ACTION_WARNINGS`` config has no effect. It previously disabled command warnings, which have since been removed. - config - The ``ACTION_WARNINGS`` config has no effect. It previously disabled command warnings, which have since been removed.

@ -34,12 +34,6 @@ class _BaseDumper(SafeDumper, metaclass=abc.ABCMeta):
class AnsibleDumper(_BaseDumper): class AnsibleDumper(_BaseDumper):
"""A simple stub class that allows us to add representers for our custom types.""" """A simple stub class that allows us to add representers for our custom types."""
# DTFIX0: need a better way to handle serialization controls during YAML dumping
def __init__(self, *args, dump_vault_tags: bool | None = None, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._dump_vault_tags = dump_vault_tags
@classmethod @classmethod
def _register_representers(cls) -> None: def _register_representers(cls) -> None:
cls.add_multi_representer(AnsibleTaggedObject, cls.represent_ansible_tagged_object) cls.add_multi_representer(AnsibleTaggedObject, cls.represent_ansible_tagged_object)
@ -49,15 +43,7 @@ class AnsibleDumper(_BaseDumper):
cls.add_multi_representer(_jinja_common.VaultExceptionMarker, cls.represent_vault_exception_marker) cls.add_multi_representer(_jinja_common.VaultExceptionMarker, cls.represent_vault_exception_marker)
def get_node_from_ciphertext(self, data: object) -> ScalarNode | None: def get_node_from_ciphertext(self, data: object) -> ScalarNode | None:
if self._dump_vault_tags is not False and (ciphertext := VaultHelper.get_ciphertext(data, with_tags=False)): if ciphertext := VaultHelper.get_ciphertext(data, with_tags=False):
# deprecated: description='enable the deprecation warning below' core_version='2.23'
# if self._dump_vault_tags is None:
# Display().deprecated(
# msg="Implicit YAML dumping of vaulted value ciphertext is deprecated.",
# version="2.27",
# help_text="Set `dump_vault_tags` to explicitly specify the desired behavior.",
# )
return self.represent_scalar('!vault', ciphertext, style='|') return self.represent_scalar('!vault', ciphertext, style='|')
return None return None

@ -14,7 +14,6 @@ import sys
import typing as t import typing as t
import argparse import argparse
import functools
from ansible import constants as C from ansible import constants as C
from ansible import context from ansible import context
@ -162,11 +161,10 @@ class InventoryCLI(CLI):
def dump(stuff): def dump(stuff):
if context.CLIARGS['yaml']: if context.CLIARGS['yaml']:
import yaml import yaml
from ansible.parsing.yaml.dumper import AnsibleDumper from ansible.parsing.yaml.dumper import AnsibleDumper
# DTFIX0: need shared infra to smuggle custom kwargs to dumpers, since yaml.dump cannot (as of PyYAML 6.0.1) results = to_text(yaml.dump(stuff, Dumper=AnsibleDumper, default_flow_style=False, allow_unicode=True))
dumper = functools.partial(AnsibleDumper, dump_vault_tags=True)
results = to_text(yaml.dump(stuff, Dumper=dumper, default_flow_style=False, allow_unicode=True))
elif context.CLIARGS['toml']: elif context.CLIARGS['toml']:
results = toml_dumps(stuff) results = toml_dumps(stuff)
else: else:

@ -48,11 +48,9 @@ UUID_NAMESPACE_ANSIBLE = uuid.UUID('361E6D51-FAEC-444A-9079-341386DA8E2E')
@accept_lazy_markers @accept_lazy_markers
def to_yaml(a, *_args, default_flow_style: bool | None = None, dump_vault_tags: bool | None = None, **kwargs) -> str: def to_yaml(a, *_args, default_flow_style: bool | None = None, **kwargs) -> str:
"""Serialize input as terse flow-style YAML.""" """Serialize input as terse flow-style YAML."""
dumper = partial(AnsibleDumper, dump_vault_tags=dump_vault_tags) return yaml.dump(a, Dumper=AnsibleDumper, allow_unicode=True, default_flow_style=default_flow_style, **kwargs)
return yaml.dump(a, Dumper=dumper, allow_unicode=True, default_flow_style=default_flow_style, **kwargs)
@accept_lazy_markers @accept_lazy_markers

@ -234,29 +234,6 @@
- result is failed - result is failed
- result.msg is contains("undecryptable variable") - result.msg is contains("undecryptable variable")
- name: undecryptable vault values in a scalar template should trip on YAML serialization
debug:
msg: '{{ dict_with_undecryptable_vault | to_yaml(dump_vault_tags=False) }}'
ignore_errors: true
register: result
- assert:
that:
- result is failed
- result.msg is contains("undecryptable variable")
- name: undecryptable vault values in a list of templates should trip on YAML serialization
debug:
msg:
- '{{ dict_with_undecryptable_vault | to_yaml(dump_vault_tags=False) }}'
ignore_errors: true
register: result
- assert:
that:
- result is failed
- result.msg is contains("undecryptable variable")
- name: vaulted values are not trusted for templating - name: vaulted values are not trusted for templating
vars: vars:
# value is {{ "not trusted" }} encrypted with "blarblar" # value is {{ "not trusted" }} encrypted with "blarblar"

@ -2,12 +2,11 @@ from __future__ import annotations
import pytest import pytest
from ansible.errors import AnsibleUndefinedVariable, AnsibleTemplateError from ansible.errors import AnsibleUndefinedVariable
from ansible.parsing.utils.yaml import from_yaml from ansible.parsing.utils.yaml import from_yaml
from ansible.parsing.vault import EncryptedString from ansible.parsing.vault import EncryptedString
from ansible.template import Templar, trust_as_template, is_trusted_as_template from ansible.template import Templar, trust_as_template, is_trusted_as_template
from units.mock.vault_helper import VaultTestHelper from units.mock.vault_helper import VaultTestHelper
from units.test_utils.controller.display import emits_warnings
undecryptable_value = EncryptedString( undecryptable_value = EncryptedString(
ciphertext="$ANSIBLE_VAULT;1.1;AES256\n" ciphertext="$ANSIBLE_VAULT;1.1;AES256\n"
@ -17,33 +16,22 @@ undecryptable_value = EncryptedString(
) )
@pytest.mark.parametrize("filter_name, dump_vault_tags", ( @pytest.mark.parametrize("filter_name", (
("to_yaml", True), "to_yaml",
("to_yaml", False), "to_nice_yaml",
("to_yaml", None), # cover the future case that will trigger a deprecation warning
("to_nice_yaml", True),
("to_nice_yaml", False),
("to_nice_yaml", None), # cover the future case that will trigger a deprecation warning
)) ))
def test_yaml_dump(filter_name: str, _vault_secrets_context: VaultTestHelper, dump_vault_tags: bool) -> None: def test_yaml_dump(filter_name: str, _vault_secrets_context: VaultTestHelper) -> None:
"""Verify YAML dumping round-trips only values which are expected to be supported.""" """Verify YAML dumping round-trips only values which are expected to be supported."""
payload = dict( payload = dict(
trusted=trust_as_template('trusted'), trusted=trust_as_template('trusted'),
untrusted="untrusted", untrusted="untrusted",
decryptable=VaultTestHelper().make_encrypted_string("hi mom"), decryptable=VaultTestHelper().make_encrypted_string("hi mom"),
)
if dump_vault_tags:
payload.update(
undecryptable=undecryptable_value, undecryptable=undecryptable_value,
) )
original = dict(a_list=[payload]) original = dict(a_list=[payload])
templar = Templar(variables=dict(original=original)) templar = Templar(variables=dict(original=original))
result = templar.template(trust_as_template(f"{{{{ original | {filter_name} }}}}"))
with emits_warnings(warning_pattern=[], deprecation_pattern=[]): # this will require updates once implicit vault tag dumping is deprecated
result = templar.template(trust_as_template(f"{{{{ original | {filter_name}(dump_vault_tags={dump_vault_tags}) }}}}"))
data = from_yaml(trust_as_template(result)) data = from_yaml(trust_as_template(result))
assert len(data) == len(original) assert len(data) == len(original)
@ -58,9 +46,8 @@ def test_yaml_dump(filter_name: str, _vault_secrets_context: VaultTestHelper, du
assert is_trusted_as_template(result_item['trusted']) assert is_trusted_as_template(result_item['trusted'])
assert is_trusted_as_template(result_item['untrusted']) # round-tripping trust is NOT supported assert is_trusted_as_template(result_item['untrusted']) # round-tripping trust is NOT supported
assert is_trusted_as_template(result_item['decryptable']) is not dump_vault_tags assert not is_trusted_as_template(result_item['decryptable'])
if dump_vault_tags:
assert result_item['decryptable']._ciphertext == original_item['decryptable']._ciphertext assert result_item['decryptable']._ciphertext == original_item['decryptable']._ciphertext
assert result_item['undecryptable']._ciphertext == original_item['undecryptable']._ciphertext assert result_item['undecryptable']._ciphertext == original_item['undecryptable']._ciphertext
@ -74,13 +61,6 @@ def test_yaml_dump_undefined() -> None:
templar.template(trust_as_template("{{ dict_with_undefined | to_yaml }}")) templar.template(trust_as_template("{{ dict_with_undefined | to_yaml }}"))
def test_yaml_dump_undecryptable_without_vault_tags(_vault_secrets_context: VaultTestHelper) -> None:
templar = Templar(variables=dict(list_with_undecrypted=[undecryptable_value]))
with pytest.raises(AnsibleTemplateError, match="undecryptable"):
templar.template(trust_as_template('{{ list_with_undecrypted | to_yaml(dump_vault_tags=false) }}'))
@pytest.mark.parametrize("value, expected", ( @pytest.mark.parametrize("value, expected", (
((1, 2, 3), "[1, 2, 3]\n"), ((1, 2, 3), "[1, 2, 3]\n"),
({1, 2, 3}, "!!set {1: null, 2: null, 3: null}\n"), ({1, 2, 3}, "!!set {1: null, 2: null, 3: null}\n"),

@ -86,19 +86,13 @@ class TestAnsibleDumper(unittest.TestCase, YamlTestUtils):
self._dump_string(_DEFAULT_UNDEF, dumper=self.dumper) self._dump_string(_DEFAULT_UNDEF, dumper=self.dumper)
@pytest.mark.parametrize("filter_impl, dump_vault_tags, expected_output, expected_warning", [ @pytest.mark.parametrize("filter_impl, expected_output", [
(to_yaml, True, "!vault |-\n ciphertext\n", None), (to_yaml, "!vault |-\n ciphertext\n"),
(to_yaml, None, "!vault |-\n ciphertext\n", "Implicit YAML dumping"), (to_nice_yaml, "!vault |-\n ciphertext\n"),
(to_yaml, False, "secret plaintext\n", None),
(to_nice_yaml, True, "!vault |-\n ciphertext\n", None),
(to_nice_yaml, None, "!vault |-\n ciphertext\n", "Implicit YAML dumping"),
(to_nice_yaml, False, "secret plaintext\n", None),
]) ])
def test_vaulted_value_dump( def test_vaulted_value_dump(
filter_impl: t.Callable, filter_impl: t.Callable,
dump_vault_tags: bool | None,
expected_output: str, expected_output: str,
expected_warning: str | None,
mocker: pytest_mock.MockerFixture mocker: pytest_mock.MockerFixture
) -> None: ) -> None:
"""Validate that strings tagged VaultedValue are represented properly.""" """Validate that strings tagged VaultedValue are represented properly."""
@ -108,15 +102,10 @@ def test_vaulted_value_dump(
_deprecated_spy = mocker.spy(Display(), 'deprecated') _deprecated_spy = mocker.spy(Display(), 'deprecated')
res = filter_impl(value, dump_vault_tags=dump_vault_tags) res = filter_impl(value)
assert res == expected_output assert res == expected_output
# deprecated: description='enable the assertion for the deprecation warning below' core_version='2.21'
# if expected_warning:
# assert _deprecated_spy.call_count == 1
# assert expected_warning in _deprecated_spy.call_args.kwargs['msg']
_test_tag = Deprecated(msg="test") _test_tag = Deprecated(msg="test")

Loading…
Cancel
Save