diff --git a/changelog/62565.fixed b/changelog/62565.fixed new file mode 100644 index 00000000000..398b82e6d85 --- /dev/null +++ b/changelog/62565.fixed @@ -0,0 +1 @@ +Fix runas with cmd module when using the onedir bundled packages diff --git a/salt/modules/cmdmod.py b/salt/modules/cmdmod.py index a55afc7d04a..4d628ecbe08 100644 --- a/salt/modules/cmdmod.py +++ b/salt/modules/cmdmod.py @@ -25,6 +25,7 @@ import salt.utils.data import salt.utils.files import salt.utils.json import salt.utils.path +import salt.utils.pkg import salt.utils.platform import salt.utils.powershell import salt.utils.stringutils @@ -509,30 +510,51 @@ def _run( env_cmd.extend(["-s", "--", shell, "-c"]) else: env_cmd.extend(["-i", "--"]) - env_cmd.extend([sys.executable]) elif __grains__["os"] in ["FreeBSD"]: - env_cmd = ( + env_cmd = [ "su", "-", runas, "-c", - "{} -c {}".format(shell, sys.executable), - ) + ] elif __grains__["os_family"] in ["Solaris"]: - env_cmd = ("su", "-", runas, "-c", sys.executable) + env_cmd = ["su", "-", runas, "-c"] elif __grains__["os_family"] in ["AIX"]: - env_cmd = ("su", "-", runas, "-c", sys.executable) + env_cmd = ["su", "-", runas, "-c"] else: - env_cmd = ("su", "-s", shell, "-", runas, "-c", sys.executable) + env_cmd = ["su", "-s", shell, "-", runas, "-c"] + + if not salt.utils.pkg.check_bundled(): + if __grains__["os"] in ["FreeBSD"]: + env_cmd.extend(["{} -c {}".format(shell, sys.executable)]) + else: + env_cmd.extend([sys.executable]) + else: + with tempfile.NamedTemporaryFile("w", delete=False) as fp: + if __grains__["os"] in ["FreeBSD"]: + env_cmd.extend( + [ + "{} -c {} python {}".format( + shell, sys.executable, fp.name + ) + ] + ) + else: + env_cmd.extend(["{} python {}".format(sys.executable, fp.name)]) + fp.write(py_code) + fp.seek(0) + shutil.chown(fp.name, runas) + msg = "env command: {}".format(env_cmd) log.debug(log_callback(msg)) - env_bytes, env_encoded_err = subprocess.Popen( env_cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE, ).communicate(salt.utils.stringutils.to_bytes(py_code)) + if salt.utils.pkg.check_bundled(): + os.remove(fp.name) marker_count = env_bytes.count(marker_b) if marker_count == 0: # Possibly PAM prevented the login diff --git a/salt/utils/pkg/__init__.py b/salt/utils/pkg/__init__.py index 24460ec89ab..9a11e492a04 100644 --- a/salt/utils/pkg/__init__.py +++ b/salt/utils/pkg/__init__.py @@ -6,6 +6,7 @@ import errno import logging import os import re +import sys import salt.utils.data import salt.utils.files @@ -92,3 +93,12 @@ def match_version(desired, available, cmp_func=None, ignore_epoch=False): ): return candidate return None + + +def check_bundled(): + """ + Gather run-time information to indicate if we are running from source or bundled. + """ + if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + return True + return False diff --git a/tests/pytests/functional/modules/cmd/test_runas.py b/tests/pytests/functional/modules/cmd/test_runas.py new file mode 100644 index 00000000000..b92cc5cbf4d --- /dev/null +++ b/tests/pytests/functional/modules/cmd/test_runas.py @@ -0,0 +1,25 @@ +import pytest +import salt.modules.cmdmod as cmdmod + + +@pytest.fixture(scope="module") +def account(): + with pytest.helpers.create_account(create_group=True) as _account: + yield _account + + +@pytest.fixture(scope="module") +def configure_loader_modules(): + return { + cmdmod: { + "__grains__": {"os": "linux", "os_family": "linux"}, + } + } + + +@pytest.mark.skip_on_windows +@pytest.mark.skip_if_not_root +def test_run_as(account, caplog): + ret = cmdmod.run("id", runas=account.username) + assert "gid={}".format(account.info.gid) in ret + assert "uid={}".format(account.info.uid) in ret diff --git a/tests/pytests/unit/modules/test_cmdmod.py b/tests/pytests/unit/modules/test_cmdmod.py index 619a92702a6..85c7115d0f9 100644 --- a/tests/pytests/unit/modules/test_cmdmod.py +++ b/tests/pytests/unit/modules/test_cmdmod.py @@ -844,3 +844,205 @@ def test_cmd_script_saltenv_from_config_windows(): assert mock_cp_get_template.call_args[0][3] == "base" assert mock_run.call_count == 2 assert mock_run.call_args[1]["saltenv"] == "base" + + +@pytest.mark.parametrize( + "test_os,test_family", + [ + ("FreeBSD", "FreeBSD"), + ("linux", "Solaris"), + ("linux", "AIX"), + ("linux", "linux"), + ], +) +@pytest.mark.skip_on_darwin +@pytest.mark.skip_on_windows +def test_runas_env_all_os(test_os, test_family): + """ + cmd.run executes command and the environment is returned + when the runas parameter is specified + on all different OS types and os_family + """ + bundled = [False, True] + + for _bundled in bundled: + with patch("pwd.getpwnam") as getpwnam_mock: + with patch("subprocess.Popen") as popen_mock: + popen_mock.return_value = Mock( + communicate=lambda *args, **kwags: [b"", None], + pid=lambda: 1, + retcode=0, + ) + file_name = "/tmp/doesnotexist" + + with patch.dict( + cmdmod.__grains__, {"os": test_os, "os_family": test_family} + ): + with patch("salt.utils.pkg.check_bundled", return_value=_bundled): + with patch("shutil.chown"): + with patch("os.remove"): + with patch.object( + tempfile, "NamedTemporaryFile" + ) as mock_fp: + mock_fp.return_value.__enter__.return_value.name = ( + file_name + ) + if sys.platform.startswith(("freebsd", "openbsd")): + shell = "/bin/sh" + else: + shell = "/bin/bash" + _user = "foobar" + cmdmod._run( + "ls", + cwd=tempfile.gettempdir(), + runas=_user, + shell=shell, + ) + if not _bundled: + if test_family in ("Solaris", "AIX"): + env_cmd = ["su", "-", _user, "-c"] + elif test_os == "FreeBSD": + env_cmd = ["su", "-", _user, "-c"] + else: + env_cmd = [ + "su", + "-s", + shell, + "-", + _user, + "-c", + ] + if test_os == "FreeBSD": + env_cmd.extend( + [ + "{} -c {}".format( + shell, sys.executable + ) + ] + ) + else: + env_cmd.extend([sys.executable]) + assert ( + popen_mock.call_args_list[0][0][0] + == env_cmd + ) + else: + if test_family in ("Solaris", "AIX"): + env_cmd = ["su", "-", _user, "-c"] + elif test_os == "FreeBSD": + env_cmd = ["su", "-", _user, "-c"] + else: + env_cmd = [ + "su", + "-s", + shell, + "-", + _user, + "-c", + ] + if test_os == "FreeBSD": + env_cmd.extend( + [ + "{} -c {} python {}".format( + shell, sys.executable, file_name + ) + ] + ) + else: + env_cmd.extend( + [ + "{} python {}".format( + sys.executable, file_name + ) + ] + ) + assert ( + popen_mock.call_args_list[0][0][0] + == env_cmd + ) + + +@pytest.mark.skip_on_darwin +@pytest.mark.skip_on_windows +def test_runas_env_sudo_group(): + """ + cmd.run executes command and the environment is returned + when the runas parameter is specified + when group is passed and use_sudo=True + """ + bundled = [False, True] + + for _bundled in bundled: + + with patch("pwd.getpwnam") as getpwnam_mock: + with patch("subprocess.Popen") as popen_mock: + popen_mock.return_value = Mock( + communicate=lambda *args, **kwags: [b"", None], + pid=lambda: 1, + retcode=0, + ) + file_name = "/tmp/doesnotexist" + + with patch.dict( + cmdmod.__grains__, {"os": "linux", "os_family": "linux"} + ): + with patch("grp.getgrnam"): + with patch( + "salt.utils.pkg.check_bundled", return_value=_bundled + ): + with patch("shutil.chown"): + with patch("os.remove"): + with patch.object( + tempfile, "NamedTemporaryFile" + ) as mock_fp: + mock_fp.return_value.__enter__.return_value.name = ( + file_name + ) + if sys.platform.startswith( + ("freebsd", "openbsd") + ): + shell = "/bin/sh" + else: + shell = "/bin/bash" + _user = "foobar" + _group = "foobar" + + cmdmod._run( + "ls", + cwd=tempfile.gettempdir(), + runas=_user, + shell=shell, + group=_group, + ) + if not _bundled: + assert popen_mock.call_args_list[0][0][ + 0 + ] == [ + "sudo", + "-u", + _user, + "-g", + _group, + "-s", + "--", + shell, + "-c", + sys.executable, + ] + else: + assert popen_mock.call_args_list[0][0][ + 0 + ] == [ + "sudo", + "-u", + _user, + "-g", + _group, + "-s", + "--", + shell, + "-c", + "{} python {}".format( + sys.executable, file_name + ), + ]