Added CORS support to Nova
This adds the CORS support middleware to Nova, allowing a deployer to optionally configure rules under which a javascript client may break the single-origin policy and access the API directly. 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, 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. The test suite was expanded, to permit the inclusion of HTTP headers, and to add the ability to send an OPTIONS request. 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 bp:nova-cors Change-Id: Ibc95df0860799ff36f682da7d032d5422b0c39c1
This commit is contained in:
parent
cb92a37d5a
commit
e37e738aae
@ -6,7 +6,7 @@ use = egg:Paste#urlmap
|
||||
/: meta
|
||||
|
||||
[pipeline:meta]
|
||||
pipeline = ec2faultwrap logrequest metaapp
|
||||
pipeline = cors ec2faultwrap logrequest metaapp
|
||||
|
||||
[app:metaapp]
|
||||
paste.app_factory = nova.api.metadata.handler:MetadataRequestHandler.factory
|
||||
@ -22,8 +22,8 @@ use = egg:Paste#urlmap
|
||||
|
||||
[composite:ec2cloud]
|
||||
use = call:nova.api.auth:pipeline_factory
|
||||
noauth2 = ec2faultwrap logrequest ec2noauth cloudrequest validator ec2executor
|
||||
keystone = ec2faultwrap logrequest ec2keystoneauth cloudrequest validator ec2executor
|
||||
noauth2 = cors ec2faultwrap logrequest ec2noauth cloudrequest validator ec2executor
|
||||
keystone = cors ec2faultwrap logrequest ec2keystoneauth cloudrequest validator ec2executor
|
||||
|
||||
[filter:ec2faultwrap]
|
||||
paste.filter_factory = nova.api.ec2:FaultWrapper.factory
|
||||
@ -82,19 +82,19 @@ use = call:nova.api.openstack.urlmap:urlmap_factory
|
||||
# NOTE: this is deprecated in favor of openstack_compute_api_v21_legacy_v2_compatible
|
||||
[composite:openstack_compute_api_legacy_v2]
|
||||
use = call:nova.api.auth:pipeline_factory
|
||||
noauth2 = compute_req_id faultwrap sizelimit noauth2 legacy_ratelimit osapi_compute_app_legacy_v2
|
||||
keystone = compute_req_id faultwrap sizelimit authtoken keystonecontext legacy_ratelimit osapi_compute_app_legacy_v2
|
||||
keystone_nolimit = compute_req_id faultwrap sizelimit authtoken keystonecontext osapi_compute_app_legacy_v2
|
||||
noauth2 = cors compute_req_id faultwrap sizelimit noauth2 legacy_ratelimit osapi_compute_app_legacy_v2
|
||||
keystone = cors compute_req_id faultwrap sizelimit authtoken keystonecontext legacy_ratelimit osapi_compute_app_legacy_v2
|
||||
keystone_nolimit = cors compute_req_id faultwrap sizelimit authtoken keystonecontext osapi_compute_app_legacy_v2
|
||||
|
||||
[composite:openstack_compute_api_v21]
|
||||
use = call:nova.api.auth:pipeline_factory_v21
|
||||
noauth2 = compute_req_id faultwrap sizelimit noauth2 osapi_compute_app_v21
|
||||
keystone = compute_req_id faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v21
|
||||
noauth2 = cors compute_req_id faultwrap sizelimit noauth2 osapi_compute_app_v21
|
||||
keystone = cors compute_req_id faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v21
|
||||
|
||||
[composite:openstack_compute_api_v21_legacy_v2_compatible]
|
||||
use = call:nova.api.auth:pipeline_factory_v21
|
||||
noauth2 = compute_req_id faultwrap sizelimit noauth2 legacy_v2_compatible osapi_compute_app_v21
|
||||
keystone = compute_req_id faultwrap sizelimit authtoken keystonecontext legacy_v2_compatible osapi_compute_app_v21
|
||||
noauth2 = cors compute_req_id faultwrap sizelimit noauth2 legacy_v2_compatible osapi_compute_app_v21
|
||||
keystone = cors compute_req_id faultwrap sizelimit authtoken keystonecontext legacy_v2_compatible osapi_compute_app_v21
|
||||
|
||||
[filter:request_id]
|
||||
paste.filter_factory = oslo_middleware:RequestId.factory
|
||||
@ -133,6 +133,10 @@ paste.app_factory = nova.api.openstack.compute.versions:Versions.factory
|
||||
# Shared #
|
||||
##########
|
||||
|
||||
[filter:cors]
|
||||
paste.filter_factory = oslo_middleware.cors:filter_factory
|
||||
oslo_config_project = nova
|
||||
|
||||
[filter:keystonecontext]
|
||||
paste.filter_factory = nova.api.auth:NovaKeystoneContext.factory
|
||||
|
||||
|
@ -314,8 +314,8 @@ class ApiSampleTestBase(integrated_helpers._IntegratedTestBase):
|
||||
}
|
||||
|
||||
def _get_response(self, url, method, body=None, strip_version=False,
|
||||
api_version=None):
|
||||
headers = {}
|
||||
api_version=None, headers=None):
|
||||
headers = headers or {}
|
||||
headers['Content-Type'] = 'application/' + self.ctype
|
||||
headers['Accept'] = 'application/' + self.ctype
|
||||
if api_version:
|
||||
@ -323,26 +323,39 @@ class ApiSampleTestBase(integrated_helpers._IntegratedTestBase):
|
||||
return self.api.api_request(url, body=body, method=method,
|
||||
headers=headers, strip_version=strip_version)
|
||||
|
||||
def _do_get(self, url, strip_version=False, api_version=None):
|
||||
def _do_options(self, url, strip_version=False, api_version=None,
|
||||
headers=None):
|
||||
return self._get_response(url, 'OPTIONS', strip_version=strip_version,
|
||||
api_version=(api_version or
|
||||
self.request_api_version),
|
||||
headers=headers)
|
||||
|
||||
def _do_get(self, url, strip_version=False, api_version=None,
|
||||
headers=None):
|
||||
return self._get_response(url, 'GET', strip_version=strip_version,
|
||||
api_version=(api_version or
|
||||
self.request_api_version))
|
||||
self.request_api_version),
|
||||
headers=headers)
|
||||
|
||||
def _do_post(self, url, name, subs, method='POST', api_version=None):
|
||||
def _do_post(self, url, name, subs, method='POST', api_version=None,
|
||||
headers=None):
|
||||
body = self._read_template(name) % subs
|
||||
sample = self._get_sample(name, self.request_api_version)
|
||||
if self.generate_samples and not os.path.exists(sample):
|
||||
self._write_sample(name, body)
|
||||
return self._get_response(url, method, body,
|
||||
api_version=(api_version or
|
||||
self.request_api_version))
|
||||
self.request_api_version),
|
||||
headers=headers)
|
||||
|
||||
def _do_put(self, url, name, subs, api_version=None):
|
||||
def _do_put(self, url, name, subs, api_version=None, headers=None):
|
||||
return self._do_post(url, name, subs, method='PUT',
|
||||
api_version=(api_version or
|
||||
self.request_api_version))
|
||||
self.request_api_version),
|
||||
headers=headers)
|
||||
|
||||
def _do_delete(self, url, api_version=None):
|
||||
def _do_delete(self, url, api_version=None, headers=None):
|
||||
return self._get_response(url, 'DELETE',
|
||||
api_version=(api_version or
|
||||
self.request_api_version))
|
||||
self.request_api_version),
|
||||
headers=headers)
|
||||
|
98
nova/tests/functional/test_middleware.py
Normal file
98
nova/tests/functional/test_middleware.py
Normal file
@ -0,0 +1,98 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
#
|
||||
# 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 to assert that various incorporated middleware works as expected.
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from nova.tests.functional.api_sample_tests import api_sample_base
|
||||
|
||||
|
||||
class TestCORSMiddleware(api_sample_base.ApiSampleTestBaseV21):
|
||||
'''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):
|
||||
# Here we monkeypatch GroupAttr.__getattr__, necessary because the
|
||||
# paste.ini method of initializing this middleware creates its own
|
||||
# ConfigOpts instance, bypassing the regular config fixture.
|
||||
# Mocking also does not work, as accessing an attribute on a mock
|
||||
# object will return a MagicMock instance, which will fail
|
||||
# configuration type checks.
|
||||
def _mock_getattr(instance, key):
|
||||
if key != 'allowed_origin':
|
||||
return self._original_call_method(instance, key)
|
||||
return "http://valid.example.com"
|
||||
|
||||
self._original_call_method = cfg.ConfigOpts.GroupAttr.__getattr__
|
||||
cfg.ConfigOpts.GroupAttr.__getattr__ = _mock_getattr
|
||||
|
||||
# Initialize the application after all the config overrides are in
|
||||
# place.
|
||||
super(TestCORSMiddleware, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
super(TestCORSMiddleware, self).tearDown()
|
||||
|
||||
# Reset the configuration overrides.
|
||||
cfg.ConfigOpts.GroupAttr.__getattr__ = self._original_call_method
|
||||
|
||||
def test_valid_cors_options_request(self):
|
||||
response = self._do_options('servers',
|
||||
headers={
|
||||
'Origin': 'http://valid.example.com',
|
||||
'Access-Control-Request-Method': 'GET'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('Access-Control-Allow-Origin', response.headers)
|
||||
self.assertEqual('http://valid.example.com',
|
||||
response.headers['Access-Control-Allow-Origin'])
|
||||
|
||||
def test_invalid_cors_options_request(self):
|
||||
response = self._do_options('servers',
|
||||
headers={
|
||||
'Origin': 'http://invalid.example.com',
|
||||
'Access-Control-Request-Method': 'GET'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn('Access-Control-Allow-Origin', response.headers)
|
||||
|
||||
def test_valid_cors_get_request(self):
|
||||
response = self._do_get('servers',
|
||||
headers={
|
||||
'Origin': 'http://valid.example.com',
|
||||
'Access-Control-Request-Method': 'GET'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('Access-Control-Allow-Origin', response.headers)
|
||||
self.assertEqual('http://valid.example.com',
|
||||
response.headers['Access-Control-Allow-Origin'])
|
||||
|
||||
def test_invalid_cors_get_request(self):
|
||||
response = self._do_get('servers',
|
||||
headers={
|
||||
'Origin': 'http://invalid.example.com',
|
||||
'Access-Control-Request-Method': 'GET'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn('Access-Control-Allow-Origin', response.headers)
|
Loading…
x
Reference in New Issue
Block a user