Improve UX around "unauthorized" API exceptions.

Instead of blindly logging out the user when any API returns
a 401 or 403 response (which in most cases is due to a service
being down or misconfigured) we catch the error and inform the
user that they are not authorized for that data.

This is separate from being unauthorized for a dashboard or
panel in Horizon, since those are within our control and involve
security concerns around exposing admin functionalities to end
users. Those checks function as they have previously.

Fixes bug 1060426.

Change-Id: Ied800f10926ac5fb3b9ac1c1c26bbb4fa94a2557
This commit is contained in:
Gabriel Hurley 2013-03-17 12:14:29 -07:00
parent a53d6491a9
commit e1d12a4e49
4 changed files with 29 additions and 7 deletions

View File

@ -274,15 +274,22 @@ def handle(request, message=None, redirect=None, ignore=False,
if issubclass(exc_type, UNAUTHORIZED):
if ignore:
return NotAuthorized
logout(request)
if not force_silence and not handled:
log_method(error_color("Unauthorized: %s" % exc_value))
if not handled:
if message:
message = _("Unauthorized: %s") % message
# We get some pretty useless error messages back from
# some clients, so let's define our own fallback.
fallback = _("Unauthorized. Please try logging in again.")
messages.error(request, message or fallback, extra_tags="login")
raise NotAuthorized # Redirect handled in middleware
messages.error(request, message or fallback)
# Escalation means logging the user out and raising NotAuthorized
# so the middleware will redirect them appropriately.
if escalate:
logout(request)
raise NotAuthorized
# Otherwise continue and present our "unauthorized" error message.
return NotAuthorized
if issubclass(exc_type, NOT_FOUND):
wrap = True

View File

@ -19,6 +19,9 @@
# under the License.
from django.conf import settings
from django.utils.translation import ugettext as _
from horizon import exceptions
from openstack_dashboard import api
from openstack_dashboard import usage
@ -37,8 +40,12 @@ class GlobalOverview(usage.UsageView):
def get_data(self):
data = super(GlobalOverview, self).get_data()
# Pre-fill tenant names
tenants = api.keystone.tenant_list(self.request,
admin=True)
try:
tenants = api.keystone.tenant_list(self.request, admin=True)
except:
tenants = []
exceptions.handle(self.request,
_('Unable to retrieve project list.'))
for instance in data:
tenant = filter(lambda t: t.id == instance.tenant_id, tenants)
if tenant:

View File

@ -55,18 +55,23 @@ class UsageViewTests(test.TestCase):
self.assertContains(res, 'form-horizontal')
def test_unauthorized(self):
exc = self.exceptions.keystone_unauthorized
exc = self.exceptions.nova_unauthorized
now = timezone.now()
quota_data = self.quota_usages.first()
self.mox.StubOutWithMock(api.nova, 'usage_get')
self.mox.StubOutWithMock(quotas, 'tenant_quota_usages')
api.nova.usage_get(IsA(http.HttpRequest), self.tenant.id,
datetime.datetime(now.year, now.month, 1, 0, 0, 0),
Func(usage.almost_now)) \
.AndRaise(exc)
quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(quota_data)
self.mox.ReplayAll()
url = reverse('horizon:project:overview:index')
res = self.client.get(url)
self.assertRedirects(res, reverse("login") + "?next=" + url)
self.assertTemplateUsed(res, 'project/overview/usage.html')
self.assertMessageCount(res, error=1)
self.assertContains(res, 'Unauthorized:')
def test_usage_csv(self):
now = timezone.now()

View File

@ -54,6 +54,9 @@ def data(TEST):
nova_exception = nova_exceptions.ClientException
TEST.exceptions.nova = create_stubbed_exception(nova_exception)
nova_unauth = nova_exceptions.Unauthorized
TEST.exceptions.nova_unauthorized = create_stubbed_exception(nova_unauth)
glance_exception = glance_exceptions.ClientException
TEST.exceptions.glance = create_stubbed_exception(glance_exception)