Permalink
Browse files

Merge pull request #429 from glogiotatidis/stronghold

Stronghold
  • Loading branch information...
2 parents 32358d9 + 0ccade4 commit 165f47ce6b3a618a48ca3440856168bef171140f @Sancus Sancus committed Mar 15, 2013
View
@@ -0,0 +1,21 @@
+from functools import partial
+
+
+def _set_attribute_func(function, attribute, value):
+ """Helper to set attributes to func and methods."""
+ orig_func = function
+ while isinstance(orig_func, partial):
+ orig_func = orig_func.func
+ setattr(orig_func, attribute, value)
+
+
+def allow_public(function):
+ """Allow view to be accessed by anonymous users."""
+ _set_attribute_func(function, '_allow_public', True)
+ return function
+
+
+def allow_unvouched(function):
+ """Allow view to be accessed by unvouched users."""
+ _set_attribute_func(function, '_allow_unvouched', True)
+ return function
View
@@ -1,69 +1,51 @@
import re
-import os
from contextlib import contextmanager
-
-from django.contrib.auth.models import User
-from django.http import (HttpResponseForbidden, HttpResponseNotAllowed,
- HttpResponseRedirect, HttpResponsePermanentRedirect)
+from django.conf import settings
from django.core.urlresolvers import is_valid_path, reverse
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.contrib import messages
+from django.http import HttpResponseRedirect
from django.utils.encoding import iri_to_uri
-
-import commonware.log
-from funfactory.manage import ROOT
+from django.shortcuts import redirect
+from tower import ugettext as _
from apps.groups.models import Group, GroupAlias
-# TODO: this is hackish. Once we update mozillians to the newest playdoh layout
-error_page = __import__('%s.urls' % os.path.basename(ROOT)).urls.error_page
-log = commonware.log.getLogger('m.phonebook')
+class StrongholdMiddleware(object):
+ """Keep unvouched users out, unless explicitly allowed in.
-class PermissionDeniedMiddleware(object):
- """Add a generic 40x 'not allowed' handler.
+ Inspired by https://github.com/mgrouchy/django-stronghold/
- TODO: Currently uses the 500.html error template, but in the
- future should display a more tailored-to-the-actual-error 'not
- allowed' page.
"""
- def process_response(self, request, response):
- if isinstance(response, (HttpResponseForbidden,
- HttpResponseNotAllowed)):
- if request.user.is_authenticated():
- log.debug('Permission denied middleware, user was '
- 'authenticated, sending 500')
- else:
- if isinstance(response, (HttpResponseForbidden)):
- log.debug('Response was forbidden')
- elif isinstance(response, (HttpResponseNotAllowed)):
- log.debug('Response was not allowed')
- return error_page(request, 500, status=response.status_code)
- return response
+ def __init__(self):
+ self.exceptions = getattr(settings, 'STRONGHOLD_EXCEPTIONS', [])
+ def process_view(self, request, view_func, view_args, view_kwargs):
+ for view_url in self.exceptions:
+ if re.match(view_url, request.path):
+ return None
-class RemoveSlashMiddleware(object):
- """Middleware that tries to remove a trailing slash if there was a 404.
+ allow_public = getattr(view_func, '_allow_public', None)
+ if allow_public:
+ return None
- If the response is a 404 because url resolution failed, we'll look
- for a better url without a trailing slash.
+ if not request.user.is_authenticated():
+ messages.warning(request, _('You must be logged in to continue.'))
+ return login_required(view_func)(request, *view_args,
+ **view_kwargs)
- Cribbed from kitsune:
- https://github.com/mozilla/kitsune/blob/master/apps/sumo/middleware.py
+ if request.user.userprofile.is_vouched:
+ return None
- """
+ allow_unvouched = getattr(view_func, '_allow_unvouched', None)
+ if allow_unvouched:
+ return None
- def process_response(self, request, response):
- if (response.status_code == 404
- and request.path_info.endswith('/')
- and not is_valid_path(request.path_info)
- and is_valid_path(request.path_info[:-1])):
- # Use request.path because we munged app/locale in path_info.
- newurl = request.path[:-1]
- if request.GET:
- with safe_query_string(request):
- newurl += '?' + request.META['QUERY_STRING']
- return HttpResponsePermanentRedirect(newurl)
- return response
+ messages.error(request, _('You must be vouched to continue.'))
+ return redirect('home')
class UsernameRedirectionMiddleware(object):
@@ -79,7 +61,9 @@ def process_response(self, request, response):
and not request.path_info.startswith('/u/')
and not is_valid_path(request.path_info)
and User.objects.filter(
- username__iexact=request.path_info[1:].strip('/')).exists()):
+ username__iexact=request.path_info[1:].strip('/')).exists()
+ and request.user.is_authenticated()
+ and request.user.userprofile.is_vouched):
newurl = '/u' + request.path_info
if request.GET:
No changes.
File renamed without changes.
@@ -0,0 +1,55 @@
+from django.core.urlresolvers import reverse
+from django.test.utils import override_settings
+from nose.tools import eq_
+
+from apps.common.tests.init import ESTestCase
+from apps.common.decorators import allow_public, allow_unvouched
+
+
+class TestDecorators(ESTestCase):
+
+ def test_allow_public_decorator(self):
+
+ def foo():
+ pass
+
+ eq_(getattr(foo, '_allow_public', None), None)
+ allow_public(foo)
+ self.assertTrue(foo._allow_public)
+
+ def test_allow_unvouched_decorator(self):
+
+ def foo():
+ pass
+ eq_(getattr(foo, '_allow_unvouched', None), None)
+ allow_unvouched(foo)
+ self.assertTrue(foo._allow_unvouched)
+
+
+class TestStrongholdMiddleware(ESTestCase):
+ """Stronghold Testcases."""
+ urls = 'apps.common.tests.test_urls'
+
+ def test_stronghold(self):
+ """Test stronhold middleware functionality."""
+ self.excepted_results = {
+ 'vouched': {'vouched': True, 'unvouched': False, 'public': False},
+ 'unvouched': {'vouched': True, 'unvouched': True, 'public': False},
+ 'public': {'vouched': True, 'unvouched': True, 'public': True},
+ 'excepted': {'vouched': True, 'unvouched': True, 'public': True}}
+
+ self.clients = {
+ 'vouched': self.mozillian_client,
+ 'unvouched': self.pending_client,
+ 'public': self.client}
+
+ for url in self.excepted_results:
+ for user, client in self.clients.items():
+ r_url = reverse(url, prefix='/en-US/')
+ response = client.get(r_url, follow=True)
+ if self.excepted_results[url][user]:
+ eq_(response.content, 'Hi!')
+
+ else:
+ eq_(len(response.redirect_chain), 2)
+ eq_(len(response.context['messages']), 1)
@@ -0,0 +1,29 @@
+from django.conf import settings
+from django.conf.urls.defaults import patterns, url
+from django.http import HttpResponse
+from apps.common.decorators import allow_public, allow_unvouched
+
+from urls import urlpatterns
+
+def vouched(request):
+ return HttpResponse('Hi!')
+
+
+@allow_unvouched
+def unvouched(request):
+ return HttpResponse('Hi!')
+
+@allow_public
+def public(request):
+ return HttpResponse('Hi!')
+
+
+urlpatterns += patterns(
+ '',
+ url(r'^vouched/$', vouched, name='vouched'),
+ url(r'^unvouched/$', unvouched, name='unvouched'),
+ url(r'^public/$', public, name='public'),
+ url(r'^excepted/$', vouched, name='excepted'))
+
+
+settings.STRONGHOLD_EXCEPTIONS += ['^/en-US/excepted/$']
@@ -8,15 +8,15 @@
from nose.tools import eq_
from pyquery import PyQuery as pq
-import common.tests
+import apps.common.tests.init
from ..cron import assign_autocomplete_to_groups
from ..helpers import stringify_groups
from ..models import AUTO_COMPLETE_COUNT, Group, GroupAlias
from ..utils import merge_groups
-class GroupTest(common.tests.ESTestCase):
+class GroupTest(apps.common.tests.init.ESTestCase):
"""Test the group/grouping system."""
def setUp(self):
@@ -5,13 +5,13 @@
from funfactory.urlresolvers import reverse
from nose.tools import eq_
-import common.tests
+import apps.common.tests.init
from ..cron import assign_autocomplete_to_groups
from ..models import AUTO_COMPLETE_COUNT, Language
-class LanguagesTest(common.tests.ESTestCase):
+class LanguagesTest(apps.common.tests.init.ESTestCase):
def test_autocomplete_api(self):
self.client.login(email=self.mozillian.email)
@@ -1,7 +1,7 @@
from django.core.urlresolvers import reverse
from nose.tools import eq_
-from common.tests import ESTestCase
+from apps.common.tests.init import ESTestCase
from ..models import Group, GroupAlias
@@ -5,13 +5,13 @@
from funfactory.urlresolvers import reverse
from nose.tools import eq_
-import common.tests
+import apps.common.tests.init
from ..cron import assign_autocomplete_to_groups
from ..models import AUTO_COMPLETE_COUNT, Skill
-class SkillsTest(common.tests.ESTestCase):
+class SkillsTest(apps.common.tests.init.ESTestCase):
def test_autocomplete_api(self):
self.client.login(email=self.mozillian.email)
View
@@ -1,20 +1,16 @@
from django.conf.urls.defaults import patterns, url
-from django.contrib import admin
-admin.autodiscover()
-
import views
from models import Group, Skill, Language
urlpatterns = patterns('',
- url('^groups$', views.index, name='group_index'),
- url('^group/(?P<url>[^/]+)$', views.show, name='group'),
- url('^group/(?P<url>[^/]+)/toggle$', views.toggle,
- name='group_toggle'),
- url('^groups/search$', views.search,
+ url('^groups/$', views.index, name='group_index'),
+ url('^group/(?P<url>[-\w]+)/$', views.show, name='group'),
+ url('^group/(?P<url>[-\w]+)/toggle/$', views.toggle, name='group_toggle'),
+ url('^groups/search/$', views.search,
dict(searched_object=Group), name='group_search'),
- url('^skills/search$', views.search,
+ url('^skills/search/$', views.search,
dict(searched_object=Skill), name='skill_search'),
- url('^languages/search$', views.search,
+ url('^languages/search/$', views.search,
dict(searched_object=Language), name='language_search'),
)
View
@@ -1,26 +1,23 @@
import json
-from django.contrib.auth.decorators import login_required
from django.core.paginator import EmptyPage, Paginator, PageNotAnInteger
from django.db.models import Count
-from django.http import Http404, HttpResponse
+from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.cache import cache_control, never_cache
from django.views.decorators.http import require_POST
import commonware.log
from funfactory.urlresolvers import reverse
+from apps.common.decorators import allow_unvouched
from apps.groups.models import Group, Skill
from apps.phonebook import forms
-from apps.phonebook.views import vouch_required
-from apps.users.models import UserProfile
from apps.users.tasks import update_basket_task
log = commonware.log.getLogger('m.groups')
-@login_required
def index(request):
"""Lists all public groups (in use) on Mozillians."""
paginator = Paginator(Group.objects.all(), forms.PAGINATION_LIMIT)
@@ -37,25 +34,23 @@ def index(request):
return render(request, 'groups/index.html', data)
-@login_required
+@allow_unvouched
@cache_control(must_revalidate=True, max_age=3600)
def search(request, searched_object=Group):
- """Simple wildcard search for a group using a GET parameter."""
- data = dict(search=True)
- data['groups'] = list(searched_object.search(request.GET
- .get('term')).values_list('name', flat=True))
+ """Simple wildcard search for a group using a GET parameter.
- if request.is_ajax():
- return HttpResponse(json.dumps(data['groups']),
+ Used for group/skill/language auto-completion.
+
+ """
+ term = request.GET.get('term', None)
+ if request.is_ajax() and term:
+ groups = searched_object.search(term).values_list('name', flat=True)
+ return HttpResponse(json.dumps(list(groups)),
mimetype='application/json')
- if searched_object == Group:
- return render(request, 'groups/index.html', data)
- # Raise a 404 if this is a Skill page that isn't ajax
- raise Http404
+ return redirect('home')
-@vouch_required
@never_cache
def show(request, url):
"""List all vouched users with this group."""
@@ -105,7 +100,6 @@ def show(request, url):
@require_POST
-@vouch_required
def toggle(request, url):
"""Toggle the current user's membership of a group."""
group = get_object_or_404(Group, url=url)
View
@@ -20,8 +20,8 @@
class SearchForm(happyforms.Form):
q = forms.CharField(widget=forms.HiddenInput, required=False)
limit = forms.CharField(widget=forms.HiddenInput, required=False)
- include_non_vouched = forms.BooleanField(label=_lazy(u'Include non-vouched'),
- required=False)
+ include_non_vouched = forms.BooleanField(
+ label=_lazy(u'Include non-vouched'), required=False)
def clean_limit(self):
"""Validate that this limit is numeric and greater than 1."""
@@ -6,7 +6,7 @@
from funfactory.urlresolvers import reverse
from nose.tools import eq_
-from apps.common.tests import ESTestCase, user
+from apps.common.tests.init import ESTestCase, user
ASSERTION = 'asldkfjasldfka'
Oops, something went wrong.

0 comments on commit 165f47c

Please sign in to comment.