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.
pull/82418/head
Harm Geerts 2 years ago
parent 8328153121
commit 4ef61dcad9
No known key found for this signature in database
GPG Key ID: 9B5DAC50E1850C10

@ -425,11 +425,19 @@ class FileVaultSecret(VaultSecret):
class ScriptVaultSecret(FileVaultSecret): class ScriptVaultSecret(FileVaultSecret):
def _read_file(self, filename): @property
if not self.loader.is_executable(filename): def bytes(self):
raise AnsibleVaultError("The vault password script %s was not executable" % filename) 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() command = self._build_command()
display.vvvvv(u'The vault password script is reading the secret from %r' % self)
stdout, stderr, p = self._run(command) stdout, stderr, p = self._run(command)

@ -268,12 +268,22 @@ class TestScriptVaultSecret(unittest.TestCase):
mock_popen_instance.communicate = communicate mock_popen_instance.communicate = communicate
@patch('ansible.parsing.vault.subprocess.Popen') @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') self._mock_popen(mock_popen, stdout=b'some_password')
secret = vault.ScriptVaultSecret() secret = vault.ScriptVaultSecret()
with patch.object(secret, 'loader') as mock_loader: with patch.object(secret, 'loader') as mock_loader:
mock_loader.is_executable = MagicMock(return_value=True) mock_loader.is_executable = MagicMock(return_value=True)
secret.load() secret.load()
self.assertIsNone(secret._bytes)
self.assertEqual(secret.bytes, b'some_password')
@patch('ansible.parsing.vault.subprocess.Popen') @patch('ansible.parsing.vault.subprocess.Popen')
def test_read_file_empty(self, mock_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) mock_loader.is_executable = MagicMock(return_value=True)
self.assertRaisesRegex(vault.AnsibleVaultPasswordError, self.assertRaisesRegex(vault.AnsibleVaultPasswordError,
'Invalid vault password was provided from script', 'Invalid vault password was provided from script',
secret.load) lambda: secret.bytes)
@patch('ansible.parsing.vault.subprocess.Popen') @patch('ansible.parsing.vault.subprocess.Popen')
def test_read_file_os_error(self, mock_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') mock_popen.side_effect = OSError('That is not an executable')
secret = vault.ScriptVaultSecret() secret = vault.ScriptVaultSecret()
with patch.object(secret, 'loader') as mock_loader: with patch.object(secret, 'loader') as mock_loader:
mock_loader.is_executable = MagicMock(return_value=True) mock_loader.is_executable = MagicMock(return_value=True)
self.assertRaisesRegex(errors.AnsibleError, self.assertRaisesRegex(errors.AnsibleError,
'Problem running vault password script.*', 'Problem running vault password script.*',
secret.load) lambda: secret.bytes)
@patch('ansible.parsing.vault.subprocess.Popen') @patch('ansible.parsing.vault.subprocess.Popen')
def test_read_file_not_executable(self, mock_popen): def test_read_file_not_executable(self, mock_popen):
self._mock_popen(mock_popen) self._mock_popen(mock_popen)
self._mock_popen(mock_popen, stdout=b'')
secret = vault.ScriptVaultSecret() secret = vault.ScriptVaultSecret()
with patch.object(secret, 'loader') as mock_loader: with patch.object(secret, 'loader') as mock_loader:
mock_loader.is_executable = MagicMock(return_value=False) mock_loader.is_executable = MagicMock(return_value=False)
@ -309,15 +320,16 @@ class TestScriptVaultSecret(unittest.TestCase):
@patch('ansible.parsing.vault.subprocess.Popen') @patch('ansible.parsing.vault.subprocess.Popen')
def test_read_file_non_zero_return_code(self, mock_popen): def test_read_file_non_zero_return_code(self, mock_popen):
stderr = b'That did not work for a random reason' stderr = b'That did not work for a random reason'
stdout = b''
rc = 37 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') secret = vault.ScriptVaultSecret(filename='/dev/null/some_vault_secret')
with patch.object(secret, 'loader') as mock_loader: with patch.object(secret, 'loader') as mock_loader:
mock_loader.is_executable = MagicMock(return_value=True) mock_loader.is_executable = MagicMock(return_value=True)
self.assertRaisesRegex(errors.AnsibleError, self.assertRaisesRegex(errors.AnsibleError,
r'Vault password script.*returned non-zero \(%s\): %s' % (rc, stderr), r'Vault password script.*returned non-zero \(%s\): %s' % (rc, stderr),
secret.load) lambda: secret.bytes)
class TestScriptIsClient(unittest.TestCase): class TestScriptIsClient(unittest.TestCase):

Loading…
Cancel
Save