diff --git a/etc/nova/api-paste.ini b/etc/nova/api-paste.ini index cb5ea6713ab0..3e73ba867d6b 100644 --- a/etc/nova/api-paste.ini +++ b/etc/nova/api-paste.ini @@ -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 diff --git a/nova/tests/functional/api_samples_test_base.py b/nova/tests/functional/api_samples_test_base.py index 54f447c800a1..f6756fa35068 100644 --- a/nova/tests/functional/api_samples_test_base.py +++ b/nova/tests/functional/api_samples_test_base.py @@ -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) diff --git a/nova/tests/functional/test_middleware.py b/nova/tests/functional/test_middleware.py new file mode 100644 index 000000000000..00630b0e81da --- /dev/null +++ b/nova/tests/functional/test_middleware.py @@ -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)