Package: release.debian.org Severity: normal User: [email protected] Usertags: unblock
A great Christmas to all the release team! The last upload of Glance fixes a nasty bug where users of Glance basically have access to all of the filesystem of the Glance server. Please unblock glance/2014.1.3-6. Cheers, Thomas Goirand (zigo)
diff -Nru glance-2014.1.3/debian/changelog glance-2014.1.3/debian/changelog --- glance-2014.1.3/debian/changelog 2014-10-08 17:02:24.000000000 +0000 +++ glance-2014.1.3/debian/changelog 2014-12-25 09:29:14.000000000 +0000 @@ -1,3 +1,12 @@ +glance (2014.1.3-6) unstable; urgency=high + + * Added restrict_client_download_and_delete_files_in_glance-api_juno.patch + from upstream (Closes: #773836). + * Build-depends on openstack-pkg-tools (>= 20~) to ensure we have the + systemd fixes. + + -- Thomas Goirand <[email protected]> Thu, 25 Dec 2014 17:28:05 +0800 + glance (2014.1.3-5) unstable; urgency=medium * Fixed glance-api and glance-registry config file path. diff -Nru glance-2014.1.3/debian/control glance-2014.1.3/debian/control --- glance-2014.1.3/debian/control 2014-10-08 17:02:24.000000000 +0000 +++ glance-2014.1.3/debian/control 2014-12-25 09:29:14.000000000 +0000 @@ -5,7 +5,7 @@ Uploaders: Thomas Goirand <[email protected]> Build-Depends: debhelper (>= 9), dh-systemd, - openstack-pkg-tools (>= 14~), + openstack-pkg-tools (>= 20~), po-debconf, python-all (>= 2.6.6-3~), python-pbr (>= 0.6), diff -Nru glance-2014.1.3/debian/patches/restrict_client_download_and_delete_files_in_glance-api.patch glance-2014.1.3/debian/patches/restrict_client_download_and_delete_files_in_glance-api.patch --- glance-2014.1.3/debian/patches/restrict_client_download_and_delete_files_in_glance-api.patch 1970-01-01 00:00:00.000000000 +0000 +++ glance-2014.1.3/debian/patches/restrict_client_download_and_delete_files_in_glance-api.patch 2014-12-25 09:29:14.000000000 +0000 @@ -0,0 +1,611 @@ +Subject: To prevent client use v2 patch api to handle file and swift location + The change will be used to restrict client to download and delete any file in + glance-api server. The same resone and logic as what we did in v1: + https://github.com/openstack/glance/blob/master/glance/api/v1/images.py#L429 +Author: Zhi Yan Liu <[email protected]> +Date: Mon, 15 Dec 2014 04:29:55 +0000 (+0800) +X-Git-Url: https://review.openstack.org/gitweb?p=openstack%2Fglance.git;a=commitdiff_plain;h=8bdb7ed9f5beaf816e7abba726904646bf3680dd +Bug-Ubuntu: https://bugs.launchpad.net/glance/+bug/1400966 +Bug-Debian: https://bugs.debian.org/773836 +Change-Id: I72dbead3cb2dcb87f52658ddb880e26880cc229b +Origin: upstream, https://review.openstack.org/#/c/142788/ +Last-Update: 2014-12-25 + +diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py +index dd6ec06..9aff3f1 100644 +--- a/glance/api/v1/images.py ++++ b/glance/api/v1/images.py +@@ -21,7 +21,6 @@ import copy + + import eventlet + from oslo.config import cfg +-import six.moves.urllib.parse as urlparse + from webob.exc import HTTPBadRequest + from webob.exc import HTTPConflict + from webob.exc import HTTPForbidden +@@ -48,6 +47,7 @@ from glance.store import get_known_schemes + from glance.store import get_size_from_backend + from glance.store import get_store_from_location + from glance.store import get_store_from_scheme ++from glance.store import validate_external_location + + LOG = logging.getLogger(__name__) + SUPPORTED_PARAMS = glance.api.v1.SUPPORTED_PARAMS +@@ -404,23 +404,19 @@ class Controller(controller.BaseController): + @staticmethod + def _validate_source(source, req): + """ +- External sources (as specified via the location or copy-from headers) +- are supported only over non-local store types, i.e. S3, Swift, HTTP. +- Note the absence of file:// for security reasons, see LP bug #942118. +- If the above constraint is violated, we reject with 400 "Bad Request". ++ To validate if external sources (as specified via the location ++ or copy-from headers) are supported. Otherwise we reject ++ with 400 "Bad Request". + """ + if source: +- pieces = urlparse.urlparse(source) +- schemes = [scheme for scheme in get_known_schemes() +- if scheme != 'file'] +- for scheme in schemes: +- if pieces.scheme == scheme: +- return source +- msg = _("External sourcing not supported for store %s") % source +- LOG.debug(msg) +- raise HTTPBadRequest(explanation=msg, +- request=req, +- content_type="text/plain") ++ if validate_external_location(source): ++ return source ++ else: ++ msg = _("External source are not supported: '%s'") % source ++ LOG.debug(msg) ++ raise HTTPBadRequest(explanation=msg, ++ request=req, ++ content_type="text/plain") + + @staticmethod + def _copy_from(req): +diff --git a/glance/store/__init__.py b/glance/store/__init__.py +index 273b7c7..344311b 100644 +--- a/glance/store/__init__.py ++++ b/glance/store/__init__.py +@@ -19,6 +19,7 @@ import sys + + from oslo.config import cfg + import six ++import six.moves.urllib.parse as urlparse + + from glance.common import exception + from glance.common import utils +@@ -421,6 +422,24 @@ def set_acls(context, location_uri, public=False, read_tenants=[], + LOG.debug(_("Skipping store.set_acls... not implemented.")) + + ++def validate_external_location(uri): ++ """ ++ Validate if URI of external location are supported. ++ ++ Only over non-local store types are OK, i.e. S3, Swift, ++ HTTP. Note the absence of 'file://' for security reasons, ++ see LP bug #942118, 1400966, 'swift+config://' is also ++ absent for security reasons, see LP bug #1334196. ++ ++ :param uri: The URI of external image location. ++ :return: Whether given URI of external image location are OK. ++ """ ++ pieces = urlparse.urlparse(uri) ++ valid_schemes = [scheme for scheme in get_known_schemes() ++ if scheme != 'file' and scheme != 'swift+config'] ++ return pieces.scheme in valid_schemes ++ ++ + class ImageRepoProxy(glance.domain.proxy.Repo): + + def __init__(self, image_repo, context, store_api): +@@ -453,22 +472,23 @@ class ImageRepoProxy(glance.domain.proxy.Repo): + + + def _check_location_uri(context, store_api, uri): +- """ +- Check if an image location uri is valid. ++ """Check if an image location is valid. + + :param context: Glance request context + :param store_api: store API module + :param uri: location's uri string + """ ++ + is_ok = True + try: +- size = store_api.get_size_from_backend(context, uri) + # NOTE(zhiyan): Some stores return zero when it catch exception +- is_ok = size > 0 ++ is_ok = (store_api.validate_external_location(uri) and ++ store_api.get_size_from_backend(context, uri) > 0) + except (exception.UnknownScheme, exception.NotFound): + is_ok = False + if not is_ok: +- raise exception.BadStoreUri(_('Invalid location: %s') % uri) ++ reason = _('Invalid location') ++ raise exception.BadStoreUri(message=reason) + + + def _check_image_location(context, store_api, location): +diff --git a/glance/tests/functional/v1/test_copy_to_file.py b/glance/tests/functional/v1/test_copy_to_file.py +index ae2c320..2c5d833 100644 +--- a/glance/tests/functional/v1/test_copy_to_file.py ++++ b/glance/tests/functional/v1/test_copy_to_file.py +@@ -248,9 +248,35 @@ class TestCopyToFile(functional.FunctionalTest): + path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port) + http = httplib2.Http() + response, content = http.request(path, 'POST', headers=headers) +- self.assertEqual(response.status, 400, content) ++ self.assertEqual(400, response.status, content) + +- expected = 'External sourcing not supported for store ' + copy_from ++ expected = 'External source are not supported: \'%s\'' % copy_from ++ msg = 'expected "%s" in "%s"' % (expected, content) ++ self.assertTrue(expected in content, msg) ++ ++ self.stop_servers() ++ ++ @skip_if_disabled ++ def test_copy_from_swift_config(self): ++ """ ++ Ensure we can't copy from swift+config ++ """ ++ self.cleanup() ++ ++ self.start_servers(**self.__dict__.copy()) ++ ++ # POST /images with public image copied from file (to file) ++ headers = {'X-Image-Meta-Name': 'copied', ++ 'X-Image-Meta-disk_format': 'raw', ++ 'X-Image-Meta-container_format': 'ovf', ++ 'X-Image-Meta-Is-Public': 'True', ++ 'X-Glance-API-Copy-From': 'swift+config://xxx'} ++ path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port) ++ http = httplib2.Http() ++ response, content = http.request(path, 'POST', headers=headers) ++ self.assertEqual(400, response.status, content) ++ ++ expected = 'External source are not supported: \'swift+config://xxx\'' + msg = 'expected "%s" in "%s"' % (expected, content) + self.assertTrue(expected in content, msg) + +diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py +index 4247434..ef3f944 100644 +--- a/glance/tests/functional/v2/test_images.py ++++ b/glance/tests/functional/v2/test_images.py +@@ -15,7 +15,6 @@ + + import os + import signal +-import tempfile + import uuid + + import requests +@@ -38,6 +37,19 @@ class TestImages(functional.FunctionalTest): + self.cleanup() + self.api_server.deployment_flavor = 'noauth' + self.start_servers(**self.__dict__.copy()) ++ for i in range(3): ++ ret = test_http.http_server("foo_image_id%d" % i, ++ "foo_image%d" % i) ++ setattr(self, 'http_server%d_pid' % i, ret[0]) ++ setattr(self, 'http_port%d' % i, ret[1]) ++ ++ def tearDown(self): ++ for i in range(3): ++ pid = getattr(self, 'http_server%d_pid' % i, None) ++ if pid: ++ os.kill(pid, signal.SIGKILL) ++ ++ super(TestImages, self).tearDown() + + def _url(self, path): + return 'http://127.0.0.1:%d%s' % (self.api_port, path) +@@ -282,21 +294,15 @@ class TestImages(functional.FunctionalTest): + self.assertEqual(413, response.status_code, response.text) + + # Adding 3 image locations should fail since configured limit is 2 +- for i in range(3): +- file_path = os.path.join(self.test_dir, 'fake_image_%i' % i) +- with open(file_path, 'w') as fap: +- fap.write('glance') +- + path = self._url('/v2/images/%s' % image_id) + media_type = 'application/openstack-images-v2.1-json-patch' + headers = self._headers({'content-type': media_type}) + changes = [] + for i in range(3): ++ url = ('http://127.0.0.1:%s/foo_image' % ++ getattr(self, 'http_port%d' % i)) + changes.append({'op': 'add', 'path': '/locations/-', +- 'value': {'url': 'file://{0}'.format( +- os.path.join(self.test_dir, +- 'fake_image_%i' % i)), +- 'metadata': {}}, ++ 'value': {'url': url, 'metadata': {}}, + }) + + data = jsonutils.dumps(changes) +@@ -1811,17 +1817,14 @@ class TestImages(functional.FunctionalTest): + self.assertNotIn('size', image) + self.assertNotIn('virtual_size', image) + +- file_path = os.path.join(self.test_dir, 'fake_image') +- with open(file_path, 'w') as fap: +- fap.write('glance') +- + # Update locations for the queued image + path = self._url('/v2/images/%s' % image_id) + media_type = 'application/openstack-images-v2.1-json-patch' + headers = self._headers({'content-type': media_type}) ++ url = 'http://127.0.0.1:%s/foo_image' % self.http_port0 + data = jsonutils.dumps([{'op': 'replace', 'path': '/locations', +- 'value': [{'url': 'file://' + file_path, +- 'metadata': {}}]}]) ++ 'value': [{'url': url, 'metadata': {}}] ++ }]) + response = requests.patch(path, headers=headers, data=data) + self.assertEqual(200, response.status_code, response.text) + +@@ -1830,7 +1833,42 @@ class TestImages(functional.FunctionalTest): + response = requests.get(path, headers=headers) + self.assertEqual(200, response.status_code) + image = jsonutils.loads(response.text) +- self.assertEqual(image['size'], 6) ++ self.assertEqual(10, image['size']) ++ ++ def test_update_locations_with_restricted_sources(self): ++ self.start_servers(**self.__dict__.copy()) ++ # Create an image ++ path = self._url('/v2/images') ++ headers = self._headers({'content-type': 'application/json'}) ++ data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', ++ 'container_format': 'aki'}) ++ response = requests.post(path, headers=headers, data=data) ++ self.assertEqual(201, response.status_code) ++ ++ # Returned image entity should have a generated id and status ++ image = jsonutils.loads(response.text) ++ image_id = image['id'] ++ self.assertEqual('queued', image['status']) ++ self.assertNotIn('size', image) ++ self.assertNotIn('virtual_size', image) ++ ++ # Update locations for the queued image ++ path = self._url('/v2/images/%s' % image_id) ++ media_type = 'application/openstack-images-v2.1-json-patch' ++ headers = self._headers({'content-type': media_type}) ++ data = jsonutils.dumps([{'op': 'replace', 'path': '/locations', ++ 'value': [{'url': 'file:///foo_image', ++ 'metadata': {}}] ++ }]) ++ response = requests.patch(path, headers=headers, data=data) ++ self.assertEqual(400, response.status_code, response.text) ++ ++ data = jsonutils.dumps([{'op': 'replace', 'path': '/locations', ++ 'value': [{'url': 'swift+config:///foo_image', ++ 'metadata': {}}] ++ }]) ++ response = requests.patch(path, headers=headers, data=data) ++ self.assertEqual(400, response.status_code, response.text) + + + class TestImageDirectURLVisibility(functional.FunctionalTest): +@@ -2040,16 +2078,17 @@ class TestImageLocationSelectionStrategy(functional.FunctionalTest): + super(TestImageLocationSelectionStrategy, self).setUp() + self.cleanup() + self.api_server.deployment_flavor = 'noauth' +- self.foo_image_file = tempfile.NamedTemporaryFile() +- self.foo_image_file.write("foo image file") +- self.foo_image_file.flush() +- self.addCleanup(self.foo_image_file.close) +- ret = test_http.http_server("foo_image_id", "foo_image") +- self.http_server_pid, self.http_port = ret ++ for i in range(3): ++ ret = test_http.http_server("foo_image_id%d" % i, ++ "foo_image%d" % i) ++ setattr(self, 'http_server%d_pid' % i, ret[0]) ++ setattr(self, 'http_port%d' % i, ret[1]) + + def tearDown(self): +- if self.http_server_pid is not None: +- os.kill(self.http_server_pid, signal.SIGKILL) ++ for i in range(3): ++ pid = getattr(self, 'http_server%d_pid' % i, None) ++ if pid: ++ os.kill(pid, signal.SIGKILL) + + super(TestImageLocationSelectionStrategy, self).tearDown() + +@@ -2098,14 +2137,14 @@ class TestImageLocationSelectionStrategy(functional.FunctionalTest): + self.assertTrue('locations' in image) + self.assertTrue(image["locations"] == []) + +- # Update image locations via PATCH ++ # Update image locations via PATCH + path = self._url('/v2/images/%s' % image_id) + media_type = 'application/openstack-images-v2.1-json-patch' + headers = self._headers({'content-type': media_type}) +- values = [{'url': 'file://%s' % self.foo_image_file.name, +- 'metadata': {'idx': '1'}}, +- {'url': 'http://127.0.0.1:%s/foo_image' % self.http_port, +- 'metadata': {'idx': '0'}}] ++ values = [{'url': 'http://127.0.0.1:%s/foo_image' % self.http_port0, ++ 'metadata': {}}, ++ {'url': 'http://127.0.0.1:%s/foo_image' % self.http_port1, ++ 'metadata': {}}] + doc = [{'op': 'replace', + 'path': '/locations', + 'value': values}] +@@ -2126,67 +2165,6 @@ class TestImageLocationSelectionStrategy(functional.FunctionalTest): + + self.stop_servers() + +- def test_image_locatons_with_store_type_strategy(self): +- self.api_server.show_image_direct_url = True +- self.api_server.show_multiple_locations = True +- self.image_location_quota = 10 +- self.api_server.location_strategy = 'store_type' +- preference = "http, swift, filesystem" +- self.api_server.store_type_location_strategy_preference = preference +- self.start_servers(**self.__dict__.copy()) +- +- # Create an image +- path = self._url('/v2/images') +- headers = self._headers({'content-type': 'application/json'}) +- data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', +- 'foo': 'bar', 'disk_format': 'aki', +- 'container_format': 'aki'}) +- response = requests.post(path, headers=headers, data=data) +- self.assertEqual(201, response.status_code) +- +- # Get the image id +- image = jsonutils.loads(response.text) +- image_id = image['id'] +- +- # Image locations 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 = jsonutils.loads(response.text) +- self.assertTrue('locations' in image) +- self.assertTrue(image["locations"] == []) +- +- # Update image locations via PATCH +- path = self._url('/v2/images/%s' % image_id) +- media_type = 'application/openstack-images-v2.1-json-patch' +- headers = self._headers({'content-type': media_type}) +- values = [{'url': 'file://%s' % self.foo_image_file.name, +- 'metadata': {'idx': '1'}}, +- {'url': 'http://127.0.0.1:%s/foo_image' % self.http_port, +- 'metadata': {'idx': '0'}}] +- doc = [{'op': 'replace', +- 'path': '/locations', +- 'value': values}] +- data = jsonutils.dumps(doc) +- response = requests.patch(path, headers=headers, data=data) +- self.assertEqual(200, response.status_code) +- +- values.sort(key=lambda loc: int(loc['metadata']['idx'])) +- +- # Image locations 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 = jsonutils.loads(response.text) +- self.assertTrue('locations' in image) +- self.assertEqual(image['locations'], values) +- self.assertTrue('direct_url' in image) +- self.assertEqual(image['direct_url'], values[0]['url']) +- +- self.stop_servers() +- + + class TestImageMembers(functional.FunctionalTest): + +diff --git a/glance/tests/unit/test_store_image.py b/glance/tests/unit/test_store_image.py +index 424915b..9d6cc4e 100644 +--- a/glance/tests/unit/test_store_image.py ++++ b/glance/tests/unit/test_store_image.py +@@ -16,6 +16,7 @@ import mox + + from glance.common import exception + import glance.store ++from glance.tests.unit import base as unit_test_base + from glance.tests.unit import utils as unit_test_utils + from glance.tests import utils + +@@ -731,7 +732,7 @@ class TestStoreImageRepo(utils.BaseTestCase): + self.assertEqual(acls['read'], [TENANT2]) + + +-class TestImageFactory(utils.BaseTestCase): ++class TestImageFactory(unit_test_base.StoreClearingUnitTest): + + def setUp(self): + super(TestImageFactory, self).setUp() +diff --git a/glance/tests/unit/test_store_location.py b/glance/tests/unit/test_store_location.py +index df8d5d7..eac5590 100644 +--- a/glance/tests/unit/test_store_location.py ++++ b/glance/tests/unit/test_store_location.py +@@ -24,6 +24,7 @@ import glance.store.s3 + import glance.store.swift + import glance.store.vmware_datastore + from glance.tests.unit import base ++from glance.tests.unit import utils + + + class TestStoreLocation(base.StoreClearingUnitTest): +@@ -488,11 +489,14 @@ class TestStoreLocation(base.StoreClearingUnitTest): + ctx, + store) + ++ class FakeImageProxy(object): ++ size = None ++ context = None ++ ++ def __init__(self, store_api): ++ self.store_api = store_api ++ + def test_add_location_for_image_without_size(self): +- class FakeImageProxy(): +- size = None +- context = None +- store_api = mock.Mock() + + def fake_get_size_from_backend(context, uri): + return 1 +@@ -504,14 +508,31 @@ class TestStoreLocation(base.StoreClearingUnitTest): + loc2 = {'url': 'file:///fake2.img.tar.gz', 'metadata': {}} + + # Test for insert location +- image1 = FakeImageProxy() ++ image1 = TestStoreLocation.FakeImageProxy(mock.Mock()) + locations = glance.store.StoreLocations(image1, []) + locations.insert(0, loc2) + self.assertEqual(image1.size, 1) + + # Test for set_attr of _locations_proxy +- image2 = FakeImageProxy() ++ image2 = TestStoreLocation.FakeImageProxy(mock.Mock()) + locations = glance.store.StoreLocations(image2, [loc1]) + locations[0] = loc2 + self.assertTrue(loc2 in locations) + self.assertEqual(image2.size, 1) ++ ++ def test_add_location_with_restricted_sources(self): ++ ++ loc1 = {'url': 'file:///fake1.img.tar.gz', 'metadata': {}} ++ loc2 = {'url': 'swift+config:///xxx', 'metadata': {}} ++ ++ # Test for insert location ++ image1 = TestStoreLocation.FakeImageProxy(utils.FakeStoreAPI()) ++ locations = glance.store.StoreLocations(image1, []) ++ self.assertRaises(exception.BadStoreUri, locations.insert, 0, loc1) ++ self.assertNotIn(loc1, locations) ++ ++ # Test for set_attr of _locations_proxy ++ image2 = TestStoreLocation.FakeImageProxy(utils.FakeStoreAPI()) ++ locations = glance.store.StoreLocations(image2, [loc1]) ++ self.assertRaises(exception.BadStoreUri, locations.insert, 0, loc2) ++ self.assertNotIn(loc2, locations) +diff --git a/glance/tests/unit/utils.py b/glance/tests/unit/utils.py +index 1c4e16a..bd6b8c4 100644 +--- a/glance/tests/unit/utils.py ++++ b/glance/tests/unit/utils.py +@@ -14,9 +14,9 @@ + # under the License. + + import urllib +-import urlparse + + from oslo.config import cfg ++import six.moves.urllib.parse as urlparse + + from glance.common import exception + from glance.common import wsgi +@@ -188,6 +188,12 @@ class FakeStoreAPI(object): + def check_location_metadata(self, val, key=''): + glance.store.check_location_metadata(val) + ++ def validate_external_location(self, uri): ++ if uri and urlparse.urlparse(uri).scheme: ++ return glance.store.validate_external_location(uri) ++ else: ++ return True ++ + + class FakePolicyEnforcer(object): + def __init__(self, *_args, **kwargs): +diff --git a/glance/tests/unit/v1/test_api.py b/glance/tests/unit/v1/test_api.py +index 5618cb0..bea15c7 100644 +--- a/glance/tests/unit/v1/test_api.py ++++ b/glance/tests/unit/v1/test_api.py +@@ -379,7 +379,7 @@ class TestGlanceAPI(base.IsolatedUnitTest): + + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) +- self.assertTrue('External sourcing not supported' in res.body) ++ self.assertIn('External source are not supported', res.body) + + def test_create_with_location_bad_store_uri(self): + fixture_headers = { +@@ -962,6 +962,36 @@ class TestGlanceAPI(base.IsolatedUnitTest): + res = req.get_response(self.api) + self.assertEqual(res.status_int, 409) + ++ def test_add_location_with_invalid_location_on_restricted_sources(self): ++ """Tests creates an image from location and restricted sources""" ++ fixture_headers = {'x-image-meta-store': 'file', ++ 'x-image-meta-disk-format': 'vhd', ++ 'x-image-meta-location': 'file:///etc/passwd', ++ 'x-image-meta-container-format': 'ovf', ++ 'x-image-meta-name': 'fake image #F'} ++ ++ req = webob.Request.blank("/images") ++ req.headers['Content-Type'] = 'application/octet-stream' ++ req.method = 'POST' ++ for k, v in fixture_headers.iteritems(): ++ req.headers[k] = v ++ res = req.get_response(self.api) ++ self.assertEqual(400, res.status_int) ++ ++ fixture_headers = {'x-image-meta-store': 'file', ++ 'x-image-meta-disk-format': 'vhd', ++ 'x-image-meta-location': 'swift+config://xxx', ++ 'x-image-meta-container-format': 'ovf', ++ 'x-image-meta-name': 'fake image #F'} ++ ++ req = webob.Request.blank("/images") ++ req.headers['Content-Type'] = 'application/octet-stream' ++ req.method = 'POST' ++ for k, v in fixture_headers.iteritems(): ++ req.headers[k] = v ++ res = req.get_response(self.api) ++ self.assertEqual(400, res.status_int) ++ + def test_add_copy_from_with_location(self): + """Tests creates an image from copy-from and location""" + fixture_headers = {'x-image-meta-store': 'file', +@@ -978,6 +1008,34 @@ class TestGlanceAPI(base.IsolatedUnitTest): + res = req.get_response(self.api) + self.assertEqual(res.status_int, 400) + ++ def test_add_copy_from_with_restricted_sources(self): ++ """Tests creates an image from copy-from with restricted sources""" ++ fixture_headers = {'x-image-meta-store': 'file', ++ 'x-image-meta-disk-format': 'vhd', ++ 'x-glance-api-copy-from': 'file:///etc/passwd', ++ 'x-image-meta-container-format': 'ovf', ++ 'x-image-meta-name': 'fake image #F'} ++ ++ req = webob.Request.blank("/images") ++ req.method = 'POST' ++ for k, v in six.iteritems(fixture_headers): ++ req.headers[k] = v ++ res = req.get_response(self.api) ++ self.assertEqual(400, res.status_int) ++ ++ fixture_headers = {'x-image-meta-store': 'file', ++ 'x-image-meta-disk-format': 'vhd', ++ 'x-glance-api-copy-from': 'swift+config://xxx', ++ 'x-image-meta-container-format': 'ovf', ++ 'x-image-meta-name': 'fake image #F'} ++ ++ req = webob.Request.blank("/images") ++ req.method = 'POST' ++ for k, v in six.iteritems(fixture_headers): ++ req.headers[k] = v ++ res = req.get_response(self.api) ++ self.assertEqual(400, res.status_int) ++ + def test_add_copy_from_upload_image_unauthorized_with_body(self): + rules = {"upload_image": '!', "modify_image": '@', + "add_image": '@'} diff -Nru glance-2014.1.3/debian/patches/series glance-2014.1.3/debian/patches/series --- glance-2014.1.3/debian/patches/series 2014-10-08 17:02:24.000000000 +0000 +++ glance-2014.1.3/debian/patches/series 2014-12-25 09:29:14.000000000 +0000 @@ -1,3 +1,4 @@ disable-network-for-docs.patch default-config.patch sql_conn-registry.patch +restrict_client_download_and_delete_files_in_glance-api.patch

