From ca1acb656cbd1ec30e327fa67cd9f6e75345b14f Mon Sep 17 00:00:00 2001 From: Vlad Gusev Date: Thu, 7 Mar 2019 15:38:57 +0300 Subject: [PATCH] Add http_proxy_to_wsgi middleware This sets up the HTTPProxyToWSGI middleware in front of Mistral 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 Mistral API. The HTTPProxyToWSGI is off by default and needs to be enabled via a configuration value. It can be enabled with the option in mistral.conf: [oslo_middleware] enable_proxy_headers_parsing=True Closes-Bug: #1590608 Closes-Bug: #1816364 Change-Id: I04ba85488b27cb05c3b81ad8c973c3cc3fe56d36 --- mistral/api/app.py | 4 ++ mistral/api/controllers/root.py | 2 +- mistral/api/controllers/v2/root.py | 3 +- mistral/context.py | 2 +- .../tests/unit/api/test_oslo_middleware.py | 42 ++++++++++++ mistral/tests/unit/api/v2/test_root.py | 67 +++++++++++++++++++ mistral/utils/rest_utils.py | 2 +- tools/config/config-generator.mistral.conf | 1 + 8 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 mistral/tests/unit/api/test_oslo_middleware.py diff --git a/mistral/api/app.py b/mistral/api/app.py index 9643bb9e3..0e02f3576 100644 --- a/mistral/api/app.py +++ b/mistral/api/app.py @@ -15,6 +15,7 @@ from oslo_config import cfg import oslo_middleware.cors as cors_middleware +import oslo_middleware.http_proxy_to_wsgi as http_proxy_to_wsgi_middleware import osprofiler.web import pecan @@ -82,6 +83,9 @@ def setup_app(config=None): enabled=cfg.CONF.profiler.enabled ) + # 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) diff --git a/mistral/api/controllers/root.py b/mistral/api/controllers/root.py index 9183b9359..355c3b1ba 100644 --- a/mistral/api/controllers/root.py +++ b/mistral/api/controllers/root.py @@ -67,7 +67,7 @@ class RootController(object): def index(self): LOG.debug("Fetching API versions.") - host_url_v2 = '%s/%s' % (pecan.request.host_url, 'v2') + host_url_v2 = '%s/%s' % (pecan.request.application_url, 'v2') api_v2 = APIVersion( id='v2.0', status='CURRENT', diff --git a/mistral/api/controllers/v2/root.py b/mistral/api/controllers/v2/root.py index 1c5b1f74d..156659f1a 100644 --- a/mistral/api/controllers/v2/root.py +++ b/mistral/api/controllers/v2/root.py @@ -59,4 +59,5 @@ class Controller(object): @wsme_pecan.wsexpose(RootResource) def index(self): - return RootResource(uri='%s/%s' % (pecan.request.host_url, 'v2')) + return RootResource(uri='%s/%s' % (pecan.request.application_url, + 'v2')) diff --git a/mistral/context.py b/mistral/context.py index 567a8e582..bf48a6f62 100644 --- a/mistral/context.py +++ b/mistral/context.py @@ -31,7 +31,7 @@ from mistral import utils CONF = cfg.CONF _CTX_THREAD_LOCAL_NAME = "MISTRAL_APP_CTX_THREAD_LOCAL" -ALLOWED_WITHOUT_AUTH = ['/', '/v2/'] +ALLOWED_WITHOUT_AUTH = ['/', '/v2/', '/workflowv2/', '/workflowv2/v2/'] class MistralContext(oslo_context.RequestContext): diff --git a/mistral/tests/unit/api/test_oslo_middleware.py b/mistral/tests/unit/api/test_oslo_middleware.py new file mode 100644 index 000000000..ab7038dcf --- /dev/null +++ b/mistral/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 mistral.tests.unit.api import base +from oslo_config import cfg +from oslo_middleware import http_proxy_to_wsgi as http_proxy_to_wsgi_middleware + + +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/mistral/tests/unit/api/v2/test_root.py b/mistral/tests/unit/api/v2/test_root.py index f09807fc2..334581393 100644 --- a/mistral/tests/unit/api/v2/test_root.py +++ b/mistral/tests/unit/api/v2/test_root.py @@ -15,6 +15,7 @@ from oslo_serialization import jsonutils from mistral.tests.unit.api import base from mistral.tests.unit.api import test_auth +from mistral.tests.unit.api import test_oslo_middleware class TestRootController(base.APITest): @@ -71,3 +72,69 @@ class TestRootControllerWithAuth(test_auth.TestKeystoneMiddleware): 'http://localhost/v2', data['uri'] ) + + +class TestRootControllerWithHTTPProxyToWSGI(test_oslo_middleware. + TestHTTPProxyToWSGIMiddleware): + def test_index(self): + resp = self.app.get('/', headers={'Accept': 'application/json', + 'Host': 'localhost'}) + + self.assertEqual(200, resp.status_int) + + data = jsonutils.loads(resp.body.decode()) + data = data['versions'] + self.assertEqual('v2.0', data[0]['id']) + self.assertEqual('CURRENT', data[0]['status']) + self.assertEqual( + [{'href': 'http://localhost/v2', 'rel': 'self', 'target': 'v2'}], + data[0]['links'] + ) + + def test_v2_root(self): + resp = self.app.get('/v2/', headers={'Accept': 'application/json', + 'Host': 'localhost'}) + + self.assertEqual(200, resp.status_int) + + data = jsonutils.loads(resp.body.decode()) + + self.assertEqual( + 'http://localhost/v2', + data['uri'] + ) + + def test_index_with_prefix(self): + resp = self.app.get('/', + headers={'Accept': 'application/json', + 'Host': 'openstack', + 'X-Forwarded-Proto': 'https', + 'X-Forwarded-Prefix': '/workflowv2'}) + + self.assertEqual(200, resp.status_int) + + data = jsonutils.loads(resp.body.decode()) + data = data['versions'] + self.assertEqual('v2.0', data[0]['id']) + self.assertEqual('CURRENT', data[0]['status']) + self.assertEqual( + [{'href': 'https://openstack/workflowv2/v2', 'rel': 'self', + 'target': 'v2'}], + data[0]['links'] + ) + + def test_v2_root_with_prefix(self): + resp = self.app.get('/v2/', + headers={'Accept': 'application/json', + 'Host': 'openstack', + 'X-Forwarded-Proto': 'https', + 'X-Forwarded-Prefix': '/workflowv2'}) + + self.assertEqual(200, resp.status_int) + + data = jsonutils.loads(resp.body.decode()) + + self.assertEqual( + 'https://openstack/workflowv2/v2', + data['uri'] + ) diff --git a/mistral/utils/rest_utils.py b/mistral/utils/rest_utils.py index e941b40aa..70a146d64 100644 --- a/mistral/utils/rest_utils.py +++ b/mistral/utils/rest_utils.py @@ -235,7 +235,7 @@ def get_all(list_cls, cls, get_all_function, get_function, return list_cls.convert_with_links( rest_resources, limit, - pecan.request.host_url, + pecan.request.application_url, sort_keys=','.join(sort_keys), sort_dirs=','.join(sort_dirs), fields=','.join(fields) if fields else '', diff --git a/tools/config/config-generator.mistral.conf b/tools/config/config-generator.mistral.conf index 677a2301a..a6c9ac28d 100644 --- a/tools/config/config-generator.mistral.conf +++ b/tools/config/config-generator.mistral.conf @@ -3,6 +3,7 @@ namespace = mistral.config namespace = oslo.db namespace = oslo.messaging namespace = oslo.middleware.cors +namespace = oslo.middleware.http_proxy_to_wsgi namespace = keystonemiddleware.auth_token namespace = periodic.config namespace = oslo.log