From 4ef61dcad9c02a39b3ece4538e4294211362ac82 Mon Sep 17 00:00:00 2001 From: Harm Geerts Date: Wed, 13 Dec 2023 15:22:25 +0100 Subject: [PATCH] Execute ScriptVaultSecret on demand instead of on load A vault secret script can do whatever is needed to return a vault password. This can mean loading something from a local password store or accessing a remote password storage solution. When combining the password script with multiple vault_identity_list entries this adds a lot of unneeded loading of vault indentity passwords. Most of which may not be required to complete the ansible run. This commit delays executing for Script and ClientScriptVaultSecret classes until the bytes password is accessed. --- lib/ansible/parsing/vault/__init__.py | 14 +++++++++++--- test/units/parsing/vault/test_vault.py | 24 ++++++++++++++++++------ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/lib/ansible/parsing/vault/__init__.py b/lib/ansible/parsing/vault/__init__.py index 432609d14f9..40cdaa8d51e 100644 --- a/lib/ansible/parsing/vault/__init__.py +++ b/lib/ansible/parsing/vault/__init__.py @@ -425,11 +425,19 @@ class FileVaultSecret(VaultSecret): class ScriptVaultSecret(FileVaultSecret): - def _read_file(self, filename): - if not self.loader.is_executable(filename): - raise AnsibleVaultError("The vault password script %s was not executable" % filename) + @property + def bytes(self): + if self._bytes is None: + self._bytes = self._read_file(self.filename) + return self._bytes + def load(self): + if not self.loader.is_executable(self.filename): + raise AnsibleVaultError("The vault password script %s was not executable" % self.filename) + + def _read_file(self, filename): command = self._build_command() + display.vvvvv(u'The vault password script is reading the secret from %r' % self) stdout, stderr, p = self._run(command) diff --git a/test/units/parsing/vault/test_vault.py b/test/units/parsing/vault/test_vault.py index 982ae82c406..c8b195e6eeb 100644 --- a/test/units/parsing/vault/test_vault.py +++ b/test/units/parsing/vault/test_vault.py @@ -268,12 +268,22 @@ class TestScriptVaultSecret(unittest.TestCase): mock_popen_instance.communicate = communicate @patch('ansible.parsing.vault.subprocess.Popen') - def test_read_file(self, mock_popen): + def test_load(self, mock_popen): + self._mock_popen(mock_popen) + secret = vault.ScriptVaultSecret() + with patch.object(secret, 'loader') as mock_loader: + mock_loader.is_executable = MagicMock(return_value=True) + secret.load() + + @patch('ansible.parsing.vault.subprocess.Popen') + def test_lazy_read_file(self, mock_popen): self._mock_popen(mock_popen, stdout=b'some_password') secret = vault.ScriptVaultSecret() with patch.object(secret, 'loader') as mock_loader: mock_loader.is_executable = MagicMock(return_value=True) secret.load() + self.assertIsNone(secret._bytes) + self.assertEqual(secret.bytes, b'some_password') @patch('ansible.parsing.vault.subprocess.Popen') def test_read_file_empty(self, mock_popen): @@ -283,22 +293,23 @@ class TestScriptVaultSecret(unittest.TestCase): mock_loader.is_executable = MagicMock(return_value=True) self.assertRaisesRegex(vault.AnsibleVaultPasswordError, 'Invalid vault password was provided from script', - secret.load) + lambda: secret.bytes) @patch('ansible.parsing.vault.subprocess.Popen') def test_read_file_os_error(self, mock_popen): - self._mock_popen(mock_popen) + self._mock_popen(mock_popen, stdout=b'') mock_popen.side_effect = OSError('That is not an executable') secret = vault.ScriptVaultSecret() with patch.object(secret, 'loader') as mock_loader: mock_loader.is_executable = MagicMock(return_value=True) self.assertRaisesRegex(errors.AnsibleError, 'Problem running vault password script.*', - secret.load) + lambda: secret.bytes) @patch('ansible.parsing.vault.subprocess.Popen') def test_read_file_not_executable(self, mock_popen): self._mock_popen(mock_popen) + self._mock_popen(mock_popen, stdout=b'') secret = vault.ScriptVaultSecret() with patch.object(secret, 'loader') as mock_loader: mock_loader.is_executable = MagicMock(return_value=False) @@ -309,15 +320,16 @@ class TestScriptVaultSecret(unittest.TestCase): @patch('ansible.parsing.vault.subprocess.Popen') def test_read_file_non_zero_return_code(self, mock_popen): stderr = b'That did not work for a random reason' + stdout = b'' rc = 37 - self._mock_popen(mock_popen, return_code=rc, stderr=stderr) + self._mock_popen(mock_popen, return_code=rc, stderr=stderr, stdout=stdout) secret = vault.ScriptVaultSecret(filename='/dev/null/some_vault_secret') with patch.object(secret, 'loader') as mock_loader: mock_loader.is_executable = MagicMock(return_value=True) self.assertRaisesRegex(errors.AnsibleError, r'Vault password script.*returned non-zero \(%s\): %s' % (rc, stderr), - secret.load) + lambda: secret.bytes) class TestScriptIsClient(unittest.TestCase):