Repository: ansible/ansible

Issue:
ansible-vault decrypt does no longer read from stdin
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
vault

##### ANSIBLE VERSION
```
ansible 2.3.0.0
  config file = /home/user/ansible/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.11 (default, Mar 20 2016, 15:46:50) [GCC 5.3.0]

```

##### CONFIGURATION
```
[defaults]
vault_password_file = my_vault_key
```


##### OS / ENVIRONMENT
Linux

##### SUMMARY
```ansible-vault decrypt``` before 2.3 used to work with data from stdin. Ansible 2.3 does no longer read data from stdin. ```ansible-vault encrypt``` seems to be affected likewise.

##### STEPS TO REPRODUCE
```
$ cat host_vars/host.example.com/vault.yml | ansible-vault decrypt
ERROR! [Errno 2] No such file or directory: u'/home/user/ansible/-'
```
Running ```ansible-vault decrypt host_vars/host.example.com/vault.yml``` works as expected.

##### EXPECTED RESULTS
The decrypted content of vault.yml on stdout.

##### ACTUAL RESULTS
```
ERROR! [Errno 2] No such file or directory: u'/home/user/ansible/-'
```



Discussion:
looks like 6c6b647182f2dc8fa254e8b1855c3bc646d742cf busted this.  That commit uses os.path.realpath() to find the target of symlinks, but it also applies CWD to the path. For stdin case, the filename is just '-' and os.path.realpath(filename) expands that to '/home/user/wherever/-' and code deeper in vault is expecting just '-'.

Working on fix (likely just only using realpath on symlinks)

Resolving patch:
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
@@ -402,6 +402,14 @@ def _edit_file_helper(self, filename, existing_data=None, force_save=False):
         # shuffle tmp file into place
         self.shuffle_files(tmp_path, filename)
 
+    def _real_path(self, filename):
+        # '-' is special to VaultEditor, dont expand it.
+        if filename == '-':
+            return filename
+
+        real_path = os.path.realpath(filename)
+        return real_path
+
     def encrypt_bytes(self, b_plaintext):
         check_prereqs()
 
@@ -417,7 +425,7 @@ def encrypt_file(self, filename, output_file=None):
         # so treat the contents as a byte string.
 
         # follow the symlink
-        filename = os.path.realpath(filename)
+        filename = self._real_path(filename)
 
         b_plaintext = self.read_data(filename)
         b_ciphertext = self.vault.encrypt(b_plaintext)
@@ -428,7 +436,7 @@ def decrypt_file(self, filename, output_file=None):
         check_prereqs()
 
         # follow the symlink
-        filename = os.path.realpath(filename)
+        filename = self._real_path(filename)
 
         ciphertext = self.read_data(filename)
 
@@ -455,7 +463,7 @@ def edit_file(self, filename):
         check_prereqs()
 
         # follow the symlink
-        filename = os.path.realpath(filename)
+        filename = self._real_path(filename)
 
         ciphertext = self.read_data(filename)
 
@@ -487,7 +495,7 @@ def rekey_file(self, filename, b_new_password):
         check_prereqs()
 
         # follow the symlink
-        filename = os.path.realpath(filename)
+        filename = self._real_path(filename)
 
         prev = os.stat(filename)
         ciphertext = self.read_data(filename)
@@ -536,7 +544,8 @@ def write_data(self, data, filename, shred=True):
         b_file_data = to_bytes(data, errors='strict')
 
         if filename == '-':
-            sys.stdout.write(b_file_data)
+            file_data = to_text(b_file_data, encoding='utf-8', errors='strict', nonstring='strict')
+            sys.stdout.write(file_data)
         else:
             if os.path.isfile(filename):
                 if shred:


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
@@ -9,6 +9,8 @@ trap 'rm -rf "${MYTMPDIR}"' EXIT
 TEST_FILE="${MYTMPDIR}/test_file"
 echo "This is a test file" > "${TEST_FILE}"
 
+TEST_FILE_OUTPUT="${MYTMPDIR}/test_file_output"
+
 # encrypt it
 ansible-vault encrypt "$@" --vault-password-file vault-password "${TEST_FILE}"
 
@@ -28,6 +30,11 @@ ansible-vault view "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" "${TEST_FI
 
 ansible-vault decrypt "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" "${TEST_FILE}"
 
+# reading/writing to/from stdin/stdin  (See https://github.com/ansible/ansible/issues/23567)
+ansible-vault encrypt "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" --output="${TEST_FILE_OUTPUT}" < "${TEST_FILE}"
+ansible-vault view "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" - < "${TEST_FILE_OUTPUT}"
+ansible-vault decrypt "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" --output=- < "${TEST_FILE_OUTPUT}"
+
 ansible-vault encrypt_string "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" "a test string"
 
 ansible-vault encrypt_string "$@" --vault-password-file "${NEW_VAULT_PASSWORD}" --name "blippy" "a test string names blippy"
diff --git a/test/units/parsing/vault/test_vault_editor.py b/test/units/parsing/vault/test_vault_editor.py
--- a/test/units/parsing/vault/test_vault_editor.py
+++ b/test/units/parsing/vault/test_vault_editor.py
@@ -516,3 +516,29 @@ def test_rekey_migration(self):
         assert vl.cipher_name == "AES256", "wrong cipher name set after rekey: %s" % vl.cipher_name
         assert error_hit is False, "error decrypting migrated 1.0 file"
         assert dec_data.strip() == b"foo", "incorrect decryption of rekeyed/migrated file: %s" % dec_data
+
+    def test_real_path_dash(self):
+        filename = '-'
+        ve = vault.VaultEditor('password')
+
+        res = ve._real_path(filename)
+        self.assertEqual(res, '-')
+
+    def test_real_path_dev_null(self):
+        filename = '/dev/null'
+        ve = vault.VaultEditor('password')
+
+        res = ve._real_path(filename)
+        self.assertEqual(res, '/dev/null')
+
+    def test_real_path_symlink(self):
+        self._test_dir = self._create_test_dir()
+        file_path = self._create_file(self._test_dir, 'test_file', content=b'this is a test file')
+        file_link_path = os.path.join(self._test_dir, 'a_link_to_test_file')
+
+        os.symlink(file_path, file_link_path)
+
+        ve = vault.VaultEditor('password')
+
+        res = ve._real_path(file_link_path)
+        self.assertEqual(res, file_path)