AnsibleModule.set_mode_if_different: handle symlink is in a sticky directory (#45198)
* file: add symlink is in a sticky directory tests * file: handle symlink in a sticky directory Co-Authored-By: Sviatoslav Sydorenko <wk.cvs.github@sydorenko.org.ua> * Add changelog and fix unit test The builtins import was removed since it was unused, but it is now needed.
This commit is contained in:
parent
5226ac5778
commit
b464d18fd1
5 changed files with 133 additions and 1 deletions
|
@ -0,0 +1,4 @@
|
||||||
|
bugfixes:
|
||||||
|
- >
|
||||||
|
set_mode_if_different - handle symlink if it is inside a directory with
|
||||||
|
sticky bit set (https://github.com/ansible/ansible/pull/45198)
|
|
@ -1186,7 +1186,11 @@ class AnsibleModule(object):
|
||||||
if underlying_stat.st_mode != new_underlying_stat.st_mode:
|
if underlying_stat.st_mode != new_underlying_stat.st_mode:
|
||||||
os.chmod(b_path, stat.S_IMODE(underlying_stat.st_mode))
|
os.chmod(b_path, stat.S_IMODE(underlying_stat.st_mode))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
if os.path.islink(b_path) and e.errno in (errno.EPERM, errno.EROFS): # Can't set mode on symbolic links
|
if os.path.islink(b_path) and e.errno in (
|
||||||
|
errno.EACCES, # can't access symlink in sticky directory (stat)
|
||||||
|
errno.EPERM, # can't set mode on symbolic links (chmod)
|
||||||
|
errno.EROFS, # can't set mode on read-only filesystem
|
||||||
|
):
|
||||||
pass
|
pass
|
||||||
elif e.errno in (errno.ENOENT, errno.ELOOP): # Can't set mode on broken symbolic links
|
elif e.errno in (errno.ENOENT, errno.ELOOP): # Can't set mode on broken symbolic links
|
||||||
pass
|
pass
|
||||||
|
|
2
test/integration/targets/file/defaults/main.yml
Normal file
2
test/integration/targets/file/defaults/main.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
---
|
||||||
|
remote_unprivileged_user: tmp_ansible_test_user
|
|
@ -116,6 +116,88 @@
|
||||||
that:
|
that:
|
||||||
- "file6a_result.changed == false"
|
- "file6a_result.changed == false"
|
||||||
|
|
||||||
|
# In order for a symlink in a sticky world writable directory to be followed, it must
|
||||||
|
# either be owned by the follower,
|
||||||
|
# or the directory and symlink must have the same owner.
|
||||||
|
- name: symlink in sticky directory
|
||||||
|
block:
|
||||||
|
- name: Create remote unprivileged remote user
|
||||||
|
user:
|
||||||
|
name: '{{ remote_unprivileged_user }}'
|
||||||
|
register: user
|
||||||
|
|
||||||
|
- name: Create a local temporary directory
|
||||||
|
tempfile:
|
||||||
|
state: directory
|
||||||
|
register: tempdir
|
||||||
|
|
||||||
|
- name: Set sticky bit
|
||||||
|
file:
|
||||||
|
path: '{{ tempdir.path }}'
|
||||||
|
mode: o=rwXt
|
||||||
|
|
||||||
|
- name: 'Check mode: force creation soft link in sticky directory owned by another user (mode is used)'
|
||||||
|
file:
|
||||||
|
src: '{{ user.home }}/nonexistent'
|
||||||
|
dest: '{{ tempdir.path }}/soft3.txt'
|
||||||
|
mode: 0640
|
||||||
|
state: 'link'
|
||||||
|
owner: '{{ remote_unprivileged_user }}'
|
||||||
|
force: true
|
||||||
|
follow: false
|
||||||
|
check_mode: true
|
||||||
|
register: missing_dst_no_follow_enable_force_use_mode1
|
||||||
|
|
||||||
|
- name: force creation soft link in sticky directory owned by another user (mode is used)
|
||||||
|
file:
|
||||||
|
src: '{{ user.home }}/nonexistent'
|
||||||
|
dest: '{{ tempdir.path }}/soft3.txt'
|
||||||
|
mode: 0640
|
||||||
|
state: 'link'
|
||||||
|
owner: '{{ remote_unprivileged_user }}'
|
||||||
|
force: true
|
||||||
|
follow: false
|
||||||
|
register: missing_dst_no_follow_enable_force_use_mode2
|
||||||
|
|
||||||
|
- name: Get stat info for the link
|
||||||
|
stat:
|
||||||
|
path: '{{ tempdir.path }}/soft3.txt'
|
||||||
|
follow: false
|
||||||
|
register: soft3_result
|
||||||
|
|
||||||
|
- name: 'Idempotence: force creation soft link in sticky directory owned by another user (mode is used)'
|
||||||
|
file:
|
||||||
|
src: '{{ user.home }}/nonexistent'
|
||||||
|
dest: '{{ tempdir.path }}/soft3.txt'
|
||||||
|
mode: 0640
|
||||||
|
state: 'link'
|
||||||
|
owner: '{{ remote_unprivileged_user }}'
|
||||||
|
force: yes
|
||||||
|
follow: false
|
||||||
|
register: missing_dst_no_follow_enable_force_use_mode3
|
||||||
|
always:
|
||||||
|
- name: Delete remote unprivileged remote user
|
||||||
|
user:
|
||||||
|
name: '{{ remote_unprivileged_user }}'
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: Delete unprivileged user home and tempdir
|
||||||
|
file:
|
||||||
|
path: "{{ item }}"
|
||||||
|
state: absent
|
||||||
|
loop:
|
||||||
|
- '{{ tempdir.path }}'
|
||||||
|
- '{{ user.home }}'
|
||||||
|
|
||||||
|
- name: verify that link was created
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- "missing_dst_no_follow_enable_force_use_mode1 is changed"
|
||||||
|
- "missing_dst_no_follow_enable_force_use_mode2 is changed"
|
||||||
|
- "missing_dst_no_follow_enable_force_use_mode3 is not changed"
|
||||||
|
- "soft3_result['stat'].islnk"
|
||||||
|
- "soft3_result['stat'].lnk_target == '{{ user.home }}/nonexistent'"
|
||||||
|
|
||||||
#
|
#
|
||||||
# Test creating a link to a directory https://github.com/ansible/ansible/issues/1369
|
# Test creating a link to a directory https://github.com/ansible/ansible/issues/1369
|
||||||
#
|
#
|
||||||
|
|
|
@ -7,9 +7,16 @@
|
||||||
from __future__ import (absolute_import, division, print_function)
|
from __future__ import (absolute_import, division, print_function)
|
||||||
__metaclass__ = type
|
__metaclass__ = type
|
||||||
|
|
||||||
|
import errno
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from itertools import product
|
from itertools import product
|
||||||
|
|
||||||
|
try:
|
||||||
|
import builtins
|
||||||
|
except ImportError:
|
||||||
|
import __builtin__ as builtins
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@ -141,3 +148,36 @@ def test_missing_lchmod_is_link(am, mock_stats, mocker, monkeypatch, check_mode)
|
||||||
|
|
||||||
mocker.resetall()
|
mocker.resetall()
|
||||||
mocker.stopall()
|
mocker.stopall()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('stdin,',
|
||||||
|
({},),
|
||||||
|
indirect=['stdin'])
|
||||||
|
def test_missing_lchmod_is_link_in_sticky_dir(am, mock_stats, mocker):
|
||||||
|
"""Some platforms have lchmod (*BSD) others do not (Linux)"""
|
||||||
|
|
||||||
|
am.check_mode = False
|
||||||
|
original_hasattr = hasattr
|
||||||
|
|
||||||
|
def _hasattr(obj, name):
|
||||||
|
if obj == os and name == 'lchmod':
|
||||||
|
return False
|
||||||
|
return original_hasattr(obj, name)
|
||||||
|
|
||||||
|
mock_lstat = mocker.MagicMock()
|
||||||
|
mock_lstat.st_mode = 0o777
|
||||||
|
|
||||||
|
mocker.patch('os.lstat', side_effect=[mock_lstat, mock_lstat])
|
||||||
|
mocker.patch.object(builtins, 'hasattr', side_effect=_hasattr)
|
||||||
|
mocker.patch('os.path.islink', return_value=True)
|
||||||
|
mocker.patch('os.path.exists', return_value=True)
|
||||||
|
m_stat = mocker.patch('os.stat', side_effect=OSError(errno.EACCES, 'Permission denied'))
|
||||||
|
m_chmod = mocker.patch('os.chmod', return_value=None)
|
||||||
|
|
||||||
|
# not changed: can't set mode on symbolic links
|
||||||
|
assert not am.set_mode_if_different('/path/to/file/no_lchmod', 0o660, False)
|
||||||
|
m_stat.assert_called_with(b'/path/to/file/no_lchmod')
|
||||||
|
m_chmod.assert_not_called()
|
||||||
|
|
||||||
|
mocker.resetall()
|
||||||
|
mocker.stopall()
|
||||||
|
|
Loading…
Reference in a new issue