diff --git a/actions/bepasty b/actions/bepasty
new file mode 100755
index 000000000..5b0c810c8
--- /dev/null
+++ b/actions/bepasty
@@ -0,0 +1,213 @@
+#!/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 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
+}}
+"""
+
+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',
+ 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.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 is 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:
+ parts = line.split('#')
+ try:
+ comment = parts[1].strip()
+ except IndexError:
+ comment = ''
+
+ parts = parts[0].split(':')
+ password = parts[0].replace("'", '').strip()
+ permissions = parts[1].replace(
+ "'", '').strip().rstrip(',').split(',')
+ passwords.append({
+ 'password': password,
+ 'permissions': ', '.join(permissions),
+ 'comment': comment
+ })
+
+ print(json.dumps(passwords))
+
+
+def subcommand_add_password(arguments):
+ """Generate a password with given permissions"""
+ if arguments.permissions:
+ permissions = set(bepasty.PERMISSIONS.keys()).intersection(
+ arguments.permissions)
+ permissions = ','.join(permissions)
+ else:
+ 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 _generate_password():
+ """Generate a random password"""
+ alphabet = string.ascii_letters + string.digits
+ return ''.join(secrets.choice(alphabet) for i 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()
diff --git a/debian/copyright b/debian/copyright
index 1894d725a..64e4126f9 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -39,6 +39,11 @@ Copyright: Marie Van den Broeck (https://thenounproject.com/marie49/)
Comment: https://thenounproject.com/icon/162372/
License: CC-BY-SA-3.0
+Files: static/themes/default/icons/bepasty.svg
+Copyright: (c) 2014 by the Bepasty Team, see the AUTHORS file.
+Comment: https://github.com/bepasty/bepasty-server/blob/master/src/bepasty/static/app/bepasty.svg
+License: BSD-2-clause
+
Files: static/themes/default/icons/cockpit.svg
Copyright: Cockpit Authors (https://github.com/cockpit-project/cockpit/blob/master/AUTHORS)
Comment: https://github.com/cockpit-project/cockpit/blob/master/src/branding/default/logo.svg
@@ -979,6 +984,27 @@ License: Apache-2.0
On Debian systems, the full text of the Apache Software License version 2 can
be found in the file `/usr/share/common-licenses/Apache-2.0'.
+License: BSD-2-clause
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ .
+ 1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ .
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
License: BSD-3-clause
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
diff --git a/plinth/modules/bepasty/__init__.py b/plinth/modules/bepasty/__init__.py
new file mode 100644
index 000000000..bad653efd
--- /dev/null
+++ b/plinth/modules/bepasty/__init__.py
@@ -0,0 +1,115 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""
+FreedomBox app for bepasty.
+"""
+
+import json
+
+from django.utils.translation import ugettext_lazy as _
+
+from plinth import actions
+from plinth import app as app_module
+from plinth import frontpage, menu
+from plinth.modules.apache.components import Uwsgi, Webserver
+from plinth.modules.firewall.components import Firewall
+
+from .manifest import backup, clients # noqa, pylint: disable=unused-import
+
+version = 1
+
+managed_packages = ['bepasty', 'uwsgi', 'uwsgi-plugin-python3']
+
+managed_services = ['uwsgi']
+
+description = [
+ _('bepasty is a web application that allows all types of files to be '
+ 'uploaded and shared.'),
+ _('bepasty does not use usernames for login. It only uses passwords. For '
+ 'each password, a set of permissions can be selected. Once you have '
+ 'created a password, you can share it with the users who should have the'
+ ' associated permissions.'),
+ _('You can also create multiple passwords with the same set of privileges,'
+ ' and distribute them to different people or groups. This will allow '
+ 'you to later revoke access for a single person or group, by removing '
+ 'their password from the list.'),
+]
+
+app = None
+
+PERMISSIONS = {
+ 'read': _('Read files (using their web address)'),
+ 'create': _('Create or upload files'),
+ 'list': _('List all files'),
+ 'delete': _('Delete files'),
+ 'admin': _('Admin (lock/unlock files)'),
+}
+
+
+class BepastyApp(app_module.App):
+ """FreedomBox app for bepasty."""
+
+ app_id = 'bepasty'
+
+ def __init__(self):
+ """Create components for the app."""
+ super().__init__()
+
+ info = app_module.Info(self.app_id, version, name=_('bepasty'),
+ icon_filename='bepasty',
+ short_description=_('File Sharing'),
+ description=description, manual_page='bepasty',
+ clients=clients)
+ self.add(info)
+
+ menu_item = menu.Menu('menu-bepasty', info.name,
+ info.short_description, info.icon_filename,
+ 'bepasty:index', parent_url_name='apps')
+ self.add(menu_item)
+
+ shortcut = frontpage.Shortcut('shortcut-bepasty', info.name,
+ info.short_description,
+ info.icon_filename, '/bepasty',
+ clients=clients)
+ self.add(shortcut)
+
+ firewall = Firewall('firewall-bepasty', info.name,
+ ports=['http', 'https'], is_external=True)
+ self.add(firewall)
+
+ uwsgi = Uwsgi('uwsgi-bepasty', 'bepasty-freedombox')
+ self.add(uwsgi)
+
+ webserver = Webserver('webserver-bepasty', 'bepasty-freedombox')
+ self.add(webserver)
+
+
+def setup(helper, old_version=None):
+ """Install and configure the module."""
+ helper.install(managed_packages)
+ helper.call('post', actions.superuser_run, 'bepasty',
+ ['setup', '--domain-name', 'freedombox.local'])
+ helper.call('post', app.enable)
+
+
+def list_passwords():
+ """Get a list of passwords, their permissions and comments"""
+ output = actions.superuser_run('bepasty', ['list-passwords'])
+ return json.loads(output)
+
+
+def add_password(permissions=None, comment=None):
+ """Generate a password with given permissions"""
+ command = ['add-password']
+ if permissions:
+ command += ['--permissions'] + permissions
+
+ if comment:
+ command += ['--comment', comment]
+
+ actions.superuser_run('bepasty', command)
+
+
+def remove_password(password):
+ """Remove a password and its permissions"""
+ actions.superuser_run('bepasty',
+ ['remove-password', '--password', password])
diff --git a/plinth/modules/bepasty/data/etc/apache2/conf-available/bepasty-freedombox.conf b/plinth/modules/bepasty/data/etc/apache2/conf-available/bepasty-freedombox.conf
new file mode 100644
index 000000000..7ca23e624
--- /dev/null
+++ b/plinth/modules/bepasty/data/etc/apache2/conf-available/bepasty-freedombox.conf
@@ -0,0 +1,16 @@
+##
+## On all sites, provide bepasty on a path: /bepasty
+##
+
+# Redirect /bepasty to /bepasty/
+
{% trans 'No passwords currently configured.' %}
+ {% else %} +| {% trans "Password" %} | +{% trans "Permissions" %} | +{% trans "Comment" %} | ++ |
|---|---|---|---|
| {{ password.password }} | +{{ password.permissions }} | +{{ password.comment }} | ++ + | +