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 json
import collections
import grp
import json
import os
import pathlib
import pwd
import re
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'
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
CONF_FILE = pathlib.Path('/etc/bepasty-freedombox.conf')
def parse_arguments():
"""Return parsed command line arguments as dictionary."""
@ -47,9 +39,7 @@ def parse_arguments():
setup.add_argument('--domain-name', required=True,
help='The domain name that will be used by bepasty')
subparsers.add_parser(
'list-passwords',
help='Get a list of passwords, their permissions and comments')
subparsers.add_parser('get-configuration', help='Get all configuration')
add_password = subparsers.add_parser(
'add-password', help='Generate a password with given permissions')
@ -61,12 +51,8 @@ def parse_arguments():
'--comment', required=False,
help='A comment for the password and its permissions')
remove_password = subparsers.add_parser(
'remove-password', 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')
subparsers.add_parser('remove-password',
help='Remove a password and its permissions')
set_default = subparsers.add_parser('set-default',
help='Set default permissions')
@ -79,6 +65,45 @@ def parse_arguments():
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.
@ -102,126 +127,70 @@ def subcommand_setup(arguments):
shutil.chown(DATA_DIR, user='bepasty', group='bepasty')
# Create configuration file if needed.
if not os.path.isfile(CONF_FILE):
# Generate secrets
secret_key = secrets.token_hex(64)
passwords = []
for i in range(3):
passwords.append(_generate_password())
with open(CONF_FILE, 'w') as conf_file:
conf_file.write(
CONF_CONTENTS.format(arguments.domain_name, secret_key,
*passwords))
os.chmod(CONF_FILE, 0o640)
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': '',
}
conf_file_write(conf)
CONF_FILE.chmod(0o640)
shutil.chown(CONF_FILE, user='bepasty', group='bepasty')
def subcommand_list_passwords(_):
"""Get a list of passwords, their permissions and comments"""
with open(CONF_FILE, 'r') as conf_file:
lines = conf_file.readlines()
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_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"""
"""Generate a password with given permissions."""
conf = conf_file_read()
permissions = _format_permissions(arguments.permissions)
password = _generate_password()
with open(CONF_FILE, 'r') as conf_file:
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:
conf_file.write(' # {}'.format(arguments.comment))
conf_file.write('\n')
conf_file.write(line)
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"""
with open(CONF_FILE, 'r') as conf_file:
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
elif arguments.password in line:
continue
conf_file.write(line)
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_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):
"""Set default permissions"""
permissions = _format_permissions(arguments.permissions)
with open(CONF_FILE, 'r') as conf_file:
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)
"""Set default permissions."""
conf = {'DEFAULT_PERMISSIONS': _format_permissions(arguments.permissions)}
conf_file_write(conf)
action_utils.service_try_restart('uwsgi')

View File

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

View File

@ -24,16 +24,51 @@ class BepastyView(AppView):
form_class = SetDefaultPermissionsForm
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):
"""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['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
def get_initial(self):
"""Return the status of the service to fill in the form."""
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
def form_valid(self, form):