From 65f4b6750bd8f03eb15ad621f25ac8388ff3ccf7 Mon Sep 17 00:00:00 2001 From: James Valleroy Date: Sun, 9 Aug 2020 09:33:23 -0400 Subject: [PATCH] bepasty: Add public access config form Signed-off-by: James Valleroy Reviewed-by: Sunil Mohan Adapa --- actions/bepasty | 79 ++++++++++++++----- plinth/modules/bepasty/__init__.py | 19 +++++ plinth/modules/bepasty/forms.py | 10 +++ plinth/modules/bepasty/tests/bepasty.feature | 12 +++ .../modules/bepasty/tests/test_functional.py | 61 ++++++++++++-- plinth/modules/bepasty/views.py | 27 ++++++- 6 files changed, 177 insertions(+), 31 deletions(-) diff --git a/actions/bepasty b/actions/bepasty index 5b0c810c8..88ccba531 100755 --- a/actions/bepasty +++ b/actions/bepasty @@ -9,6 +9,7 @@ import json import grp import os import pwd +import re import secrets import shutil import string @@ -30,6 +31,7 @@ PERMISSIONS = {{ '{}': 'list,create,read,delete', # editor '{}': 'list,read', # viewer }} +DEFAULT_PERMISSIONS = '' """ PASSWORD_LENGTH = 20 @@ -64,6 +66,15 @@ def parse_arguments(): 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() @@ -121,34 +132,25 @@ def subcommand_list_passwords(_): if line.startswith('}'): in_permissions = False else: - parts = line.split('#') - try: - comment = parts[1].strip() - except IndexError: - comment = '' + 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() - 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 - }) + 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 = '' - + permissions = _format_permissions(arguments.permissions) password = _generate_password() with open(CONF_FILE, 'r') as conf_file: lines = conf_file.readlines() @@ -194,6 +196,41 @@ def subcommand_remove_password(arguments): 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 diff --git a/plinth/modules/bepasty/__init__.py b/plinth/modules/bepasty/__init__.py index bad653efd..cde0933e4 100644 --- a/plinth/modules/bepasty/__init__.py +++ b/plinth/modules/bepasty/__init__.py @@ -44,6 +44,12 @@ PERMISSIONS = { 'admin': _('Admin (lock/unlock files)'), } +DEFAULT_PERMISSIONS = { + '': _('None (password always required)'), + 'read': _('Read files (using their web address)'), + 'read list': _('List and read all files'), +} + class BepastyApp(app_module.App): """FreedomBox app for bepasty.""" @@ -113,3 +119,16 @@ 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() + + +def set_default_permissions(permissions): + """Set default permissions""" + perm = permissions.split() + actions.superuser_run('bepasty', ['set-default', '--permissions'] + perm) diff --git a/plinth/modules/bepasty/forms.py b/plinth/modules/bepasty/forms.py index 9cfc150dc..afd1e3ca4 100644 --- a/plinth/modules/bepasty/forms.py +++ b/plinth/modules/bepasty/forms.py @@ -9,6 +9,16 @@ from django.utils.translation import ugettext_lazy as _ from plinth.modules import bepasty +class SetDefaultPermissionsForm(forms.Form): + """Form to set default permissions""" + default_permissions = forms.ChoiceField( + choices=bepasty.DEFAULT_PERMISSIONS.items(), required=False, + widget=forms.RadioSelect(), + label=_('Public Access (default permissions)'), + help_text=_('Permissions for anonymous users, who have not provided a ' + 'password.')) + + class AddPasswordForm(forms.Form): """Form to add a new password.""" diff --git a/plinth/modules/bepasty/tests/bepasty.feature b/plinth/modules/bepasty/tests/bepasty.feature index e381c8c7a..6bcc64652 100644 --- a/plinth/modules/bepasty/tests/bepasty.feature +++ b/plinth/modules/bepasty/tests/bepasty.feature @@ -13,6 +13,18 @@ Scenario: Enable bepasty application When I enable the bepasty application Then the bepasty site should be available +Scenario: Set default permissions to List and read all files + Given the bepasty application is enabled + And I am not logged in to bepasty + When I set the default permissions to List and read all files + Then I should be able to List all Items in bepasty + +Scenario: Set default permissions to Read files + Given the bepasty application is enabled + And I am not logged in to bepasty + When I set the default permissions to Read files + Then I should not be able to List all Items in bepasty + Scenario: Add password Given the bepasty application is enabled When I add a password diff --git a/plinth/modules/bepasty/tests/test_functional.py b/plinth/modules/bepasty/tests/test_functional.py index b0196a0ea..58f1cd54a 100644 --- a/plinth/modules/bepasty/tests/test_functional.py +++ b/plinth/modules/bepasty/tests/test_functional.py @@ -3,7 +3,7 @@ Functional, browser based tests for bepasty app. """ -from pytest_bdd import scenarios, then, when +from pytest_bdd import given, scenarios, then, when from plinth.tests import functional @@ -12,6 +12,21 @@ scenarios('bepasty.feature') last_password_added = None +@given('I am not logged in to bepasty') +def not_logged_in(session_browser): + _logout(session_browser) + + +@when('I set the default permissions to Read files') +def set_default_permissions_read(session_browser): + _set_default_permissions(session_browser, 'read') + + +@when('I set the default permissions to List and read all files') +def set_default_permissions_list_read(session_browser): + _set_default_permissions(session_browser, 'read list') + + @when('I add a password') def add_password(session_browser): global last_password_added @@ -35,6 +50,22 @@ def should_not_login(session_browser): assert not _can_login(session_browser, last_password_added) +@then('I should be able to List all Items in bepasty') +def should_list_all(session_browser): + assert _can_list_all(session_browser) + + +@then('I should not be able to List all Items in bepasty') +def should_not_list_all(session_browser): + assert _cannot_list_all(session_browser) + + +def _set_default_permissions(browser, permissions=''): + functional.nav_to_module(browser, 'bepasty') + browser.choose('default_permissions', permissions) + functional.submit(browser, form_class='form-configuration') + + def _add_password(browser): functional.visit(browser, '/plinth/apps/bepasty/add') for permission in ['read', 'create', 'list', 'delete', 'admin']: @@ -45,7 +76,7 @@ def _add_password(browser): def _remove_all_passwords(browser): - functional.visit(browser, '/plinth/apps/bepasty') + functional.nav_to_module(browser, 'bepasty') while True: remove_button = browser.find_by_css('.password-remove') if remove_button: @@ -55,18 +86,32 @@ def _remove_all_passwords(browser): def _get_password(browser): - functional.visit(browser, '/plinth/apps/bepasty') + functional.nav_to_module(browser, 'bepasty') return browser.find_by_css('.password-password').first.text def _can_login(browser, password): - functional.visit(browser, '/bepasty') - logout = browser.find_by_value('Logout') - if logout: - logout.click() - + _logout(browser) browser.fill('token', password) login = browser.find_by_xpath('//form//button') functional.submit(browser, login, '/bepasty') return bool(browser.find_by_value('Logout')) + + +def _logout(browser): + functional.visit(browser, '/bepasty') + logout = browser.find_by_value('Logout') + if logout: + logout.click() + + +def _can_list_all(browser): + functional.visit(browser, '/bepasty') + return functional.eventually(browser.links.find_by_href, + ['/bepasty/+list'], 5) + + +def _cannot_list_all(browser): + functional.visit(browser, '/bepasty/+list') + return functional.eventually(browser.is_text_present, ['Forbidden'], 5) diff --git a/plinth/modules/bepasty/views.py b/plinth/modules/bepasty/views.py index edbcf12c5..be5e4580f 100644 --- a/plinth/modules/bepasty/views.py +++ b/plinth/modules/bepasty/views.py @@ -11,16 +11,17 @@ from django.utils.translation import ugettext_lazy as _ from django.views.decorators.http import require_POST from django.views.generic import FormView +from plinth.errors import ActionError from plinth.modules import bepasty from plinth.views import AppView -from .forms import AddPasswordForm +from .forms import AddPasswordForm, SetDefaultPermissionsForm class BepastyView(AppView): """Serve configuration page.""" app_id = 'bepasty' - diagnostics_module_name = 'bepasty' + form_class = SetDefaultPermissionsForm template_name = 'bepasty.html' def get_context_data(self, **kwargs): @@ -29,6 +30,28 @@ class BepastyView(AppView): context['passwords'] = bepasty.list_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() + return initial + + def form_valid(self, form): + """Apply the changes submitted in the form.""" + old_data = form.initial + form_data = form.cleaned_data + + if old_data['default_permissions'] != form_data['default_permissions']: + try: + bepasty.set_default_permissions( + form_data['default_permissions']) + messages.success(self.request, _('Configuration updated.')) + except ActionError: + messages.error(self.request, + _('An error occurred during configuration.')) + + return super().form_valid(form) + class AddPasswordView(SuccessMessageMixin, FormView): """View to add a new password."""