diff --git a/doc/dev/reference/actions.rst b/doc/dev/reference/actions.rst index 91d7cf59b..53f21c8cd 100644 --- a/doc/dev/reference/actions.rst +++ b/doc/dev/reference/actions.rst @@ -13,4 +13,4 @@ else. These actions are also directly usable by a skilled administrator. The following documentation for the ``actions`` module. .. automodule:: plinth.actions - :members: run, superuser_run, run_as_user, _run + :members: run, superuser_run, run_as_user, _run, privileged diff --git a/doc/dev/tutorial/code.rst b/doc/dev/tutorial/code.rst index 4cde693a7..0d4281b7a 100644 --- a/doc/dev/tutorial/code.rst +++ b/doc/dev/tutorial/code.rst @@ -24,6 +24,12 @@ plinth/modules/transmission/manifest.py .. literalinclude:: ../../../plinth/modules/transmission/manifest.py :language: python3 +plinth/modules/transmission/privileged.py +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. literalinclude:: ../../../plinth/modules/transmission/privileged.py + :language: python3 + plinth/modules/transmission/urls.py ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -53,9 +59,3 @@ plinth/modules/transmission/tests/__init__.py .. literalinclude:: ../../../plinth/modules/transmission/tests/__init__.py :language: python3 - -actions/transmission -^^^^^^^^^^^^^^^^^^^^ - -.. literalinclude:: ../../../actions/transmission - :language: python3 diff --git a/doc/dev/tutorial/customizing.rst b/doc/dev/tutorial/customizing.rst index e113f4e69..ff4eaa51d 100644 --- a/doc/dev/tutorial/customizing.rst +++ b/doc/dev/tutorial/customizing.rst @@ -114,9 +114,7 @@ the user submits it. Let us implement that in ``views.py``. def get_initial(self): """Get the current settings from Transmission server.""" status = super().get_initial() - configuration = actions.superuser_run('transmission', - ['get-configuration']) - configuration = json.loads(configuration) + configuration = privileged.get_configuration() status['storage_path'] = configuration['download-dir'] status['hostname'] = socket.gethostname() @@ -130,9 +128,7 @@ the user submits it. Let us implement that in ``views.py``. new_configuration = { 'download-dir': new_status['storage_path'], } - - actions.superuser_run('transmission', ['merge-configuration'], - input=json.dumps(new_configuration).encode()) + privileged.merge_configuration(new_configuration) messages.success(self.request, 'Configuration updated') return super().form_valid(form) @@ -150,85 +146,51 @@ the action was successful or that nothing happened. We use the Django messaging framework to accomplish this. See :doc:`Django messaging framework ` for more information. -Writing actions -^^^^^^^^^^^^^^^ +Writing privileged actions +^^^^^^^^^^^^^^^^^^^^^^^^^^ -The actual work of performing the configuration change is carried out by an -*action*. Actions are independent scripts that run with higher privileges -required to perform a task. They are placed in a separate directory and invoked -as scripts via sudo. For our application we need to write an action that can -enable and disable the web configuration. We will do this by creating a file -``actions/transmission``. +The actual work of performing the configuration change is carried out by +privileged actions. These actions are independent scripts that run with higher +privileges required to perform a task. They are placed in a separate python +module 'privileged.py' and invoked as regular methods. For our application we +need to write two privileged actions that can read and write the configuration +for transmission daemon. We will do this by creating a file ``privileged.py``. .. code-block:: python3 - :caption: ``actions/transmission`` + :caption: ``privileged.py`` - import argparse import json - import sys + import pathlib + from typing import Union from plinth import action_utils + from plinth.actions import privileged - TRANSMISSION_CONFIG = '/etc/transmission-daemon/settings.json' + _transmission_config = pathlib.Path('/etc/transmission-daemon/settings.json') - def parse_arguments(): - """Return parsed command line arguments as dictionary.""" - parser = argparse.ArgumentParser() - subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') - - subparsers.add_parser('get-configuration', - help='Return the current configuration') - subparsers.add_parser( - 'merge-configuration', - help='Merge JSON configuration from stdin with existing') - - subparsers.required = True - return parser.parse_args() - - - def subcommand_get_configuration(_): + @privileged + def get_configuration() -> dict[str, str]: """Return the current configuration in JSON format.""" - configuration = open(TRANSMISSION_CONFIG, 'r').read() - print(configuration) + return json.loads(_transmission_config.read_text(encoding='utf-8')) - def subcommand_merge_configuration(arguments): + @privileged + def merge_configuration(configuration: dict[str, Union[str, bool]]) -> None: """Merge given JSON configuration with existing configuration.""" - configuration = sys.stdin.read() - configuration = json.loads(configuration) - - current_configuration = open(TRANSMISSION_CONFIG, 'r').read() + current_configuration = _transmission_config.read_bytes() current_configuration = json.loads(current_configuration) new_configuration = current_configuration new_configuration.update(configuration) new_configuration = json.dumps(new_configuration, indent=4, sort_keys=True) - open(TRANSMISSION_CONFIG, 'w').write(new_configuration) + _transmission_config.write_text(new_configuration, encoding='utf-8') action_utils.service_reload('transmission-daemon') - - def main(): - """Parse arguments and perform all duties.""" - arguments = parse_arguments() - - subcommand = arguments.subcommand.replace('-', '_') - subcommand_method = globals()['subcommand_' + subcommand] - subcommand_method(arguments) - - - if __name__ == '__main__': - main() - -This is a simple Python3 program that parses command line arguments. While -Python3 is preferred, it can be written in other languages also. It may use -various helper utilities provided by the FreedomBox framework in -:obj:`plinth.action_utils` to easily perform it's duties. - -This script is automatically installed to ``/usr/share/plinth/actions`` by -FreedomBox's installation script ``setup.py``. Only from here will there is a -possibility of running the script under ``sudo``. If you are writing an -application that resides indenpendently of FreedomBox's source code, your app's -``setup.py`` script will need to take care of copying the file to this target -location. +This is a simple Python3 module but it runs in a separate process with superuser +privileges due to the :meth:`plinth.actions.privileged` decorator. All such +methods must have full type annotations for the method parameters. Further, the +parameters and return value must be JSON serializable. It may use various helper +utilities provided by the FreedomBox framework in :obj:`plinth.action_utils` to +easily perform it's duties. diff --git a/doc/dev/tutorial/setup.rst b/doc/dev/tutorial/setup.rst index 63b82c9ca..da73d3529 100644 --- a/doc/dev/tutorial/setup.rst +++ b/doc/dev/tutorial/setup.rst @@ -24,10 +24,7 @@ installation: 'rpc-whitelist-enabled': False, 'rpc-authentication-required': False } - helper.call('post', actions.superuser_run, 'transmission', - ['merge-configuration'], - input=json.dumps(new_configuration).encode()) - + helper.call('post', privileged.merge_configuration, new_configuration) helper.call('post', app.enable) The first time this app's view is accessed, FreedomBox shows an app installation diff --git a/doc/dev/tutorial/skeleton.rst b/doc/dev/tutorial/skeleton.rst index 156d68dcd..e79071838 100644 --- a/doc/dev/tutorial/skeleton.rst +++ b/doc/dev/tutorial/skeleton.rst @@ -12,26 +12,25 @@ Create a directory structure as follows with empty files. We will fill them up in a step-by-step manner:: ─┬ / - ├─┬ plinth/ - │ └─┬ modules/ - │ └─┬ transmission/ - │ ├─ __init__.py - │ ├─ forms.py - │ ├─ manifest.py - │ ├─ urls.py - │ ├─ views.py - │ ├─┬ data/ - │ │ └─┬ etc/ - │ │ ├─┬ plinth/ - │ │ │ └─┬ modules-enabled/ - │ │ │ └─ transmission - │ │ └─┬ apache2/ - │ │ └─┬ conf-available/ - │ │ └─ transmission-freedombox.conf - │ └─┬ tests - │ └─ __init__.py - └─┬ actions/ - └─ transmission + └─┬ plinth/ + └─┬ modules/ + └─┬ transmission/ + ├─ __init__.py + ├─ forms.py + ├─ privileged.py + ├─ manifest.py + ├─ urls.py + ├─ views.py + ├─┬ data/ + │ └─┬ etc/ + │ ├─┬ plinth/ + │ │ └─┬ modules-enabled/ + │ │ └─ transmission + │ └─┬ apache2/ + │ └─┬ conf-available/ + │ └─ transmission-freedombox.conf + └─┬ tests + └─ __init__.py The file ``__init__.py`` indicates that the directory in which it is present is a Python module. For now, it is an empty file.