sharing: Finish implementation

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Joseph Nuthalapati <njoseph@thoughtworks.com>
This commit is contained in:
Sunil Mohan Adapa 2018-02-18 23:38:40 +05:30 committed by Joseph Nuthalapati
parent a42aed78f1
commit ebabb2f8aa
No known key found for this signature in database
GPG Key ID: 5398F00A2FA43C35
10 changed files with 446 additions and 154 deletions

185
actions/sharing Normal file → Executable file
View File

@ -1,7 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
# -*- mode: python -*-
# #
# This file is part of Plinth. # This file is part of FreedomBox.
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -16,77 +15,177 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
""" """
Configuration helper for the sharing module Configuration helper for the sharing app.
""" """
import argparse import argparse
import json
import os import os
import pathlib
import re
import augeas import augeas
from plinth import action_utils
APACHE_CONFIGURATION = '/etc/apache2/conf-available/sharing-freedombox.conf'
def parse_arguments(): def parse_arguments():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(help='Sub command') subparsers = parser.add_subparsers(dest='subcommand', help='Sub command')
subparsers.add_parser('add', help='Add a new share') subparsers.add_parser('list', help='List all existing shares')
subparsers.add_parser('remove', help='Remove an existing share')
subparsers.add_parser('list', help='Remove an existing share') add_parser = subparsers.add_parser('add', help='Add a new share')
add_parser.add_argument('--name', required=True, help='Name of the share')
add_parser.add_argument('--path', required=True, help='Disk path to share')
add_parser.add_argument('--groups', nargs='*',
help='List of groups that can access the share')
remove_parser = subparsers.add_parser('remove',
help='Remove an existing share')
remove_parser.add_argument('--name', required=True,
help='Name of the share to remove')
subparsers.required = True subparsers.required = True
return parser.parse_args() return parser.parse_args()
def load_augeas(conf_file): def load_augeas():
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + """Initialize augeas for this app's configuration file."""
augeas.Augeas.NO_MODL_AUTOLOAD) aug = augeas.Augeas(
flags=augeas.Augeas.NO_LOAD + augeas.Augeas.NO_MODL_AUTOLOAD)
aug.set('/augeas/load/Httpd/lens', 'Httpd.lns') aug.set('/augeas/load/Httpd/lens', 'Httpd.lns')
aug.set('/augeas/load/Httpd/incl[last() + 1]', conf_file) aug.set('/augeas/load/Httpd/incl[last() + 1]', APACHE_CONFIGURATION)
aug.load() aug.load()
aug.defvar('conf', '/files' + APACHE_CONFIGURATION)
return aug return aug
# TODO: Handle the error case scenarios
def subcommand_add(arguments): def subcommand_add(arguments):
share_url = arguments.url """Add a share to Apache configuration."""
share_path = arguments.path name = arguments.name
share_user = arguments.user path = arguments.path
groups = arguments.groups
url = '/share/' + name
aug = load_augeas('/etc/apache2/sites-available/sharing.conf') if not os.path.exists(APACHE_CONFIGURATION):
aug.defvar('conf', '/files/etc/apache2/sites-available/sharing.conf') pathlib.Path(APACHE_CONFIGURATION).touch()
aug = load_augeas()
shares = _list(aug)
if any([share for share in shares if share['name'] == name]):
raise Exception('Share already present')
aug.set('$conf/directive[last() + 1]', 'Alias')
aug.set('$conf/directive[last()]/arg[1]', url)
aug.set('$conf/directive[last()]/arg[2]', path)
aug.set('$conf/Location[last() + 1]/arg', url)
aug.set('$conf/Location[last()]/directive[last() + 1]', 'Include')
aug.set('$conf/Location[last()]/directive[last()]/arg',
'includes/freedombox-sharing.conf')
aug.set('$conf/Location[last()]/directive[last() + 1]', 'Include')
aug.set('$conf/Location[last()]/directive[last()]/arg',
'includes/freedombox-single-sign-on.conf')
aug.set('$conf/Location[last()]/IfModule/arg', 'mod_auth_pubtkt.c')
aug.set('$conf/Location[last()]/IfModule/directive[1]', 'TKTAuthToken')
for group_name in groups:
aug.set('$conf/Location[last()]/IfModule/directive[1]/arg[last() + 1]',
group_name)
aug.save()
with action_utils.WebserverChange() as webserver_change:
webserver_change.enable('sharing-freedombox')
def subcommand_remove(arguments):
"""Remove a share from Apache configuration."""
url_to_remove = '/share/' + arguments.name
aug = load_augeas()
for directive in aug.match('$conf/directive'):
if aug.get(directive) != 'Alias':
continue
url = aug.get(directive + '/arg[1]')
if url == url_to_remove:
aug.remove(directive)
for location in aug.match('$conf/Location'):
url = aug.get(location + '/arg')
if url == url_to_remove:
aug.remove(location)
aug.save()
with action_utils.WebserverChange() as webserver_change:
webserver_change.enable('sharing-freedombox')
def _get_name_from_url(url):
"""Return the name of the share given the URL for it."""
matches = re.match(r'/share/([a-z0-9\-]*)', url)
if not matches:
raise ValueError
return matches[1]
def _list(aug=None):
"""List all Apache configuration shares."""
if not aug:
aug = load_augeas()
shares = []
for match in aug.match('$conf/directive'):
if aug.get(match) != 'Alias':
continue
url = aug.get(match + '/arg[1]')
path = aug.get(match + '/arg[2]')
if os.path.exists(share_path):
try: try:
aug.set('$conf/directive[last() + 1]', 'Alias') name = _get_name_from_url(url)
aug.set('$conf/directive[last()]/arg[last() + 1]', share_url) shares.append({
aug.set('$conf/directive[last()]/arg[last() + 1]', share_path) 'name': name,
'path': path,
'url': '/share/' + name
})
except ValueError:
continue
aug.set('$conf/Directory[last() + 1]') for location in aug.match('$conf/Location'):
aug.set('$conf/Directory[last()]/arg', share_path) url = aug.get(location + '/arg')
aug.set('$conf/Directory[last()]/directive[last() + 1]', 'Include')
aug.set('$conf/Directory[last()]/directive[last()]/arg', 'includes/freedombox-sharing.conf') try:
aug.set('$conf/Directory[last()]/directive[last() + 1]', 'Require') name = _get_name_from_url(url)
aug.set('$conf/Directory[last()]/directive[last()]/arg', share_user) except ValueError:
except Exception: continue
pass
groups = []
for group in aug.match(location + '//directive["TKTAuthToken"]/arg'):
groups.append(aug.get(group))
for share in shares:
if share['name'] == name:
share['groups'] = groups
return shares
def subcommand_list(): def subcommand_list(_):
aug = load_augeas('/etc/apache2/sites-available/sharing.conf') """List all Apache configuration shares and print as JSON."""
aug.defvar('conf', '/files/etc/apache2/sites-available/sharing.conf') print(json.dumps({'shares': _list()}))
path = '/files/etc/apache2/conf-available/sharing.conf/Directory'
list_of_shares = []
for match in aug.match(path):
path = aug.get(match + '/arg')
for directive_name in aug.match(match + '/directive'):
if directive_name == 'Require':
user = aug.get(directive_name + '/arg')
list_of_shares.append(dict(path=path, user_group=user))
return list_of_shares
def main(): def main():

View File

@ -1 +1,2 @@
Options -FollowSymLinks Options +Indexes -FollowSymLinks -ExecCGI -Includes -IncludesNOEXEC
AllowOverride None

View File

@ -1,5 +1,5 @@
# #
# This file is part of Plinth. # This file is part of FreedomBox.
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -14,94 +14,46 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
""" """
Plinth module to configure sharing. FreedomBox app to configure sharing.
""" """
import os
from django import forms import json
from django.template.response import TemplateResponse
from django.urls import reverse_lazy from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _, ugettext_lazy
from plinth import actions from plinth import actions
from plinth.menu import main_menu from plinth.menu import main_menu
from plinth.modules.users import groups
version = 1 version = 1
name = _('Sharing') name = _('Sharing')
short_description = _('File Sharing')
description = [ description = [
_('Sharing allows you to share your content over web with a group of ' _('Sharing allows you to share your content over web with chosen groups'
'users. Add the content you would like to share in the sharing app.'), 'of users. Add the content you would like to share in the sharing app.'),
_('Sharing app will be available from <a href="/plinth/apps/add_share">'
'/sharing</a> path on the web server.'),
] ]
subsubmenu = [{'url': reverse_lazy('sharing:about'),
'text': ugettext_lazy('About')},
{'url': reverse_lazy('sharing:add_share'),
'text': ugettext_lazy('Add share')}]
def init(): def init():
"""Initialize the module.""" """Initialize the module."""
menu = main_menu.get('apps') menu = main_menu.get('apps')
menu.add_urlname(name, 'glyphicon-share', 'sharing:about') menu.add_urlname(name, 'glyphicon-share', 'sharing:index')
def index(request): def list_shares():
return TemplateResponse(request, 'about.html', """Return a list of shares."""
{'title': name, output = actions.superuser_run('sharing', ['list'])
'description': description, return json.loads(output)['shares']
'subsubmenu': subsubmenu})
# TODO: handle the error case def add_share(name, path, groups):
def add_path_to_share(url, path, user_group): """Add a new share by called the action script."""
if os.path.exists(path): actions.superuser_run(
actions.superuser_run('sharing', options=['add', url, path, user_group]) 'sharing',
else: ['add', '--name', name, '--path', path, '--groups'] + groups)
pass
def share(request): def remove_share(name):
if request.method == 'POST': """Remove a share by calling the action script."""
form = AddShareForm(request.POST) actions.superuser_run('sharing', ['remove', '--name', name])
if form.is_valid():
path = form.cleaned_data['share_path']
user_group = form.cleaned_data['user_group']
share_url = 'share_' + path.split("/")[len(path.split("/")) - 1]
add_path_to_share(share_url, path, user_group)
form = AddShareForm()
else:
form = AddShareForm()
return TemplateResponse(request, 'share.html',
{'title': name,
'subsubmenu': subsubmenu,
'form': form})
class AddShareForm(forms.Form):
share_path = forms.CharField(
label=_('Add path'),
help_text=_('Add the path to the folder you want to share'))
user_group = forms.ChoiceField(
required=False,
choices=groups,
label=_('User-group'),
initial=None)
def __init__(self, *args, **kwargs):
super(forms.Form, self).__init__(*args, **kwargs)
return

View File

@ -0,0 +1,64 @@
#
# This file is part of FreedomBox.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Django forms for sharing app.
"""
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from plinth.modules.users.forms import get_group_choices
from plinth.modules import sharing
class AddShareForm(forms.Form):
"""Form to add a new share."""
name = forms.RegexField(
label=_('Name of the share'), strip=True, regex=r'^[a-z0-9]+$',
help_text=_(
'A lowercase alpha-numeric string that uniquely identifies a '
'share. Example: <em>media</em>.'))
path = forms.CharField(
label=_('Path to share'), strip=True, help_text=_(
'Disk path to a folder on this server that you intend to share.'))
groups = forms.MultipleChoiceField(
widget=forms.CheckboxSelectMultiple,
label=_('User groups who can read the files in the share'),
help_text=_(
'Users who have these permissions will also be able to read the '
'files in the share.'))
def __init__(self, *args, **kwargs):
"""Initialize the form with extra request argument."""
super().__init__(*args, **kwargs)
self.fields['groups'].choices = get_group_choices()
def clean_name(self):
"""Check if the name is valid."""
name = self.cleaned_data['name']
if 'name' in self.initial and name == self.initial['name']:
return name
if any((share for share in sharing.list_shares()
if name == share['name'])):
raise ValidationError(_('A share with this name already exists.'))
return name

View File

@ -1,25 +0,0 @@
{% extends "simple_service.html" %}
{% comment %}
#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load i18n %}
{% block configuration %}
{% endblock %}

View File

@ -0,0 +1,83 @@
{% extends "base.html" %}
{% comment %}
#
# This file is part of FreedomBox.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
{% endcomment %}
{% load i18n %}
{% block content %}
<h2>{{ title }}</h2>
{% for paragraph in description %}
<p>{{ paragraph|safe }}</p>
{% endfor %}
<p>
<a title="{% trans 'Add new share' %}"
role="button" class="btn btn-primary"
href="{% url 'sharing:add' %}">
{% trans 'Add new share' %}
</a>
</p>
{% if not shares %}
<p>{% trans 'No shares currently configured.' %}</p>
{% else %}
<table class="table table-bordered table-condensed table-striped" id="shares-list">
<thead>
<tr>
<th>{% trans "Share Name" %}</th>
<th>{% trans "Disk Path" %}</th>
<th>{% trans "Shared Over" %}</th>
<th>{% trans "To Groups" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for share in shares %}
<tr id="share-{{ share.name }}" class="share">
<td class="share-name">{{ share.name }}</td>
<td class="share-path">{{ share.path }}</td>
<td class="share-url">
<a href="{{ share.url }}"
title="{{ share.url}}">
{{ share.url }}
</a>
</td>
<td class="share-groups">{{ share.groups|join:", " }}</td>
<td class="share-operations">
<form class="form form-inline" method="post"
action="{% url 'sharing:remove' share.name %}">
{% csrf_token %}
<button class="share-remove btn btn-sm btn-default glyphicon glyphicon-trash pull-right"
type="submit"></button>
</form>
<a class="share-edit btn btn-sm btn-default pull-right"
href="{% url 'sharing:edit' share.name %}">
<span class="glyphicon glyphicon-edit" aria-hidden="true"></span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}

View File

@ -1,7 +1,7 @@
{% extends "simple_service.html" %} {% extends "base.html" %}
{% comment %} {% comment %}
# #
# This file is part of Plinth. # This file is part of FreedomBox.
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -21,16 +21,23 @@
{% load bootstrap %} {% load bootstrap %}
{% load i18n %} {% load i18n %}
{% block configuration %} {% block content %}
<form class="form" method="post"> <h3>{{ title }}</h3>
{% csrf_token %}
{{ form|bootstrap }} <form class="form" method="post">
{% csrf_token %}
<input type="submit" class="btn btn-primary" {{ form|bootstrap }}
value="{% trans "Submit" %}"/>
</form>
<input type="submit" class="btn btn-primary"
value="{% trans "Submit" %}"/>
</form>
{% endblock %} {% endblock %}
{% block page_js %}
<script>
$('#id_sharing-name').focus();
</script>
{% endblock %}

View File

@ -1,5 +1,5 @@
# #
# This file is part of Plinth. # This file is part of FreedomBox.
# #
# This program is free software: you can redistribute it and/or modify # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@ -14,16 +14,18 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
""" """
URLs for the sharing module. URLs for the sharing app.
""" """
from django.conf.urls import url from django.conf.urls import url
from . import index, share from .views import AddShareView, EditShareView, IndexView, remove
urlpatterns = [ urlpatterns = [
url(r'^apps/sharing/$', index, name='about'), url(r'^apps/sharing/$', IndexView.as_view(), name='index'),
url(r'^apps/sharing/add_share$', share, name='add_share'), url(r'^apps/sharing/add/$', AddShareView.as_view(), name='add'),
] url(r'^apps/sharing/(?P<name>[a-z0-9]+)/edit/$', EditShareView.as_view(),
name='edit'),
url(r'^apps/sharing/(?P<name>[a-z0-9]+)/remove/$', remove, name='remove'),
]

View File

@ -0,0 +1,109 @@
#
# This file is part of FreedomBox.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""
Views for the sharing app.
"""
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.http import require_POST
from django.views.generic import FormView, TemplateView
from plinth.modules import sharing
from .forms import AddShareForm
class IndexView(TemplateView):
"""View to show list of shares."""
template_name = 'sharing.html'
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = sharing.name
context['description'] = sharing.description
context['shares'] = sharing.list_shares()
return context
class AddShareView(SuccessMessageMixin, FormView):
"""View to add a new share."""
form_class = AddShareForm
prefix = 'sharing'
template_name = 'sharing_add_edit.html'
success_url = reverse_lazy('sharing:index')
success_message = _('Share added.')
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Add Share')
return context
def form_valid(self, form):
"""Add the share on valid form submission."""
sharing.add_share(form.cleaned_data['name'], form.cleaned_data['path'],
form.cleaned_data['groups'])
return super().form_valid(form)
class EditShareView(SuccessMessageMixin, FormView):
"""View to edit an existing share."""
form_class = AddShareForm
prefix = 'sharing'
template_name = 'sharing_add_edit.html'
success_url = reverse_lazy('sharing:index')
success_message = _('Share edited.')
def get_context_data(self, **kwargs):
"""Return additional context for rendering the template."""
context = super().get_context_data(**kwargs)
context['title'] = _('Edit Share')
return context
def get_initial(self):
"""Load information about share being edited."""
try:
return [
share for share in sharing.list_shares()
if share['name'] == self.kwargs['name']
][0]
except IndexError:
raise Http404
def form_valid(self, form):
"""Add the share on valid form submission."""
if form.initial != form.cleaned_data:
sharing.remove_share(form.initial['name'])
sharing.add_share(form.cleaned_data['name'],
form.cleaned_data['path'],
form.cleaned_data['groups'])
return super().form_valid(form)
@require_POST
def remove(request, name):
"""View to remove a share."""
sharing.remove_share(name)
messages.success(request, _('Share deleted.'))
return redirect(reverse_lazy('sharing:index'))