actions: Allow nested and top-level actions

- Currently, privileged actions are not allowed under top-level plinth module.
They are only allowed under each app module. Allow privileged actions under
plinth module.

- Currently, privileged actions are not allowed under a sub-module of
'privileged' package. They are allowed only in 'privileged' module. Allow
sub-modules under 'privileged' package.

Tests:

- Email app functional tests pass
- Functional tests for apps using package and service privileged methods pass

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2022-09-02 10:43:42 -07:00 committed by James Valleroy
parent 74678c1d69
commit 585092ca63
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
4 changed files with 23 additions and 27 deletions

View File

@ -59,7 +59,11 @@ def _call(module_name, action_name, arguments):
raise SyntaxError('Invalid module name') raise SyntaxError('Invalid module name')
cfg.read() cfg.read()
if module_name == 'plinth':
import_path = 'plinth'
else:
import_path = module_loader.get_module_import_path(module_name) import_path = module_loader.get_module_import_path(module_name)
try: try:
module = importlib.import_module(import_path + '.privileged') module = importlib.import_module(import_path + '.privileged')
except ModuleNotFoundError as exception: except ModuleNotFoundError as exception:

View File

@ -84,7 +84,6 @@ import os
import re import re
import shlex import shlex
import subprocess import subprocess
import sys
from plinth import cfg from plinth import cfg
from plinth.errors import ActionError from plinth.errors import ActionError
@ -311,5 +310,13 @@ def _check_privileged_action_arguments(func):
def _get_privileged_action_module_name(func): def _get_privileged_action_module_name(func):
"""Figure out the module name of a privileged action.""" """Figure out the module name of a privileged action."""
module_name = func.__module__ module_name = func.__module__
module = sys.modules[module_name] while module_name:
return module.__package__.rpartition('.')[2] module_name, _, last = module_name.rpartition('.')
if last == 'privileged':
break
if not module_name:
raise ValueError('Privileged actions must be placed under a '
'package/module named privileged')
return module_name.rpartition('.')[2]

View File

@ -6,3 +6,5 @@ Provides privileged actions that run as root.
from . import aliases, dkim, domain, home, postfix, spam, tls from . import aliases, dkim, domain, home, postfix, spam, tls
__all__ = ['aliases', 'domain', 'dkim', 'home', 'postfix', 'spam', 'tls'] __all__ = ['aliases', 'domain', 'dkim', 'home', 'postfix', 'spam', 'tls']
from .aliases import action_setup

View File

@ -14,13 +14,11 @@ import shutil
import tempfile import tempfile
from unittest.mock import call, patch from unittest.mock import call, patch
import apt_pkg
import pytest import pytest
from plinth import cfg from plinth import cfg
from plinth.actions import _log_command as log_command from plinth.actions import _log_command as log_command
from plinth.actions import privileged, run, superuser_run from plinth.actions import privileged, run, superuser_run
from plinth.errors import ActionError
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -36,7 +34,6 @@ def actions_test_setup(load_cfg):
cfg.actions_dir = str(tmp_directory) cfg.actions_dir = str(tmp_directory)
actions_dir = pathlib.Path(__file__).parent / '../../actions' actions_dir = pathlib.Path(__file__).parent / '../../actions'
shutil.copy(str(actions_dir / 'packages'), str(tmp_directory))
shutil.copy(str(actions_dir / 'test_path'), str(tmp_directory)) shutil.copy(str(actions_dir / 'test_path'), str(tmp_directory))
shutil.copy('/bin/echo', str(tmp_directory)) shutil.copy('/bin/echo', str(tmp_directory))
shutil.copy('/usr/bin/id', str(tmp_directory)) shutil.copy('/usr/bin/id', str(tmp_directory))
@ -143,24 +140,6 @@ def test_multiple_options_and_output():
assert options == output assert options == output
@pytest.mark.usefixtures('needs_root')
def test_is_package_manager_busy():
"""Test the behavior of `is-package-manager-busy` in both locked and
unlocked states of the dpkg lock file."""
apt_pkg.init() # initialize apt_pkg module
# In the locked state, the lsof command returns 0.
# Hence no error is thrown.
with apt_pkg.SystemLock():
superuser_run('packages', ['is-package-manager-busy'])
# In the unlocked state, the lsof command returns 1.
# An ActionError is raised in this case.
with pytest.raises(ActionError):
superuser_run('packages', ['is-package-manager-busy'])
@pytest.mark.usefixtures('develop_mode', 'needs_root') @pytest.mark.usefixtures('develop_mode', 'needs_root')
def test_action_path(monkeypatch): def test_action_path(monkeypatch):
"""Test that in development mode, python action scripts get the """Test that in development mode, python action scripts get the
@ -247,14 +226,16 @@ def test_privileged_argument_annotation_check():
privileged(func_valid) privileged(func_valid)
@patch('plinth.actions._get_privileged_action_module_name')
@patch('plinth.actions.superuser_run') @patch('plinth.actions.superuser_run')
def test_privileged_method_call(superuser_run_): def test_privileged_method_call(superuser_run_, get_module_name):
"""Test that privileged method calls the superuser action properly.""" """Test that privileged method calls the superuser action properly."""
def func_with_args(_a: int, _b: str, _c: int = 1, _d: str = 'dval', def func_with_args(_a: int, _b: str, _c: int = 1, _d: str = 'dval',
_e: str = 'eval'): _e: str = 'eval'):
return return
get_module_name.return_value = 'tests'
superuser_run_.return_value = json.dumps({ superuser_run_.return_value = json.dumps({
'result': 'success', 'result': 'success',
'return': 'bar' 'return': 'bar'
@ -269,13 +250,15 @@ def test_privileged_method_call(superuser_run_):
[call('actions', ['tests', 'func_with_args'], input=input_.encode())]) [call('actions', ['tests', 'func_with_args'], input=input_.encode())])
@patch('plinth.actions._get_privileged_action_module_name')
@patch('plinth.actions.superuser_run') @patch('plinth.actions.superuser_run')
def test_privileged_method_exceptions(superuser_run_): def test_privileged_method_exceptions(superuser_run_, get_module_name):
"""Test that exceptions on privileged methods are return properly.""" """Test that exceptions on privileged methods are return properly."""
def func_with_exception(): def func_with_exception():
raise TypeError('type error') raise TypeError('type error')
get_module_name.return_value = 'tests'
superuser_run_.return_value = json.dumps({ superuser_run_.return_value = json.dumps({
'result': 'exception', 'result': 'exception',
'exception': { 'exception': {