diff --git a/plinth/action_utils.py b/plinth/action_utils.py index 7cd46fa75..997168ff1 100644 --- a/plinth/action_utils.py +++ b/plinth/action_utils.py @@ -788,3 +788,13 @@ def move_uploaded_file(source: str | pathlib.Path, shutil.move(source, destination) shutil.chown(destination, user, group) destination.chmod(permissions) + + +def run_as_user(command, username, **kwargs): + """Run a command as another user. + + Uses 'runuser' which is similar to 'su'. Creates PAM session unlike + setpriv. Sets real/effective uid/gid and resets the environment. + """ + command = ['runuser', '--user', username, '--'] + command + return subprocess.run(command, **kwargs) diff --git a/plinth/tests/test_action_utils.py b/plinth/tests/test_action_utils.py index 980b03024..d26ba1b45 100644 --- a/plinth/tests/test_action_utils.py +++ b/plinth/tests/test_action_utils.py @@ -12,7 +12,7 @@ import pytest from plinth.action_utils import (get_addresses, get_hostname, is_systemd_running, move_uploaded_file, - service_action, service_disable, + run_as_user, service_action, service_disable, service_enable, service_is_enabled, service_is_running, service_reload, service_restart, service_start, service_stop, @@ -229,3 +229,18 @@ def test_move_uploaded_file(tmp_path, upload_dir): assert destination_file.stat().st_mode & 0o777 == 0o600 assert destination_file.read_text() == 'x-contents-2' assert not source.exists() + + +@patch('subprocess.run') +def test_run_as_user(run): + """Test running a command as another user works.""" + run.return_value = 'test-return-value' + return_value = run_as_user(['command', 'arg1', '--foo'], + username='foouser', stdout=subprocess.PIPE, + check=True) + assert return_value == 'test-return-value' + assert run.mock_calls == [ + call( + ['runuser', '--user', 'foouser', '--', 'command', 'arg1', '--foo'], + stdout=subprocess.PIPE, check=True) + ]