Friendly JSON exceptions (bug 928061, bug 928062)

Example http://pastie.org/3338663

Change-Id: I26f53488c062ebfb6e49cfcf82e0b8179a683ea8
This commit is contained in:
Dolph Mathews 2012-02-07 20:46:31 -06:00
parent 524d3d1c41
commit c64a12ffc7
6 changed files with 171 additions and 12 deletions

View File

@ -32,6 +32,7 @@ import webob
import webob.dec import webob.dec
import webob.exc import webob.exc
from keystone import exception
from keystone.common import utils from keystone.common import utils
@ -153,16 +154,13 @@ class Application(BaseApplication):
@webob.dec.wsgify @webob.dec.wsgify
def __call__(self, req): def __call__(self, req):
arg_dict = req.environ['wsgiorg.routing_args'][1] arg_dict = req.environ['wsgiorg.routing_args'][1]
action = arg_dict['action'] action = arg_dict.pop('action')
del arg_dict['action']
del arg_dict['controller'] del arg_dict['controller']
logging.debug('arg_dict: %s', arg_dict) logging.debug('arg_dict: %s', arg_dict)
# allow middleware up the stack to provide context & params
context = req.environ.get('openstack.context', {}) context = req.environ.get('openstack.context', {})
# allow middleware up the stack to override the params params = req.environ.get('openstack.params', {})
params = {}
if 'openstack.params' in req.environ:
params = req.environ['openstack.params']
params.update(arg_dict) params.update(arg_dict)
# TODO(termie): do some basic normalization on methods # TODO(termie): do some basic normalization on methods
@ -170,7 +168,12 @@ class Application(BaseApplication):
# NOTE(vish): make sure we have no unicode keys for py2.6. # NOTE(vish): make sure we have no unicode keys for py2.6.
params = self._normalize_dict(params) params = self._normalize_dict(params)
result = method(context, **params)
try:
result = method(context, **params)
except exception.Error as e:
logging.warning(e)
return render_exception(e)
if result is None or type(result) is str or type(result) is unicode: if result is None or type(result) is str or type(result) is unicode:
return result return result
@ -435,3 +438,22 @@ class ExtensionRouter(Router):
conf.update(local_config) conf.update(local_config)
return cls(app) return cls(app)
return _factory return _factory
def render_exception(error):
"""Forms a WSGI response based on the current error."""
resp = webob.Response()
resp.status = '%s %s' % (error.code, error.title)
resp.headerlist = [('Content-Type', 'application/json')]
body = {
'error': {
'code': error.code,
'title': error.title,
'message': str(error),
}
}
resp.body = json.dumps(body)
return resp

54
keystone/exception.py Normal file
View File

@ -0,0 +1,54 @@
import re
class Error(StandardError):
"""Base error class.
Child classes should define an HTTP status code, title, and a doc string.
"""
code = None
title = None
def __init__(self, message=None, **kwargs):
"""Use the doc string as the error message by default."""
message = message or self.__doc__ % kwargs
super(Error, self).__init__(message)
def __str__(self):
"""Cleans up line breaks and indentation from doc strings."""
string = super(Error, self).__str__()
string = re.sub('[ \n]+', ' ', string)
string = string.strip()
return string
class ValidationError(Error):
"""Expecting to find %(attribute)s in %(target)s.
The server could not comply with the request since it is either malformed
or otherwise incorrect.
The client is assumed to be in error.
"""
code = 400
title = 'Bad Request'
class Unauthorized(Error):
"""The request you have made requires authentication."""
code = 401
title = 'Not Authorized'
class Forbidden(Error):
"""You are not authorized to perform the requested action: %(action)s"""
code = 403
title = 'Not Authorized'
class NotFound(Error):
"""Could not find: %(target)s"""
code = 404
title = 'Not Found'

View File

@ -10,6 +10,7 @@ import webob.exc
from keystone import catalog from keystone import catalog
from keystone import config from keystone import config
from keystone import exception
from keystone import policy from keystone import policy
from keystone import token from keystone import token
from keystone.common import manager from keystone.common import manager
@ -233,7 +234,9 @@ class TenantController(wsgi.Application):
""" """
token_ref = self.token_api.get_token(context=context, token_ref = self.token_api.get_token(context=context,
token_id=context['token_id']) token_id=context['token_id'])
assert token_ref is not None
if token_ref is None:
raise exception.Unauthorized()
user_ref = token_ref['user'] user_ref = token_ref['user']
tenant_ids = self.identity_api.get_tenants_for_user( tenant_ids = self.identity_api.get_tenants_for_user(

View File

@ -10,6 +10,7 @@ import webob.dec
import webob.exc import webob.exc
from keystone import catalog from keystone import catalog
from keystone import exception
from keystone import identity from keystone import identity
from keystone import policy from keystone import policy
from keystone import token from keystone import token
@ -196,6 +197,10 @@ class TokenController(wsgi.Application):
old_token_ref = self.token_api.get_token(context=context, old_token_ref = self.token_api.get_token(context=context,
token_id=token) token_id=token)
if old_token_ref is None:
raise exception.Unauthorized()
user_ref = old_token_ref['user'] user_ref = old_token_ref['user']
tenants = self.identity_api.get_tenants_for_user(context, tenants = self.identity_api.get_tenants_for_user(context,
@ -247,6 +252,10 @@ class TokenController(wsgi.Application):
token_ref = self.token_api.get_token(context=context, token_ref = self.token_api.get_token(context=context,
token_id=token_id) token_id=token_id)
if token_ref is None:
raise exception.NotFound(target='token')
if belongs_to: if belongs_to:
assert token_ref['tenant']['id'] == belongs_to assert token_ref['tenant']['id'] == belongs_to

53
tests/test_exception.py Normal file
View File

@ -0,0 +1,53 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
import uuid
import json
from keystone.common import wsgi
from keystone import exception
from keystone import test
class ExceptionTestCase(test.TestCase):
def setUp(self):
pass
def tearDown(self):
pass
def assertValidJsonRendering(self, e):
resp = wsgi.render_exception(e)
self.assertEqual(resp.status_int, e.code)
self.assertEqual(resp.status, '%s %s' % (e.code, e.title))
j = json.loads(resp.body)
self.assertIsNotNone(j.get('error'))
self.assertIsNotNone(j['error'].get('code'))
self.assertIsNotNone(j['error'].get('title'))
self.assertIsNotNone(j['error'].get('message'))
self.assertNotIn('\n', j['error']['message'])
self.assertNotIn(' ', j['error']['message'])
self.assertTrue(type(j['error']['code']) is int)
def test_validation_error(self):
target = uuid.uuid4().hex
attribute = uuid.uuid4().hex
e = exception.ValidationError(target=target, attribute=attribute)
self.assertValidJsonRendering(e)
self.assertIn(target, str(e))
self.assertIn(attribute, str(e))
def test_unauthorized(self):
e = exception.Unauthorized()
self.assertValidJsonRendering(e)
def test_forbidden(self):
action = uuid.uuid4().hex
e = exception.Forbidden(action=action)
self.assertValidJsonRendering(e)
self.assertIn(action, str(e))
def test_not_found(self):
target = uuid.uuid4().hex
e = exception.NotFound(target=target)
self.assertValidJsonRendering(e)
self.assertIn(target, str(e))

View File

@ -125,6 +125,8 @@ class KeystoneClientTests(object):
self.assertEquals(tenants[0].id, self.tenant_bar['id']) self.assertEquals(tenants[0].id, self.tenant_bar['id'])
def test_authenticate_and_delete_token(self): def test_authenticate_and_delete_token(self):
from keystoneclient import exceptions as client_exceptions
client = self.get_client() client = self.get_client()
token = client.auth_token token = client.auth_token
token_client = self._client(token=token) token_client = self._client(token=token)
@ -133,10 +135,26 @@ class KeystoneClientTests(object):
client.tokens.delete(token_client.auth_token) client.tokens.delete(token_client.auth_token)
# FIXME(dolph): this should raise unauthorized self.assertRaises(client_exceptions.Unauthorized,
# from keystoneclient import exceptions as client_exceptions token_client.tenants.list)
# with self.assertRaises(client_exceptions.Unauthorized):
self.assertRaises(Exception, token_client.tenants.list) def test_authenticate_no_password(self):
from keystoneclient import exceptions as client_exceptions
user_ref = self.user_foo.copy()
user_ref['password'] = None
self.assertRaises(client_exceptions.AuthorizationFailure,
self.get_client,
user_ref)
def test_authenticate_no_username(self):
from keystoneclient import exceptions as client_exceptions
user_ref = self.user_foo.copy()
user_ref['name'] = None
self.assertRaises(client_exceptions.AuthorizationFailure,
self.get_client,
user_ref)
# TODO(termie): I'm not really sure that this is testing much # TODO(termie): I'm not really sure that this is testing much
def test_endpoints(self): def test_endpoints(self):