Repository: ansible/ansible

Issue:
ansible-vault edit asks to change the vault password (devel) 
##### ISSUE TYPE
Bug Report 

##### COMPONENT NAME 
Ansible-Vault

##### ANSIBLE VERSION
```
ansible 2.5.0 (devel 0c5650ee1b) last updated 2018/02/07 09:13:57 (GMT +200)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/marcel/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/marcel/Sources/github/ansible/lib/ansible
  executable location = /home/marcel/Sources/github/ansible/bin/ansible
  python version = 2.7.12 (default, Dec  4 2017, 14:50:18) [GCC 5.4.0 20160609]
```

##### CONFIGURATION
Default settings, nothing has changed

##### OS / ENVIRONMENT
N/A

##### SUMMARY
ansible-vault edit asks you to change the password every time since commit ffe0ddea96bbe8ac27af816e58667c212e74688e if you do not have a vault_password_file in your config. 

##### STEPS TO REPRODUCE

1. git clone https://github.com/ansible/ansible.git
2. create a file 
3. encrypt it with ansible-vault encrypt 
4. edit the file with ansible-vault edit 

```
# echo "---" > vault_issue.yml
# ansible-vault encrypt vault_issue.yml
# ansible-vault edit vault_issue.yml
```

##### EXPECTED RESULTS
Ansible should ask just for the current password and not set a new one. 

##### ACTUAL RESULTS
```
$ ansible-vault edit group_vars/do-stage-oam.yml -vvv
ansible-vault 2.5.0 (devel 0c5650ee1b) last updated 2018/02/07 09:13:57 (GMT +200)
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/marcel/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /home/marcel/Sources/github/ansible/lib/ansible
  executable location = /home/marcel/Sources/github/ansible/bin/ansible-vault
  python version = 2.7.12 (default, Dec  4 2017, 14:50:18) [GCC 5.4.0 20160609]
Using /etc/ansible/ansible.cfg as config file
Vault password: 
New Vault password: 
Confirm New Vault password: 
```



Discussion:
Files identified in the description:
* [lib/ansible/parsing/vault](https://github.com/ansible/ansible/blob/devel/lib/ansible/parsing/vault)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
I can reproduce this and think I have a fix under way.

Resolving patch:
diff --git a/lib/ansible/cli/vault.py b/lib/ansible/cli/vault.py
--- a/lib/ansible/cli/vault.py
+++ b/lib/ansible/cli/vault.py
@@ -107,7 +107,7 @@ def set_action(self):
             self.parser.set_usage("usage: %prog rekey [options] file_name")
 
         # For encrypting actions, we can also specify which of multiple vault ids should be used for encrypting
-        if self.action in ['create', 'encrypt', 'encrypt_string', 'rekey']:
+        if self.action in ['create', 'encrypt', 'encrypt_string', 'rekey', 'edit']:
             self.parser.add_option('--encrypt-vault-id', default=[], dest='encrypt_vault_id',
                                    action='store', type='string',
                                    help='the vault id used to encrypt (required if more than vault-id is provided)')
@@ -181,7 +181,7 @@ def run(self):
             if not vault_secrets:
                 raise AnsibleOptionsError("A vault password is required to use Ansible's Vault")
 
-        if self.action in ['encrypt', 'encrypt_string', 'create', 'edit']:
+        if self.action in ['encrypt', 'encrypt_string', 'create']:
 
             encrypt_vault_id = None
             # no --encrypt-vault-id self.options.encrypt_vault_id for 'edit'
diff --git a/lib/ansible/parsing/vault/__init__.py b/lib/ansible/parsing/vault/__init__.py
--- a/lib/ansible/parsing/vault/__init__.py
+++ b/lib/ansible/parsing/vault/__init__.py
@@ -657,7 +657,7 @@ def decrypt(self, vaulttext, filename=None):
         :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)
+        plaintext, vault_id, vault_secret = self.decrypt_and_get_vault_id(vaulttext, filename=filename)
         return plaintext
 
     def decrypt_and_get_vault_id(self, vaulttext, filename=None):
@@ -668,7 +668,7 @@ def decrypt_and_get_vault_id(self, vaulttext, filename=None):
         :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
+        :returns: a byte string containing the decrypted data and the vault-id vault-secret that was used
 
         """
         b_vaulttext = to_bytes(vaulttext, errors='strict', encoding='utf-8')
@@ -709,6 +709,7 @@ def decrypt_and_get_vault_id(self, vaulttext, filename=None):
 
         vault_id_matchers = []
         vault_id_used = None
+        vault_secret_used = None
 
         if vault_id:
             display.vvvvv('Found a vault_id (%s) in the vaulttext' % (vault_id))
@@ -737,6 +738,7 @@ def decrypt_and_get_vault_id(self, vaulttext, filename=None):
                 b_plaintext = this_cipher.decrypt(b_vaulttext, vault_secret)
                 if b_plaintext is not None:
                     vault_id_used = vault_secret_id
+                    vault_secret_used = vault_secret
                     file_slug = ''
                     if filename:
                         file_slug = ' of "%s"' % filename
@@ -765,7 +767,7 @@ def decrypt_and_get_vault_id(self, vaulttext, filename=None):
                 msg += " on %s" % to_native(filename)
             raise AnsibleError(msg)
 
-        return b_plaintext, vault_id_used
+        return b_plaintext, vault_id_used, vault_secret_used
 
 
 class VaultEditor:
@@ -931,7 +933,8 @@ def create_file(self, filename, secret, vault_id=None):
         self._edit_file_helper(filename, secret, vault_id=vault_id)
 
     def edit_file(self, filename):
-
+        vault_id_used = None
+        vault_secret_used = None
         # follow the symlink
         filename = self._real_path(filename)
 
@@ -943,7 +946,7 @@ def edit_file(self, filename):
         try:
             # vaulttext gets converted back to bytes, but alas
             # TODO: return the vault_id that worked?
-            plaintext, vault_id_used = self.vault.decrypt_and_get_vault_id(vaulttext)
+            plaintext, vault_id_used, vault_secret_used = self.vault.decrypt_and_get_vault_id(vaulttext)
         except AnsibleError as e:
             raise AnsibleError("%s for %s" % (to_bytes(e), to_bytes(filename)))
 
@@ -956,21 +959,14 @@ def edit_file(self, filename):
         # 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_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]
-
+        # Keep the same vault-id (and version) as in the header
         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, vault_id=vault_id)
+            self._edit_file_helper(filename, vault_secret_used, existing_data=plaintext,
+                                   force_save=True, vault_id=vault_id)
         else:
-            self._edit_file_helper(filename, secret, existing_data=plaintext, force_save=False, vault_id=vault_id)
+            self._edit_file_helper(filename, vault_secret_used, existing_data=plaintext,
+                                   force_save=False, vault_id=vault_id)
 
     def plaintext(self, filename):
 
@@ -996,7 +992,7 @@ def rekey_file(self, filename, new_vault_secret, new_vault_id=None):
         display.vvvvv('Rekeying file "%s" to with new vault-id "%s" and vault secret %s' %
                       (filename, new_vault_id, new_vault_secret))
         try:
-            plaintext, vault_id_used = self.vault.decrypt_and_get_vault_id(vaulttext)
+            plaintext, vault_id_used, _dummy = self.vault.decrypt_and_get_vault_id(vaulttext)
         except AnsibleError as e:
             raise AnsibleError("%s for %s" % (to_bytes(e), to_bytes(filename)))
 


Test patch:
diff --git a/test/integration/targets/vault/runme.sh b/test/integration/targets/vault/runme.sh
--- a/test/integration/targets/vault/runme.sh
+++ b/test/integration/targets/vault/runme.sh
@@ -26,6 +26,22 @@ 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}"
 
+# test case for https://github.com/ansible/ansible/issues/35834
+# (being prompted for new password on vault-edit with no configured passwords)
+
+TEST_FILE_EDIT3="${MYTMPDIR}/test_file_edit3"
+echo "This is a test file for edit3" > "${TEST_FILE_EDIT3}"
+
+# ansible-config view
+ansible-config view
+
+# ansisle-config
+ansible-config dump --only-changed
+ansible-vault encrypt "$@" --vault-id vault-password "${TEST_FILE_EDIT3}"
+# EDITOR=./faux-editor.py ansible-vault edit "$@" "${TEST_FILE_EDIT3}"
+EDITOR=./faux-editor.py ansible-vault edit --vault-id vault-password -vvvvv "${TEST_FILE_EDIT3}"
+echo $?
+
 # view the vault encrypted password file
 ansible-vault view "$@" --vault-id vault-password encrypted-vault-password
 
@@ -336,11 +352,22 @@ EDITOR=./faux-editor.py ansible-vault edit "$@" --vault-password-file vault-pass
 head -1 "${TEST_FILE_EDIT}" | grep "${FORMAT_1_1_HEADER}"
 
 # edit a 1.1 format with vault-id, should stay 1.1
+cat "${TEST_FILE_EDIT}"
 EDITOR=./faux-editor.py ansible-vault edit "$@" --vault-id vault_password@vault-password "${TEST_FILE_EDIT}"
+cat "${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}"
 
+# verify that we aren't prompted for a new vault password on edit if we are running interactively (ie, with prompts)
+# have to use setsid nd --ask-vault-pass to force a prompt to simulate.
+# See https://github.com/ansible/ansible/issues/35834
+setsid sh -c 'tty; echo password |ansible-vault edit --ask-vault-pass vault_test.yml' < /dev/null > log 2>&1 && :
+grep  'New Vault password' log && :
+WRONG_RC=$?
+echo "The stdout log had 'New Vault password' in it and it is not supposed to. rc of grep was $WRONG_RC (1 is expected)"
+[ $WRONG_RC -eq 1 ]
+
 # 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"
diff --git a/test/units/parsing/vault/test_vault.py b/test/units/parsing/vault/test_vault.py
--- a/test/units/parsing/vault/test_vault.py
+++ b/test/units/parsing/vault/test_vault.py
@@ -888,6 +888,26 @@ def test_encrypt_decrypt_aes256_bad_hmac(self):
         # assert we throw an error
         self.v.decrypt(b_invalid_ciphertext)
 
+    def test_decrypt_and_get_vault_id(self):
+        b_expected_plaintext = to_bytes('foo bar\n')
+        vaulttext = '''$ANSIBLE_VAULT;1.2;AES256;ansible_devel
+65616435333934613466373335363332373764363365633035303466643439313864663837393234
+3330656363343637313962633731333237313636633534630a386264363438363362326132363239
+39363166646664346264383934393935653933316263333838386362633534326664646166663736
+6462303664383765650a356637643633366663643566353036303162386237336233393065393164
+6264'''
+
+        vault_secrets = self._vault_secrets_from_password('ansible_devel', 'ansible')
+        v = vault.VaultLib(vault_secrets)
+
+        b_vaulttext = to_bytes(vaulttext)
+
+        b_plaintext, vault_id_used, vault_secret_used = v.decrypt_and_get_vault_id(b_vaulttext)
+
+        self.assertEqual(b_expected_plaintext, b_plaintext)
+        self.assertEqual(vault_id_used, 'ansible_devel')
+        self.assertEqual(vault_secret_used.text, 'ansible')
+
     def test_decrypt_non_default_1_2(self):
         b_expected_plaintext = to_bytes('foo bar\n')
         vaulttext = '''$ANSIBLE_VAULT;1.2;AES256;ansible_devel