FreedomBox/plinth/modules/zoph/privileged.py
Sunil Mohan Adapa b5c641097a
zoph: Re-add a safety check when reading the setup state of the app
- Similar check was removed in 6646512a0adab6943503ec47372502fb28805911 when it
was that it was not needed.

Tests:

- Run functional tests for zoph.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2024-03-31 20:48:00 -04:00

171 lines
5.6 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""Configuration helper for Zoph server."""
import configparser
import os
import pathlib
import re
import subprocess
from plinth import action_utils
from plinth.actions import privileged
APACHE_CONF = '/etc/apache2/conf-available/zoph.conf'
DB_CONF = pathlib.Path('/etc/zoph.ini')
DB_BACKUP_FILE = '/var/lib/plinth/backups-data/zoph-database.sql'
@privileged
def pre_install():
"""Preseed debconf values before packages are installed."""
action_utils.debconf_set_selections([
'zoph zoph/dbconfig-install boolean true',
'zoph zoph/dbconfig-upgrade boolean true',
'zoph zoph/dbconfig-remove boolean true',
'zoph zoph/mysql/admin-user string root',
])
@privileged
def get_configuration() -> dict[str, str]:
"""Return the current configuration."""
configuration = {}
process = subprocess.run(['zoph', '--dump-config'], stdout=subprocess.PIPE,
check=True)
for line in process.stdout.decode().splitlines():
name, value = line.partition(':')[::2]
configuration[name.strip()] = value[1:]
return configuration
def _zoph_configure(key, value):
"""Set a configure value in Zoph."""
subprocess.run(['zoph', '--config', key, value], check=True)
@privileged
def setup():
"""Setup Zoph configuration.
May be called when app is disabled.
"""
with action_utils.service_ensure_running('mysql'):
_zoph_configure('import.enable', 'true')
_zoph_configure('import.upload', 'true')
_zoph_configure('import.rotate', 'true')
_zoph_configure('path.unzip', 'unzip')
_zoph_configure('path.untar', 'tar xvf')
_zoph_configure('path.ungz', 'gunzip')
# Maps using OpenStreetMap is enabled by default.
_zoph_configure('maps.provider', 'osm')
def _get_db_config():
"""Return the name of the database configured by dbconfig."""
config = configparser.ConfigParser()
with DB_CONF.open('r', encoding='utf-8') as file_handle:
config.read_file(file_handle)
return {
'db_host': config['zoph']['db_host'].strip('"'),
'db_name': config['zoph']['db_name'].strip('"'),
'db_user': config['zoph']['db_user'].strip('"'),
'db_pass': config['zoph']['db_pass'].strip('"'),
}
@privileged
def set_configuration(enable_osm: bool | None = None,
admin_user: str | None = 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 enable_osm is not None:
value = 'osm' if enable_osm else ''
_zoph_configure('maps.provider', value)
if admin_user:
# Edit the database to rename the admin user to FreedomBox admin user.
if not re.match(r'^[\w.@][\w.@-]+\Z', admin_user, flags=re.ASCII):
# Check to avoid SQL injection
raise ValueError('Invalid username')
query = f"UPDATE zoph_users SET user_name='{admin_user}' \
WHERE user_name='admin';"
subprocess.run(['mysql', _get_db_config()['db_name']],
input=query.encode(), check=True)
@privileged
def is_configured() -> bool | None:
"""Return whether zoph app is configured."""
try:
process = subprocess.run(
['zoph', '--get-config', 'interface.user.remote'],
stdout=subprocess.PIPE, check=True)
return process.stdout.decode().strip() == 'true'
except (FileNotFoundError, subprocess.CalledProcessError):
return None
@privileged
def dump_database():
"""Dump database to file.
May be called when app is disabled.
"""
with action_utils.service_ensure_running('mysql'):
db_name = _get_db_config()['db_name']
os.makedirs(os.path.dirname(DB_BACKUP_FILE), exist_ok=True)
with open(DB_BACKUP_FILE, 'w', encoding='utf-8') as db_backup_file:
subprocess.run(['mysqldump', db_name], stdout=db_backup_file,
check=True)
@privileged
def restore_database():
"""Restore database from file.
May be called when app is disabled.
"""
with action_utils.service_ensure_running('mysql'):
db_name = _get_db_config()['db_name']
db_user = _get_db_config()['db_user']
db_host = _get_db_config()['db_host']
db_pass = _get_db_config()['db_pass']
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)
# Set the password for user from restored configuration
query = f'ALTER USER {db_user}@{db_host} IDENTIFIED BY "{db_pass}";'
subprocess.run(['mysql'], input=query.encode(), check=True)
@privileged
def uninstall():
"""Drop database, database user and database configuration.
May be called when app is disabled.
"""
with action_utils.service_ensure_running('mysql'):
try:
config = _get_db_config()
subprocess.run(
['mysqladmin', '--force', 'drop', config['db_name']],
check=False)
query = f'DROP USER IF EXISTS {config["db_user"]}@localhost;'
subprocess.run(['mysql'], input=query.encode(), check=False)
except FileNotFoundError: # Database configuration not found
pass
DB_CONF.unlink(missing_ok=True)