Use HTTPProxyToWSGI middleware from oslo

currently it is impossible to use ironic-api for both internal and
public api at the same time when both of those are using (ssl
terminating) proxies as there's only one config option to override the
resource url's in responses ([api]public_endpoint).

This patch adds the http_proxy_to_wsgi middleware from oslo.middleware
to the ironic API service, which, with properly configured proxies,
makes the choice of correct URL automatic, and thus makes such scenario
possible.

As this middleware may potentially not properly handle some
endpoint URL schemas, leave the api.public_endpoint option as a backup,
but it will be ignored when proxy headers parsing is enabled.

Change-Id: I3ce6b0726b479c2835f8777957b2cb12d8098aec
Story: #2006303
Task: #36019
This commit is contained in:
Pavlo Shchelokovskyy 2019-07-25 23:14:41 +03:00
parent de31b6ada3
commit 5e8c966a40
7 changed files with 88 additions and 7 deletions

View File

@ -1359,9 +1359,9 @@ function configure_ironic_api {
configure_auth_token_middleware $IRONIC_CONF_FILE ironic $IRONIC_AUTH_CACHE_DIR/api
if [[ "$IRONIC_USE_WSGI" == "True" ]]; then
iniset $IRONIC_CONF_FILE api public_endpoint $IRONIC_SERVICE_PROTOCOL://$IRONIC_HOSTPORT
iniset $IRONIC_CONF_FILE oslo_middleware enable_proxy_headers_parsing True
elif is_service_enabled tls-proxy; then
iniset $IRONIC_CONF_FILE api public_endpoint $IRONIC_SERVICE_PROTOCOL://$IRONIC_HOSTPORT
iniset $IRONIC_CONF_FILE oslo_middleware enable_proxy_headers_parsing True
iniset $IRONIC_CONF_FILE api port $IRONIC_SERVICE_PORT_INT
else
iniset $IRONIC_CONF_FILE api port $IRONIC_SERVICE_PORT

View File

@ -19,6 +19,7 @@ import keystonemiddleware.audit as audit_middleware
from oslo_config import cfg
import oslo_middleware.cors as cors_middleware
from oslo_middleware import healthcheck
from oslo_middleware import http_proxy_to_wsgi
import osprofiler.web as osprofiler_web
import pecan
@ -104,6 +105,10 @@ def setup_app(pecan_config=None, extra_hooks=None):
if CONF.profiler.enabled:
app = osprofiler_web.WsgiMiddleware(app)
# NOTE(pas-ha) this registers oslo_middleware.enable_proxy_headers_parsing
# option, when disabled (default) this is noop middleware
app = http_proxy_to_wsgi.HTTPProxyToWSGI(app, CONF)
# add in the healthcheck middleware if enabled
# NOTE(jroll) this is after the auth token middleware as we don't want auth
# in front of this, and WSGI works from the outside in. Requests to

View File

@ -175,5 +175,8 @@ class PublicUrlHook(hooks.PecanHook):
"""
def before(self, state):
state.request.public_url = (cfg.CONF.api.public_endpoint
or state.request.host_url)
if cfg.CONF.oslo_middleware.enable_proxy_headers_parsing:
state.request.public_url = state.request.application_url
else:
state.request.public_url = (cfg.CONF.api.public_endpoint
or state.request.host_url)

View File

@ -36,7 +36,10 @@ opts = [
" If None the links will be built using the request's "
"host URL. If the API is operating behind a proxy, you "
"will want to change this to represent the proxy's URL. "
"Defaults to None.")),
"Defaults to None. "
"Ignored when proxy headers parsing is enabled via "
"[oslo_middleware]enable_proxy_headers_parsing option.")
),
cfg.IntOpt('api_workers',
help=_('Number of workers for OpenStack Ironic API service. '
'The default is equal to the number of CPUs available '
@ -48,8 +51,10 @@ opts = [
"requests via HTTPS instead of HTTP. If there is a "
"front-end service performing HTTPS offloading from "
"the service, this option should be False; note, you "
"will want to change public API endpoint to represent "
"SSL termination URL with 'public_endpoint' option.")),
"will want to enable proxy headers parsing with "
"[oslo_middleware]enable_proxy_headers_parsing "
"option or configure [api]public_endpoint option "
"to set URLs in responses to the SSL terminated one.")),
cfg.BoolOpt('restrict_lookup',
default=True,
help=_('Whether to restrict the lookup API to only nodes '

View File

@ -0,0 +1,54 @@
# 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 proxy headers middleware works as expected.
"""
from oslo_config import cfg
from ironic.tests.unit.api import base
CONF = cfg.CONF
class TestProxyHeadersMiddleware(base.BaseApiTest):
"""Provide a basic smoke test to ensure proxy headers middleware works."""
def setUp(self):
CONF.set_override('public_endpoint', 'http://spam.ham/eggs',
group='api')
self.proxy_headers = {"X-Forwarded-Proto": "https",
"X-Forwarded-Host": "mycloud.com",
"X-Forwarded-Prefix": "/ironic"}
super(TestProxyHeadersMiddleware, self).setUp()
def test_proxy_headers_enabled(self):
"""Test enabled proxy headers middleware overriding public_endpoint"""
# NOTE(pas-ha) setting config option and re-creating app
# as the middleware registers its config option on instantiation
CONF.set_override('enable_proxy_headers_parsing', True,
group='oslo_middleware')
self.app = self._make_app()
response = self.get_json('/', path_prefix="",
headers=self.proxy_headers)
href = response["default_version"]["links"][0]["href"]
self.assertTrue(href.startswith("https://mycloud.com/ironic"))
def test_proxy_headers_disabled(self):
"""Test proxy headers middleware disabled by default"""
response = self.get_json('/', path_prefix="",
headers=self.proxy_headers)
href = response["default_version"]["links"][0]["href"]
# check that [api]public_endpoint is used when proxy headers parsing
# is disabled
self.assertTrue(href.startswith("http://spam.ham/eggs"))

View File

@ -0,0 +1,13 @@
---
features:
- |
Ironic API service now supports HTTP proxy headers parsing
with the help of oslo.middleware package, enabled via new option
``[oslo_middleware]/enable_proxy_headers_parsing`` (``False`` by default).
This enables more complex setups of Ironic API service, for example when
the same service instance serves both internal and public API endpoints
via separate proxies.
When proxy headers parsing is enabled, the value of
``[api]/public_endpoint`` option is ignored.

View File

@ -13,6 +13,7 @@ namespace = oslo.db
namespace = oslo.messaging
namespace = oslo.middleware.cors
namespace = oslo.middleware.healthcheck
namespace = oslo.middleware.http_proxy_to_wsgi
namespace = oslo.concurrency
namespace = oslo.policy
namespace = oslo.log