diff --git a/glance/api/v2/images.py b/glance/api/v2/images.py index 31c079cfa2..509bc65b73 100644 --- a/glance/api/v2/images.py +++ b/glance/api/v2/images.py @@ -273,6 +273,8 @@ class ResponseSerializer(wsgi.JSONResponseSerializer): for key in ['id', 'name', 'created_at', 'updated_at', 'tags', 'size', 'owner', 'checksum', 'status']: _image[key] = image[key] + if CONF.show_image_direct_url and image['location']: + _image['direct_url'] = image['location'] _image['visibility'] = 'public' if image['is_public'] else 'private' _image = self.schema.filter(_image) _image['self'] = self._get_image_href(image) @@ -380,6 +382,10 @@ _BASE_PROPERTIES = { 'maxLength': 255, }, }, + 'direct_url': { + 'type': 'string', + 'description': 'URL to access the image file kept in external store', + }, 'self': {'type': 'string'}, 'access': {'type': 'string'}, 'file': {'type': 'string'}, diff --git a/glance/common/config.py b/glance/common/config.py index 53a9fa6fc9..0ce34a7141 100644 --- a/glance/common/config.py +++ b/glance/common/config.py @@ -47,6 +47,10 @@ common_opts = [ cfg.IntOpt('api_limit_max', default=1000, help=_('Maximum permissible number of items that could be ' 'returned by a request')), + cfg.BoolOpt('show_image_direct_url', default=False, + help=_('Whether to include the backend image storage location ' + 'in image properties. Revealing storage location can be a ' + 'security risk, so use this setting with caution!')), ] CONF = cfg.CONF diff --git a/glance/tests/functional/__init__.py b/glance/tests/functional/__init__.py index ea8be0a3cf..0fc26be216 100644 --- a/glance/tests/functional/__init__.py +++ b/glance/tests/functional/__init__.py @@ -66,6 +66,7 @@ class Server(object): self.server_control = './bin/glance-control' self.exec_env = None self.deployment_flavor = '' + self.show_image_direct_url = False self.server_control_options = '' self.needs_database = False @@ -253,6 +254,7 @@ policy_file = %(policy_file)s policy_default_rule = %(policy_default_rule)s db_auto_create = False sql_connection = %(sql_connection)s +show_image_direct_url = %(show_image_direct_url)s [paste_deploy] flavor = %(deployment_flavor)s """ diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py index 0552658866..098e0afc17 100644 --- a/glance/tests/functional/v2/test_images.py +++ b/glance/tests/functional/v2/test_images.py @@ -535,3 +535,123 @@ class TestImages(functional.FunctionalTest): self.assertEqual(400, response.status_code) self.stop_servers() + + +class TestImageDirectURLVisibility(functional.FunctionalTest): + + def setUp(self): + super(TestImageDirectURLVisibility, self).setUp() + self.cleanup() + self.api_server.deployment_flavor = 'noauth' + + def _url(self, path): + return 'http://127.0.0.1:%d%s' % (self.api_port, path) + + def _headers(self, custom_headers=None): + base_headers = { + 'X-Identity-Status': 'Confirmed', + 'X-Auth-Token': '932c5c84-02ac-4fe5-a9ba-620af0e2bb96', + 'X-User-Id': 'f9a41d13-0c13-47e9-bee2-ce4e8bfe958e', + 'X-Tenant-Id': TENANT1, + 'X-Roles': 'member', + } + base_headers.update(custom_headers or {}) + return base_headers + + def test_image_direct_url_visible(self): + + self.api_server.show_image_direct_url = True + self.start_servers(**self.__dict__.copy()) + + # Image list should be empty + path = self._url('/v2/images') + response = requests.get(path, headers=self._headers()) + self.assertEqual(200, response.status_code) + images = json.loads(response.text)['images'] + self.assertEqual(0, len(images)) + + # Create an image + path = self._url('/v2/images') + headers = self._headers({'content-type': 'application/json'}) + data = json.dumps({'name': 'image-1', 'type': 'kernel', 'foo': 'bar'}) + response = requests.post(path, headers=headers, data=data) + self.assertEqual(200, response.status_code) + + # Get the image id + image = json.loads(response.text) + image_id = image['id'] + + # Image direct_url should not be visible before location is set + path = self._url('/v2/images/%s' % image_id) + headers = self._headers({'Content-Type': 'application/json'}) + response = requests.get(path, headers=headers) + self.assertEqual(200, response.status_code) + image = json.loads(response.text) + self.assertFalse('direct_url' in image) + + # Upload some image data, setting the image location + path = self._url('/v2/images/%s/file' % image_id) + headers = self._headers({'Content-Type': 'application/octet-stream'}) + response = requests.put(path, headers=headers, data='ZZZZZ') + self.assertEqual(200, response.status_code) + + # Image direct_url should be visible + path = self._url('/v2/images/%s' % image_id) + headers = self._headers({'Content-Type': 'application/json'}) + response = requests.get(path, headers=headers) + self.assertEqual(200, response.status_code) + image = json.loads(response.text) + self.assertTrue('direct_url' in image) + + # Image direct_url should be visible in a list + path = self._url('/v2/images') + headers = self._headers({'Content-Type': 'application/json'}) + response = requests.get(path, headers=headers) + self.assertEqual(200, response.status_code) + image = json.loads(response.text)['images'][0] + self.assertTrue('direct_url' in image) + + def test_image_direct_url_not_visible(self): + + self.api_server.show_image_direct_url = False + self.start_servers(**self.__dict__.copy()) + + # Image list should be empty + path = self._url('/v2/images') + response = requests.get(path, headers=self._headers()) + self.assertEqual(200, response.status_code) + images = json.loads(response.text)['images'] + self.assertEqual(0, len(images)) + + # Create an image + path = self._url('/v2/images') + headers = self._headers({'content-type': 'application/json'}) + data = json.dumps({'name': 'image-1', 'type': 'kernel', 'foo': 'bar'}) + response = requests.post(path, headers=headers, data=data) + self.assertEqual(200, response.status_code) + + # Get the image id + image = json.loads(response.text) + image_id = image['id'] + + # Upload some image data, setting the image location + path = self._url('/v2/images/%s/file' % image_id) + headers = self._headers({'Content-Type': 'application/octet-stream'}) + response = requests.put(path, headers=headers, data='ZZZZZ') + self.assertEqual(200, response.status_code) + + # Image direct_url should not be visible + path = self._url('/v2/images/%s' % image_id) + headers = self._headers({'Content-Type': 'application/json'}) + response = requests.get(path, headers=headers) + self.assertEqual(200, response.status_code) + image = json.loads(response.text) + self.assertFalse('direct_url' in image) + + # Image direct_url should not be visible in a list + path = self._url('/v2/images') + headers = self._headers({'Content-Type': 'application/json'}) + response = requests.get(path, headers=headers) + self.assertEqual(200, response.status_code) + image = json.loads(response.text)['images'][0] + self.assertFalse('direct_url' in image) diff --git a/glance/tests/functional/v2/test_schemas.py b/glance/tests/functional/v2/test_schemas.py index 488967cafc..826cd5720d 100644 --- a/glance/tests/functional/v2/test_schemas.py +++ b/glance/tests/functional/v2/test_schemas.py @@ -52,6 +52,7 @@ class TestSchemas(functional.FunctionalTest): 'status', 'access', 'schema', + 'direct_url', ]) self.assertEqual(expected, set(image_schema['properties'].keys())) diff --git a/glance/tests/unit/v2/test_images_resource.py b/glance/tests/unit/v2/test_images_resource.py index 9260f516ea..45d3ec64f3 100644 --- a/glance/tests/unit/v2/test_images_resource.py +++ b/glance/tests/unit/v2/test_images_resource.py @@ -1284,3 +1284,83 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase): response = webob.Response() serializer.show(response, self.fixture) self.assertEqual(expected, json.loads(response.body)) + + +class TestImagesSerializerDirectUrl(test_utils.BaseTestCase): + + def setUp(self): + super(TestImagesSerializerDirectUrl, self).setUp() + self.serializer = glance.api.v2.images.ResponseSerializer() + self.active_image = { + 'id': unit_test_utils.UUID1, + 'name': 'image-1', + 'is_public': True, + 'properties': {}, + 'checksum': None, + 'owner': TENANT1, + 'status': 'active', + 'created_at': DATETIME, + 'updated_at': DATETIME, + 'tags': ['one', 'two'], + 'size': 1024, + 'location': 'http://some/fake/location', + } + self.queued_image = { + 'id': unit_test_utils.UUID2, + 'name': 'image-2', + 'is_public': False, + 'owner': None, + 'status': 'queued', + 'properties': {}, + 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', + 'created_at': DATETIME, + 'updated_at': DATETIME, + 'tags': [], + 'size': None, + 'location': None, + } + self.fixtures = [self.active_image, self.queued_image] + + def _do_index(self): + request = webob.Request.blank('/v2/images') + response = webob.Response(request=request) + self.serializer.index(response, {'images': self.fixtures}) + return json.loads(response.body)['images'] + + def _do_show(self, image): + request = webob.Request.blank('/v2/images') + response = webob.Response(request=request) + self.serializer.show(response, image) + return json.loads(response.body) + + def test_index_store_location_enabled(self): + self.config(show_image_direct_url=True) + images = self._do_index() + + # NOTE(markwash): ordering sanity check + self.assertEqual(images[0]['id'], unit_test_utils.UUID1) + self.assertEqual(images[1]['id'], unit_test_utils.UUID2) + + self.assertEqual(images[0]['direct_url'], 'http://some/fake/location') + self.assertFalse('direct_url' in images[1]) + + def test_index_store_location_explicitly_disabled(self): + self.config(show_image_direct_url=False) + images = self._do_index() + self.assertFalse('direct_url' in images[0]) + self.assertFalse('direct_url' in images[1]) + + def test_show_location_enabled(self): + self.config(show_image_direct_url=True) + image = self._do_show(self.active_image) + self.assertEqual(image['direct_url'], 'http://some/fake/location') + + def test_show_location_enabled_but_not_set(self): + self.config(show_image_direct_url=True) + image = self._do_show(self.queued_image) + self.assertFalse('direct_url' in image) + + def test_show_location_explicitly_disabled(self): + self.config(show_image_direct_url=False) + image = self._do_show(self.active_image) + self.assertFalse('direct_url' in image)