# -*- coding: utf-8 -*- # (c) 2012-2014, Michael DeHaan # (c) 2016 Toshio Kuratomi # (c) 2017 Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import annotations import errno import json import pytest from unittest.mock import mock_open, patch from ansible.module_utils import basic from ansible.module_utils.common.text.converters import to_bytes import builtins @pytest.fixture def no_args_module_exec(): with patch.object(basic, '_ANSIBLE_ARGS', b'{"ANSIBLE_MODULE_ARGS": {}}'): yield # we're patching the global module object, so nothing to yield def no_args_module(selinux_enabled=None, selinux_mls_enabled=None): am = basic.AnsibleModule(argument_spec={}) # just dirty-patch the wrappers on the object instance since it's short-lived if isinstance(selinux_enabled, bool): patch.object(am, 'selinux_enabled', return_value=selinux_enabled).start() if isinstance(selinux_mls_enabled, bool): patch.object(am, 'selinux_mls_enabled', return_value=selinux_mls_enabled).start() return am # test AnsibleModule selinux wrapper methods @pytest.mark.usefixtures('no_args_module_exec') class TestSELinuxMU: def test_selinux_enabled(self): # test selinux unavailable # selinux unavailable, should return false with patch.object(basic, 'HAVE_SELINUX', False): assert no_args_module().selinux_enabled() is False # test selinux present/not-enabled disabled_mod = no_args_module() with patch.object(basic, 'selinux', create=True) as selinux: selinux.is_selinux_enabled.return_value = 0 assert disabled_mod.selinux_enabled() is False # ensure value is cached (same answer after unpatching) assert disabled_mod.selinux_enabled() is False # and present / enabled with patch.object(basic, 'HAVE_SELINUX', True): enabled_mod = no_args_module() with patch.object(basic, 'selinux', create=True) as selinux: selinux.is_selinux_enabled.return_value = 1 assert enabled_mod.selinux_enabled() is True # ensure value is cached (same answer after unpatching) assert enabled_mod.selinux_enabled() is True def test_selinux_mls_enabled(self): # selinux unavailable, should return false with patch.object(basic, 'HAVE_SELINUX', False): assert no_args_module().selinux_mls_enabled() is False # selinux disabled, should return false with patch.object(basic, 'selinux', create=True) as selinux: selinux.is_selinux_mls_enabled.return_value = 0 assert no_args_module(selinux_enabled=False).selinux_mls_enabled() is False with patch.object(basic, 'HAVE_SELINUX', True): # selinux enabled, should pass through the value of is_selinux_mls_enabled with patch.object(basic, 'selinux', create=True) as selinux: selinux.is_selinux_mls_enabled.return_value = 1 assert no_args_module(selinux_enabled=True).selinux_mls_enabled() is True def test_selinux_initial_context(self): # selinux missing/disabled/enabled sans MLS is 3-element None assert no_args_module(selinux_enabled=False, selinux_mls_enabled=False).selinux_initial_context() == [None, None, None] assert no_args_module(selinux_enabled=True, selinux_mls_enabled=False).selinux_initial_context() == [None, None, None] # selinux enabled with MLS is 4-element None assert no_args_module(selinux_enabled=True, selinux_mls_enabled=True).selinux_initial_context() == [None, None, None, None] def test_selinux_default_context(self): # selinux unavailable with patch.object(basic, 'HAVE_SELINUX', False): assert no_args_module().selinux_default_context(path='/foo/bar') == [None, None, None] am = no_args_module(selinux_enabled=True, selinux_mls_enabled=True) with patch.object(basic, 'selinux', create=True) as selinux: # matchpathcon success selinux.matchpathcon.return_value = [0, 'unconfined_u:object_r:default_t:s0'] assert am.selinux_default_context(path='/foo/bar') == ['unconfined_u', 'object_r', 'default_t', 's0'] with patch.object(basic, 'selinux', create=True) as selinux: # matchpathcon fail (return initial context value) selinux.matchpathcon.return_value = [-1, ''] assert am.selinux_default_context(path='/foo/bar') == [None, None, None, None] with patch.object(basic, 'selinux', create=True) as selinux: # matchpathcon OSError selinux.matchpathcon.side_effect = OSError assert am.selinux_default_context(path='/foo/bar') == [None, None, None, None] def test_selinux_context(self): # selinux unavailable with patch.object(basic, 'HAVE_SELINUX', False): assert no_args_module().selinux_context(path='/foo/bar') == [None, None, None] am = no_args_module(selinux_enabled=True, selinux_mls_enabled=True) # lgetfilecon_raw passthru with patch.object(basic, 'selinux', create=True) as selinux: selinux.lgetfilecon_raw.return_value = [0, 'unconfined_u:object_r:default_t:s0'] assert am.selinux_context(path='/foo/bar') == ['unconfined_u', 'object_r', 'default_t', 's0'] # lgetfilecon_raw returned a failure with patch.object(basic, 'selinux', create=True) as selinux: selinux.lgetfilecon_raw.return_value = [-1, ''] assert am.selinux_context(path='/foo/bar') == [None, None, None, None] # lgetfilecon_raw OSError (should bomb the module) with patch.object(basic, 'selinux', create=True) as selinux: selinux.lgetfilecon_raw.side_effect = OSError(errno.ENOENT, 'NotFound') with pytest.raises(SystemExit): am.selinux_context(path='/foo/bar') with patch.object(basic, 'selinux', create=True) as selinux: selinux.lgetfilecon_raw.side_effect = OSError() with pytest.raises(SystemExit): am.selinux_context(path='/foo/bar') def test_is_special_selinux_path(self): args = to_bytes(json.dumps(dict(ANSIBLE_MODULE_ARGS={'_ansible_selinux_special_fs': "nfs,nfsd,foos", '_ansible_remote_tmp': "/tmp", '_ansible_keep_remote_files': False}))) with patch.object(basic, '_ANSIBLE_ARGS', args): am = basic.AnsibleModule( argument_spec=dict(), ) def _mock_find_mount_point(path): if path.startswith('/some/path'): return '/some/path' elif path.startswith('/weird/random/fstype'): return '/weird/random/fstype' return '/' am.find_mount_point = _mock_find_mount_point am.selinux_context = lambda path: ['foo_u', 'foo_r', 'foo_t', 's0'] m = mock_open() m.side_effect = OSError with patch.object(builtins, 'open', m, create=True): assert am.is_special_selinux_path('/some/path/that/should/be/nfs') == (False, None) mount_data = [ '/dev/disk1 / ext4 rw,seclabel,relatime,data=ordered 0 0\n', '10.1.1.1:/path/to/nfs /some/path nfs ro 0 0\n', 'whatever /weird/random/fstype foos rw 0 0\n', ] # mock_open has a broken readlines() implementation apparently... # this should work by default but doesn't, so we fix it m = mock_open(read_data=''.join(mount_data)) m.return_value.readlines.return_value = mount_data with patch.object(builtins, 'open', m, create=True): assert am.is_special_selinux_path('/some/random/path') == (False, None) assert am.is_special_selinux_path('/some/path/that/should/be/nfs') == (True, ['foo_u', 'foo_r', 'foo_t', 's0']) assert am.is_special_selinux_path('/weird/random/fstype/path') == (True, ['foo_u', 'foo_r', 'foo_t', 's0']) def test_set_context_if_different(self): am = no_args_module(selinux_enabled=False) assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], True) is True assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is False am = no_args_module(selinux_enabled=True, selinux_mls_enabled=True) am.selinux_context = lambda path: ['bar_u', 'bar_r', None, None] am.is_special_selinux_path = lambda path: (False, None) with patch.object(basic, 'selinux', create=True) as selinux: selinux.lsetfilecon.return_value = 0 assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is True selinux.lsetfilecon.assert_called_with('/path/to/file', 'foo_u:foo_r:foo_t:s0') selinux.lsetfilecon.reset_mock() am.check_mode = True assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is True assert not selinux.lsetfilecon.called am.check_mode = False with patch.object(basic, 'selinux', create=True) as selinux: selinux.lsetfilecon.return_value = 1 with pytest.raises(SystemExit): am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], True) with patch.object(basic, 'selinux', create=True) as selinux: selinux.lsetfilecon.side_effect = OSError with pytest.raises(SystemExit): am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], True) am.is_special_selinux_path = lambda path: (True, ['sp_u', 'sp_r', 'sp_t', 's0']) with patch.object(basic, 'selinux', create=True) as selinux: selinux.lsetfilecon.return_value = 0 assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is True selinux.lsetfilecon.assert_called_with('/path/to/file', 'sp_u:sp_r:sp_t:s0')