diff --git a/apps/amo/decorators.py b/apps/amo/decorators.py index 411c334339..d454d68a0b 100644 --- a/apps/amo/decorators.py +++ b/apps/amo/decorators.py @@ -128,3 +128,12 @@ def set_modified_on(f): countdown=settings.MODIFIED_DELAY) return result return wrapper + + +def no_login_required(f): + """ + If you are using the LoginRequiredMiddleware mark this view + as not needing any sort of login. + """ + f._no_login_required = True + return f diff --git a/apps/amo/middleware.py b/apps/amo/middleware.py index f6ebb107e5..a8e9985f40 100644 --- a/apps/amo/middleware.py +++ b/apps/amo/middleware.py @@ -11,6 +11,7 @@ from django.conf import settings from django.contrib.sessions.middleware import SessionMiddleware from django.http import HttpResponsePermanentRedirect from django.middleware import common +from django.shortcuts import redirect from django.utils.cache import patch_vary_headers, patch_cache_control from django.utils.encoding import iri_to_uri, smart_str @@ -224,3 +225,20 @@ class LazyPjaxMiddleware(object): response.content = ''.join(html) return response + + +class LoginRequiredMiddleware(object): + """ + If enabled, will force a login on all requests. Unless the view + is decorated with the no_login_required decorator, or placed + in the NO_LOGIN_REQUIRED_MODULES tuple. + """ + + def process_view(self, request, view_func, view_args, view_kwargs): + name = '%s.%s' % (view_func.__module__, view_func.__name__) + if (request.user.is_authenticated() or + getattr(view_func, '_no_login_required', False) or + name in settings.NO_LOGIN_REQUIRED_MODULES): + return + + return redirect(settings.LOGIN_URL) diff --git a/apps/amo/tests/test_middleware.py b/apps/amo/tests/test_middleware.py index 44f2327ecb..94cd6b5db7 100644 --- a/apps/amo/tests/test_middleware.py +++ b/apps/amo/tests/test_middleware.py @@ -6,13 +6,14 @@ from django.conf import settings from django import test from commonware.middleware import HidePasswordOnException -from mock import patch +from mock import Mock, patch from nose.tools import eq_, raises from pyquery import PyQuery as pq from test_utils import RequestFactory import amo.tests -from amo.middleware import LazyPjaxMiddleware +from amo.decorators import no_login_required +from amo.middleware import LazyPjaxMiddleware, LoginRequiredMiddleware from amo.urlresolvers import reverse from zadmin.models import Config, _config_cache @@ -163,3 +164,41 @@ class TestLazyPjaxMiddleware(amo.tests.TestCase): content_type='application/json') resp = self.process(response=resp) eq_(json.loads(resp.content), {'foo': 1}) + + +class TestLoginRequiredMiddleware(amo.tests.TestCase): + + def normal_view(self, request): + return '' + + @no_login_required + def allowed_view(self, request): + return '' + + def process(self, authenticated, view=None): + if not view: + view = self.normal_view + request = RequestFactory().get('/', HTTP_X_PJAX=True) + request.user = Mock() + request.user.is_authenticated.return_value = authenticated + return LoginRequiredMiddleware().process_view(request, view, [], {}) + + def test_middleware(self): + # Middleware returns None if it doesn't need to redirect the user. + assert not self.process(True) + eq_(self.process(False).status_code, 302) + + def test_decorator_allowed(self): + assert not self.process(False, self.allowed_view) + assert not self.process(True, self.allowed_view) + + def test_decorator_normal(self): + eq_(self.process(False, self.normal_view).status_code, 302) + assert not self.process(True, self.normal_view) + + @patch.object(settings, 'NO_LOGIN_REQUIRED_MODULES', + ['zamboni.apps.amo.tests.test_middleware.normal_view']) + def test_modules(self): + assert not self.process(False) + assert not self.process(True) + diff --git a/apps/users/views.py b/apps/users/views.py index e44b0050aa..f12f214aca 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -26,8 +26,8 @@ import waffle from access.middleware import ACLMiddleware import amo from amo import messages -from amo.decorators import (json_view, login_required, permission_required, - write, post_required) +from amo.decorators import (json_view, login_required, no_login_required, + permission_required, write, post_required) from amo.forms import AbuseForm from amo.urlresolvers import reverse from amo.utils import send_mail @@ -266,6 +266,7 @@ def _clean_next_url(request): @anonymous_csrf @post_required +@no_login_required #@ratelimit(block=True, rate=settings.LOGIN_RATELIMIT_ALL_USERS) def browserid_login(request): if waffle.switch_is_active('browserid-login'): @@ -285,6 +286,7 @@ def browserid_login(request): @anonymous_csrf @mobile_template('users/{mobile/}login_modal.html') +@no_login_required #@ratelimit(block=True, rate=settings.LOGIN_RATELIMIT_ALL_USERS) def login_modal(request, template=None): return _login(request, template=template) @@ -292,6 +294,7 @@ def login_modal(request, template=None): @anonymous_csrf @mobile_template('users/{mobile/}login.html') +@no_login_required #@ratelimit(block=True, rate=settings.LOGIN_RATELIMIT_ALL_USERS) def login(request, template=None): return _login(request, template=template) @@ -446,6 +449,7 @@ def profile(request, user_id): @anonymous_csrf +@no_login_required def register(request): if request.user.is_authenticated(): messages.info(request, _("You are already logged in to an account.")) diff --git a/settings.py b/settings.py index ecbea2e705..204ea9f1e8 100644 --- a/settings.py +++ b/settings.py @@ -1274,3 +1274,10 @@ PJAX_SELECTOR = '#page' # Testing responsiveness without rate limits. CELERY_DISABLE_RATE_LIMITS = True + +# Specific view modules and methods that we don't want to force login on. +NO_LOGIN_REQUIRED_MODULES = ( + 'django.views.i18n.javascript_catalog', + 'django.contrib.auth.views.password_reset', + 'django.contrib.auth.views.password_reset_done' +)