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:
committed by
Gabriel Hurley
parent
7ab394829f
commit
e040ad12a0
@@ -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', [])
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user