summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--hyperkitty/lib/mailman.py41
-rw-r--r--hyperkitty/middleware.py30
-rw-r--r--hyperkitty/templates/error-private.html27
-rw-r--r--hyperkitty/tests/test_views.py87
-rw-r--r--hyperkitty/views/list.py3
-rw-r--r--hyperkitty/views/message.py6
-rw-r--r--hyperkitty/views/search.py6
-rw-r--r--hyperkitty/views/thread.py9
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&#39;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)