Changes HTTP response code for unsupported methods

Requests for resources with an unsupported HTTP method now return a HTTP
response 405 (Method Not Allowed) or 501 (Not Implemented) rather than a
404 (Not Found) for everything.

For example, attempting to DELETE on /v2/images will now return a 405
instead of a 404 and will provide a response header 'Allow' that lists
the valid methods for the resource.

Attempting to use NON_EXISTENT_METHOD on /v2/images will now return a
501.

Attempting to GET on /v2/non_existent_resource will, as expected, return
a 404.

Fixed for v1 and v2.

Change-Id: I5406f8ee423d3d5e66c56a26a7009b4f438a7e0f
Closes-Bug: #1297362
This commit is contained in:
Kent Wang 2014-07-01 03:34:39 -07:00
parent 0e2bc1d2cf
commit 853b5c9b24
4 changed files with 200 additions and 8 deletions

View File

@ -24,6 +24,8 @@ class API(wsgi.Router):
"""WSGI router for Glance v1 API requests."""
def __init__(self, mapper):
reject_method_resource = wsgi.Resource(wsgi.RejectMethodController())
images_resource = images.create_resource()
mapper.connect("/",
@ -37,10 +39,22 @@ class API(wsgi.Router):
controller=images_resource,
action='create',
conditions={'method': ['POST']})
mapper.connect("/images",
controller=reject_method_resource,
action='reject',
allowed_methods='GET, POST',
conditions={'method': ['PUT', 'DELETE', 'HEAD',
'PATCH']})
mapper.connect("/images/detail",
controller=images_resource,
action='detail',
conditions={'method': ['GET', 'HEAD']})
mapper.connect("/images/detail",
controller=reject_method_resource,
action='reject',
allowed_methods='GET, HEAD',
conditions={'method': ['POST', 'PUT', 'DELETE',
'PATCH']})
mapper.connect("/images/{id}",
controller=images_resource,
action="meta",
@ -57,6 +71,11 @@ class API(wsgi.Router):
controller=images_resource,
action="delete",
conditions=dict(method=["DELETE"]))
mapper.connect("/images/{id}",
controller=reject_method_resource,
action='reject',
allowed_methods='GET, HEAD, PUT, DELETE',
conditions={'method': ['POST', 'PATCH']})
members_resource = members.create_resource()
@ -68,6 +87,12 @@ class API(wsgi.Router):
controller=members_resource,
action="update_all",
conditions=dict(method=["PUT"]))
mapper.connect("/images/{image_id}/members",
controller=reject_method_resource,
action='reject',
allowed_methods='GET, PUT',
conditions={'method': ['POST', 'DELETE', 'HEAD',
'PATCH']})
mapper.connect("/images/{image_id}/members/{id}",
controller=members_resource,
action="show",
@ -80,6 +105,11 @@ class API(wsgi.Router):
controller=members_resource,
action="delete",
conditions={'method': ['DELETE']})
mapper.connect("/images/{image_id}/members/{id}",
controller=reject_method_resource,
action='reject',
allowed_methods='GET, PUT, DELETE',
conditions={'method': ['POST', 'HEAD', 'PATCH']})
mapper.connect("/shared-images/{id}",
controller=members_resource,
action="index_shared_images")

View File

@ -28,32 +28,71 @@ class API(wsgi.Router):
def __init__(self, mapper):
custom_image_properties = images.load_custom_properties()
reject_method_resource = wsgi.Resource(wsgi.RejectMethodController())
schemas_resource = schemas.create_resource(custom_image_properties)
mapper.connect('/schemas/image',
controller=schemas_resource,
action='image',
conditions={'method': ['GET']})
mapper.connect('/schemas/image',
controller=reject_method_resource,
action='reject',
allowed_methods='GET',
conditions={'method': ['POST', 'PUT', 'DELETE',
'PATCH', 'HEAD']})
mapper.connect('/schemas/images',
controller=schemas_resource,
action='images',
conditions={'method': ['GET']})
mapper.connect('/schemas/images',
controller=reject_method_resource,
action='reject',
allowed_methods='GET',
conditions={'method': ['POST', 'PUT', 'DELETE',
'PATCH', 'HEAD']})
mapper.connect('/schemas/member',
controller=schemas_resource,
action='member',
conditions={'method': ['GET']})
mapper.connect('/schemas/member',
controller=reject_method_resource,
action='reject',
allowed_methods='GET',
conditions={'method': ['POST', 'PUT', 'DELETE',
'PATCH', 'HEAD']})
mapper.connect('/schemas/members',
controller=schemas_resource,
action='members',
conditions={'method': ['GET']})
mapper.connect('/schemas/members',
controller=reject_method_resource,
action='reject',
allowed_methods='GET',
conditions={'method': ['POST', 'PUT', 'DELETE',
'PATCH', 'HEAD']})
mapper.connect('/schemas/task',
controller=schemas_resource,
action='task',
conditions={'method': ['GET']})
mapper.connect('/schemas/task',
controller=reject_method_resource,
action='reject',
allowed_methods='GET',
conditions={'method': ['POST', 'PUT', 'DELETE',
'PATCH', 'HEAD']})
mapper.connect('/schemas/tasks',
controller=schemas_resource,
action='tasks',
conditions={'method': ['GET']})
mapper.connect('/schemas/tasks',
controller=reject_method_resource,
action='reject',
allowed_methods='GET',
conditions={'method': ['POST', 'PUT', 'DELETE',
'PATCH', 'HEAD']})
images_resource = images.create_resource(custom_image_properties)
mapper.connect('/images',
@ -64,6 +103,13 @@ class API(wsgi.Router):
controller=images_resource,
action='create',
conditions={'method': ['POST']})
mapper.connect('/images',
controller=reject_method_resource,
action='reject',
allowed_methods='GET, POST',
conditions={'method': ['PUT', 'DELETE', 'PATCH',
'HEAD']})
mapper.connect('/images/{image_id}',
controller=images_resource,
action='update',
@ -76,6 +122,11 @@ class API(wsgi.Router):
controller=images_resource,
action='delete',
conditions={'method': ['DELETE']})
mapper.connect('/images/{image_id}',
controller=reject_method_resource,
action='reject',
allowed_methods='GET, PATCH, DELETE',
conditions={'method': ['POST', 'PUT', 'HEAD']})
image_data_resource = image_data.create_resource()
mapper.connect('/images/{image_id}/file',
@ -86,6 +137,12 @@ class API(wsgi.Router):
controller=image_data_resource,
action='upload',
conditions={'method': ['PUT']})
mapper.connect('/images/{image_id}/file',
controller=reject_method_resource,
action='reject',
allowed_methods='GET, PUT',
conditions={'method': ['POST', 'DELETE', 'PATCH',
'HEAD']})
image_tags_resource = image_tags.create_resource()
mapper.connect('/images/{image_id}/tags/{tag_value}',
@ -96,12 +153,29 @@ class API(wsgi.Router):
controller=image_tags_resource,
action='delete',
conditions={'method': ['DELETE']})
mapper.connect('/images/{image_id}/tags/{tag_value}',
controller=reject_method_resource,
action='reject',
allowed_methods='PUT, DELETE',
conditions={'method': ['GET', 'POST', 'PATCH',
'HEAD']})
image_members_resource = image_members.create_resource()
mapper.connect('/images/{image_id}/members',
controller=image_members_resource,
action='index',
conditions={'method': ['GET']})
mapper.connect('/images/{image_id}/members',
controller=image_members_resource,
action='create',
conditions={'method': ['POST']})
mapper.connect('/images/{image_id}/members',
controller=reject_method_resource,
action='reject',
allowed_methods='GET, POST',
conditions={'method': ['PUT', 'DELETE', 'PATCH',
'HEAD']})
mapper.connect('/images/{image_id}/members/{member_id}',
controller=image_members_resource,
action='show',
@ -110,14 +184,15 @@ class API(wsgi.Router):
controller=image_members_resource,
action='update',
conditions={'method': ['PUT']})
mapper.connect('/images/{image_id}/members',
controller=image_members_resource,
action='create',
conditions={'method': ['POST']})
mapper.connect('/images/{image_id}/members/{member_id}',
controller=image_members_resource,
action='delete',
conditions={'method': ['DELETE']})
mapper.connect('/images/{image_id}/members/{member_id}',
controller=reject_method_resource,
action='reject',
allowed_methods='GET, PUT, DELETE',
conditions={'method': ['POST', 'PATCH', 'HEAD']})
tasks_resource = tasks.create_resource()
mapper.connect('/tasks',
@ -128,6 +203,13 @@ class API(wsgi.Router):
controller=tasks_resource,
action='index',
conditions={'method': ['GET']})
mapper.connect('/tasks',
controller=reject_method_resource,
action='reject',
allowed_methods='GET, POST',
conditions={'method': ['PUT', 'DELETE', 'PATCH',
'HEAD']})
mapper.connect('/tasks/{task_id}',
controller=tasks_resource,
action='get',
@ -136,5 +218,11 @@ class API(wsgi.Router):
controller=tasks_resource,
action='delete',
conditions={'method': ['DELETE']})
mapper.connect('/tasks/{task_id}',
controller=reject_method_resource,
action='reject',
allowed_methods='GET, DELETE',
conditions={'method': ['POST', 'PUT', 'PATCH',
'HEAD']})
super(API, self).__init__(mapper)

View File

@ -99,6 +99,8 @@ profiler_opts = [
]
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.register_opts(bind_opts)
CONF.register_opts(socket_opts)
@ -447,6 +449,14 @@ class APIMapper(routes.Mapper):
return routes.Mapper.routematch(self, url, environ)
class RejectMethodController(object):
def reject(self, req, allowed_methods, *args, **kwargs):
LOG.debug("The method %s is not allowed for this resource" %
req.environ['REQUEST_METHOD'])
raise webob.exc.HTTPMethodNotAllowed(
headers=[('Allow', allowed_methods)])
class Router(object):
"""
WSGI middleware that maps incoming requests to WSGI apps.
@ -489,7 +499,7 @@ class Router(object):
def __call__(self, req):
"""
Route the incoming request to a controller based on self.map.
If no match, return a 404.
If no match, return either a 404(Not Found) or 501(Not Implemented).
"""
return self._router
@ -498,12 +508,17 @@ class Router(object):
def _dispatch(req):
"""
Called by self._router after matching the incoming request to a route
and putting the information into req.environ. Either returns 404
or the routed WSGI app's response.
and putting the information into req.environ. Either returns 404,
501, or the routed WSGI app's response.
"""
match = req.environ['wsgiorg.routing_args'][1]
if not match:
return webob.exc.HTTPNotFound()
implemented_http_methods = ['GET', 'HEAD', 'POST', 'PUT',
'DELETE', 'PATCH']
if req.environ['REQUEST_METHOD'] not in implemented_http_methods:
return webob.exc.HTTPNotImplemented()
else:
return webob.exc.HTTPNotFound()
app = match['controller']
return app

View File

@ -22,9 +22,12 @@ import eventlet.patcher
import fixtures
import gettext
import mock
import routes
import six
import webob
from glance.api.v1 import router as router_v1
from glance.api.v2 import router as router_v2
from glance.common import exception
from glance.common import utils
from glance.common import wsgi
@ -149,6 +152,62 @@ class RequestTest(test_utils.BaseTestCase):
request.headers.pop('Accept-Language')
self.assertIsNone(request.best_match_language())
def test_http_error_response_codes(self):
sample_id, member_id, tag_val, task_id = 'abc', '123', '1', '2'
"""Makes sure v1 unallowed methods return 405"""
unallowed_methods = [
('/images', ['PUT', 'DELETE', 'HEAD', 'PATCH']),
('/images/detail', ['POST', 'PUT', 'DELETE', 'PATCH']),
('/images/%s' % sample_id, ['POST', 'PATCH']),
('/images/%s/members' % sample_id,
['POST', 'DELETE', 'HEAD', 'PATCH']),
('/images/%s/members/%s' % (sample_id, member_id),
['POST', 'HEAD', 'PATCH']),
]
api = test_utils.FakeAuthMiddleware(router_v1.API(routes.Mapper()))
for uri, methods in unallowed_methods:
for method in methods:
req = webob.Request.blank(uri)
req.method = method
res = req.get_response(api)
self.assertEqual(405, res.status_int)
"""Makes sure v2 unallowed methods return 405"""
unallowed_methods = [
('/schemas/image', ['POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']),
('/schemas/images', ['POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']),
('/schemas/member', ['POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']),
('/schemas/members', ['POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']),
('/schemas/task', ['POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']),
('/schemas/tasks', ['POST', 'PUT', 'DELETE', 'PATCH', 'HEAD']),
('/images', ['PUT', 'DELETE', 'PATCH', 'HEAD']),
('/images/%s' % sample_id, ['POST', 'PUT', 'HEAD']),
('/images/%s/file' % sample_id,
['POST', 'DELETE', 'PATCH', 'HEAD']),
('/images/%s/tags/%s' % (sample_id, tag_val),
['GET', 'POST', 'PATCH', 'HEAD']),
('/images/%s/members' % sample_id,
['PUT', 'DELETE', 'PATCH', 'HEAD']),
('/images/%s/members/%s' % (sample_id, member_id),
['POST', 'PATCH', 'HEAD']),
('/tasks', ['PUT', 'DELETE', 'PATCH', 'HEAD']),
('/tasks/%s' % task_id, ['POST', 'PUT', 'PATCH', 'HEAD']),
]
api = test_utils.FakeAuthMiddleware(router_v2.API(routes.Mapper()))
for uri, methods in unallowed_methods:
for method in methods:
req = webob.Request.blank(uri)
req.method = method
res = req.get_response(api)
self.assertEqual(405, res.status_int)
"""Makes sure not implemented methods return 501"""
req = webob.Request.blank('/schemas/image')
req.method = 'NonexistentMethod'
res = req.get_response(api)
self.assertEqual(501, res.status_int)
class ResourceTest(test_utils.BaseTestCase):