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:
parent
ec2e889f4d
commit
761751064b
@ -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
|
@ -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]
|
||||
|
||||
#
|
||||
|
@ -9,3 +9,4 @@ namespace = oslo.db.concurrency
|
||||
namespace = oslo.policy
|
||||
namespace = keystonemiddleware.auth_token
|
||||
namespace = oslo.log
|
||||
namespace = oslo.middleware.cors
|
||||
|
@ -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
|
||||
"""
|
||||
|
||||
|
||||
|
85
glance/tests/functional/test_cors_middleware.py
Normal file
85
glance/tests/functional/test_cors_middleware.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user