--- /dev/null
+From 9beca533f42ae1fc87418de0c360e19bc59b24b5 Mon Sep 17 00:00:00 2001
+From: Stuart McLaren <stuart.mclaren@hp.com>
+Date: Tue, 11 Aug 2015 10:37:09 +0000
+Subject: [PATCH] Prevent image status being directly modified via v1
+
+Users shouldn't be able to change an image's status directly via the
+v1 API.
+
+Some existing consumers of Glance set the x-image-meta-status header in
+requests to the Glance API, eg:
+
+https://github.com/openstack/nova/blob/master/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance#L184
+
+We should try to prevent users setting 'status' via v1, but without breaking
+existing benign API calls such as these.
+
+I've adopted the following approach (which has some prior art in 'protected properties').
+
+If a PUT request is received which contains an x-image-meta-status header:
+
+* The user provided status is ignored if it matches the current image
+ status (this prevents benign calls such as the nova one above from
+ breaking). The usual code (eg 200) will be returned.
+
+* If the user provided status doesn't match the current image status (ie
+ there is a real attempt to change the value) 403 will be returned. This
+ will break any calls which currently intentionally change the status.
+
+APIImpact
+
+Closes-bug: 1482371
+
+Change-Id: I44fadf32abb57c962b67467091c3f51c1ccc25e6
+(cherry picked from commit 4d08db5b6d42323ac1958ef3b7417d875e7bea8c)
+---
+ glance/api/v1/__init__.py | 3 +
+ glance/api/v1/images.py | 9 +++
+ glance/tests/functional/v1/test_api.py | 89 ++++++++++++++++++++++
+ .../integration/legacy_functional/test_v1_api.py | 2 +
+ 4 files changed, 103 insertions(+)
+
+diff --git a/glance/api/v1/__init__.py b/glance/api/v1/__init__.py
+index 74de9aa1411d8e926770b67f7d851cf14e794414..9306bbb4fe78f77a26bb539c717fdfd2b38767c8 100644
+--- a/glance/api/v1/__init__.py
++++ b/glance/api/v1/__init__.py
+@@ -21,3 +21,6 @@ SUPPORTED_PARAMS = ('limit', 'marker', 'sort_key', 'sort_dir')
+
+ # Metadata which only an admin can change once the image is active
+ ACTIVE_IMMUTABLE = ('size', 'checksum')
++
++# Metadata which cannot be changed (irrespective of the current image state)
++IMMUTABLE = ('status',)
+diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py
+index e33b91fbca79377e78ccfd329fa542ad422f5ffc..95e32949d958d0f57a3b60c141b91784a5801f5a 100644
+--- a/glance/api/v1/images.py
++++ b/glance/api/v1/images.py
+@@ -57,6 +57,7 @@ _LW = i18n._LW
+ SUPPORTED_PARAMS = glance.api.v1.SUPPORTED_PARAMS
+ SUPPORTED_FILTERS = glance.api.v1.SUPPORTED_FILTERS
+ ACTIVE_IMMUTABLE = glance.api.v1.ACTIVE_IMMUTABLE
++IMMUTABLE = glance.api.v1.IMMUTABLE
+
+ CONF = cfg.CONF
+ CONF.import_opt('disk_formats', 'glance.common.config', group='image_format')
+@@ -912,6 +913,14 @@ class Controller(controller.BaseController):
+ request=req,
+ content_type="text/plain")
+
++ for key in IMMUTABLE:
++ if (image_meta.get(key) is not None and
++ image_meta.get(key) != orig_image_meta.get(key)):
++ msg = _("Forbidden to modify '%s' of image.") % key
++ raise HTTPForbidden(explanation=msg,
++ request=req,
++ content_type="text/plain")
++
+ # The default behaviour for a PUT /images/<IMAGE_ID> is to
+ # override any properties that were previously set. This, however,
+ # leads to a number of issues for the common use case where a caller
+diff --git a/glance/tests/functional/v1/test_api.py b/glance/tests/functional/v1/test_api.py
+index 9fba3bb5e40c8742530691228c7b436b385fc2ca..6b3bfbb4270f1eb0f50418504e65be30ea23d10b 100644
+--- a/glance/tests/functional/v1/test_api.py
++++ b/glance/tests/functional/v1/test_api.py
+@@ -715,3 +715,92 @@ class TestApi(functional.FunctionalTest):
+ self.assertEqual(404, response.status)
+
+ self.stop_servers()
++
++ def test_status_cannot_be_manipulated_directly(self):
++ self.cleanup()
++ self.start_servers(**self.__dict__.copy())
++ headers = minimal_headers('Image1')
++
++ # Create a 'queued' image
++ http = httplib2.Http()
++ headers = {'Content-Type': 'application/octet-stream',
++ 'X-Image-Meta-Disk-Format': 'raw',
++ 'X-Image-Meta-Container-Format': 'bare'}
++ path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
++ response, content = http.request(path, 'POST', headers=headers,
++ body=None)
++ self.assertEqual(201, response.status)
++ image = jsonutils.loads(content)['image']
++ self.assertEqual('queued', image['status'])
++
++ # Ensure status of 'queued' image can't be changed
++ path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port,
++ image['id'])
++ http = httplib2.Http()
++ headers = {'X-Image-Meta-Status': 'active'}
++ response, content = http.request(path, 'PUT', headers=headers)
++ self.assertEqual(403, response.status)
++ response, content = http.request(path, 'HEAD')
++ self.assertEqual(200, response.status)
++ self.assertEqual('queued', response['x-image-meta-status'])
++
++ # We allow 'setting' to the same status
++ http = httplib2.Http()
++ headers = {'X-Image-Meta-Status': 'queued'}
++ response, content = http.request(path, 'PUT', headers=headers)
++ self.assertEqual(200, response.status)
++ response, content = http.request(path, 'HEAD')
++ self.assertEqual(200, response.status)
++ self.assertEqual('queued', response['x-image-meta-status'])
++
++ # Make image active
++ http = httplib2.Http()
++ headers = {'Content-Type': 'application/octet-stream'}
++ response, content = http.request(path, 'PUT', headers=headers,
++ body='data')
++ self.assertEqual(200, response.status)
++ image = jsonutils.loads(content)['image']
++ self.assertEqual('active', image['status'])
++
++ # Ensure status of 'active' image can't be changed
++ http = httplib2.Http()
++ headers = {'X-Image-Meta-Status': 'queued'}
++ response, content = http.request(path, 'PUT', headers=headers)
++ self.assertEqual(403, response.status)
++ response, content = http.request(path, 'HEAD')
++ self.assertEqual(200, response.status)
++ self.assertEqual('active', response['x-image-meta-status'])
++
++ # We allow 'setting' to the same status
++ http = httplib2.Http()
++ headers = {'X-Image-Meta-Status': 'active'}
++ response, content = http.request(path, 'PUT', headers=headers)
++ self.assertEqual(200, response.status)
++ response, content = http.request(path, 'HEAD')
++ self.assertEqual(200, response.status)
++ self.assertEqual('active', response['x-image-meta-status'])
++
++ # Create a 'queued' image, ensure 'status' header is ignored
++ http = httplib2.Http()
++ path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
++ headers = {'Content-Type': 'application/octet-stream',
++ 'X-Image-Meta-Status': 'active'}
++ response, content = http.request(path, 'POST', headers=headers,
++ body=None)
++ self.assertEqual(201, response.status)
++ image = jsonutils.loads(content)['image']
++ self.assertEqual('queued', image['status'])
++
++ # Create an 'active' image, ensure 'status' header is ignored
++ http = httplib2.Http()
++ path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port)
++ headers = {'Content-Type': 'application/octet-stream',
++ 'X-Image-Meta-Disk-Format': 'raw',
++ 'X-Image-Meta-Status': 'queued',
++ 'X-Image-Meta-Container-Format': 'bare'}
++ response, content = http.request(path, 'POST', headers=headers,
++ body='data')
++ self.assertEqual(201, response.status)
++ image = jsonutils.loads(content)['image']
++ self.assertEqual('active', image['status'])
++ self.stop_servers()
+diff --git a/glance/tests/integration/legacy_functional/test_v1_api.py b/glance/tests/integration/legacy_functional/test_v1_api.py
+index dff436465919569480bdbac537d20a6d61c98f46..511d46dfe18028bb430504784cc9d24c58736c3b 100644
+--- a/glance/tests/integration/legacy_functional/test_v1_api.py
++++ b/glance/tests/integration/legacy_functional/test_v1_api.py
+@@ -358,6 +358,8 @@ class TestApi(base.ApiTest):
+ path = "/v1/images"
+ response, content = self.http.request(path, 'POST', headers=headers)
+ self.assertEqual(201, response.status)
++ image = jsonutils.loads(content)['image']
++ self.assertEqual('active', image['status'])
+
+ # 2. HEAD image-location
+ # Verify image size is zero and the status is active
+--
+2.5.0
+
--- /dev/null
+# Copyright 1999-2015 Gentoo Foundation
+# Distributed under the terms of the GNU General Public License v2
+# $Id$
+
+EAPI=5
+PYTHON_COMPAT=( python2_7 )
+
+inherit distutils-r1 user
+
+DESCRIPTION="Provides services for discovering, registering, and retrieving
+virtual machine images"
+HOMEPAGE="https://launchpad.net/glance"
+SRC_URI="https://launchpad.net/${PN}/kilo/${PV}/+download/${P}.tar.gz"
+
+LICENSE="Apache-2.0"
+SLOT="0"
+KEYWORDS="~amd64 ~x86"
+IUSE="doc mysql postgres +sqlite +swift test"
+REQUIRED_USE="|| ( mysql postgres sqlite )"
+
+DEPEND="
+ dev-python/setuptools[${PYTHON_USEDEP}]
+ >=dev-python/pbr-0.8.0[${PYTHON_USEDEP}]
+ <dev-python/pbr-1.0[${PYTHON_USEDEP}]
+ test? (
+ ${RDEPEND}
+ >=dev-python/hacking-0.10.0[${PYTHON_USEDEP}]
+ <dev-python/hacking-0.11[${PYTHON_USEDEP}]
+ ~dev-python/Babel-1.3[${PYTHON_USEDEP}]
+ >=dev-python/coverage-3.6[${PYTHON_USEDEP}]
+ >=dev-python/fixtures-0.3.14[${PYTHON_USEDEP}]
+ <dev-python/fixtures-1.3.0[${PYTHON_USEDEP}]
+ >=dev-python/mock-1.0[${PYTHON_USEDEP}]
+ <dev-python/mock-1.1.0[${PYTHON_USEDEP}]
+ >=dev-python/sphinx-1.1.2[${PYTHON_USEDEP}]
+ !~dev-python/sphinx-1.2.0[${PYTHON_USEDEP}]
+ <dev-python/sphinx-1.3[${PYTHON_USEDEP}]
+ >=dev-python/requests-2.2.0[${PYTHON_USEDEP}]
+ !~dev-python/requests-2.4.0[${PYTHON_USEDEP}]
+ >=dev-python/testrepository-0.0.18[${PYTHON_USEDEP}]
+ >=dev-python/testtools-0.9.36[${PYTHON_USEDEP}]
+ !~dev-python/testtools-1.2.0[${PYTHON_USEDEP}]
+ >=dev-python/psutil-1.1.1[${PYTHON_USEDEP}]
+ <dev-python/psutil-2.0.0[${PYTHON_USEDEP}]
+ >=dev-python/oslotest-1.5.1[${PYTHON_USEDEP}]
+ <dev-python/oslotest-1.6.0[${PYTHON_USEDEP}]
+ dev-python/mysql-python[${PYTHON_USEDEP}]
+ dev-python/psycopg[${PYTHON_USEDEP}]
+ ~dev-python/pysendfile-2.0.1[${PYTHON_USEDEP}]
+ dev-python/qpid-python[${PYTHON_USEDEP}]
+ >=dev-python/pyxattr-0.5.0[${PYTHON_USEDEP}]
+ >=dev-python/oslo-sphinx-2.5.0[${PYTHON_USEDEP}]
+ <dev-python/oslo-sphinx-2.6.0[${PYTHON_USEDEP}]
+ >=dev-python/elasticsearch-py-1.3.0[${PYTHON_USEDEP}]
+ )"
+
+#note to self, wsgiref is a python builtin, no need to package it
+#>=dev-python/wsgiref-0.1.2[${PYTHON_USEDEP}]
+
+RDEPEND="
+ >=dev-python/greenlet-0.3.2[${PYTHON_USEDEP}]
+ sqlite? (
+ >=dev-python/sqlalchemy-0.9.7[sqlite,${PYTHON_USEDEP}]
+ <=dev-python/sqlalchemy-0.9.99[sqlite,${PYTHON_USEDEP}]
+ )
+ mysql? (
+ dev-python/mysql-python
+ >=dev-python/sqlalchemy-0.9.7[${PYTHON_USEDEP}]
+ <=dev-python/sqlalchemy-0.9.99[${PYTHON_USEDEP}]
+ )
+ postgres? (
+ dev-python/psycopg:2
+ >=dev-python/sqlalchemy-0.9.7[${PYTHON_USEDEP}]
+ <=dev-python/sqlalchemy-0.9.99[${PYTHON_USEDEP}]
+ )
+ >=dev-python/anyjson-0.3.3[${PYTHON_USEDEP}]
+ >=dev-python/eventlet-0.16.1[${PYTHON_USEDEP}]
+ !~dev-python/eventlet-0.17.0[${PYTHON_USEDEP}]
+ >=dev-python/pastedeploy-1.5.0[${PYTHON_USEDEP}]
+ >=dev-python/routes-1.12.3[${PYTHON_USEDEP}]
+ !~dev-python/routes-2.0[${PYTHON_USEDEP}]
+ >=dev-python/webob-1.2.3[${PYTHON_USEDEP}]
+ >=dev-python/sqlalchemy-migrate-0.9.5[${PYTHON_USEDEP}]
+ >=dev-python/httplib2-0.7.5[${PYTHON_USEDEP}]
+ >=dev-python/kombu-2.5.0[${PYTHON_USEDEP}]
+ >=dev-python/pycrypto-2.6[${PYTHON_USEDEP}]
+ >=dev-python/iso8601-0.1.9[${PYTHON_USEDEP}]
+ dev-python/ordereddict[${PYTHON_USEDEP}]
+ >=dev-python/oslo-config-1.9.3[${PYTHON_USEDEP}]
+ <dev-python/oslo-config-1.10.0[${PYTHON_USEDEP}]
+ >=dev-python/oslo-concurrency-1.8.0[${PYTHON_USEDEP}]
+ <dev-python/oslo-concurrency-1.9.0[${PYTHON_USEDEP}]
+ >=dev-python/oslo-context-0.2.0[${PYTHON_USEDEP}]
+ <dev-python/oslo-context-0.3.0[${PYTHON_USEDEP}]
+ >=dev-python/oslo-utils-1.4.0[${PYTHON_USEDEP}]
+ <dev-python/oslo-utils-1.5.0[${PYTHON_USEDEP}]
+ >=dev-python/stevedore-1.3.0[${PYTHON_USEDEP}]
+ <dev-python/stevedore-1.4.0[${PYTHON_USEDEP}]
+ >=dev-python/taskflow-0.7.1[${PYTHON_USEDEP}]
+ <dev-python/taskflow-0.8.0[${PYTHON_USEDEP}]
+ >=dev-python/keystonemiddleware-1.5.0[${PYTHON_USEDEP}]
+ <dev-python/keystonemiddleware-1.6.0[${PYTHON_USEDEP}]
+ >=dev-python/WSME-0.6[${PYTHON_USEDEP}]
+ <dev-python/WSME-0.7[${PYTHON_USEDEP}]
+ dev-python/posix_ipc[${PYTHON_USEDEP}]
+ swift? (
+ >=dev-python/python-swiftclient-2.2.0[${PYTHON_USEDEP}]
+ <dev-python/python-swiftclient-2.5.0[${PYTHON_USEDEP}]
+ )
+ >=dev-python/oslo-vmware-0.11.1[${PYTHON_USEDEP}]
+ <dev-python/oslo-vmware-0.12.0[${PYTHON_USEDEP}]
+ dev-python/paste[${PYTHON_USEDEP}]
+ >=dev-python/jsonschema-2.0.0[${PYTHON_USEDEP}]
+ <dev-python/jsonschema-3.0.0[${PYTHON_USEDEP}]
+ >=dev-python/python-keystoneclient-1.2.0[${PYTHON_USEDEP}]
+ <dev-python/python-keystoneclient-1.4.0[${PYTHON_USEDEP}]
+ >=dev-python/pyopenssl-0.11[${PYTHON_USEDEP}]
+ >=dev-python/six-1.9.0[${PYTHON_USEDEP}]
+ >=dev-python/oslo-db-1.7.0[${PYTHON_USEDEP}]
+ <dev-python/oslo-db-1.8.0[${PYTHON_USEDEP}]
+ >=dev-python/oslo-i18n-1.5.0[${PYTHON_USEDEP}]
+ <dev-python/oslo-i18n-1.6.0[${PYTHON_USEDEP}]
+ >=dev-python/oslo-log-1.0.0[${PYTHON_USEDEP}]
+ <dev-python/oslo-log-1.1.0[${PYTHON_USEDEP}]
+ >=dev-python/oslo-messaging-1.8.0[${PYTHON_USEDEP}]
+ <dev-python/oslo-messaging-1.9.0[${PYTHON_USEDEP}]
+ >=dev-python/oslo-policy-0.3.1[${PYTHON_USEDEP}]
+ <dev-python/oslo-policy-0.4.0[${PYTHON_USEDEP}]
+ >=dev-python/oslo-serialization-1.4.0[${PYTHON_USEDEP}]
+ <dev-python/oslo-serialization-1.5.0[${PYTHON_USEDEP}]
+ >=dev-python/retrying-1.2.3[${PYTHON_USEDEP}]
+ !~dev-python/retrying-1.3.0[${PYTHON_USEDEP}]
+ >=dev-python/osprofiler-0.3.0[${PYTHON_USEDEP}]
+ >=dev-python/glance_store-0.3.0[${PYTHON_USEDEP}]
+ <dev-python/glance_store-0.5.0[${PYTHON_USEDEP}]
+ >=dev-python/semantic_version-2.3.1[${PYTHON_USEDEP}]
+"
+
+PATCHES=(
+ "${FILESDIR}/cve-2015-5163-stable-kilo.patch"
+ "${FILESDIR}/cve-2015-5251-stable-kilo.patch"
+)
+
+pkg_setup() {
+ enewgroup glance
+ enewuser glance -1 -1 /var/lib/glance glance
+}
+
+python_prepare_all() {
+ sed -i '/xattr/d' test-requirements.txt || die
+ sed -i '/pysendfile/d' test-requirements.txt || die
+ distutils-r1_python_prepare_all
+}
+
+python_compile_all() {
+ use doc && "${PYTHON}" setup.py build_sphinx
+}
+
+python_test() {
+ # https://bugs.launchpad.net/glance/+bug/1251105
+ # https://bugs.launchpad.net/glance/+bug/1242501
+ testr init
+ testr run --parallel || die "failed testsuite under python2.7"
+}
+
+python_install() {
+ distutils-r1_python_install
+
+ for svc in api registry scrubber; do
+ newinitd "${FILESDIR}/glance.initd" glance-${svc}
+ done
+
+ diropts -m 0750 -o glance -g glance
+ dodir /var/log/glance /var/lib/glance/images /var/lib/glance/scrubber
+ keepdir /etc/glance
+ keepdir /var/log/glance
+ keepdir /var/lib/glance/images
+ keepdir /var/lib/glance/scrubber
+
+ insinto /etc/glance
+ insopts -m 0640 -o glance -g glance
+ doins etc/*.ini
+ doins etc/*.conf
+ doins etc/*.sample
+}
+
+python_install_all() {
+ use doc && local HTML_DOCS=( doc/build/html/. )
+ distutils-r1_python_install_all
+}