mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-28 08:03:36 +00:00
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:
parent
028137a4e4
commit
3e2df420cf
225
actions/bepasty
225
actions/bepasty
@ -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')
|
||||
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user