From 797e6bb2204bddb37528ee2ffded2f858b4a0851 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 8 Aug 2024 15:14:05 -0400 Subject: [PATCH] Add vaulted_file test (#83717) * Add vaulted_file test * fix is_encrypted_file while we are here Co-authored-by: Martin Krizek --- .../fragments/vaulted_file_and_fixes.yml | 5 +++++ lib/ansible/parsing/dataloader.py | 4 ++-- lib/ansible/parsing/vault/__init__.py | 8 +++++--- lib/ansible/plugins/test/core.py | 16 +++++++++++++++- lib/ansible/plugins/test/vault_encrypted.yml | 12 ++++++------ lib/ansible/plugins/test/vaulted_file.yml | 19 +++++++++++++++++++ .../targets/test_core/files/notvault | 1 + .../targets/test_core/files/vault1 | 6 ++++++ .../targets/test_core/files/vault2 | 6 ++++++ .../targets/test_core/tasks/main.yml | 7 +++++++ test/units/parsing/vault/test_vault.py | 4 ++-- 11 files changed, 74 insertions(+), 14 deletions(-) create mode 100644 changelogs/fragments/vaulted_file_and_fixes.yml create mode 100644 lib/ansible/plugins/test/vaulted_file.yml create mode 100644 test/integration/targets/test_core/files/notvault create mode 100644 test/integration/targets/test_core/files/vault1 create mode 100644 test/integration/targets/test_core/files/vault2 diff --git a/changelogs/fragments/vaulted_file_and_fixes.yml b/changelogs/fragments/vaulted_file_and_fixes.yml new file mode 100644 index 00000000000..fbb85ec5471 --- /dev/null +++ b/changelogs/fragments/vaulted_file_and_fixes.yml @@ -0,0 +1,5 @@ +bugfixes: + - vault.is_encrypted_file is now optimized to be called in runtime and not for being called in tests + - vault_encrypted test documentation, name and examples have been fixed, other parts were clarified +minor_changes: + - vaulted_file test filter added, to test if the provided path is an 'Ansible vaulted' file diff --git a/lib/ansible/parsing/dataloader.py b/lib/ansible/parsing/dataloader.py index 9554aef36be..b2eaaafba50 100644 --- a/lib/ansible/parsing/dataloader.py +++ b/lib/ansible/parsing/dataloader.py @@ -18,7 +18,7 @@ from ansible.module_utils.six import binary_type, text_type from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text from ansible.parsing.quoting import unquote from ansible.parsing.utils.yaml import from_yaml -from ansible.parsing.vault import VaultLib, b_HEADER, is_encrypted, is_encrypted_file, parse_vaulttext_envelope, PromptVaultSecret +from ansible.parsing.vault import VaultLib, is_encrypted, is_encrypted_file, parse_vaulttext_envelope, PromptVaultSecret from ansible.utils.path import unfrackpath from ansible.utils.display import Display @@ -388,7 +388,7 @@ class DataLoader: # Limit how much of the file is read since we do not know # whether this is a vault file and therefore it could be very # large. - if is_encrypted_file(f, count=len(b_HEADER)): + if is_encrypted_file(f): # if the file is encrypted and no password was specified, # the decrypt call would throw an error, but we check first # since the decrypt function doesn't know the file name diff --git a/lib/ansible/parsing/vault/__init__.py b/lib/ansible/parsing/vault/__init__.py index eddc028c085..fb835bd3cfa 100644 --- a/lib/ansible/parsing/vault/__init__.py +++ b/lib/ansible/parsing/vault/__init__.py @@ -103,15 +103,17 @@ def is_encrypted(data): return False -def is_encrypted_file(file_obj, start_pos=0, count=-1): +def is_encrypted_file(file_obj, start_pos=0, count=len(b_HEADER)): """Test if the contents of a file obj are a vault encrypted data blob. :arg file_obj: A file object that will be read from. :kwarg start_pos: A byte offset in the file to start reading the header from. Defaults to 0, the beginning of the file. :kwarg count: Read up to this number of bytes from the file to determine - if it looks like encrypted vault data. The default is -1, read to the - end of file. + if it looks like encrypted vault data. The default is the size of the + the vault header, which is what is needed most times. + For some IO classes, or files that don't begin with the vault itself, + set to -1 to read to the end of file. :returns: True if the file looks like a vault file. Otherwise, False. """ # read the header and reset the file stream to where it started diff --git a/lib/ansible/plugins/test/core.py b/lib/ansible/plugins/test/core.py index 01e672bbff3..a01e0d9950d 100644 --- a/lib/ansible/plugins/test/core.py +++ b/lib/ansible/plugins/test/core.py @@ -25,8 +25,9 @@ from collections.abc import MutableMapping, MutableSequence from ansible.module_utils.compat.version import LooseVersion, StrictVersion from ansible import errors -from ansible.module_utils.common.text.converters import to_native, to_text +from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes from ansible.module_utils.parsing.convert_bool import boolean +from ansible.parsing.vault import is_encrypted_file from ansible.utils.display import Display from ansible.utils.version import SemanticVersion @@ -143,6 +144,18 @@ def vault_encrypted(value): return getattr(value, '__ENCRYPTED__', False) and value.is_encrypted() +def vaulted_file(value): + """Evaluate whether a file is a vault + + .. versionadded:: 2.18 + """ + try: + with open(to_bytes(value), 'rb') as f: + return is_encrypted_file(f) + except (OSError, IOError) as e: + raise errors.AnsibleFilterError(f"Cannot test if the file {value} is a vault", orig_exc=e) + + def match(value, pattern='', ignorecase=False, multiline=False): ''' Perform a `re.match` returning a boolean ''' return regex(value, pattern, ignorecase, multiline, 'match') @@ -282,4 +295,5 @@ class TestModule(object): # vault 'vault_encrypted': vault_encrypted, + 'vaulted_file': vaulted_file, } diff --git a/lib/ansible/plugins/test/vault_encrypted.yml b/lib/ansible/plugins/test/vault_encrypted.yml index 276b07f931f..2e8d2df6de2 100644 --- a/lib/ansible/plugins/test/vault_encrypted.yml +++ b/lib/ansible/plugins/test/vault_encrypted.yml @@ -1,18 +1,18 @@ DOCUMENTATION: - name: truthy + name: vault_encrypted author: Ansible Core version_added: "2.10" - short_description: Is this an encrypted vault + short_description: Is this an encrypted vault string description: - - Verifies if the input is an Ansible vault. + - Verifies if the input string is an Ansible vault or not options: _input: - description: The possible vault. + description: The possible vault string type: string required: True EXAMPLES: | - thisisfalse: '{{ "any string" is ansible_vault }}' - thisistrue: '{{ "$ANSIBLE_VAULT;1.2;AES256;dev...." is ansible_vault }}' + thisisfalse: '{{ "any string" is vault_encryped}}' + thisistrue: '{{ "$ANSIBLE_VAULT;1.2;AES256;dev...." is vault_encrypted}}' RETURN: _value: description: Returns V(True) if the input is a valid ansible vault, V(False) otherwise. diff --git a/lib/ansible/plugins/test/vaulted_file.yml b/lib/ansible/plugins/test/vaulted_file.yml new file mode 100644 index 00000000000..2fa28d06ae5 --- /dev/null +++ b/lib/ansible/plugins/test/vaulted_file.yml @@ -0,0 +1,19 @@ +DOCUMENTATION: + name: vaulted_file + author: Ansible Core + version_added: "2.18" + short_description: Is this file an encrypted vault + description: + - Verifies if the input path is an Ansible vault file. + options: + _input: + description: The path to the possible vault. + type: path + required: True +EXAMPLES: | + thisisfalse: '{{ "/etc/hosts" is vaulted_file}}' + thisistrue: '{{ "/path/to/vaulted/file" is vaulted_file}}' +RETURN: + _value: + description: Returns V(True) if the path is a valid ansible vault, V(False) otherwise. + type: boolean diff --git a/test/integration/targets/test_core/files/notvault b/test/integration/targets/test_core/files/notvault new file mode 100644 index 00000000000..59e2674ada6 --- /dev/null +++ b/test/integration/targets/test_core/files/notvault @@ -0,0 +1 @@ +this is not a vault!!! diff --git a/test/integration/targets/test_core/files/vault1 b/test/integration/targets/test_core/files/vault1 new file mode 100644 index 00000000000..5ade6bf2c22 --- /dev/null +++ b/test/integration/targets/test_core/files/vault1 @@ -0,0 +1,6 @@ +$ANSIBLE_VAULT;1.1;AES256 +36383565336439373638626535313963373838613433343434663465656531316530663865363434 +3137636638393164646537643034303962633365613463310a303164353037663062376539313834 +65303131346266643934306364653063663061326165323737666636633363383337393731613030 +3164613334616465620a386234323862623764363834336364336662313932633866303262646565 +3861 diff --git a/test/integration/targets/test_core/files/vault2 b/test/integration/targets/test_core/files/vault2 new file mode 100644 index 00000000000..59bf6f9abd3 --- /dev/null +++ b/test/integration/targets/test_core/files/vault2 @@ -0,0 +1,6 @@ +$ANSIBLE_VAULT;1.2;AES256;yolo +61303130346664303063346433663231333561616535623536613465663137626332633339326666 +3565393632323837663930656461643266663634643332300a343030333136353966636537326537 +36383536386164373565636335316436316538313135346264383561316333386461343262323766 +6335663033373666370a316432313733303338363936313364663532313335383239646561313864 +6663 diff --git a/test/integration/targets/test_core/tasks/main.yml b/test/integration/targets/test_core/tasks/main.yml index ac06d67eb00..7c4ed65e48b 100644 --- a/test/integration/targets/test_core/tasks/main.yml +++ b/test/integration/targets/test_core/tasks/main.yml @@ -361,3 +361,10 @@ that: - vaulted_value is vault_encrypted - inventory_hostname is not vault_encrypted + +- name: Check vaulted_file test + assert: + that: + - "'files/notvault' is not vaulted_file" + - "'files/vault1' is vaulted_file" + - "'files/vault2' is vaulted_file" diff --git a/test/units/parsing/vault/test_vault.py b/test/units/parsing/vault/test_vault.py index 982ae82c406..3b84b475f4d 100644 --- a/test/units/parsing/vault/test_vault.py +++ b/test/units/parsing/vault/test_vault.py @@ -450,12 +450,12 @@ class TestVaultIsEncryptedFile(unittest.TestCase): data = u"$ANSIBLE_VAULT;9.9;TEST\n%s" % u"ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ " b_data = to_bytes(data) b_data_fo = io.BytesIO(b_data) - self.assertFalse(vault.is_encrypted_file(b_data_fo)) + self.assertFalse(vault.is_encrypted_file(b_data_fo, count=-1)) def test_text_file_handle_invalid(self): data = u"$ANSIBLE_VAULT;9.9;TEST\n%s" % u"ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ " data_fo = io.StringIO(data) - self.assertFalse(vault.is_encrypted_file(data_fo)) + self.assertFalse(vault.is_encrypted_file(data_fo, count=-1)) def test_file_already_read_from_finds_header(self): b_data = b"$ANSIBLE_VAULT;9.9;TEST\n%s" % hexlify(b"ansible\ntesting\nfile pos")