zoph: Use privileged decorator for actions

Tests:

- Functional tests work
  - Dump/restore of database works
- Initial setup works
  - MySQL Database is created
  - Configuration options are set
  - OSM is enabled by default
  - User who installed the app becomes admin
- Setting configuration works
  - Enabling OSM
  - Setting admin user

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-08-26 15:32:18 -07:00 committed by James Valleroy
parent d68a84d245
commit 7f8eebce4c
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
3 changed files with 42 additions and 102 deletions

View File

@ -1,14 +1,10 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app to configure Zoph web application
"""
"""FreedomBox app to configure Zoph web application."""
import json
import logging
from django.utils.translation import gettext_lazy as _
from plinth import actions
from plinth import app as app_module
from plinth import cfg, frontpage, menu
from plinth.modules.apache.components import Webserver
@ -17,7 +13,7 @@ from plinth.modules.firewall.components import Firewall
from plinth.package import Packages
from plinth.utils import format_lazy
from . import manifest
from . import manifest, privileged
logger = logging.getLogger(__name__)
@ -88,45 +84,21 @@ class ZophApp(app_module.App):
def setup(self, old_version):
"""Install and configure the app."""
actions.superuser_run('zoph', ['pre-install'])
privileged.pre_install()
super().setup(old_version)
actions.superuser_run('zoph', ['setup'])
privileged.setup()
self.enable()
def set_configuration(admin_user=None, enable_osm=None):
"""Configure Zoph."""
args = []
if admin_user:
args += ['--admin-user', admin_user]
if enable_osm is not None:
args += ['--enable-osm', str(enable_osm)]
actions.superuser_run('zoph', ['set-configuration'] + args)
def is_configured():
"""Return whether the Zoph app is configured."""
output = actions.superuser_run('zoph', ['is-configured'])
return output.strip() == 'true'
def get_configuration():
"""Return full configuration of Zoph."""
configuration = actions.superuser_run('zoph', ['get-configuration'])
return json.loads(configuration)
class ZophBackupRestore(BackupRestore):
"""Component to backup/restore Zoph database"""
def backup_pre(self, packet):
"""Save database contents."""
super().backup_pre(packet)
actions.superuser_run('zoph', ['dump-database'])
privileged.dump_database()
def restore_post(self, packet):
"""Restore database contents."""
super().restore_post(packet)
actions.superuser_run('zoph', ['restore-database'])
privileged.restore_database()

84
actions/zoph → plinth/modules/zoph/privileged.py Executable file → Normal file
View File

@ -1,47 +1,21 @@
#!/usr/bin/python3
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Configuration helper for Zoph server.
"""
"""Configuration helper for Zoph server."""
import argparse
import configparser
import json
import os
import re
import subprocess
from typing import Optional
from plinth import action_utils
from plinth.actions import privileged
APACHE_CONF = '/etc/apache2/conf-available/zoph.conf'
DB_BACKUP_FILE = '/var/lib/plinth/backups-data/zoph-database.sql'
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('pre-install',
help='Perform Zoph pre-install configuration')
subparser = subparsers.add_parser('setup',
help='Perform Zoph configuration setup')
subparsers.add_parser('get-configuration',
help='Return the current configuration')
subparser = subparsers.add_parser('set-configuration',
help='Configure zoph')
subparser.add_argument('--admin-user', help='Name of the admin user')
subparser.add_argument('--enable-osm', help='Enable OpenSteetMap maps')
subparsers.add_parser('is-configured', help='return true if configured')
subparsers.add_parser('dump-database', help='Dump database to file')
subparsers.add_parser('restore-database',
help='Restore database from file')
subparsers.required = True
return parser.parse_args()
def subcommand_pre_install(_):
@privileged
def pre_install():
"""Preseed debconf values before packages are installed."""
action_utils.debconf_set_selections([
'zoph zoph/dbconfig-install boolean true',
@ -49,7 +23,8 @@ def subcommand_pre_install(_):
])
def subcommand_get_configuration(_):
@privileged
def get_configuration() -> dict[str, str]:
"""Return the current configuration."""
configuration = {}
process = subprocess.run(['zoph', '--dump-config'], stdout=subprocess.PIPE,
@ -58,7 +33,7 @@ def subcommand_get_configuration(_):
name, value = line.partition(':')[::2]
configuration[name.strip()] = value[1:]
print(json.dumps(configuration))
return configuration
def _zoph_configure(key, value):
@ -66,7 +41,8 @@ def _zoph_configure(key, value):
subprocess.run(['zoph', '--config', key, value], check=True)
def subcommand_setup(_):
@privileged
def setup():
"""Setup Zoph configuration."""
_zoph_configure('import.enable', 'true')
_zoph_configure('import.upload', 'true')
@ -88,19 +64,20 @@ def _get_db_name():
return config['zoph']['db_name'].strip('"')
def subcommand_set_configuration(arguments):
@privileged
def set_configuration(enable_osm: Optional[bool] = None,
admin_user: Optional[str] = None):
"""Setup Zoph Apache configuration."""
_zoph_configure('interface.user.remote', 'true')
# Note that using OpenSteetmap as a mapping provider is a very nice
# feature, but some people may regard its use as a privacy issue
if arguments.enable_osm:
value = 'osm' if arguments.enable_osm == 'True' else ''
if enable_osm is not None:
value = 'osm' if enable_osm else ''
_zoph_configure('maps.provider', value)
if arguments.admin_user:
if admin_user:
# Edit the database to rename the admin user to FreedomBox admin user.
admin_user = arguments.admin_user
if not re.match(r'^[\w.@][\w.@-]+\Z', admin_user, flags=re.ASCII):
# Check to avoid SQL injection
raise ValueError('Invalid username')
@ -112,13 +89,16 @@ def subcommand_set_configuration(arguments):
check=True)
def subcommand_is_configured(_):
"""Print whether zoph app is configured."""
subprocess.run(['zoph', '--get-config', 'interface.user.remote'],
check=True)
@privileged
def is_configured() -> bool:
"""Return whether zoph app is configured."""
process = subprocess.run(['zoph', '--get-config', 'interface.user.remote'],
stdout=subprocess.PIPE, check=True)
return process.stdout.decode().strip() == 'true'
def subcommand_dump_database(_):
@privileged
def dump_database():
"""Dump database to file."""
db_name = _get_db_name()
os.makedirs(os.path.dirname(DB_BACKUP_FILE), exist_ok=True)
@ -127,23 +107,11 @@ def subcommand_dump_database(_):
check=True)
def subcommand_restore_database(_):
@privileged
def restore_database():
"""Restore database from file."""
db_name = _get_db_name()
subprocess.run(['mysqladmin', '--force', 'drop', db_name], check=False)
subprocess.run(['mysqladmin', 'create', db_name], check=True)
with open(DB_BACKUP_FILE, 'r', encoding='utf-8') as db_restore_file:
subprocess.run(['mysql', db_name], stdin=db_restore_file, check=True)
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()

View File

@ -1,7 +1,5 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
FreedomBox app for configuring Zoph photo organiser.
"""
"""FreedomBox app for configuring Zoph photo organiser."""
import logging
@ -14,9 +12,8 @@ from django.views.generic import TemplateView
from plinth import app as app_module
from plinth import views
from plinth.errors import ActionError
from plinth.modules import zoph
from . import privileged
from .forms import ZophForm
logger = logging.getLogger(__name__)
@ -24,6 +21,7 @@ logger = logging.getLogger(__name__)
class SetupView(TemplateView):
"""Show zoph setup page."""
template_name = 'zoph-pre-setup.html'
success_url = reverse_lazy('zoph:index')
@ -38,18 +36,19 @@ class SetupView(TemplateView):
def post(self, _request, *args, **kwargs):
"""Handle form submission."""
admin_user = self.request.user.get_username()
zoph.set_configuration(admin_user=admin_user)
privileged.set_configuration(admin_user=admin_user)
return HttpResponseRedirect(reverse_lazy('zoph:index'))
class ZophAppView(views.AppView):
"""App configuration page."""
form_class = ZophForm
app_id = 'zoph'
def dispatch(self, request, *args, **kwargs):
"""Redirect to setup page if setup is not done yet."""
if not zoph.is_configured():
if not privileged.is_configured():
return redirect('zoph:setup')
return super().dispatch(request, *args, **kwargs)
@ -57,7 +56,7 @@ class ZophAppView(views.AppView):
def get_initial(self):
"""Get the current settings from Zoph."""
status = super().get_initial()
config = zoph.get_configuration()
config = privileged.get_configuration()
status['enable_osm'] = (config['maps.provider'] == 'osm')
return status
@ -67,9 +66,10 @@ class ZophAppView(views.AppView):
new_status = form.cleaned_data
if old_status['enable_osm'] != new_status['enable_osm']:
try:
zoph.set_configuration(enable_osm=new_status['enable_osm'])
privileged.set_configuration(
enable_osm=new_status['enable_osm'])
messages.success(self.request, _('Configuration updated.'))
except ActionError:
except Exception:
messages.error(self.request,
_('An error occurred during configuration.'))