From 761751064b563c7c15dcfc1bfbc48409f896e1f7 Mon Sep 17 00:00:00 2001 From: Michael Krotscheck Date: Mon, 19 Oct 2015 06:40:02 -0700 Subject: [PATCH] Added CORS support to Glance This adds the CORS support middleware to Glance, allowing a deployer to optionally configure rules under which a javascript client may break the single-origin policy and access the API directly. For Glance, the paste.ini method of deploying the middleware was chosen, because it needs to be able to annotate responses created by keystonemiddleware. If the middleware were explicitly included as in the previous patch, keystone would reject the request before the cross-domain headers could be annotated, resulting in an error response that was unreadable by the user agent. A special consideration has been made to accomodate Glance's nonstandard configuration files, by using 'glance-api' as the value of oslo_config_project in paste.ini. This is to trigger automatic oslo configuration loading for paste-loaded middleware, in order to ensure that it finds glance-api.conf rather than glance.conf. DocImpact: Add link to CORS configuration in Admin Guide OpenStack CrossProject Spec: http://specs.openstack.org/openstack/openstack-specs/specs/cors-support.html Oslo_Middleware Docs: http://docs.openstack.org/developer/oslo.middleware/cors.html OpenStack Cloud Admin Guide: http://docs.openstack.org/admin-guide-cloud/cross_project_cors.html Change-Id: Icf5fb91a0b9e6736e70314c72c1c99c5f170ba53 --- etc/glance-api-paste.ini | 38 +++++++-- etc/glance-api.conf | 59 +++++++++++++ etc/oslo-config-generator/glance-api.conf | 1 + glance/tests/functional/__init__.py | 19 ++++- .../tests/functional/test_cors_middleware.py | 85 +++++++++++++++++++ 5 files changed, 190 insertions(+), 12 deletions(-) create mode 100644 glance/tests/functional/test_cors_middleware.py diff --git a/etc/glance-api-paste.ini b/etc/glance-api-paste.ini index 5b0e6b4bdf..543c8ebb0d 100644 --- a/etc/glance-api-paste.ini +++ b/etc/glance-api-paste.ini @@ -1,38 +1,38 @@ # Use this pipeline for no auth or image caching - DEFAULT [pipeline:glance-api] -pipeline = healthcheck versionnegotiation osprofiler unauthenticated-context rootapp +pipeline = cors healthcheck versionnegotiation osprofiler unauthenticated-context rootapp # Use this pipeline for image caching and no auth [pipeline:glance-api-caching] -pipeline = healthcheck versionnegotiation osprofiler unauthenticated-context cache rootapp +pipeline = cors healthcheck versionnegotiation osprofiler unauthenticated-context cache rootapp # Use this pipeline for caching w/ management interface but no auth [pipeline:glance-api-cachemanagement] -pipeline = healthcheck versionnegotiation osprofiler unauthenticated-context cache cachemanage rootapp +pipeline = cors healthcheck versionnegotiation osprofiler unauthenticated-context cache cachemanage rootapp # Use this pipeline for keystone auth [pipeline:glance-api-keystone] -pipeline = healthcheck versionnegotiation osprofiler authtoken context rootapp +pipeline = cors healthcheck versionnegotiation osprofiler authtoken context rootapp # Use this pipeline for keystone auth with image caching [pipeline:glance-api-keystone+caching] -pipeline = healthcheck versionnegotiation osprofiler authtoken context cache rootapp +pipeline = cors healthcheck versionnegotiation osprofiler authtoken context cache rootapp # Use this pipeline for keystone auth with caching and cache management [pipeline:glance-api-keystone+cachemanagement] -pipeline = healthcheck versionnegotiation osprofiler authtoken context cache cachemanage rootapp +pipeline = cors healthcheck versionnegotiation osprofiler authtoken context cache cachemanage rootapp # Use this pipeline for authZ only. This means that the registry will treat a # user as authenticated without making requests to keystone to reauthenticate # the user. [pipeline:glance-api-trusted-auth] -pipeline = healthcheck versionnegotiation osprofiler context rootapp +pipeline = cors healthcheck versionnegotiation osprofiler context rootapp # Use this pipeline for authZ only. This means that the registry will treat a # user as authenticated without making requests to keystone to reauthenticate # the user and uses cache management [pipeline:glance-api-trusted-auth+cachemanagement] -pipeline = healthcheck versionnegotiation osprofiler context cache cachemanage rootapp +pipeline = cors healthcheck versionnegotiation osprofiler context cache cachemanage rootapp [composite:rootapp] paste.composite_factory = glance.api:root_app_factory @@ -84,3 +84,25 @@ paste.filter_factory = glance.api.middleware.gzip:GzipMiddleware.factory paste.filter_factory = osprofiler.web:WsgiMiddleware.factory hmac_keys = SECRET_KEY enabled = yes + +[filter:cors] +paste.filter_factory = oslo_middleware.cors:filter_factory +oslo_config_project = glance +oslo_config_program = glance-api +# Basic Headers (Automatic) +# Accept = Origin, Accept, Accept-Language, Content-Type, Cache-Control, Content-Language, Expires, Last-Modified, Pragma +# Expose = Origin, Accept, Accept-Language, Content-Type, Cache-Control, Content-Language, Expires, Last-Modified, Pragma + +# Glance Headers +# Accept = Content-MD5, X-Image-Meta-Checksum, X-Storage-Token, Accept-Encoding +# Expose = X-Image-Meta-Checksum + +# Keystone Headers +# Accept = X-Auth-Token, X-Identity-Status, X-Roles, X-Service-Catalog, X-User-Id, X-Tenant-Id +# Expose = X-Auth-Token, X-Subject-Token, X-Service-Token + +# Request ID Middleware Headers +# Accept = X-OpenStack-Request-ID +# Expose = X-OpenStack-Request-ID +latent_allow_headers = Content-MD5, X-Image-Meta-Checksum, X-Storage-Token, Accept-Encoding, X-Auth-Token, X-Identity-Status, X-Roles, X-Service-Catalog, X-User-Id, X-Tenant-Id, X-OpenStack-Request-ID +latent_expose_headers = X-Image-Meta-Checksum, X-Auth-Token, X-Subject-Token, X-Service-Token, X-OpenStack-Request-ID \ No newline at end of file diff --git a/etc/glance-api.conf b/etc/glance-api.conf index ce44c4243e..36669addf9 100644 --- a/etc/glance-api.conf +++ b/etc/glance-api.conf @@ -627,6 +627,65 @@ #use_tpool = false +[cors] + +# +# From oslo.middleware.cors +# + +# Indicate whether this resource may be shared with the domain +# received in the requests "origin" header. (string value) +#allowed_origin = + +# Indicate that the actual request can include user credentials +# (boolean value) +#allow_credentials = true + +# Indicate which headers are safe to expose to the API. Defaults to +# HTTP Simple Headers. (list value) +#expose_headers = Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma + +# Maximum cache age of CORS preflight requests. (integer value) +#max_age = 3600 + +# Indicate which methods can be used during the actual request. (list +# value) +#allow_methods = GET,POST,PUT,DELETE,OPTIONS + +# Indicate which header field names may be used during the actual +# request. (list value) +#allow_headers = Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma + + +[cors.subdomain] + +# +# From oslo.middleware.cors +# + +# Indicate whether this resource may be shared with the domain +# received in the requests "origin" header. (string value) +#allowed_origin = + +# Indicate that the actual request can include user credentials +# (boolean value) +#allow_credentials = true + +# Indicate which headers are safe to expose to the API. Defaults to +# HTTP Simple Headers. (list value) +#expose_headers = Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma + +# Maximum cache age of CORS preflight requests. (integer value) +#max_age = 3600 + +# Indicate which methods can be used during the actual request. (list +# value) +#allow_methods = GET,POST,PUT,DELETE,OPTIONS + +# Indicate which header field names may be used during the actual +# request. (list value) +#allow_headers = Content-Type,Cache-Control,Content-Language,Expires,Last-Modified,Pragma + [glance_store] # diff --git a/etc/oslo-config-generator/glance-api.conf b/etc/oslo-config-generator/glance-api.conf index 3f24718f7f..1a8d4aea27 100644 --- a/etc/oslo-config-generator/glance-api.conf +++ b/etc/oslo-config-generator/glance-api.conf @@ -9,3 +9,4 @@ namespace = oslo.db.concurrency namespace = oslo.policy namespace = keystonemiddleware.auth_token namespace = oslo.log +namespace = oslo.middleware.cors diff --git a/glance/tests/functional/__init__.py b/glance/tests/functional/__init__.py index d3fa152702..ce1c2775ee 100644 --- a/glance/tests/functional/__init__.py +++ b/glance/tests/functional/__init__.py @@ -370,14 +370,21 @@ filesystem_store_datadir=%(image_dir)s default_store = %(default_store)s """ self.paste_conf_base = """[pipeline:glance-api] -pipeline = healthcheck versionnegotiation gzip unauthenticated-context rootapp +pipeline = + cors + healthcheck + versionnegotiation + gzip + unauthenticated-context + rootapp [pipeline:glance-api-caching] -pipeline = healthcheck versionnegotiation gzip unauthenticated-context +pipeline = cors healthcheck versionnegotiation gzip unauthenticated-context cache rootapp [pipeline:glance-api-cachemanagement] pipeline = + cors healthcheck versionnegotiation gzip @@ -387,10 +394,10 @@ pipeline = rootapp [pipeline:glance-api-fakeauth] -pipeline = healthcheck versionnegotiation gzip fakeauth context rootapp +pipeline = cors healthcheck versionnegotiation gzip fakeauth context rootapp [pipeline:glance-api-noauth] -pipeline = healthcheck versionnegotiation gzip context rootapp +pipeline = cors healthcheck versionnegotiation gzip context rootapp [composite:rootapp] paste.composite_factory = glance.api:root_app_factory @@ -439,6 +446,10 @@ paste.filter_factory = [filter:fakeauth] paste.filter_factory = glance.tests.utils:FakeAuthMiddleware.factory + +[filter:cors] +paste.filter_factory = oslo_middleware.cors:filter_factory +allowed_origin=http://valid.example.com """ diff --git a/glance/tests/functional/test_cors_middleware.py b/glance/tests/functional/test_cors_middleware.py new file mode 100644 index 0000000000..d2a1156297 --- /dev/null +++ b/glance/tests/functional/test_cors_middleware.py @@ -0,0 +1,85 @@ +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Tests cors middleware.""" + +import httplib2 + +from glance.tests import functional + + +class TestCORSMiddleware(functional.FunctionalTest): + '''Provide a basic smoke test to ensure CORS middleware is active. + + The tests below provide minimal confirmation that the CORS middleware + is active, and may be configured. For comprehensive tests, please consult + the test suite in oslo_middleware. + ''' + + def setUp(self): + super(TestCORSMiddleware, self).setUp() + # Cleanup is handled in teardown of the parent class. + self.start_servers(**self.__dict__.copy()) + self.http = httplib2.Http() + self.api_path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port) + + def test_valid_cors_options_request(self): + (r_headers, content) = self.http.request( + self.api_path, + 'OPTIONS', + headers={ + 'Origin': 'http://valid.example.com', + 'Access-Control-Request-Method': 'GET' + }) + + self.assertEqual(r_headers.status, 200) + self.assertIn('access-control-allow-origin', r_headers) + self.assertEqual('http://valid.example.com', + r_headers['access-control-allow-origin']) + + def test_invalid_cors_options_request(self): + (r_headers, content) = self.http.request( + self.api_path, + 'OPTIONS', + headers={ + 'Origin': 'http://invalid.example.com', + 'Access-Control-Request-Method': 'GET' + }) + + self.assertEqual(r_headers.status, 200) + self.assertNotIn('access-control-allow-origin', r_headers) + + def test_valid_cors_get_request(self): + (r_headers, content) = self.http.request( + self.api_path, + 'GET', + headers={ + 'Origin': 'http://valid.example.com' + }) + + self.assertEqual(r_headers.status, 200) + self.assertIn('access-control-allow-origin', r_headers) + self.assertEqual('http://valid.example.com', + r_headers['access-control-allow-origin']) + + def test_invalid_cors_get_request(self): + (r_headers, content) = self.http.request( + self.api_path, + 'GET', + headers={ + 'Origin': 'http://invalid.example.com' + }) + + self.assertEqual(r_headers.status, 200) + self.assertNotIn('access-control-allow-origin', r_headers)