From 86dc3c09ac875f2ab34a3299c47ab5a6617de301 Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Wed, 15 Nov 2017 14:01:32 -0500 Subject: [PATCH] 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 --- lib/ansible/cli/__init__.py | 8 ++-- test/integration/targets/vault/runme.sh | 39 ++++++++++++++++++++ test/units/cli/test_cli.py | 49 +++++++++++++++++++++---- test/units/cli/test_vault.py | 15 +++++++- 4 files changed, 97 insertions(+), 14 deletions(-) diff --git a/lib/ansible/cli/__init__.py b/lib/ansible/cli/__init__.py index 18559d69c9..ffaece5bbc 100644 --- a/lib/ansible/cli/__init__.py +++ b/lib/ansible/cli/__init__.py @@ -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 diff --git a/test/integration/targets/vault/runme.sh b/test/integration/targets/vault/runme.sh index 0895f4ffe5..5bdf5bb46e 100755 --- a/test/integration/targets/vault/runme.sh +++ b/test/integration/targets/vault/runme.sh @@ -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 diff --git a/test/units/cli/test_cli.py b/test/units/cli/test_cli.py index c881cf78c2..dc94c204d2 100644 --- a/test/units/cli/test_cli.py +++ b/test/units/cli/test_cli.py @@ -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') diff --git a/test/units/cli/test_vault.py b/test/units/cli/test_vault.py index 4f703121a5..d12a14fad9 100644 --- a/test/units/cli/test_vault.py +++ b/test/units/cli/test_vault.py @@ -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,