Fix vault --ask-vault-pass with no tty (#31493)

* Fix vault --ask-vault-pass with no tty

2.4.0 added a check for isatty() that would skip setting up interactive
vault password prompts if not running on a tty.

But... getpass.getpass() will fallback to reading from stdin if
it gets that far without a tty. Since 2.4.0 skipped the interactive
prompts / getpass.getpass() in that case, it would never get a chance
to fall back to stdin.

So if 'echo $VAULT_PASSWORD| ansible-playbook --ask-vault-pass site.yml'
was ran without a tty (ie, from a jenkins job or via the vagrant
ansible provisioner) the 2.4 behavior was different than 2.3. 2.4
would never read the password from stdin, resulting in a vault password
error like:

        ERROR! Attempting to decrypt but no vault secrets found

Fix is just to always call the interactive password prompts based
on getpass.getpass() on --ask-vault-pass or --vault-id @prompt and
let getpass sort it out.

* up test_prompt_no_tty to expect prompt with no tty

We do call the PromptSecret class if there is no tty, but
we are back to expecting it to read from stdin in that case.

* Fix logic for when to auto-prompt vault pass

If --ask-vault-pass is used, then pretty much always
prompt.

If it is not used, then prompt if there are no other
vault ids provided and 'auto_prompt==True'.

Fixes vagrant bug https://github.com/hashicorp/vagrant/issues/9033

Fixes #30993
This commit is contained in:
Adrian Likins 2017-11-15 14:01:32 -05:00 committed by GitHub
parent f93b98661a
commit 86dc3c09ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 97 additions and 14 deletions

View file

@ -213,7 +213,9 @@ class CLI(with_metaclass(ABCMeta, object)):
# if an action needs an encrypt password (create_new_password=True) and we dont
# have other secrets setup, then automatically add a password prompt as well.
if ask_vault_pass or (auto_prompt and not vault_ids):
# prompts cant/shouldnt work without a tty, so dont add prompt secrets
if ask_vault_pass or (not vault_ids and auto_prompt):
id_slug = u'%s@%s' % (C.DEFAULT_VAULT_IDENTITY, u'prompt_ask_vault_pass')
vault_ids.append(id_slug)
@ -260,10 +262,6 @@ class CLI(with_metaclass(ABCMeta, object)):
vault_id_name, vault_id_value = CLI.split_vault_id(vault_id_slug)
if vault_id_value in ['prompt', 'prompt_ask_vault_pass']:
# prompts cant/shouldnt work without a tty, so dont add prompt secrets
if not sys.stdin.isatty():
continue
# --vault-id some_name@prompt_ask_vault_pass --vault-id other_name@prompt_ask_vault_pass will be a little
# confusing since it will use the old format without the vault id in the prompt
built_vault_id = vault_id_name or C.DEFAULT_VAULT_IDENTITY

View file

@ -34,6 +34,45 @@ WRONG_RC=$?
echo "rc was $WRONG_RC (1 is expected)"
[ $WRONG_RC -eq 1 ]
# Use linux setsid to test without a tty. No setsid if osx/bsd though...
if [ -x "$(command -v setsid)" ]; then
# tests related to https://github.com/ansible/ansible/issues/30993
CMD='ansible-playbook -vvvvv --ask-vault-pass test_vault.yml'
setsid sh -c "echo test-vault-password|${CMD}" < /dev/null > log 2>&1 && :
WRONG_RC=$?
cat log
echo "rc was $WRONG_RC (0 is expected)"
[ $WRONG_RC -eq 0 ]
setsid sh -c 'tty; ansible-vault --ask-vault-pass -vvvvv view test_vault.yml' < /dev/null > log 2>&1 && :
WRONG_RC=$?
echo "rc was $WRONG_RC (1 is expected)"
[ $WRONG_RC -eq 1 ]
cat log
setsid sh -c 'tty; echo passbhkjhword|ansible-playbook -vvvvv --ask-vault-pass test_vault.yml' < /dev/null > log 2>&1 && :
WRONG_RC=$?
echo "rc was $WRONG_RC (1 is expected)"
[ $WRONG_RC -eq 1 ]
cat log
setsid sh -c 'tty; echo test-vault-password |ansible-playbook -vvvvv --ask-vault-pass test_vault.yml' < /dev/null > log 2>&1
echo $?
cat log
setsid sh -c 'tty; echo test-vault-password|ansible-playbook -vvvvv --ask-vault-pass test_vault.yml' < /dev/null > log 2>&1
echo $?
cat log
setsid sh -c 'tty; echo test-vault-password |ansible-playbook -vvvvv --ask-vault-pass test_vault.yml' < /dev/null > log 2>&1
echo $?
cat log
setsid sh -c 'tty; echo test-vault-password|ansible-vault --ask-vault-pass -vvvvv view vaulted.inventory' < /dev/null > log 2>&1
echo $?
cat log
fi
# old format
ansible-vault view "$@" --vault-password-file vault-password-ansible format_1_0_AES.yml

View file

@ -46,6 +46,13 @@ class TestCliVersion(unittest.TestCase):
class TestCliBuildVaultIds(unittest.TestCase):
def setUp(self):
self.tty_patcher = patch('ansible.cli.sys.stdin.isatty', return_value=True)
self.mock_isatty = self.tty_patcher.start()
def tearDown(self):
self.tty_patcher.stop()
def test(self):
res = cli.CLI.build_vault_ids(['foo@bar'])
self.assertEqual(res, ['foo@bar'])
@ -102,6 +109,7 @@ class TestCliBuildVaultIds(unittest.TestCase):
ask_vault_pass=True,
create_new_password=True,
auto_prompt=False)
self.assertEqual(set(res), set(['blip@prompt', 'baz@prompt_ask_vault_pass',
'default@prompt_ask_vault_pass',
'some-password-file', 'qux@another-password-file',
@ -115,8 +123,14 @@ class TestCliSetupVaultSecrets(unittest.TestCase):
self.tty_patcher = patch('ansible.cli.sys.stdin.isatty', return_value=True)
self.mock_isatty = self.tty_patcher.start()
self.display_v_patcher = patch('ansible.cli.display.verbosity', return_value=6)
self.mock_display_v = self.display_v_patcher.start()
cli.display.verbosity = 5
def tearDown(self):
self.tty_patcher.stop()
self.display_v_patcher.stop()
cli.display.verbosity = 0
def test(self):
res = cli.CLI.setup_vault_secrets(None, None, auto_prompt=False)
@ -144,7 +158,8 @@ class TestCliSetupVaultSecrets(unittest.TestCase):
res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
vault_ids=['prompt1@prompt'],
ask_vault_pass=True)
ask_vault_pass=True,
auto_prompt=False)
self.assertIsInstance(res, list)
matches = vault.match_secrets(res, ['prompt1'])
@ -156,15 +171,19 @@ class TestCliSetupVaultSecrets(unittest.TestCase):
def test_prompt_no_tty(self, mock_prompt_secret):
self.mock_isatty.return_value = False
mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password',
vault_id='prompt1')
vault_id='prompt1',
name='bytes_should_be_prompt1_password',
spec=vault.PromptVaultSecret)
res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
vault_ids=['prompt1@prompt'],
ask_vault_pass=True)
ask_vault_pass=True,
auto_prompt=False)
self.assertIsInstance(res, list)
self.assertEqual(len(res), 0)
self.assertEqual(len(res), 2)
matches = vault.match_secrets(res, ['prompt1'])
self.assertEquals(len(matches), 0)
self.assertIn('prompt1', [x[0] for x in matches])
self.assertEquals(len(matches), 1)
@patch('ansible.cli.get_file_vault_secret')
@patch('ansible.cli.PromptVaultSecret')
@ -192,7 +211,7 @@ class TestCliSetupVaultSecrets(unittest.TestCase):
self.assertIsInstance(res, list)
len_ids = len(vault_id_names)
matches = vault.match_secrets(res, vault_id_names)
self.assertEqual(len(res), len_ids)
self.assertEqual(len(res), len_ids, 'len(res):%s does not match len_ids:%s' % (len(res), len_ids))
self.assertEqual(len(matches), len_ids)
for index, prompt in enumerate(vault_id_names):
self.assertIn(prompt, [x[0] for x in matches])
@ -214,6 +233,7 @@ class TestCliSetupVaultSecrets(unittest.TestCase):
@patch('ansible.cli.PromptVaultSecret')
def test_multiple_prompts_and_ask_vault_pass(self, mock_prompt_secret):
self.mock_isatty.return_value = False
mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password',
vault_id='prompt1')
@ -223,6 +243,8 @@ class TestCliSetupVaultSecrets(unittest.TestCase):
'prompt3@prompt_ask_vault_pass'],
ask_vault_pass=True)
# We provide some vault-ids and secrets, so auto_prompt shouldn't get triggered,
# so there is
vault_id_names = ['prompt1', 'prompt2', 'prompt3', 'default']
self._assert_ids(vault_id_names, res)
@ -242,14 +264,27 @@ class TestCliSetupVaultSecrets(unittest.TestCase):
res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
vault_ids=[],
create_new_password=False,
ask_vault_pass=True)
ask_vault_pass=False)
self.assertIsInstance(res, list)
matches = vault.match_secrets(res, ['default'])
# --vault-password-file/DEFAULT_VAULT_PASSWORD_FILE is higher precendce than prompts
# if the same vault-id ('default') regardless of cli order since it didn't matter in 2.3
self.assertEqual(matches[0][1].bytes, b'file1_password')
self.assertEqual(len(matches), 1)
res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
vault_ids=[],
create_new_password=False,
ask_vault_pass=True,
auto_prompt=True)
self.assertIsInstance(res, list)
matches = vault.match_secrets(res, ['default'])
self.assertEqual(matches[0][1].bytes, b'file1_password')
self.assertEqual(matches[1][1].bytes, b'prompt1_password')
self.assertEqual(len(matches), 2)
@patch('ansible.cli.get_file_vault_secret')
@patch('ansible.cli.PromptVaultSecret')

View file

@ -32,6 +32,13 @@ from ansible.cli.vault import VaultCLI
class TestVaultCli(unittest.TestCase):
def setUp(self):
self.tty_patcher = patch('ansible.cli.sys.stdin.isatty', return_value=False)
self.mock_isatty = self.tty_patcher.start()
def tearDown(self):
self.tty_patcher.stop()
def test_parse_empty(self):
cli = VaultCLI([])
self.assertRaisesRegexp(errors.AnsibleOptionsError,
@ -46,14 +53,18 @@ class TestVaultCli(unittest.TestCase):
cli = VaultCLI(args=['ansible-vault', 'view', '/dev/null/foo'])
cli.parse()
def test_view_missing_file_no_secret(self):
@patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
def test_view_missing_file_no_secret(self, mock_setup_vault_secrets):
mock_setup_vault_secrets.return_value = []
cli = VaultCLI(args=['ansible-vault', 'view', '/dev/null/foo'])
cli.parse()
self.assertRaisesRegexp(errors.AnsibleOptionsError,
"A vault password is required to use Ansible's Vault",
cli.run)
def test_encrypt_missing_file_no_secret(self):
@patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
def test_encrypt_missing_file_no_secret(self, mock_setup_vault_secrets):
mock_setup_vault_secrets.return_value = []
cli = VaultCLI(args=['ansible-vault', 'encrypt', '/dev/null/foo'])
cli.parse()
self.assertRaisesRegexp(errors.AnsibleOptionsError,