diff --git a/glance/api/v2/router.py b/glance/api/v2/router.py index 45dbf5c6aa..2653101d41 100644 --- a/glance/api/v2/router.py +++ b/glance/api/v2/router.py @@ -40,7 +40,8 @@ class API(wsgi.Router): mapper.connect('/schemas/image', controller=schemas_resource, action='image', - conditions={'method': ['GET']}) + conditions={'method': ['GET']}, + body_reject=True) mapper.connect('/schemas/image', controller=reject_method_resource, action='reject', @@ -48,7 +49,8 @@ class API(wsgi.Router): mapper.connect('/schemas/images', controller=schemas_resource, action='images', - conditions={'method': ['GET']}) + conditions={'method': ['GET']}, + body_reject=True) mapper.connect('/schemas/images', controller=reject_method_resource, action='reject', @@ -56,7 +58,8 @@ class API(wsgi.Router): mapper.connect('/schemas/member', controller=schemas_resource, action='member', - conditions={'method': ['GET']}) + conditions={'method': ['GET']}, + body_reject=True) mapper.connect('/schemas/member', controller=reject_method_resource, action='reject', @@ -65,7 +68,8 @@ class API(wsgi.Router): mapper.connect('/schemas/members', controller=schemas_resource, action='members', - conditions={'method': ['GET']}) + conditions={'method': ['GET']}, + body_reject=True) mapper.connect('/schemas/members', controller=reject_method_resource, action='reject', @@ -388,11 +392,13 @@ class API(wsgi.Router): mapper.connect('/images/{image_id}', controller=images_resource, action='show', - conditions={'method': ['GET']}) + conditions={'method': ['GET']}, + body_reject=True) mapper.connect('/images/{image_id}', controller=images_resource, action='delete', - conditions={'method': ['DELETE']}) + conditions={'method': ['DELETE']}, + body_reject=True) mapper.connect('/images/{image_id}', controller=reject_method_resource, action='reject', @@ -402,11 +408,13 @@ class API(wsgi.Router): mapper.connect('/images/{image_id}/actions/deactivate', controller=image_actions_resource, action='deactivate', - conditions={'method': ['POST']}) + conditions={'method': ['POST']}, + body_reject=True) mapper.connect('/images/{image_id}/actions/reactivate', controller=image_actions_resource, action='reactivate', - conditions={'method': ['POST']}) + conditions={'method': ['POST']}, + body_reject=True) mapper.connect('/images/{image_id}/actions/deactivate', controller=reject_method_resource, action='reject', @@ -420,7 +428,8 @@ class API(wsgi.Router): mapper.connect('/images/{image_id}/file', controller=image_data_resource, action='download', - conditions={'method': ['GET']}) + conditions={'method': ['GET']}, + body_reject=True) mapper.connect('/images/{image_id}/file', controller=image_data_resource, action='upload', @@ -434,11 +443,13 @@ class API(wsgi.Router): mapper.connect('/images/{image_id}/tags/{tag_value}', controller=image_tags_resource, action='update', - conditions={'method': ['PUT']}) + conditions={'method': ['PUT']}, + body_reject=True) mapper.connect('/images/{image_id}/tags/{tag_value}', controller=image_tags_resource, action='delete', - conditions={'method': ['DELETE']}) + conditions={'method': ['DELETE']}, + body_reject=True) mapper.connect('/images/{image_id}/tags/{tag_value}', controller=reject_method_resource, action='reject', @@ -448,7 +459,8 @@ class API(wsgi.Router): mapper.connect('/images/{image_id}/members', controller=image_members_resource, action='index', - conditions={'method': ['GET']}) + conditions={'method': ['GET']}, + body_reject=True) mapper.connect('/images/{image_id}/members', controller=image_members_resource, action='create', @@ -461,7 +473,8 @@ class API(wsgi.Router): mapper.connect('/images/{image_id}/members/{member_id}', controller=image_members_resource, action='show', - conditions={'method': ['GET']}) + conditions={'method': ['GET']}, + body_reject=True) mapper.connect('/images/{image_id}/members/{member_id}', controller=image_members_resource, action='update', @@ -469,7 +482,8 @@ class API(wsgi.Router): mapper.connect('/images/{image_id}/members/{member_id}', controller=image_members_resource, action='delete', - conditions={'method': ['DELETE']}) + conditions={'method': ['DELETE']}, + body_reject=True) mapper.connect('/images/{image_id}/members/{member_id}', controller=reject_method_resource, action='reject', diff --git a/glance/common/wsgi.py b/glance/common/wsgi.py index 212cddf558..902522e440 100644 --- a/glance/common/wsgi.py +++ b/glance/common/wsgi.py @@ -38,6 +38,7 @@ from oslo_concurrency import processutils from oslo_config import cfg from oslo_log import log as logging from oslo_serialization import jsonutils +from oslo_utils import strutils import routes import routes.middleware import six @@ -877,8 +878,13 @@ class Resource(object): """WSGI method that controls (de)serialization and method dispatch.""" action_args = self.get_action_args(request.environ) action = action_args.pop('action', None) + body_reject = strutils.bool_from_string( + action_args.pop('body_reject', None)) try: + if body_reject and self.deserializer.has_body(request): + msg = _('A body is not expected with this request.') + raise webob.exc.HTTPBadRequest(explanation=msg) deserialized_request = self.dispatch(self.deserializer, action, request) action_args.update(deserialized_request) diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py index 278010900a..d55c8d15fe 100644 --- a/glance/tests/functional/v2/test_images.py +++ b/glance/tests/functional/v2/test_images.py @@ -638,6 +638,79 @@ class TestImages(functional.FunctionalTest): self.stop_servers() + def test_methods_that_dont_accept_illegal_bodies(self): + # Check images can be reached + self.start_servers(**self.__dict__.copy()) + path = self._url('/v2/images') + response = requests.get(path, headers=self._headers()) + self.assertEqual(200, response.status_code) + + # Test all the schemas + schema_urls = [ + '/v2/schemas/images', + '/v2/schemas/image', + '/v2/schemas/members', + '/v2/schemas/member', + ] + for value in schema_urls: + path = self._url(value) + data = jsonutils.dumps(["body"]) + response = requests.get(path, headers=self._headers(), data=data) + self.assertEqual(400, response.status_code) + + # Create image for use with tests + path = self._url('/v2/images') + headers = self._headers({'content-type': 'application/json'}) + data = jsonutils.dumps({'name': 'image'}) + response = requests.post(path, headers=headers, data=data) + self.assertEqual(201, response.status_code) + image = jsonutils.loads(response.text) + image_id = image['id'] + + test_urls = [ + ('/v2/images/%s', 'get'), + ('/v2/images/%s/actions/deactivate', 'post'), + ('/v2/images/%s/actions/reactivate', 'post'), + ('/v2/images/%s/tags/mytag', 'put'), + ('/v2/images/%s/tags/mytag', 'delete'), + ('/v2/images/%s/members', 'get'), + ('/v2/images/%s/file', 'get'), + ('/v2/images/%s', 'delete'), + ] + + for link, method in test_urls: + path = self._url(link % image_id) + data = jsonutils.dumps(["body"]) + response = getattr(requests, method)( + path, headers=self._headers(), data=data) + self.assertEqual(400, response.status_code) + + # DELETE /images/imgid without legal json + path = self._url('/v2/images/%s' % image_id) + data = '{"hello"]' + response = requests.delete(path, headers=self._headers(), data=data) + self.assertEqual(400, response.status_code) + + # POST /images/imgid/members + path = self._url('/v2/images/%s/members' % image_id) + data = jsonutils.dumps({'member': TENANT3}) + response = requests.post(path, headers=self._headers(), data=data) + self.assertEqual(200, response.status_code) + + # GET /images/imgid/members/memid + path = self._url('/v2/images/%s/members/%s' % (image_id, TENANT3)) + data = jsonutils.dumps(["body"]) + response = requests.get(path, headers=self._headers(), data=data) + self.assertEqual(400, response.status_code) + + # DELETE /images/imgid/members/memid + path = self._url('/v2/images/%s/members/%s' % (image_id, TENANT3)) + data = jsonutils.dumps(["body"]) + response = requests.delete(path, headers=self._headers(), data=data) + self.assertEqual(400, response.status_code) + + self.stop_servers() + def test_download_random_access(self): self.start_servers(**self.__dict__.copy()) # Create another image (with two deployer-defined properties)