bepasty: Simplify configuration file handling

- Stick to a subset of allowed configuration file syntax (full syntax). Only KEY
= VALUE statements are allowed. Values can be full JSON (valid python).

- Use augeas to read as key/value pairs and then parse the values in JSON.

- Add convenience methods to read and write configuration files.

- Read the entire configuration file in a single action.

- Internationalize the permission strings displayed to the user.

- Pass password during remove-password operation via stdin instead of command
line.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Joseph Nuthalapati <njoseph@riseup.net>
This commit is contained in:
Sunil Mohan Adapa 2020-08-18 20:32:18 -07:00 committed by Joseph Nuthalapati
parent 028137a4e4
commit 3e2df420cf
No known key found for this signature in database
GPG Key ID: 5398F00A2FA43C35
3 changed files with 140 additions and 143 deletions

View File

@ -5,37 +5,29 @@ Configuration helper for bepasty.
""" """
import argparse import argparse
import json import collections
import grp import grp
import json
import os import os
import pathlib
import pwd import pwd
import re
import secrets import secrets
import shutil import shutil
import string import string
import subprocess import subprocess
import sys
import augeas
from plinth import action_utils from plinth import action_utils
from plinth.modules import bepasty from plinth.modules import bepasty
DATA_DIR = '/var/lib/bepasty' DATA_DIR = '/var/lib/bepasty'
CONF_FILE = '/etc/bepasty-freedombox.conf'
CONF_CONTENTS = """
SITENAME = '{}'
STORAGE_FILESYSTEM_DIRECTORY = '/var/lib/bepasty'
SECRET_KEY = '{}'
PERMISSIONS = {{
'{}': 'admin,list,create,read,delete', # admin
'{}': 'list,create,read,delete', # editor
'{}': 'list,read', # viewer
}}
DEFAULT_PERMISSIONS = ''
"""
PASSWORD_LENGTH = 20 PASSWORD_LENGTH = 20
CONF_FILE = pathlib.Path('/etc/bepasty-freedombox.conf')
def parse_arguments(): def parse_arguments():
"""Return parsed command line arguments as dictionary.""" """Return parsed command line arguments as dictionary."""
@ -47,9 +39,7 @@ def parse_arguments():
setup.add_argument('--domain-name', required=True, setup.add_argument('--domain-name', required=True,
help='The domain name that will be used by bepasty') help='The domain name that will be used by bepasty')
subparsers.add_parser( subparsers.add_parser('get-configuration', help='Get all configuration')
'list-passwords',
help='Get a list of passwords, their permissions and comments')
add_password = subparsers.add_parser( add_password = subparsers.add_parser(
'add-password', help='Generate a password with given permissions') 'add-password', help='Generate a password with given permissions')
@ -61,12 +51,8 @@ def parse_arguments():
'--comment', required=False, '--comment', required=False,
help='A comment for the password and its permissions') help='A comment for the password and its permissions')
remove_password = subparsers.add_parser( subparsers.add_parser('remove-password',
'remove-password', help='Remove a password and its permissions') help='Remove a password and its permissions')
remove_password.add_argument('--password', required=True,
help='The password to be removed')
subparsers.add_parser('get-default', help='Get default permissions')
set_default = subparsers.add_parser('set-default', set_default = subparsers.add_parser('set-default',
help='Set default permissions') help='Set default permissions')
@ -79,6 +65,45 @@ def parse_arguments():
return parser.parse_args() 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): def subcommand_setup(arguments):
"""Post installation actions for bepasty.""" """Post installation actions for bepasty."""
# Create bepasty group if needed. # Create bepasty group if needed.
@ -102,126 +127,70 @@ def subcommand_setup(arguments):
shutil.chown(DATA_DIR, user='bepasty', group='bepasty') shutil.chown(DATA_DIR, user='bepasty', group='bepasty')
# Create configuration file if needed. # Create configuration file if needed.
if not os.path.isfile(CONF_FILE): if not CONF_FILE.is_file():
# Generate secrets passwords = [_generate_password() for _ in range(3)]
secret_key = secrets.token_hex(64) conf = {
passwords = [] '#comment':
for i in range(3): 'This file is managed by FreedomBox. Only a small subset of '
passwords.append(_generate_password()) 'the original configuration format is supported. Each line '
'should be in KEY = VALUE format. VALUE must be a JSON '
with open(CONF_FILE, 'w') as conf_file: 'encoded string.',
conf_file.write( 'SITENAME': arguments.domain_name,
CONF_CONTENTS.format(arguments.domain_name, secret_key, 'STORAGE_FILESYSTEM_DIRECTORY': '/var/lib/bepasty',
*passwords)) 'SECRET_KEY': secrets.token_hex(64),
'PERMISSIONS': {
os.chmod(CONF_FILE, 0o640) 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': '',
}
conf_file_write(conf)
CONF_FILE.chmod(0o640)
shutil.chown(CONF_FILE, user='bepasty', group='bepasty') shutil.chown(CONF_FILE, user='bepasty', group='bepasty')
def subcommand_list_passwords(_): def subcommand_get_configuration(_):
"""Get a list of passwords, their permissions and comments""" """Get default permissions, passwords, permissions and comments."""
with open(CONF_FILE, 'r') as conf_file: conf = conf_file_read()
lines = conf_file.readlines() print(json.dumps(conf))
passwords = []
in_permissions = False
for line in lines:
if line.startswith('PERMISSIONS'):
in_permissions = True
elif in_permissions:
if line.startswith('}'):
in_permissions = False
else:
match = re.match(r"\s*'(.*)': '(.*)',\s*(#\s*.*)?", line)
if match:
password = match.group(1)
permissions = match.group(2).split(',')
comment = match.group(3) or ''
comment = comment.lstrip('#').strip()
passwords.append({
'password': password,
'permissions': ', '.join(permissions),
'comment': comment
})
print(json.dumps(passwords))
def subcommand_add_password(arguments): def subcommand_add_password(arguments):
"""Generate a password with given permissions""" """Generate a password with given permissions."""
conf = conf_file_read()
permissions = _format_permissions(arguments.permissions) permissions = _format_permissions(arguments.permissions)
password = _generate_password() password = _generate_password()
with open(CONF_FILE, 'r') as conf_file: conf['PERMISSIONS'][password] = permissions
lines = conf_file.readlines()
with open(CONF_FILE, 'w') as conf_file:
in_permissions = False
for line in lines:
if line.startswith('PERMISSIONS'):
in_permissions = True
elif in_permissions:
if line.startswith('}'):
in_permissions = False
conf_file.write(" '{}': '{}',".format(
password, permissions))
if arguments.comment: if arguments.comment:
conf_file.write(' # {}'.format(arguments.comment)) conf['PERMISSION_COMMENTS'][password] = arguments.comment
conf_file.write('\n')
conf_file.write(line)
conf_file_write(conf)
action_utils.service_try_restart('uwsgi') action_utils.service_try_restart('uwsgi')
def subcommand_remove_password(arguments): def subcommand_remove_password(_arguments):
"""Remove a password and its permissions""" """Remove a password and its permissions."""
with open(CONF_FILE, 'r') as conf_file: conf = conf_file_read()
lines = conf_file.readlines() password = ''.join(sys.stdin)
if password in conf['PERMISSIONS']:
with open(CONF_FILE, 'w') as conf_file: del conf['PERMISSIONS'][password]
in_permissions = False
for line in lines:
if line.startswith('PERMISSIONS'):
in_permissions = True
elif in_permissions:
if line.startswith('}'):
in_permissions = False
elif arguments.password in line:
continue
conf_file.write(line)
if password in conf['PERMISSION_COMMENTS']:
del conf['PERMISSION_COMMENTS'][password]
conf_file_write(conf)
action_utils.service_try_restart('uwsgi') action_utils.service_try_restart('uwsgi')
def subcommand_get_default(_):
"""Get default permissions"""
with open(CONF_FILE, 'r') as conf_file:
lines = conf_file.readlines()
for line in lines:
match = re.match(r"DEFAULT_PERMISSIONS = '(.*)'", line)
if match:
print(match.group(1).replace(',', ' '))
return
def subcommand_set_default(arguments): def subcommand_set_default(arguments):
"""Set default permissions""" """Set default permissions."""
permissions = _format_permissions(arguments.permissions) conf = {'DEFAULT_PERMISSIONS': _format_permissions(arguments.permissions)}
with open(CONF_FILE, 'r') as conf_file: conf_file_write(conf)
lines = conf_file.readlines()
with open(CONF_FILE, 'w') as conf_file:
for line in lines:
if line.startswith('DEFAULT_PERMISSIONS'):
conf_file.write(
"DEFAULT_PERMISSIONS = '{}'\n".format(permissions))
else:
conf_file.write(line)
action_utils.service_try_restart('uwsgi') action_utils.service_try_restart('uwsgi')

View File

@ -100,9 +100,9 @@ def setup(helper, old_version=None):
helper.call('post', app.enable) helper.call('post', app.enable)
def list_passwords(): def get_configuration():
"""Get a list of passwords, their permissions and comments""" """Get a full configuration including passwords and defaults."""
output = actions.superuser_run('bepasty', ['list-passwords']) output = actions.superuser_run('bepasty', ['get-configuration'])
return json.loads(output) return json.loads(output)
@ -116,16 +116,9 @@ def add_password(permissions, comment=None):
def remove_password(password): def remove_password(password):
"""Remove a password and its permissions""" """Remove a password and its permissions."""
actions.superuser_run('bepasty', actions.superuser_run('bepasty', ['remove-password'],
['remove-password', '--password', password]) input=password.encode())
def get_default_permissions():
"""Get default permissions"""
output = actions.superuser_run('bepasty', ['get-default']).strip()
output = 'read list' if output == 'list read' else output
return output.strip()
def set_default_permissions(permissions): def set_default_permissions(permissions):

View File

@ -24,16 +24,51 @@ class BepastyView(AppView):
form_class = SetDefaultPermissionsForm form_class = SetDefaultPermissionsForm
template_name = 'bepasty.html' template_name = 'bepasty.html'
def __init__(self, *args, **kwargs):
"""Initialize the view."""
super().__init__(*args, **kwargs)
self.conf = None
def _get_configuration(self):
"""Return the current configuration."""
if not self.conf:
self.conf = bepasty.get_configuration()
return self.conf
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
"""Return additional context for rendering the template.""" """Return additional context for rendering the template."""
permissions_short_text = {
'read': _('Read'),
'create': _('Create'),
'list': _('List'),
'delete': _('Delete'),
'admin': _('Admin')
}
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['passwords'] = bepasty.list_passwords() conf = self._get_configuration()
passwords = []
for password, permissions in conf['PERMISSIONS'].items():
permissions = permissions.split(',')
permissions = [
str(permissions_short_text[permission])
for permission in permissions if permission
]
passwords.append({
'password': password,
'permissions': ', '.join(permissions),
'comment': conf['PERMISSION_COMMENTS'].get(password) or ''
})
context['passwords'] = passwords
return context return context
def get_initial(self): def get_initial(self):
"""Return the status of the service to fill in the form.""" """Return the status of the service to fill in the form."""
initial = super().get_initial() initial = super().get_initial()
initial['default_permissions'] = bepasty.get_default_permissions() default = self._get_configuration().get('DEFAULT_PERMISSIONS', '')
default = ' '.join(default.split(','))
default = 'read list' if default == 'list read' else default
initial['default_permissions'] = default
return initial return initial
def form_valid(self, form): def form_valid(self, form):