Repository: ansible/ansible

Issue:
Unprivileged become on HP-UX/UNIX does not work
##### ISSUE TYPE
- Bug Report
##### ANSIBLE VERSION

```
ansible 2.1.0.0
```
##### CONFIGURATION

remote_user = ansible
##### OS / ENVIRONMENT

Ansible Host: SUSE Linux Enterprise Server 12 SP1
Managed Node: HP-UX B.11.31 U ia64
##### SUMMARY

Executing a play task with `become`on HP-UX fails with following error message:

```
[WARNING]: Using world-readable permissions for temporary files Ansible needs to create when becoming an unprivileged user which may be insecure. For information on securing this, see
https://docs.ansible.com/ansible/become.html#becoming-an-unprivileged-user

fatal: [host]: FAILED! => {"failed": true, "msg": "Failed to set file mode on remote files (rc: 1, err: chmod: can't traverse /tmp/ansible-tmp-1465816244.81-217081167531834/\n)"
```

This is due to the fact that a `chown` on HP-UX (probably all UNIX systems) behaves different as on Linux:
- Linux: Executing `chown user2 file` as unprivileged `user1` will fail
- HP-UX: Executing `chown user2 file` as unprivileged `user1` will **not** fail

In https://github.com/ansible/ansible/blob/2bf2e0fdfdb7293e28e3bb0543b2d0d14a65169f/lib/ansible/plugins/action/__init__.py#L296 you can see that ansible expects that a `chown` will fail in case it is executed by a unprivileged user.
##### STEPS TO REPRODUCE

Example Playbook

```
- name: Privilege Escalation on HP-UX
  hosts: all

  tasks:
    - name: test
      command: whoami
      become_user: user1
```
##### EXPECTED RESULTS

<!--- What did you expect to happen when running the steps above? -->
##### ACTUAL RESULTS

<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
##### POSSIBLE SOLUTION
1. Try `_remote_set_user_facl` first
2. If it fails: try `_remote_chmod`
3. If it fails: raise AnsibeError
4. If it works: try `_remote_chown`
   ...



Discussion:
By not fail you mean it returns exit code 0 but does nothing?

I don't believe the sequence you describe as a possible solution will work.  Trying facls first should be okay but _remote_chmod needs to be the last thing tried (as it is insecure) and using chown after chmod succeeded would have no additional affect.

Maybe we can do this:
1. Try _remote_set_user_facl
2. If it fails: try _remote_chown
3. If that succeeds, stat the file to see who the owner is
4. If _remote_chown failed or the owner is not the new owner: _remote_chmod

I'd like to avoid the remote stat if there's another choice as it's another ssh connection to execute that which may take a while.  Can you give me more information about chown's behaviour here?

The chown on HP-UX does work:

```
host1:(/root/home/user1/test)(user1)#touch test
host1:(/root/home/user1/test)(user1)#chown user2 test
host1:(/root/home/user1/test)(user1)#ll test
-rw-r-----   1 user2     users    0 Jun 13 16:10 test
```

Afterwards the file is no longer accessible by 'user1'. Due to that it would make sense to 'chmod' the file on HP-UX first. I cannot comment if all UNIX systems handle this as on HP-UX. AIX seems to be not but on Solaris it is the same behavior.

As stated previously, chmod'ing the file is wrong.  It's a chmod a+r which makes any sensitive information in the file readable by other users on the target box.

Hmmmm..... I think I see what you're looking at -- there's a chmod for execute permissions that happens after the chown.  I think we can move that to happen before chown is tried.  However, the error message that you're reporting doesn't match up with that chmod.  It matches up with the chmod in the path the code takes if chown fails.  We can't move that chmod to the front as it's the insecure fallback that I was talking about earlier.

Maybe you tried this a few times and the error message is from a different run than the one where the chown worked unexpectedly (to the code) and made the initial chmod fail?

Most Unix systems removed the ability of non privileged users to assign a file to another user as this is a security risk, that is why the test works for most modern Unicies.

The code has many complex interactions, this is not something we can change lightly,  I'm not sure how we can support HP-UX in this case, we need to check some alternatives and then thoroughly test them.

Yes, you are right. The task fails because of the following line (as the previous `chown` was successfully although ansible expected it to fail):

```
res = self._remote_chmod('u+x', remote_path, recursive=recursive)
```

(https://github.com/ansible/ansible/blob/2bf2e0fdfdb7293e28e3bb0543b2d0d14a65169f/lib/ansible/plugins/action/__init__.py#L347.
As you can see it is a `u+x` and not a `a+r`. Maybe the following approach is acceptable:
- Try `_remote_set_user_facl`
- If it fails:
  - If executable: `_remote_chmod u+x`
  - `_remote_chown`
  - If it fails and `C.ALLOW_WORLD_READABLE_TMPFILES`: 
    - `_remot_chown a+r`


Resolving patch:
diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py
--- a/lib/ansible/plugins/action/__init__.py
+++ b/lib/ansible/plugins/action/__init__.py
@@ -301,17 +301,17 @@ def _fixup_perms(self, remote_path, remote_user, execute=True, recursive=True):
         information.  We achieve this in one of these ways:
 
         * If no sudo is performed or the remote_user is sudo'ing to
-          themselves, we don't have to change permisions.
+          themselves, we don't have to change permissions.
         * If the remote_user sudo's to a privileged user (for instance, root),
           we don't have to change permissions
-        * If the remote_user is a privileged user and sudo's to an
-          unprivileged user then we change the owner of the file to the
-          unprivileged user so they can read it.
-        * If the remote_user is an unprivieged user and we're sudo'ing to
-          a second unprivileged user then we attempt to grant the second
-          unprivileged user access via file system acls.
-        * If granting file system acls fails we can set the file to be world
-          readable so that the second unprivileged user can read the file.
+        * If the remote_user sudo's to an unprivileged user then we attempt to
+          grant the unprivileged user access via file system acls.
+        * If granting file system acls fails we try to change the owner of the
+          file with chown which only works in case the remote_user is
+          privileged or the remote systems allows chown calls by unprivileged
+          users (e.g. HP-UX)
+        * If the chown fails we can set the file to be world readable so that
+          the second unprivileged user can read the file.
           Since this could allow other users to get access to private
           information we only do this ansible is configured with
           "allow_world_readable_tmpfiles" in the ansible.cfg
@@ -333,35 +333,31 @@ def _fixup_perms(self, remote_path, remote_user, execute=True, recursive=True):
             # Unprivileged user that's different than the ssh user.  Let's get
             # to work!
 
-            # Try chown'ing the file. This will only work if our SSH user has
-            # root privileges, but since we can't reliably determine that from
-            # the username (think "toor" on FreeBSD), let's just try first and
-            # apologize later:
-            res = self._remote_chown(remote_path, self._play_context.become_user, recursive=recursive)
-            if res['rc'] == 0:
-                # root can read things that don't have read bit but can't
-                # execute them without the execute bit, so we might need to
-                # set that even if we're root. We just ran chown successfully,
-                # so apparently we are root.
+            # Try to use file system acls to make the files readable for sudo'd
+            # user
+            if execute:
+                mode = 'rx'
+            else:
+                mode = 'rX'
+
+            res = self._remote_set_user_facl(remote_path, self._play_context.become_user, mode, recursive=recursive, sudoable=False)
+            if res['rc'] != 0:
+                # File system acls failed; let's try to use chown next
+                # Set executable bit first as on some systems an
+                # unprivileged user can use chown
                 if execute:
                     res = self._remote_chmod('u+x', remote_path, recursive=recursive)
                     if res['rc'] != 0:
                         raise AnsibleError('Failed to set file mode on remote temporary files (rc: {0}, err: {1})'.format(res['rc'], res['stderr']))
 
-            elif remote_user == 'root':
-                raise AnsibleError('Failed to change ownership of the temporary files Ansible needs to create despite connecting as root.  Unprivileged become user would be unable to read the file.')
-            else:
-                # Chown'ing failed. We're probably lacking root privileges; let's try something else.
-                if execute:
-                    mode = 'rx'
-                else:
-                    mode = 'rX'
-                # Try to use fs acls to solve this problem
-                res = self._remote_set_user_facl(remote_path, self._play_context.become_user, mode, recursive=recursive, sudoable=False)
-                if res['rc'] != 0:
+                res = self._remote_chown(remote_path, self._play_context.become_user, recursive=recursive)
+                if res['rc'] != 0 and remote_user == 'root':
+                    # chown failed even if remove_user is root
+                    raise AnsibleError('Failed to change ownership of the temporary files Ansible needs to create despite connecting as root.  Unprivileged become user would be unable to read the file.')
+                elif res['rc'] != 0:
                     if C.ALLOW_WORLD_READABLE_TMPFILES:
-                        # fs acls failed -- do things this insecure way only
-                        # if the user opted in in the config file
+                        # chown and fs acls failed -- do things this insecure
+                        # way only if the user opted in in the config file
                         display.warning('Using world-readable permissions for temporary files Ansible needs to create when becoming an unprivileged user which may be insecure. For information on securing this, see https://docs.ansible.com/ansible/become.html#becoming-an-unprivileged-user')
                         res = self._remote_chmod('a+%s' % mode, remote_path, recursive=recursive)
                         if res['rc'] != 0:


Test patch: