[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#773914: unblock: glance/2014.1.3-6



Package: release.debian.org
Severity: normal
User: release.debian.org@packages.debian.org
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 <zigo@debian.org>  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 <zigo@debian.org>
 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 <zhiyanl@cn.ibm.com>
+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

Reply to: