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
This commit is contained in:
Vlad Gusev 2019-03-07 15:38:57 +03:00
parent 608367f28d
commit ca1acb656c
8 changed files with 119 additions and 4 deletions

View File

@ -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)

View File

@ -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',

View File

@ -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'))

View File

@ -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):

View File

@ -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()

View File

@ -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']
)

View File

@ -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 '',

View File

@ -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