#!/usr/bin/python3 # SPDX-License-Identifier: AGPL-3.0-or-later """ Configuration helper for bepasty. """ import argparse import collections import grp import json import os import pathlib import pwd import secrets import shutil import string import subprocess import sys import augeas from plinth import action_utils from plinth.modules import bepasty DATA_DIR = '/var/lib/bepasty' PASSWORD_LENGTH = 20 CONF_FILE = pathlib.Path('/etc/bepasty-freedombox.conf') def parse_arguments(): """Return parsed command line arguments as dictionary.""" parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') setup = subparsers.add_parser( 'setup', help='Perform post-installation operations for bepasty') setup.add_argument('--domain-name', required=True, help='The domain name that will be used by bepasty') subparsers.add_parser('get-configuration', help='Get all configuration') add_password = subparsers.add_parser( 'add-password', help='Generate a password with given permissions') add_password.add_argument( '--permissions', nargs='+', help='Any number of permissions from the set: {}'.format(', '.join( bepasty.PERMISSIONS.keys()))) add_password.add_argument( '--comment', required=False, help='A comment for the password and its permissions') subparsers.add_parser('remove-password', help='Remove a password and its permissions') set_default = subparsers.add_parser('set-default', help='Set default permissions') set_default.add_argument( '--permissions', nargs='*', help='Any number of permissions from the set: {}'.format(', '.join( bepasty.PERMISSIONS.keys()))) subparsers.required = True return parser.parse_args() def _augeas_load(): """Initialize Augeas.""" aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD) aug.set('/augeas/load/Simplevars/lens', 'Simplevars.lns') aug.set('/augeas/load/Simplevars/incl[last() + 1]', str(CONF_FILE)) aug.load() return aug def _key_path(key): """Return the augeas path for the key.""" return '/files' + str(CONF_FILE) + '/' + key def conf_file_read(): """Read and return the configuration.""" aug = _augeas_load() conf = collections.OrderedDict() for path in aug.match(_key_path('*')): key = path.rsplit('/', 1)[-1] if key[0] != '#': conf[key] = json.loads(aug.get(path)) return conf def conf_file_write(conf): """Write configuration to the file.""" aug = _augeas_load() for key, value in conf.items(): if not key.startswith('#'): value = json.dumps(value) aug.set(_key_path(key), value) aug.save() def subcommand_setup(arguments): """Post installation actions for bepasty.""" # Create bepasty group if needed. try: grp.getgrnam('bepasty') except KeyError: subprocess.run(['addgroup', '--system', 'bepasty'], check=True) # Create bepasty user if needed. try: pwd.getpwnam('bepasty') except KeyError: subprocess.run([ 'adduser', '--system', '--ingroup', 'bepasty', '--home', '/var/lib/bepasty', '--gecos', 'bepasty file sharing', 'bepasty' ], check=True) # Create data directory if needed. if not os.path.exists(DATA_DIR): os.makedirs(DATA_DIR, mode=0o750) shutil.chown(DATA_DIR, user='bepasty', group='bepasty') # Create configuration file if needed. if not CONF_FILE.is_file(): passwords = [_generate_password() for _ in range(3)] conf = { '#comment': 'This file is managed by FreedomBox. Only a small subset of ' 'the original configuration format is supported. Each line ' 'should be in KEY = VALUE format. VALUE must be a JSON ' 'encoded string.', 'SITENAME': arguments.domain_name, 'STORAGE_FILESYSTEM_DIRECTORY': '/var/lib/bepasty', 'SECRET_KEY': secrets.token_hex(64), 'PERMISSIONS': { passwords[0]: 'admin,list,create,read,delete', passwords[1]: 'list,create,read,delete', passwords[2]: 'list,read', }, 'PERMISSION_COMMENTS': { passwords[0]: 'admin', passwords[1]: 'editor', passwords[2]: 'viewer', }, 'DEFAULT_PERMISSIONS': 'read', } conf_file_write(conf) CONF_FILE.chmod(0o640) shutil.chown(CONF_FILE, user='bepasty', group='bepasty') def subcommand_get_configuration(_): """Get default permissions, passwords, permissions and comments.""" conf = conf_file_read() print(json.dumps(conf)) def subcommand_add_password(arguments): """Generate a password with given permissions.""" conf = conf_file_read() permissions = _format_permissions(arguments.permissions) password = _generate_password() conf['PERMISSIONS'][password] = permissions if arguments.comment: conf['PERMISSION_COMMENTS'][password] = arguments.comment conf_file_write(conf) action_utils.service_try_restart('uwsgi') def subcommand_remove_password(_arguments): """Remove a password and its permissions.""" conf = conf_file_read() password = ''.join(sys.stdin) if password in conf['PERMISSIONS']: del conf['PERMISSIONS'][password] if password in conf['PERMISSION_COMMENTS']: del conf['PERMISSION_COMMENTS'][password] conf_file_write(conf) action_utils.service_try_restart('uwsgi') def subcommand_set_default(arguments): """Set default permissions.""" conf = {'DEFAULT_PERMISSIONS': _format_permissions(arguments.permissions)} conf_file_write(conf) action_utils.service_try_restart('uwsgi') def _format_permissions(permissions=None): """Format permissions as comma-separated.""" return ','.join(set(bepasty.PERMISSIONS.keys()).intersection( permissions)) if permissions else '' def _generate_password(): """Generate a random password.""" alphabet = string.ascii_letters + string.digits return ''.join(secrets.choice(alphabet) for _ in range(PASSWORD_LENGTH)) 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()