diff --git a/CHANGELOG.md b/CHANGELOG.md
index 216a2744844..d75d010ec2e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,6 +42,7 @@ Ansible Changes By Release
* Fix cpu facts on sparc64 (https://github.com/ansible/ansible/pull/30261)
* Fix ansible_distribution fact for Arch linux (https://github.com/ansible/ansible/issues/30600)
* remove print statements from play_context/become
+* Fix vault errors after 'ansible-vault edit' (https://github.com/ansible/ansible/issues/30575)
diff --git a/lib/ansible/parsing/vault/__init__.py b/lib/ansible/parsing/vault/__init__.py
index e02587e63de..ef08e602fb1 100644
--- a/lib/ansible/parsing/vault/__init__.py
+++ b/lib/ansible/parsing/vault/__init__.py
@@ -491,6 +491,20 @@ class VaultLib:
return b_vaulttext
def decrypt(self, vaulttext, filename=None):
+ '''Decrypt a piece of vault encrypted data.
+
+ :arg vaulttext: a string to decrypt. Since vault encrypted data is an
+ ascii text format this can be either a byte str or unicode string.
+ :kwarg filename: a filename that the data came from. This is only
+ used to make better error messages in case the data cannot be
+ decrypted.
+ :returns: a byte string containing the decrypted data and the vault-id that was used
+
+ '''
+ plaintext, vault_id = self.decrypt_and_get_vault_id(vaulttext, filename=filename)
+ return plaintext
+
+ def decrypt_and_get_vault_id(self, vaulttext, filename=None):
"""Decrypt a piece of vault encrypted data.
:arg vaulttext: a string to decrypt. Since vault encrypted data is an
@@ -498,7 +512,8 @@ class VaultLib:
:kwarg filename: a filename that the data came from. This is only
used to make better error messages in case the data cannot be
decrypted.
- :returns: a byte string containing the decrypted data
+ :returns: a byte string containing the decrypted data and the vault-id that was used
+
"""
b_vaulttext = to_bytes(vaulttext, errors='strict', encoding='utf-8')
@@ -536,6 +551,7 @@ class VaultLib:
# we check it first.
vault_id_matchers = []
+ vault_id_used = None
if vault_id:
display.vvvvv('Found a vault_id (%s) in the vaulttext' % (vault_id))
@@ -563,6 +579,7 @@ class VaultLib:
display.vvvv('Trying secret %s for vault_id=%s' % (vault_secret, vault_secret_id))
b_plaintext = this_cipher.decrypt(b_vaulttext, vault_secret)
if b_plaintext is not None:
+ vault_id_used = vault_secret_id
display.vvvvv('decrypt succesful with secret=%s and vault_id=%s' % (vault_secret, vault_secret_id))
break
except AnsibleError as e:
@@ -581,7 +598,7 @@ class VaultLib:
msg += " on %s" % filename
raise AnsibleError(msg)
- return b_plaintext
+ return b_plaintext, vault_id_used
class VaultEditor:
@@ -692,6 +709,7 @@ class VaultEditor:
# shuffle tmp file into place
self.shuffle_files(tmp_path, filename)
+ display.vvvvv('Saved edited file "%s" encrypted using %s and vault id "%s"' % (filename, secret, vault_id))
def _real_path(self, filename):
# '-' is special to VaultEditor, dont expand it.
@@ -754,7 +772,8 @@ class VaultEditor:
try:
# vaulttext gets converted back to bytes, but alas
- plaintext = self.vault.decrypt(vaulttext)
+ # TODO: return the vault_id that worked?
+ plaintext, vault_id_used = self.vault.decrypt_and_get_vault_id(vaulttext)
except AnsibleError as e:
raise AnsibleError("%s for %s" % (to_bytes(e), to_bytes(filename)))
@@ -762,15 +781,25 @@ class VaultEditor:
# (duplicates parts of decrypt, but alas...)
dummy, dummy, cipher_name, vault_id = parse_vaulttext_envelope(b_vaulttext)
- # if we could decrypt, the vault_id should be in secrets
+ # vault id here may not be the vault id actually used for decrypting
+ # as when the edited file has no vault-id but is decrypted by non-default id in secrets
+ # (vault_id=default, while a different vault-id decrypted)
+
+ # if we could decrypt, the vault_id should be in secrets or we use vault_id_used
# though we could have multiple secrets for a given vault_id, pick the first one
- secrets = match_secrets(self.vault.secrets, [vault_id])
+ secrets = match_secrets(self.vault.secrets, [vault_id_used, vault_id])
+
+ if not secrets:
+ raise AnsibleVaultError('Attempting to encrypt "%s" but no vault secrets were found for vault ids "%s" or "%s"' %
+ (filename, vault_id, vault_id_used))
+
secret = secrets[0][1]
+
if cipher_name not in CIPHER_WRITE_WHITELIST:
# we want to get rid of files encrypted with the AES cipher
- self._edit_file_helper(filename, secret, existing_data=plaintext, force_save=True)
+ self._edit_file_helper(filename, secret, existing_data=plaintext, force_save=True, vault_id=vault_id)
else:
- self._edit_file_helper(filename, secret, existing_data=plaintext, force_save=False)
+ self._edit_file_helper(filename, secret, existing_data=plaintext, force_save=False, vault_id=vault_id)
def plaintext(self, filename):
diff --git a/test/integration/targets/vault/faux-editor.py b/test/integration/targets/vault/faux-editor.py
new file mode 100755
index 00000000000..7f9983c07c9
--- /dev/null
+++ b/test/integration/targets/vault/faux-editor.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see .
+#
+# ansible-vault is a script that encrypts/decrypts YAML files. See
+# http://docs.ansible.com/playbooks_vault.html for more details.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+import time
+import os
+
+
+def main(args):
+ path = os.path.abspath(args[1])
+
+ fo = open(path, 'r+')
+
+ content = fo.readlines()
+
+ content.append('faux editor added at %s\n' % time.time())
+
+ fo.seek(0)
+ fo.write(''.join(content))
+ fo.close()
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[:]))
diff --git a/test/integration/targets/vault/runme.sh b/test/integration/targets/vault/runme.sh
index 2d4db75267e..4adfa83fbcb 100755
--- a/test/integration/targets/vault/runme.sh
+++ b/test/integration/targets/vault/runme.sh
@@ -14,7 +14,14 @@ echo "This is a test file for format 1.2" > "${TEST_FILE_1_2}"
TEST_FILE_OUTPUT="${MYTMPDIR}/test_file_output"
+TEST_FILE_EDIT="${MYTMPDIR}/test_file_edit"
+echo "This is a test file for edit" > "${TEST_FILE_EDIT}"
+TEST_FILE_EDIT2="${MYTMPDIR}/test_file_edit2"
+echo "This is a test file for edit2" > "${TEST_FILE_EDIT2}"
+
+FORMAT_1_1_HEADER="\$ANSIBLE_VAULT;1.1;AES256"
+FORMAT_1_2_HEADER="\$ANSIBLE_VAULT;1.2;AES256"
# old format
ansible-vault view "$@" --vault-password-file vault-password-ansible format_1_0_AES.yml
@@ -234,6 +241,27 @@ ansible-vault encrypt_string "$@" --vault-password-file "${NEW_VAULT_PASSWORD}"
# write to file
ansible-vault encrypt_string "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" --name "blippy" "a test string names blippy" --output "${MYTMPDIR}/enc_string_test_file"
+# test ansible-vault edit with a faux editor
+ansible-vault encrypt "$@" --vault-password-file vault-password "${TEST_FILE_EDIT}"
+
+# edit a 1.1 format with no vault-id, should stay 1.1
+EDITOR=./faux-editor.py ansible-vault edit "$@" --vault-password-file vault-password "${TEST_FILE_EDIT}"
+head -1 "${TEST_FILE_EDIT}" | grep "${FORMAT_1_1_HEADER}"
+
+# edit a 1.1 format with vault-id, should stay 1.1
+EDITOR=./faux-editor.py ansible-vault edit "$@" --vault-id vault_password@vault-password "${TEST_FILE_EDIT}"
+head -1 "${TEST_FILE_EDIT}" | grep "${FORMAT_1_1_HEADER}"
+
+ansible-vault encrypt "$@" --vault-id vault_password@vault-password "${TEST_FILE_EDIT2}"
+
+# edit a 1.2 format with vault id, should keep vault id and 1.2 format
+EDITOR=./faux-editor.py ansible-vault edit "$@" --vault-id vault_password@vault-password "${TEST_FILE_EDIT2}"
+head -1 "${TEST_FILE_EDIT2}" | grep "${FORMAT_1_2_HEADER};vault_password"
+
+# edit a 1.2 file with no vault-id, should keep vault id and 1.2 format
+EDITOR=./faux-editor.py ansible-vault edit "$@" --vault-password-file vault-password "${TEST_FILE_EDIT2}"
+head -1 "${TEST_FILE_EDIT2}" | grep "${FORMAT_1_2_HEADER};vault_password"
+
# test playbooks using vaulted files
ansible-playbook test_vault.yml -i ../../inventory -v "$@" --vault-password-file vault-password --list-tasks
diff --git a/test/units/parsing/vault/test_vault_editor.py b/test/units/parsing/vault/test_vault_editor.py
index 3c6e73b65db..7608e776b3c 100644
--- a/test/units/parsing/vault/test_vault_editor.py
+++ b/test/units/parsing/vault/test_vault_editor.py
@@ -309,7 +309,7 @@ class TestVaultEditor(unittest.TestCase):
self._assert_file_is_link(src_file_link_path, src_file_path)
@patch('ansible.parsing.vault.subprocess.call')
- def test_edit_file(self, mock_sp_call):
+ def test_edit_file_no_vault_id(self, mock_sp_call):
self._test_dir = self._create_test_dir()
src_contents = to_bytes("some info in a file\nyup.")
@@ -330,6 +330,36 @@ class TestVaultEditor(unittest.TestCase):
new_src_file = open(src_file_path, 'rb')
new_src_file_contents = new_src_file.read()
+ self.assertTrue(b'$ANSIBLE_VAULT;1.1;AES256' in new_src_file_contents)
+
+ src_file_plaintext = ve.vault.decrypt(new_src_file_contents)
+ self.assertEqual(src_file_plaintext, new_src_contents)
+
+ @patch('ansible.parsing.vault.subprocess.call')
+ def test_edit_file_with_vault_id(self, mock_sp_call):
+ self._test_dir = self._create_test_dir()
+ src_contents = to_bytes("some info in a file\nyup.")
+
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents)
+
+ new_src_contents = to_bytes("The info is different now.")
+
+ def faux_editor(editor_args):
+ self._faux_editor(editor_args, new_src_contents)
+
+ mock_sp_call.side_effect = faux_editor
+
+ ve = self._vault_editor()
+
+ ve.encrypt_file(src_file_path, self.vault_secret,
+ vault_id='vault_secrets')
+ ve.edit_file(src_file_path)
+
+ new_src_file = open(src_file_path, 'rb')
+ new_src_file_contents = new_src_file.read()
+
+ self.assertTrue(b'$ANSIBLE_VAULT;1.2;AES256;vault_secrets' in new_src_file_contents)
+
src_file_plaintext = ve.vault.decrypt(new_src_file_contents)
self.assertEqual(src_file_plaintext, new_src_contents)