mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-20 10:34:30 +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 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()
|
if arguments.comment:
|
||||||
|
conf['PERMISSION_COMMENTS'][password] = arguments.comment
|
||||||
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_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')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user