diff options
author | Aurélien Bompard <aurelien@bompard.org> | 2013-10-18 16:15:40 +0200 |
---|---|---|
committer | Aurélien Bompard <aurelien@bompard.org> | 2013-10-18 16:15:40 +0200 |
commit | f1a0a71d971ffa4c01a88b92aa4869dd3a12a3fa (patch) | |
tree | 8289deaa33daafde6572e5e6abc6a8df11475ab1 /hyperkitty | |
parent | 5b0796d955930ff953f503c9f1966dfbbe876fec (diff) | |
download | hyperkitty-f1a0a71d971ffa4c01a88b92aa4869dd3a12a3fa.tar.gz hyperkitty-f1a0a71d971ffa4c01a88b92aa4869dd3a12a3fa.tar.xz hyperkitty-f1a0a71d971ffa4c01a88b92aa4869dd3a12a3fa.zip |
Handle permissions on private mailing-lists
Diffstat (limited to 'hyperkitty')
-rw-r--r-- | hyperkitty/lib/mailman.py | 41 | ||||
-rw-r--r-- | hyperkitty/middleware.py | 30 | ||||
-rw-r--r-- | hyperkitty/templates/error-private.html | 27 | ||||
-rw-r--r-- | hyperkitty/tests/test_views.py | 87 | ||||
-rw-r--r-- | hyperkitty/views/list.py | 3 | ||||
-rw-r--r-- | hyperkitty/views/message.py | 6 | ||||
-rw-r--r-- | hyperkitty/views/search.py | 6 | ||||
-rw-r--r-- | hyperkitty/views/thread.py | 9 |
8 files changed, 205 insertions, 4 deletions
diff --git a/hyperkitty/lib/mailman.py b/hyperkitty/lib/mailman.py index f0c970f..090902b 100644 --- a/hyperkitty/lib/mailman.py +++ b/hyperkitty/lib/mailman.py @@ -19,12 +19,21 @@ # Author: Aurelien Bompard <abompard@fedoraproject.org> # +from __future__ import absolute_import + +from functools import wraps + from django.conf import settings from django.core.urlresolvers import reverse from django.utils.http import urlquote +from django.utils.decorators import available_attrs +from django.shortcuts import redirect, render +from django.http import Http404 +from mailman.interfaces.archiver import ArchivePolicy from mailmanclient import Client from hyperkitty.models import Rating +from hyperkitty.lib import get_store def subscribe(list_address, user): @@ -80,3 +89,35 @@ def get_subscriptions(store, client, mm_user): "posts_count": len(email_hashes), }) return subscriptions + + +# View decorator: check that the list is authorized +def check_mlist_private(func): + @wraps(func, assigned=available_attrs(func)) + def inner(request, *args, **kwargs): + if "mlist_fqdn" in kwargs: + mlist_fqdn = kwargs["mlist_fqdn"] + else: + mlist_fqdn = args[0] + try: + store = get_store(request) + except KeyError: + return func(request, *args, **kwargs) # Unittesting? + mlist = store.get_list(mlist_fqdn) + if mlist is None: + raise Http404("No archived mailing-list by that name.") + #return HttpResponse(request.session.get("subscribed", "NO KEY"), content_type="text/plain") + if not is_mlist_authorized(request, mlist): + return render(request, "error-private.html", { + "mlist": mlist, + }, status=403) + return func(request, *args, **kwargs) + return inner + +def is_mlist_authorized(request, mlist): + if mlist.archive_policy == ArchivePolicy.private and \ + not (request.user.is_authenticated() and + hasattr(request, "session") and + mlist.name in request.session.get("subscribed", [])): + return False + return True diff --git a/hyperkitty/middleware.py b/hyperkitty/middleware.py index 2b59744..823dd88 100644 --- a/hyperkitty/middleware.py +++ b/hyperkitty/middleware.py @@ -90,3 +90,33 @@ class TimezoneMiddleware(object): return if user_profile.timezone: timezone.activate(user_profile.timezone) + + + +# Cache some metadata from Mailman about the logged in user + +from mailmanclient import Client as MailmanClient +from mailmanclient import MailmanConnectionError +from django.conf import settings + +class MailmanUserMetadata(object): + + session_key = "subscribed" + + def process_view(self, request, view_func, view_args, view_kwargs): + if not request.user.is_authenticated(): + return + if not request.user.email: + return # Can this really happen? + if self.session_key in request.session: + return # Already set + client = MailmanClient('%s/3.0' % + settings.MAILMAN_REST_SERVER, + settings.MAILMAN_API_USER, + settings.MAILMAN_API_PASS) + try: + user = client.get_user(request.user.email) + except MailmanConnectionError: + return + request.session[self.session_key] = \ + [ s.address for s in user.subscriptions ] diff --git a/hyperkitty/templates/error-private.html b/hyperkitty/templates/error-private.html new file mode 100644 index 0000000..07cb5cf --- /dev/null +++ b/hyperkitty/templates/error-private.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% load url from future %} +{% load gravatar %} +{% load hk_generic %} +{% load storm %} + + +{% block title %} +Error: private list - {{ mlist.display_name|default:mlist.name|escapeemail }} - {{ app_name|title }} +{% endblock %} + +{% block content %} + +<div class="row-fluid"> + + <div class="span2"> + </div> + + <div class="span7"> + <p class="text-error"> + This mailing-list is private, you must be subscribed to view the archives. + </p> + </div> + +</div> + +{% endblock %} diff --git a/hyperkitty/tests/test_views.py b/hyperkitty/tests/test_views.py index fb3832c..2874ac0 100644 --- a/hyperkitty/tests/test_views.py +++ b/hyperkitty/tests/test_views.py @@ -21,6 +21,8 @@ # import datetime +from tempfile import mkdtemp +from shutil import rmtree from mock import Mock @@ -30,6 +32,7 @@ from django.test.client import Client, RequestFactory from django.contrib.auth.models import User, AnonymousUser from django.core.urlresolvers import reverse from mailman.email.message import Message +from mailman.interfaces.archiver import ArchivePolicy import kittystore from kittystore.utils import get_message_id_hash @@ -81,7 +84,7 @@ class AccountViewsTestCase(TestCase): from hyperkitty.views.accounts import last_views from hyperkitty.views.thread import thread_index -from hyperkitty.views.list import archives +from hyperkitty.views.list import archives, overview class LastViewsTestCase(TestCase): @@ -147,12 +150,11 @@ class LastViewsTestCase(TestCase): count=2, status_code=200) def test_overview(self): - now = datetime.datetime.now() request = self.factory.get(reverse('list_overview', args=["list@example.com"])) request.user = self.user - response = archives(request, "list@example.com", now.year, now.month) + response = overview(request, "list@example.com") self.assertContains(response, "icon-eye-close", - count=2, status_code=200) + count=4, status_code=200) from hyperkitty.views.message import vote @@ -404,3 +406,80 @@ class ReattachTestCase(TestCase): count=1, status_code=200) self.assertContains(response, "Can't attach an older thread to a newer thread.", count=1, status_code=200) + + + +from django.contrib.auth.models import AnonymousUser +from hyperkitty.views.list import archives +from hyperkitty.views.message import index as message_view +from hyperkitty.views.search import search as search_view + +class PrivateArchivesTestCase(TestCase): + + def setUp(self): + self.tmpdir = mkdtemp(prefix="hyperkitty-testing-") + self.user = User.objects.create_user('testuser', 'test@example.com', 'testPass') + #self.user.is_staff = True + #self.client.login(username='testuser', password='testPass') + settings = SettingsModule() + settings.KITTYSTORE_SEARCH_INDEX = self.tmpdir + self.store = kittystore.get_store(settings, debug=False) + ml = FakeList("list@example.com") + ml.subject_prefix = u"[example] " + ml.archive_policy = ArchivePolicy.private + msg = Message() + msg["From"] = "dummy@example.com" + msg["Message-ID"] = "<msgid>" + msg["Subject"] = "Dummy message" + msg.set_payload("Dummy message") + msg["Message-ID-Hash"] = self.msgid = self.store.add_to_list(ml, msg) + # Factory + defaults = {"kittystore.store": self.store, + "HTTP_USER_AGENT": "testbot", + "session": {}, } + self.factory = RequestFactory(**defaults) + + def tearDown(self): + rmtree(self.tmpdir) + + def test_month_view(self): + now = datetime.datetime.now() + request = self.factory.get(reverse('archives_with_month', args=["list@example.com", now.year, now.month])) + self._do_test(request, archives, ["list@example.com", now.year, now.month]) + + def test_overview(self): + request = self.factory.get(reverse('list_overview', args=["list@example.com"])) + self._do_test(request, overview, ["list@example.com"]) + + def test_thread_view(self): + request = self.factory.get(reverse('thread', + args=["list@example.com", self.msgid])) + self._do_test(request, thread_index, ["list@example.com", self.msgid]) + + def test_message_view(self): + request = self.factory.get(reverse('message_index', + args=["list@example.com", self.msgid])) + self._do_test(request, message_view, ["list@example.com", self.msgid]) + + def test_search_list(self): + request = self.factory.get(reverse('search'), + {"list": "list@example.com", "query": "dummy"}) + self._do_test(request, search_view) + + def test_search_all_lists(self): + # When searching all lists, we only search public lists regardless of + # the user's subscriptions + request = self.factory.get(reverse('search'), {"query": "dummy"}) + request.user = AnonymousUser() + response = search_view(request) + self.assertNotContains(response, "Dummy message", status_code=200) + + def _do_test(self, request, view, view_args=[]): + #self.assertRaises(HttpForbidden, view, request, *view_args) + request.user = AnonymousUser() + response = view(request, *view_args) + self.assertEquals(response.status_code, 403) + request.user = self.user + setattr(request, "session", {"subscribed": ["list@example.com"]}) + response = view(request, *view_args) + self.assertContains(response, "Dummy message", status_code=200) diff --git a/hyperkitty/views/list.py b/hyperkitty/views/list.py index b4a97a6..cbcc678 100644 --- a/hyperkitty/views/list.py +++ b/hyperkitty/views/list.py @@ -37,6 +37,7 @@ from hyperkitty.lib.view_helpers import FLASH_MESSAGES, paginate, \ get_category_widget, get_months, get_display_dates, daterange, \ is_thread_unread from hyperkitty.lib.voting import set_message_votes, set_thread_votes +from hyperkitty.lib.mailman import check_mlist_private if settings.USE_MOCKUPS: @@ -49,6 +50,7 @@ Thread = namedtuple('Thread', [ ]) +@check_mlist_private def archives(request, mlist_fqdn, year=None, month=None, day=None): if year is None and month is None: today = datetime.date.today() @@ -136,6 +138,7 @@ def _thread_list(request, mlist, threads, template_name='thread_list.html', extr return render(request, template_name, context) +@check_mlist_private def overview(request, mlist_fqdn=None): if not mlist_fqdn: return redirect('/') diff --git a/hyperkitty/views/message.py b/hyperkitty/views/message.py index 6e3a640..414df68 100644 --- a/hyperkitty/views/message.py +++ b/hyperkitty/views/message.py @@ -35,10 +35,12 @@ from hyperkitty.lib import get_store from hyperkitty.lib.view_helpers import get_months from hyperkitty.lib.posting import post_to_list, PostingFailed from hyperkitty.lib.voting import set_message_votes +from hyperkitty.lib.mailman import check_mlist_private from hyperkitty.models import Rating from forms import ReplyForm, PostForm +@check_mlist_private def index(request, mlist_fqdn, message_id_hash): ''' Displays a single message identified by its message_id_hash (derived from @@ -62,6 +64,7 @@ def index(request, mlist_fqdn, message_id_hash): return render(request, "message.html", context) +@check_mlist_private def attachment(request, mlist_fqdn, message_id_hash, counter, filename): """ Sends the numbered attachment for download. The filename is not used for @@ -87,6 +90,7 @@ def attachment(request, mlist_fqdn, message_id_hash, counter, filename): return response +@check_mlist_private def vote(request, mlist_fqdn, message_id_hash): """ Add a rating to a given message identified by messageid. """ if request.method != 'POST': @@ -142,6 +146,7 @@ def vote(request, mlist_fqdn, message_id_hash): @login_required +@check_mlist_private def reply(request, mlist_fqdn, message_id_hash): """ Sends a reply to the list. TODO: unit tests @@ -186,6 +191,7 @@ def reply(request, mlist_fqdn, message_id_hash): @login_required +@check_mlist_private def new_message(request, mlist_fqdn): """ Sends a new thread-starting message to the list. TODO: unit tests diff --git a/hyperkitty/views/search.py b/hyperkitty/views/search.py index d398ae2..3eb179f 100644 --- a/hyperkitty/views/search.py +++ b/hyperkitty/views/search.py @@ -30,6 +30,7 @@ from hyperkitty.lib.view_helpers import paginate from hyperkitty.lib.voting import set_message_votes from hyperkitty.views.list import _thread_list +from hyperkitty.lib.mailman import check_mlist_private, is_mlist_authorized class SearchPaginator(Paginator): @@ -46,6 +47,7 @@ class SearchPaginator(Paginator): return Page(self.object_list, number, self) +@check_mlist_private def search_tag(request, mlist_fqdn, tag): '''Returns threads having a particular tag''' store = get_store(request) @@ -84,6 +86,10 @@ def search(request, page=1): mlist = store.get_list(mlist_fqdn) if mlist is None: raise Http404("No archived mailing-list by that name.") + if not is_mlist_authorized(request, mlist): + return render(request, "error-private.html", { + "mlist": mlist, + }, status=403) if not query: return render(request, "search_results.html", { diff --git a/hyperkitty/views/thread.py b/hyperkitty/views/thread.py index 76bfbc9..6f70b86 100644 --- a/hyperkitty/views/thread.py +++ b/hyperkitty/views/thread.py @@ -40,6 +40,7 @@ from hyperkitty.lib import get_store, stripped_subject from hyperkitty.lib.view_helpers import (get_months, get_category_widget, FLASH_MESSAGES) from hyperkitty.lib.voting import set_message_votes +from hyperkitty.lib.mailman import check_mlist_private def _get_thread_replies(request, thread, offset=1, limit=None): @@ -73,6 +74,7 @@ def _get_thread_replies(request, thread, offset=1, limit=None): return emails +@check_mlist_private def thread_index(request, mlist_fqdn, threadid, month=None, year=None): ''' Displays all the email for a given thread identifier ''' store = get_store(request) @@ -180,6 +182,7 @@ def thread_index(request, mlist_fqdn, threadid, month=None, year=None): return render(request, "thread.html", context) +@check_mlist_private def replies(request, mlist_fqdn, threadid): """Get JSON encoded lists with the replies and the participants""" chunk_size = 5 @@ -216,6 +219,7 @@ def replies(request, mlist_fqdn, threadid): mimetype='application/javascript') +@check_mlist_private def tags(request, mlist_fqdn, threadid): """ Add or remove a tag on a given thread. """ if not request.user.is_authenticated(): @@ -263,6 +267,7 @@ def tags(request, mlist_fqdn, threadid): return HttpResponse(json.dumps(response), mimetype='application/javascript') +@check_mlist_private def suggest_tags(request, mlist_fqdn, threadid): term = request.GET.get("term") current_tags = Tag.objects.filter( @@ -277,6 +282,7 @@ def suggest_tags(request, mlist_fqdn, threadid): return HttpResponse(json.dumps(tags), mimetype='application/javascript') +@check_mlist_private def favorite(request, mlist_fqdn, threadid): """ Add or remove from favorites""" if not request.user.is_authenticated(): @@ -305,6 +311,7 @@ def favorite(request, mlist_fqdn, threadid): return HttpResponse("success", mimetype='text/plain') +@check_mlist_private def set_category(request, mlist_fqdn, threadid): """ Set the category for a given thread. """ if not request.user.is_authenticated(): @@ -334,6 +341,7 @@ def set_category(request, mlist_fqdn, threadid): return render(request, "threads/category.html", context) +@check_mlist_private def reattach(request, mlist_fqdn, threadid): if not request.user.is_staff: return HttpResponse('You must be a staff member to reattach a thread', @@ -385,6 +393,7 @@ def reattach(request, mlist_fqdn, threadid): return render(request, "reattach.html", context) +@check_mlist_private def reattach_suggest(request, mlist_fqdn, threadid): store = get_store(request) mlist = store.get_list(mlist_fqdn) |