Sunil Mohan Adapa 028137a4e4
bepasty: Require at least one permission on a password
- Since a password without any permissions is not useful.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Joseph Nuthalapati <njoseph@riseup.net>
2020-08-21 22:42:30 +05:30

251 lines
7.7 KiB
Python
Executable File

#!/usr/bin/python3
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
Configuration helper for bepasty.
"""
import argparse
import json
import grp
import os
import pwd
import re
import secrets
import shutil
import string
import subprocess
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
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(
'list-passwords',
help='Get a list of passwords, their permissions and comments')
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')
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')
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 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 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)
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_add_password(arguments):
"""Generate a password with given permissions"""
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)
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)
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)
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()