FreedomBox/plinth/db/postgres.py
Sunil Mohan Adapa 8ba444990b
backups: Set proper permissions for backups-data directory
Fixes: #2554

- Update permissions on the backups-data directory so that files are only
accessible by root users.

- Ensure that the directory is created by the 'backups' app and not by each of
the apps that take the backup.

Tests:

- Run functional tests for miniflux, dynamicdns, wordpress, zoph, and nextlcoud.
There was an unrelated functional test case failure in nextcloud.

- On a fresh installation, apply patch. Service is restarted. The directory is
created with proper permissions and ownership.

- On a fresh installation, without the patch. Backup the dynamicdns app. The
directory is created with incorrect permissions. Apply the patch. Service is
restarted. Proper permissions are set on the directory.

- On a setup with incorrect permissions, re-run backups app's setup. The
permissions are updated correctly.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
2025-12-15 20:16:36 -05:00

116 lines
3.6 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""Utilities to help with PostgreSQL databases.
Uses utilities from 'postgres' package such as 'psql' and 'pg_dump'.
"""
import os
import pathlib
from plinth import action_utils
def _run_as(command, **kwargs):
"""Run a command as 'postgres' user."""
command = ['sudo', '--user', 'postgres'] + command
return action_utils.run(command, check=True, **kwargs)
def run_query(query):
"""Run a database query as 'postgres' user.
Does not ensure that database server is running.
"""
env = os.environ.copy()
env['ON_ERROR_EXIT'] = '1'
return _run_as(['psql', '--echo-errors'], env=env,
input=query.encode('utf-8'))
def _create_user(database_user: str, database_password: str):
"""Create a new user account with given credentials.
Ignore errors if user already exists. Set password on the account either
way. Passwords must be alphanumeric.
"""
query = f'''
DO $$
BEGIN
CREATE ROLE {database_user} WITH
PASSWORD '{database_password}'
NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN NOREPLICATION
NOBYPASSRLS;
EXCEPTION WHEN duplicate_object THEN
ALTER ROLE {database_user} WITH
PASSWORD '{database_password}';
END
$$;'''
run_query(query)
def _drop_user(database_user: str):
"""Remove a user account with given username."""
run_query(f'DROP ROLE {database_user};')
def create_database(database_name: str, database_user: str,
database_password: str):
"""Create a new database and a user account to access it.
Database server is temporarily started if it is not running.
"""
query = f'''
CREATE EXTENSION IF NOT EXISTS dblink;
DO $$
BEGIN
PERFORM dblink_exec('',
'CREATE DATABASE {database_name} WITH OWNER {database_user}');
EXCEPTION WHEN duplicate_database THEN
ALTER DATABASE {database_name}
OWNER TO {database_user};
END
$$;'''
with action_utils.service_ensure_running('postgresql'):
_create_user(database_user, database_password)
run_query(query)
def drop_database(database_name: str, database_user: str):
"""Delete the database and the user account owning it.
Database server is temporarily started if it is not running.
"""
query = f'DROP DATABASE {database_name};'
with action_utils.service_ensure_running('postgresql'):
run_query(query)
_drop_user(database_user)
def dump_database(backup_file: str | pathlib.Path, database_name: str):
"""Dump PostgreSQL database to a file.
Database server is temporarily started if it is not running. Overwrite
file if it exists.
"""
backup_path = pathlib.Path(backup_file)
with action_utils.service_ensure_running('postgresql'):
with open(backup_path, 'w', encoding='utf-8') as file_handle:
_run_as([
'pg_dump', '--create', '--clean', '--if-exists', database_name
], stdout=file_handle)
def restore_database(backup_file: str | pathlib.Path, database_name: str,
database_user: str, database_password: str):
"""Restore database from a file.
Database server is temporarily started if it is not running. User account
is removed and recreated if it already exists. Drop database and recreate
if it already exists.
"""
with action_utils.service_ensure_running('postgresql'):
drop_database(database_name, database_user)
create_database(database_name, database_user, database_password)
with open(backup_file, 'r', encoding='utf-8') as file_handle:
_run_as(['psql', '--dbname', database_name], stdin=file_handle)