Allow macOS ACLs to work for unpriv -> unpriv (#70785)
Change: - Use `chmod +a` in the fallback chain to allow MacOS to use ACLs to allow an unprivileged user to become an unprivileged user. Test Plan: - CI, new tests Tickets: - Fixes #70648 Signed-off-by: Rick Elrod <rick@elrod.me>
This commit is contained in:
parent
79f7104556
commit
0d7c144ce4
7 changed files with 97 additions and 24 deletions
2
changelogs/fragments/macos-chmod-acl.yml
Normal file
2
changelogs/fragments/macos-chmod-acl.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- When connecting as an unprivileged user, and becoming an unprivileged user, we now fall back to also trying ``chmod +a`` which works on macOS and makes use of ACLs.
|
|
@ -147,7 +147,10 @@ Next, if POSIX ACLs are **not** available or :command:`setfacl` could not be
|
||||||
run, Ansible will attempt to change ownership of the module file using
|
run, Ansible will attempt to change ownership of the module file using
|
||||||
:command:`chown` for systems which support doing so as an unprivileged user.
|
:command:`chown` for systems which support doing so as an unprivileged user.
|
||||||
|
|
||||||
New in Ansible 2.10, if the :command:`chown` fails, Ansible will then check the
|
New in Ansible 2.11, at this point, Ansible will try :command:`chmod +a` which
|
||||||
|
is a macOS-specific way of setting ACLs on files.
|
||||||
|
|
||||||
|
New in Ansible 2.10, if all of the above fails, Ansible will then check the
|
||||||
value of the configuration setting ``ansible_common_remote_group``. Many
|
value of the configuration setting ``ansible_common_remote_group``. Many
|
||||||
systems will allow a given user to change the group ownership of a file to a
|
systems will allow a given user to change the group ownership of a file to a
|
||||||
group the user is in. As a result, if the second unprivileged user (the
|
group the user is in. As a result, if the second unprivileged user (the
|
||||||
|
|
|
@ -518,7 +518,9 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
||||||
file with chown which only works in case the remote_user is
|
file with chown which only works in case the remote_user is
|
||||||
privileged or the remote systems allows chown calls by unprivileged
|
privileged or the remote systems allows chown calls by unprivileged
|
||||||
users (e.g. HP-UX)
|
users (e.g. HP-UX)
|
||||||
* If the chown fails, we check if ansible_common_remote_group is set.
|
* If the above fails, we next try 'chmod +a' which is a macOS way of
|
||||||
|
setting ACLs on files.
|
||||||
|
* If the above fails, we check if ansible_common_remote_group is set.
|
||||||
If it is, we attempt to chgrp the file to its value. This is useful
|
If it is, we attempt to chgrp the file to its value. This is useful
|
||||||
if the remote_user has a group in common with the become_user. As the
|
if the remote_user has a group in common with the become_user. As the
|
||||||
remote_user, we can chgrp the file to that group and allow the
|
remote_user, we can chgrp the file to that group and allow the
|
||||||
|
@ -571,12 +573,16 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
||||||
if execute:
|
if execute:
|
||||||
chmod_mode = 'rx'
|
chmod_mode = 'rx'
|
||||||
setfacl_mode = 'r-x'
|
setfacl_mode = 'r-x'
|
||||||
|
# Apple patches their "file_cmds" chmod with ACL support
|
||||||
|
chmod_acl_mode = '{0} allow read,execute'.format(become_user)
|
||||||
else:
|
else:
|
||||||
chmod_mode = 'rX'
|
chmod_mode = 'rX'
|
||||||
# TODO: this form fails silently on freebsd. We currently
|
# TODO: this form fails silently on freebsd. We currently
|
||||||
# never call _fixup_perms2() with execute=False but if we
|
# never call _fixup_perms2() with execute=False but if we
|
||||||
# start to we'll have to fix this.
|
# start to we'll have to fix this.
|
||||||
setfacl_mode = 'r-X'
|
setfacl_mode = 'r-X'
|
||||||
|
# Apple
|
||||||
|
chmod_acl_mode = '{0} allow read'.format(become_user)
|
||||||
|
|
||||||
# Step 3a: Are we able to use setfacl to add user ACLs to the file?
|
# Step 3a: Are we able to use setfacl to add user ACLs to the file?
|
||||||
res = self._remote_set_user_facl(
|
res = self._remote_set_user_facl(
|
||||||
|
@ -613,7 +619,15 @@ class ActionBase(with_metaclass(ABCMeta, object)):
|
||||||
'Unprivileged become user would be unable to read the '
|
'Unprivileged become user would be unable to read the '
|
||||||
'file.')
|
'file.')
|
||||||
|
|
||||||
# Step 3d: Common group
|
# Step 3d: Try macOS's special chmod + ACL
|
||||||
|
# macOS chmod's +a flag takes its own argument. As a slight hack, we
|
||||||
|
# pass that argument as the first element of remote_paths. So we end
|
||||||
|
# up running `chmod +a [that argument] [file 1] [file 2] ...`
|
||||||
|
res = self._remote_chmod([chmod_acl_mode] + remote_paths, '+a')
|
||||||
|
if res['rc'] == 0:
|
||||||
|
return remote_paths
|
||||||
|
|
||||||
|
# Step 3e: Common group
|
||||||
# Otherwise, we're a normal user. We failed to chown the paths to the
|
# Otherwise, we're a normal user. We failed to chown the paths to the
|
||||||
# unprivileged user, but if we have a common group with them, we should
|
# unprivileged user, but if we have a common group with them, we should
|
||||||
# be able to chown it to that.
|
# be able to chown it to that.
|
||||||
|
|
|
@ -2,3 +2,4 @@ destructive
|
||||||
shippable/posix/group1
|
shippable/posix/group1
|
||||||
skip/aix
|
skip/aix
|
||||||
needs/ssh
|
needs/ssh
|
||||||
|
needs/root
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
- name: Tests for chmod +a ACL functionality on macOS
|
||||||
|
hosts: ssh
|
||||||
|
gather_facts: yes
|
||||||
|
remote_user: unpriv1
|
||||||
|
become: yes
|
||||||
|
become_user: unpriv2
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Get AnsiballZ temp directory
|
||||||
|
action: tmpdir
|
||||||
|
register: tmpdir
|
||||||
|
become_user: unpriv2
|
||||||
|
become: yes
|
||||||
|
|
||||||
|
- name: run whoami
|
||||||
|
command: whoami
|
||||||
|
register: whoami
|
||||||
|
|
||||||
|
- name: Ensure we used the right fallback
|
||||||
|
shell: ls -le /var/tmp/ansible*/*_command.py
|
||||||
|
register: ls
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- whoami.stdout == "unpriv2"
|
||||||
|
- "'user:unpriv2 allow read' in ls.stdout"
|
|
@ -2,6 +2,11 @@
|
||||||
|
|
||||||
set -eux
|
set -eux
|
||||||
|
|
||||||
|
export ANSIBLE_KEEP_REMOTE_FILES=True
|
||||||
|
ANSIBLE_ACTION_PLUGINS="$(pwd)/action_plugins"
|
||||||
|
export ANSIBLE_ACTION_PLUGINS
|
||||||
|
export ANSIBLE_BECOME_PASS='iWishIWereCoolEnoughForRoot!'
|
||||||
|
|
||||||
begin_sandwich() {
|
begin_sandwich() {
|
||||||
ansible-playbook setup_unpriv_users.yml -i inventory -v "$@"
|
ansible-playbook setup_unpriv_users.yml -i inventory -v "$@"
|
||||||
}
|
}
|
||||||
|
@ -24,12 +29,24 @@ end_sandwich() {
|
||||||
trap "end_sandwich \"\$@\"" EXIT
|
trap "end_sandwich \"\$@\"" EXIT
|
||||||
|
|
||||||
# Common group tests
|
# Common group tests
|
||||||
begin_sandwich "$@"
|
# Skip on macOS, chmod fallback will take over.
|
||||||
ansible-playbook common_remote_group/setup.yml -i inventory -v "$@"
|
# 1) chmod is stupidly hard to disable, so hitting this test case on macOS would
|
||||||
export ANSIBLE_KEEP_REMOTE_FILES=True
|
# be a suuuuuuper edge case scenario
|
||||||
export ANSIBLE_COMMON_REMOTE_GROUP=commongroup
|
# 2) even if we can trick it so chmod doesn't exist, then other things break.
|
||||||
export ANSIBLE_BECOME_PASS='iWishIWereCoolEnoughForRoot!'
|
# Ansible wants a `chmod` around, even if it's not the final thing that gets
|
||||||
ANSIBLE_ACTION_PLUGINS="$(pwd)/action_plugins"
|
# us enough permission to run the task.
|
||||||
export ANSIBLE_ACTION_PLUGINS
|
if [[ "$OSTYPE" != darwin* ]]; then
|
||||||
ansible-playbook common_remote_group/test.yml -i inventory -v "$@"
|
begin_sandwich "$@"
|
||||||
end_sandwich "$@"
|
ansible-playbook common_remote_group/setup.yml -i inventory -v "$@"
|
||||||
|
export ANSIBLE_COMMON_REMOTE_GROUP=commongroup
|
||||||
|
ansible-playbook common_remote_group/test.yml -i inventory -v "$@"
|
||||||
|
end_sandwich "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$OSTYPE" == darwin* ]]; then
|
||||||
|
begin_sandwich "$@"
|
||||||
|
# In the default case this should happen on macOS, so no need for a setup
|
||||||
|
# It should just work.
|
||||||
|
ansible-playbook chmod_acl_macos/test.yml -i inventory -v "$@"
|
||||||
|
end_sandwich "$@"
|
||||||
|
fi
|
||||||
|
|
|
@ -358,6 +358,9 @@ class TestActionBase(unittest.TestCase):
|
||||||
return args_kv.get(option, default)
|
return args_kv.get(option, default)
|
||||||
return _helper
|
return _helper
|
||||||
|
|
||||||
|
action_base.get_become_option = MagicMock()
|
||||||
|
action_base.get_become_option.return_value = 'remoteuser2'
|
||||||
|
|
||||||
# Step 1: On windows, we just return remote_paths
|
# Step 1: On windows, we just return remote_paths
|
||||||
action_base._connection._shell._IS_WINDOWS = True
|
action_base._connection._shell._IS_WINDOWS = True
|
||||||
assertSuccess(execute=False)
|
assertSuccess(execute=False)
|
||||||
|
@ -412,12 +415,6 @@ class TestActionBase(unittest.TestCase):
|
||||||
'stdout': '',
|
'stdout': '',
|
||||||
'stderr': '',
|
'stderr': '',
|
||||||
}
|
}
|
||||||
action_base._remote_chmod.return_value = {
|
|
||||||
'rc': 0,
|
|
||||||
'stdout': 'some stuff here',
|
|
||||||
'stderr': '',
|
|
||||||
}
|
|
||||||
assertSuccess(execute=True)
|
|
||||||
action_base._remote_chmod.return_value = {
|
action_base._remote_chmod.return_value = {
|
||||||
'rc': 1,
|
'rc': 1,
|
||||||
'stdout': 'some stuff here',
|
'stdout': 'some stuff here',
|
||||||
|
@ -426,6 +423,12 @@ class TestActionBase(unittest.TestCase):
|
||||||
assertThrowRegex(
|
assertThrowRegex(
|
||||||
'Failed to set file mode on remote temporary file',
|
'Failed to set file mode on remote temporary file',
|
||||||
execute=True)
|
execute=True)
|
||||||
|
action_base._remote_chmod.return_value = {
|
||||||
|
'rc': 0,
|
||||||
|
'stdout': 'some stuff here',
|
||||||
|
'stderr': '',
|
||||||
|
}
|
||||||
|
assertSuccess(execute=True)
|
||||||
|
|
||||||
# Step 3c: chown
|
# Step 3c: chown
|
||||||
action_base._remote_chown = MagicMock()
|
action_base._remote_chown = MagicMock()
|
||||||
|
@ -446,7 +449,18 @@ class TestActionBase(unittest.TestCase):
|
||||||
assertThrowRegex('user would be unable to read the file.')
|
assertThrowRegex('user would be unable to read the file.')
|
||||||
remote_user = 'remoteuser1'
|
remote_user = 'remoteuser1'
|
||||||
|
|
||||||
# Step 3d: Common group
|
# Step 3d: chmod +a on osx
|
||||||
|
assertSuccess()
|
||||||
|
action_base._remote_chmod.assert_called_with(
|
||||||
|
['remoteuser2 allow read'] + remote_paths,
|
||||||
|
'+a')
|
||||||
|
|
||||||
|
# Step 3e: Common group
|
||||||
|
action_base._remote_chmod = MagicMock()
|
||||||
|
action_base._remote_chmod.side_effect = \
|
||||||
|
lambda x, y: \
|
||||||
|
dict(rc=1, stdout='', stderr='') if y == '+a' \
|
||||||
|
else dict(rc=0, stdout='', stderr='')
|
||||||
|
|
||||||
get_shell_option = action_base.get_shell_option
|
get_shell_option = action_base.get_shell_option
|
||||||
action_base.get_shell_option = MagicMock()
|
action_base.get_shell_option = MagicMock()
|
||||||
|
@ -463,11 +477,6 @@ class TestActionBase(unittest.TestCase):
|
||||||
}
|
}
|
||||||
# TODO: Add test to assert warning is shown if
|
# TODO: Add test to assert warning is shown if
|
||||||
# ALLOW_WORLD_READABLE_TMPFILES is set in this case.
|
# ALLOW_WORLD_READABLE_TMPFILES is set in this case.
|
||||||
action_base._remote_chmod.return_value = {
|
|
||||||
'rc': 0,
|
|
||||||
'stdout': '',
|
|
||||||
'stderr': '',
|
|
||||||
}
|
|
||||||
assertSuccess()
|
assertSuccess()
|
||||||
action_base._remote_chgrp.assert_called_once_with(
|
action_base._remote_chgrp.assert_called_once_with(
|
||||||
remote_paths,
|
remote_paths,
|
||||||
|
@ -486,6 +495,7 @@ class TestActionBase(unittest.TestCase):
|
||||||
'stderr': '',
|
'stderr': '',
|
||||||
}
|
}
|
||||||
assertSuccess()
|
assertSuccess()
|
||||||
|
action_base._remote_chmod = MagicMock()
|
||||||
action_base._remote_chmod.return_value = {
|
action_base._remote_chmod.return_value = {
|
||||||
'rc': 1,
|
'rc': 1,
|
||||||
'stdout': 'some stuff here',
|
'stdout': 'some stuff here',
|
||||||
|
|
Loading…
Reference in a new issue