From e18f80345ae519c9308cfc93fdf53b82c9be7618 Mon Sep 17 00:00:00 2001 From: Gaetan Trellu Date: Tue, 28 May 2019 17:58:40 -0400 Subject: [PATCH] Use oslo_middleware to support CORS configuration Currently there is no way in Qinling configuration file to configure the CORS. oslo_middleware should be implemented to be able to set [cors] section in qinling.conf. Change-Id: Ib4664ba7cbda28a9dfda1c1dd9f0e7457c0ee7de Story: 2005788 Task: 33515 --- doc/requirements.txt | 11 ++- lower-constraints.txt | 5 + qinling/api/app.py | 22 ++++- qinling/api/controllers/root.py | 2 +- qinling/api/controllers/v1/root.py | 3 +- qinling/api/wsgi.py | 17 ++++ qinling/config.py | 32 +++++++ .../tests/unit/api/test_cors_middleware.py | 96 +++++++++++++++++++ .../tests/unit/api/test_oslo_middleware.py | 42 ++++++++ .../add-cors-support-ce060ee8513a1acf.yaml | 9 ++ requirements.txt | 1 + setup.cfg | 8 +- tools/config/config-generator.qinling.conf | 2 + 13 files changed, 242 insertions(+), 8 deletions(-) create mode 100644 qinling/api/wsgi.py create mode 100644 qinling/tests/unit/api/test_cors_middleware.py create mode 100644 qinling/tests/unit/api/test_oslo_middleware.py create mode 100644 releasenotes/notes/add-cors-support-ce060ee8513a1acf.yaml diff --git a/doc/requirements.txt b/doc/requirements.txt index 6745e591..86270b8a 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,7 +1,10 @@ # The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. - -sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD -openstackdocstheme>=1.24.0 # Apache-2.0 -reno>=2.5.0 # Apache-2.0 +# this is required for the docs build jobs +sphinx>=1.6.2,<2.0.0;python_version=='2.7' # BSD +sphinx>=1.6.2;python_version>='3.4' # BSD +sphinxcontrib-apidoc>=0.2.0 # BSD +openstackdocstheme>=1.11.0 # Apache-2.0 +reno>=1.8.0 # Apache-2.0 +os-api-ref>=1.0.0 # Apache-2.0 diff --git a/lower-constraints.txt b/lower-constraints.txt index d9be4092..6859fe7f 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -67,6 +67,7 @@ oslo.serialization==2.18.0 oslo.service==1.24.0 oslo.upgradecheck==0.1.0 oslo.utils==3.33.0 +oslosphinx==4.7.0 oslotest==3.2.0 paramiko==2.4.1 Paste==2.0.3 @@ -106,6 +107,10 @@ setproctitle==1.1.10 setuptools==21.0.0 simplegeneric==0.8.1 six==1.10.0 +Sphinx==1.6.2 +sphinxcontrib-httpdomain==1.3.0 +sphinxcontrib-pecanwsme==0.10.0 +sphinxcontrib-websupport==1.0.1 SQLAlchemy==1.0.10 sqlalchemy-migrate==0.11.0 sqlparse==0.2.4 diff --git a/qinling/api/app.py b/qinling/api/app.py index aa1a1fa4..4dd7052e 100644 --- a/qinling/api/app.py +++ b/qinling/api/app.py @@ -14,9 +14,12 @@ from oslo_config import cfg from oslo_log import log as logging +import oslo_middleware.cors as cors_middleware +import oslo_middleware.http_proxy_to_wsgi as http_proxy_to_wsgi_middleware import pecan from qinling.api import access_control +from qinling import config as q_config from qinling import context as ctx from qinling.db import api as db_api from qinling.services import periodics @@ -43,6 +46,9 @@ def get_pecan_config(): def setup_app(config=None): if not config: config = get_pecan_config() + + q_config.set_config_defaults() + app_conf = dict(config.app) db_api.setup_db() @@ -61,4 +67,18 @@ def setup_app(config=None): # Set up access control. app = access_control.setup(app) - return app + # Create HTTPProxyToWSGI wrapper + app = http_proxy_to_wsgi_middleware.HTTPProxyToWSGI(app, cfg.CONF) + + # Create a CORS wrapper, and attach mistral-specific defaults that must be + # included in all CORS responses. + return cors_middleware.CORS(app, cfg.CONF) + + +def init_wsgi(): + # By default, oslo.config parses the CLI args if no args is provided. + # As a result, invoking this wsgi script from gunicorn leads to the error + # with argparse complaining that the CLI options have already been parsed. + q_config.parse_args(args=[]) + + return setup_app() diff --git a/qinling/api/controllers/root.py b/qinling/api/controllers/root.py index d1b85d35..4cd9da96 100644 --- a/qinling/api/controllers/root.py +++ b/qinling/api/controllers/root.py @@ -67,7 +67,7 @@ class RootController(object): def index(self): LOG.info("Fetching API versions.") - host_url_v1 = '%s/%s' % (pecan.request.host_url, 'v1') + host_url_v1 = '%s/%s' % (pecan.request.application_url, 'v1') api_v1 = APIVersion( id='v1.0', status='CURRENT', diff --git a/qinling/api/controllers/v1/root.py b/qinling/api/controllers/v1/root.py index 3159d1a6..5708835c 100644 --- a/qinling/api/controllers/v1/root.py +++ b/qinling/api/controllers/v1/root.py @@ -43,4 +43,5 @@ class Controller(object): @wsme_pecan.wsexpose(RootResource) def index(self): - return RootResource(uri='%s/%s' % (pecan.request.host_url, 'v1')) + return RootResource(uri='%s/%s' % (pecan.request.application_url, + 'v1')) diff --git a/qinling/api/wsgi.py b/qinling/api/wsgi.py new file mode 100644 index 00000000..110d7bcf --- /dev/null +++ b/qinling/api/wsgi.py @@ -0,0 +1,17 @@ +# Copyright 2019 - Ormuco, Inc. +# +# 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. + +from qinling.api import app + +application = app.init_wsgi() diff --git a/qinling/config.py b/qinling/config.py index cfb44234..372f1125 100644 --- a/qinling/config.py +++ b/qinling/config.py @@ -16,6 +16,7 @@ from keystonemiddleware import auth_token from oslo_concurrency import processutils from oslo_config import cfg from oslo_log import log +from oslo_middleware import cors from qinling import version @@ -335,3 +336,34 @@ def parse_args(args=None, usage=None, default_config_files=None): usage=usage, default_config_files=default_config_files ) + + +def set_config_defaults(): + """This method updates all configuration default values.""" + set_cors_middleware_defaults() + + +def set_cors_middleware_defaults(): + """Update default configuration options for oslo.middleware.""" + cors.set_defaults( + allow_headers=['X-Auth-Token', + 'X-Identity-Status', + 'X-Roles', + 'X-Service-Catalog', + 'X-User-Id', + 'X-Tenant-Id', + 'X-Project-Id', + 'X-User-Name', + 'X-Project-Name'], + allow_methods=['GET', + 'PUT', + 'POST', + 'DELETE', + 'PATCH'], + expose_headers=['X-Auth-Token', + 'X-Subject-Token', + 'X-Service-Token', + 'X-Project-Id', + 'X-User-Name', + 'X-Project-Name'] + ) diff --git a/qinling/tests/unit/api/test_cors_middleware.py b/qinling/tests/unit/api/test_cors_middleware.py new file mode 100644 index 00000000..c7a0626a --- /dev/null +++ b/qinling/tests/unit/api/test_cors_middleware.py @@ -0,0 +1,96 @@ +# 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.""" + +from oslo_config import cfg +from oslo_middleware import cors as cors_middleware +from qinling.tests.unit.api import base + + +class TestCORSMiddleware(base.APITest): + """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): + # Make sure the CORS options are registered + cfg.CONF.register_opts(cors_middleware.CORS_OPTS, 'cors') + + # Load up our valid domain values before the application is created. + self.override_config( + "allowed_origin", + "http://valid.example.com", + group='cors' + ) + + # Create the application. + super(TestCORSMiddleware, self).setUp() + + def test_valid_cors_options_request(self): + response = self.app.options( + '/', + headers={ + 'Origin': 'http://valid.example.com', + 'Access-Control-Request-Method': 'GET' + } + ) + + self.assertEqual(200, response.status_code) + 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.app.options( + '/', + headers={ + 'Origin': 'http://invalid.example.com', + 'Access-Control-Request-Method': 'GET' + } + ) + + self.assertEqual(200, response.status_code) + self.assertNotIn('access-control-allow-origin', response.headers) + + def test_valid_cors_get_request(self): + response = self.app.get( + '/', + headers={ + 'Origin': 'http://valid.example.com' + } + ) + + self.assertEqual(200, response.status_code) + 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.app.get( + '/', + headers={ + 'Origin': 'http://invalid.example.com' + } + ) + + self.assertEqual(200, response.status_code) + self.assertNotIn('access-control-allow-origin', response.headers) diff --git a/qinling/tests/unit/api/test_oslo_middleware.py b/qinling/tests/unit/api/test_oslo_middleware.py new file mode 100644 index 00000000..5dd0ff7a --- /dev/null +++ b/qinling/tests/unit/api/test_oslo_middleware.py @@ -0,0 +1,42 @@ +# 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 http_proxy_to_wsgi middleware.""" + +from oslo_config import cfg +from oslo_middleware import http_proxy_to_wsgi as http_proxy_to_wsgi_middleware +from qinling.tests.unit.api import base + + +class TestHTTPProxyToWSGIMiddleware(base.APITest): + """Test oslo_middleware HTTPProxyToWSGI. + + It checks that oslo_middleware middleware HTTPProxyToWSGI is executed + when enabled. + """ + + def setUp(self): + # Make sure the HTTPProxyToWSGI options are registered + cfg.CONF.register_opts(http_proxy_to_wsgi_middleware.OPTS, + 'oslo_middleware') + + # Enable proxy headers parsing in HTTPProxyToWSGI middleware. + self.override_config( + "enable_proxy_headers_parsing", + "True", + group='oslo_middleware' + ) + + # Create the application. + super(TestHTTPProxyToWSGIMiddleware, self).setUp() diff --git a/releasenotes/notes/add-cors-support-ce060ee8513a1acf.yaml b/releasenotes/notes/add-cors-support-ce060ee8513a1acf.yaml new file mode 100644 index 00000000..a3d9e13c --- /dev/null +++ b/releasenotes/notes/add-cors-support-ce060ee8513a1acf.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Add CORS and HTTPProxyToWSGI support based on oslo_middleware in front + of the Qinling API. The purpose of this middleware is to set up the + request URL correctly in the case there is a proxy (for instance, a + loadbalancer such as HAProxy) in front of the Qinling API. + The HTTPProxyToWSGI is off by default and needs to be enabled via a + configuration value. diff --git a/requirements.txt b/requirements.txt index 57f5a4a4..9977aab8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ oslo.concurrency>=3.26.0 # Apache-2.0 oslo.config>=5.2.0 # Apache-2.0 oslo.db>=4.27.0 # Apache-2.0 oslo.messaging>=5.29.0 # Apache-2.0 +oslo.middleware>=3.35.0 # Apache-2.0 oslo.policy>=1.30.0 # Apache-2.0 oslo.upgradecheck>=0.1.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index a04b3893..2e7a5d12 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,8 +30,11 @@ console_scripts = qinling-db-manage = qinling.db.sqlalchemy.migration.cli:main qinling-status = qinling.cmd.status:main +wsgi_scripts = + qinling-wsgi-api = qinling.api.app:init_wsgi + qinling.storage.provider: - local = qinling.storage.file_system:FileSystemStorage + local = qinling.storage.file_system:FileSystemStorage qinling.orchestrator = kubernetes = qinling.orchestrator.kubernetes.manager:KubernetesManager @@ -39,6 +42,9 @@ qinling.orchestrator = oslo.config.opts = qinling.config = qinling.config:list_opts +oslo.config.opts.defaults = + qinling.config = qinling.config:set_cors_middleware_defaults + tempest.test_plugins = qinling_test = qinling_tempest_plugin.plugin:QinlingTempestPlugin diff --git a/tools/config/config-generator.qinling.conf b/tools/config/config-generator.qinling.conf index 3d57c9a4..9f9ac7da 100644 --- a/tools/config/config-generator.qinling.conf +++ b/tools/config/config-generator.qinling.conf @@ -2,6 +2,8 @@ namespace = qinling.config namespace = keystonemiddleware.auth_token namespace = oslo.messaging +namespace = oslo.middleware.cors +namespace = oslo.middleware.http_proxy_to_wsgi namespace = oslo.log namespace = oslo.policy namespace = oslo.db