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
This commit is contained in:
Michael Krotscheck 2015-10-19 06:40:02 -07:00
parent ec2e889f4d
commit 761751064b
5 changed files with 190 additions and 12 deletions

View File

@ -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

View File

@ -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 = <None>
# 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 = <None>
# 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]
#

View File

@ -9,3 +9,4 @@ namespace = oslo.db.concurrency
namespace = oslo.policy
namespace = keystonemiddleware.auth_token
namespace = oslo.log
namespace = oslo.middleware.cors

View File

@ -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
"""

View File

@ -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)