Prevents unauth'd view calls from popping up login in modal window.

Fixes bug 929309

Note: The authentication logic is slightly changed -
login_required decorator is replaced by a Horizon decorator,
that raises NotAuthorized exception instead redirecting
to login page. Then, all unauthorized requests are now
handled by Horizon Middleware, and performs a check if
the request is from ajax call(then returns error 401),
otherwise - redirects to login page, persisting the
requested page in ?next= variable.

Change-Id: Ic90658bff2eabfe630b1f9912cf4a5aa45edf58e
This commit is contained in:
Tihomir Trifonov
2012-02-27 20:45:57 +02:00
committed by Gabriel Hurley
parent 7ab394829f
commit e040ad12a0
9 changed files with 128 additions and 14 deletions

View File

@@ -28,7 +28,6 @@ import logging
from django.conf import settings
from django.conf.urls.defaults import patterns, url, include
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
from django.utils.functional import SimpleLazyObject
@@ -36,8 +35,8 @@ from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
from django.utils.translation import ugettext as _
from horizon.decorators import (require_roles, require_services,
_current_component)
from horizon.decorators import (require_auth, require_roles,
require_services, _current_component)
LOG = logging.getLogger(__name__)
@@ -421,7 +420,7 @@ class Dashboard(Registry, HorizonComponent):
# Require login if not public.
if not self.public:
_decorate_urlconf(urlpatterns, login_required)
_decorate_urlconf(urlpatterns, require_auth)
# Apply access controls to all views in the patterns
roles = getattr(self, 'roles', [])
services = getattr(self, 'services', [])

View File

@@ -24,6 +24,7 @@ General-purpose decorators for use with Horizon.
import functools
from django.utils.decorators import available_attrs
from django.utils.translation import ugettext as _
from horizon.exceptions import NotAuthorized, NotFound
@@ -40,6 +41,25 @@ def _current_component(view_func, dashboard=None, panel=None):
return dec
def require_auth(view_func):
""" Performs user authentication check.
Similar to Django's `login_required` decorator, except that this
throws NotAuthorized exception if the user is not signed-in.
Raises a :exc:`~horizon.exceptions.NotAuthorized` exception if the
user is not authenticated.
"""
@functools.wraps(view_func, assigned=available_attrs(view_func))
def dec(request, *args, **kwargs):
if request.user.is_authenticated():
return view_func(request, *args, **kwargs)
raise NotAuthorized(_("You are not authorized to access %s")
% request.path)
return dec
def require_roles(view_func, required):
""" Enforces role-based access controls.
@@ -69,7 +89,7 @@ def require_roles(view_func, required):
# set operator <= tests that all members of set 1 are in set 2
if view_func._required_roles <= set(roles):
return view_func(request, *args, **kwargs)
raise NotAuthorized("You are not authorized to access %s"
raise NotAuthorized(_("You are not authorized to access %s")
% request.path)
# If we don't have any roles, just return the original view.
@@ -110,7 +130,7 @@ def require_services(view_func, required):
# set operator <= tests that all members of set 1 are in set 2
if view_func._required_services <= set(services):
return view_func(request, *args, **kwargs)
raise NotFound("The services for this view are not available.")
raise NotFound(_("The services for this view are not available."))
# If we don't have any services, just return the original view.
if required:

View File

@@ -25,8 +25,12 @@ import logging
from django import http
from django import shortcuts
from django.core.urlresolvers import reverse
from django.contrib import messages
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.utils.translation import ugettext as _
from django.utils.encoding import iri_to_uri
from horizon import api
from horizon import exceptions
@@ -71,8 +75,19 @@ class HorizonMiddleware(object):
NotFound and Http302 and handles them gracefully.
"""
if isinstance(exception, exceptions.NotAuthorized):
auth_url = reverse("horizon:auth_login")
next_url = iri_to_uri(request.get_full_path())
if next_url != auth_url:
param = "?%s=%s" % (REDIRECT_FIELD_NAME, next_url)
redirect_to = "".join((auth_url, param))
else:
redirect_to = auth_url
messages.error(request, unicode(exception))
return shortcuts.redirect('/auth/login')
if request.is_ajax():
response_401 = http.HttpResponse(status=401)
response_401["REDIRECT_URL"] = redirect_to
return response_401
return shortcuts.redirect(redirect_to)
# If an internal "NotFound" error gets this far, return a real 404.
if isinstance(exception, exceptions.NotFound):

View File

@@ -1,7 +1,7 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-header %}Log In{% endblock %}
{% block modal-header %}{% trans "Log In" %}{% endblock %}
{% block modal_class %}modal login{% endblock %}
{% block form_action %}{% url horizon:auth_login %}{% endblock %}
@@ -9,10 +9,11 @@
{% block modal-body %}
{% include "horizon/_messages.html" %}
<fieldset>
{% if next %}<input type="hidden" name="{{ redirect_field_name }}" value="{{ next }}" />{% endif %}
{% include "horizon/common/_form_fields.html" %}
</fieldset>
{% endblock %}
{% block modal-footer %}
<input type="submit" class="btn btn-primary pull-right" value="{% trans "Login" %}" />
<input type="submit" class="btn btn-primary pull-right" value="{% trans "Log In" %}" />
{% endblock %}

View File

@@ -119,6 +119,7 @@ class TestCase(django_test.TestCase):
user=username,
tenant_id=tenant_id,
service_catalog=service_catalog,
roles=roles,
authorized_tenants=authorized_tenants)
def override_times(self):

View File

@@ -21,10 +21,13 @@
from django.conf import settings
from django.core import urlresolvers
from django import http
from django.test.client import Client
from django.utils.importlib import import_module
from mox import IsA
import horizon
from horizon import api
from horizon import base
from horizon import test
from horizon import users
@@ -203,7 +206,16 @@ class HorizonTests(BaseHorizonTests):
client = Client()
client.logout()
resp = client.get(url)
self.assertRedirectsNoFollow(resp, '/accounts/login/?next=/settings/')
redirect_url = "?".join([urlresolvers.reverse("horizon:auth_login"),
"next=%s" % url])
self.assertRedirectsNoFollow(resp, redirect_url)
# Simulate ajax call
resp = client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
# Response should be HTTP 401 with redirect header
self.assertEquals(resp.status_code, 401)
self.assertEquals(resp["REDIRECT_URL"],
"?".join([urlresolvers.reverse("horizon:auth_login"),
"next=%s" % url]))
def test_required_services(self):
horizon.register(MyDash)
@@ -228,3 +240,46 @@ class HorizonTests(BaseHorizonTests):
authorized_tenants=tenants)
resp = self.client.get(panel.get_absolute_url())
self.assertEqual(resp.status_code, 404)
def test_required_roles(self):
syspanel = horizon.get_dashboard("syspanel")
user_panel = syspanel.get_panel("users")
# Non-admin user
self.setActiveUser(token=self.token.id,
username=self.user.name,
tenant_id=self.tenant.id,
roles=[])
resp = self.client.get(user_panel.get_absolute_url())
self.assertEqual(resp.status_code, 302)
resp = self.client.get(user_panel.get_absolute_url(),
follow=False,
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(resp.status_code, 401)
self.mox.StubOutWithMock(api, 'flavor_list')
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
api.flavor_list(IsA(http.HttpRequest)).AndReturn(self.flavors.list())
self.mox.ReplayAll()
# Set roles for admin user
self.setActiveUser(token=self.token.id,
username=self.user.name,
tenant_id=self.tenant.id,
service_catalog=self.request.user.service_catalog,
roles=[{'name': 'admin'}])
resp = self.client.get(
urlresolvers.reverse('horizon:syspanel:flavors:create'))
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp, "syspanel/flavors/create.html")
# Test modal form
resp = self.client.get(
urlresolvers.reverse('horizon:syspanel:flavors:create'),
follow=False,
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp, "syspanel/flavors/_create.html")

View File

@@ -22,6 +22,7 @@ import logging
from django import shortcuts
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.utils.translation import ugettext as _
import horizon
@@ -49,6 +50,13 @@ class LoginView(forms.ModalFormView):
form_class = Login
template_name = "horizon/auth/login.html"
def get_context_data(self, **kwargs):
context = super(LoginView, self).get_context_data(**kwargs)
redirect_to = self.request.REQUEST.get(REDIRECT_FIELD_NAME, "")
context["redirect_field_name"] = REDIRECT_FIELD_NAME
context["next"] = redirect_to
return context
def get_initial(self):
initial = super(LoginView, self).get_initial()
current_region = self.request.session.get('region_endpoint', None)

View File

@@ -27,6 +27,7 @@ import logging
from django import shortcuts
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.utils.translation import ugettext as _
from keystoneclient import exceptions as keystone_exceptions
@@ -83,6 +84,8 @@ class Login(forms.SelfHandlingForm):
request.session['region_endpoint'] = endpoint
request.session['region_name'] = region_name
redirect_to = request.REQUEST.get(REDIRECT_FIELD_NAME, "")
if data.get('tenant', None):
try:
token = api.token_create(request,
@@ -100,7 +103,8 @@ class Login(forms.SelfHandlingForm):
tenant = t
_set_session_data(request, token)
user = users.get_user_from_request(request)
return shortcuts.redirect(base.Horizon.get_user_home(user))
redirect = redirect_to or base.Horizon.get_user_home(user)
return shortcuts.redirect(redirect)
elif data.get('username', None):
try:
@@ -157,7 +161,8 @@ class Login(forms.SelfHandlingForm):
_set_session_data(request, token)
user = users.get_user_from_request(request)
return shortcuts.redirect(base.Horizon.get_user_home(user))
redirect = redirect_to or base.Horizon.get_user_home(user)
return shortcuts.redirect(redirect)
class LoginWithTenant(Login):

View File

@@ -7,8 +7,18 @@ horizon.addInitFunction(function() {
$('.ajax-modal').click(function (evt) {
var $this = $(this);
$.ajax($this.attr('href'), {
complete: function (jqXHR, status) {
$('body').append(jqXHR.responseText);
error: function(jqXHR, status, errorThrown){
if (jqXHR.status === 401){
var redir_url = jqXHR.getResponseHeader("REDIRECT_URL");
if (redir_url){
location.href = redir_url;
} else {
location.reload(true);
}
}
},
success: function (data, status, jqXHR) {
$('body').append(data);
$('.modal span.help-block').hide();
$('.modal:last').modal();
$('.modal:last').on('hidden', function () {