Load wsgi app(api) with paste.deploy

This change replaces the hard coded WSGI app creation with a pipeline
of WSGI apps declared in a configuration file.
Paste Deploy was used to create the pipeline since it is used by many other
OpenStack projects and it is an active project
with new contributors and supports Python 3. Dependency on Paste is
localized so switching to another library would not be hard if OpenStack moves
to another package in the future.

Change-Id: I9a45f974c2c8c67a01748583639e6a6248003b85
Closes-Bug:#1551134
This commit is contained in:
Aaron-DH 2016-03-04 14:34:14 +08:00
parent 0563743f4d
commit db378a0ee5
15 changed files with 177 additions and 145 deletions

View File

@ -44,6 +44,7 @@ MAGNUM_AUTH_CACHE_DIR=${MAGNUM_AUTH_CACHE_DIR:-/var/cache/magnum}
MAGNUM_CONF_DIR=/etc/magnum MAGNUM_CONF_DIR=/etc/magnum
MAGNUM_CONF=$MAGNUM_CONF_DIR/magnum.conf MAGNUM_CONF=$MAGNUM_CONF_DIR/magnum.conf
MAGNUM_POLICY_JSON=$MAGNUM_CONF_DIR/policy.json MAGNUM_POLICY_JSON=$MAGNUM_CONF_DIR/policy.json
MAGNUM_API_PASTE=$MAGNUM_CONF_DIR/api-paste.ini
if is_ssl_enabled_service "magnum" || is_service_enabled tls-proxy; then if is_ssl_enabled_service "magnum" || is_service_enabled tls-proxy; then
MAGNUM_SERVICE_PROTOCOL="https" MAGNUM_SERVICE_PROTOCOL="https"
@ -95,6 +96,8 @@ function configure_magnum {
# Rebuild the config file from scratch # Rebuild the config file from scratch
create_magnum_conf create_magnum_conf
create_api_paste_conf
update_heat_policy update_heat_policy
} }
@ -206,6 +209,11 @@ function create_magnum_conf {
iniset $MAGNUM_CONF cinder_client region_name $REGION_NAME iniset $MAGNUM_CONF cinder_client region_name $REGION_NAME
} }
function create_api_paste_conf {
# copy api_paste.ini
cp $MAGNUM_DIR/etc/magnum/api-paste.ini $MAGNUM_API_PASTE
}
function update_heat_policy { function update_heat_policy {
# enable stacks globel_index search so that magnum can use # enable stacks globel_index search so that magnum can use
# list(global_tenant=True) # list(global_tenant=True)

19
etc/magnum/api-paste.ini Normal file
View File

@ -0,0 +1,19 @@
[pipeline:main]
pipeline = cors request_id authtoken api_v1
[app:api_v1]
paste.app_factory = magnum.api.app:app_factory
[filter:authtoken]
acl_public_routes = /, /v1
paste.filter_factory = magnum.api.middleware.auth_token:AuthTokenMiddleware.factory
[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = magnum
latent_allow_methods = GET, PUT, POST, DELETE, PATCH
latent_allow_headers = X-Auth-Token, X-Identity-Status, X-Roles, X-Service-Catalog, X-User-Id, X-Tenant-Id, X-OpenStack-Request-ID
latent_expose_headers = X-Auth-Token, X-Subject-Token, X-Service-Token, X-OpenStack-Request-ID

View File

@ -9,14 +9,16 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import os
from oslo_config import cfg from oslo_config import cfg
from oslo_middleware import cors from oslo_log import log
from paste import deploy
import pecan import pecan
from magnum.api import auth
from magnum.api import config as api_config from magnum.api import config as api_config
from magnum.api import middleware from magnum.api import middleware
from magnum.i18n import _
# Register options for the service # Register options for the service
API_SERVICE_OPTS = [ API_SERVICE_OPTS = [
@ -29,7 +31,11 @@ API_SERVICE_OPTS = [
cfg.IntOpt('max_limit', cfg.IntOpt('max_limit',
default=1000, default=1000,
help='The maximum number of items returned in a single ' help='The maximum number of items returned in a single '
'response from a collection resource.') 'response from a collection resource.'),
cfg.StrOpt('api_paste_config',
default="api-paste.ini",
help="Configuration file for WSGI definition of API."
)
] ]
CONF = cfg.CONF CONF = cfg.CONF
@ -38,6 +44,8 @@ opt_group = cfg.OptGroup(name='api',
CONF.register_group(opt_group) CONF.register_group(opt_group)
CONF.register_opts(API_SERVICE_OPTS, opt_group) CONF.register_opts(API_SERVICE_OPTS, opt_group)
LOG = log.getLogger(__name__)
def get_pecan_config(): def get_pecan_config():
# Set up the pecan configuration # Set up the pecan configuration
@ -58,17 +66,22 @@ def setup_app(config=None):
**app_conf **app_conf
) )
app = auth.install(app, CONF, config.app.acl_public_routes)
# CORS must be the last one.
app = cors.CORS(app, CONF)
app.set_latent(
allow_headers=['X-Auth-Token', 'X-Identity-Status', 'X-Roles',
'X-Service-Catalog', 'X-User-Id', 'X-Tenant-Id',
'X-OpenStack-Request-ID', 'X-Server-Management-Url'],
allow_methods=['GET', 'PUT', 'POST', 'DELETE', 'PATCH'],
expose_headers=['X-Auth-Token', 'X-Subject-Token', 'X-Service-Token',
'X-OpenStack-Request-ID', 'X-Server-Management-Url']
)
return app return app
def load_app():
cfg_file = None
cfg_path = cfg.CONF.api.api_paste_config
if not os.path.isabs(cfg_path):
cfg_file = CONF.find_file(cfg_path)
elif os.path.exists(cfg_path):
cfg_file = cfg_path
if not cfg_file:
raise cfg.ConfigFilesNotFoundError([cfg.CONF.api_paste_config])
LOG.info(_("Full WSGI config used: %s") % cfg_file)
return deploy.loadapp("config:" + cfg_file)
def app_factory(global_config, **local_conf):
return setup_app()

View File

@ -1,46 +0,0 @@
# Copyright 2012 New Dream Network, LLC (DreamHost)
#
# 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.
"""Access Control Lists (ACL's) control access the API server."""
from oslo_config import cfg
from magnum.api.middleware import auth_token
AUTH_OPTS = [
cfg.BoolOpt('enable_authentication',
default=True,
help='This option enables or disables user authentication '
'via Keystone. Default value is True.'),
]
CONF = cfg.CONF
CONF.register_opts(AUTH_OPTS)
def install(app, conf, public_routes):
"""Install ACL check on application.
:param app: A WSGI application.
:param conf: Settings. Dict'ified and passed to keystone middleware
:param public_routes: The list of the routes which will be allowed to
access without authentication.
:return: The same WSGI application with ACL installed.
"""
if not cfg.CONF.get('enable_authentication'):
return app
return auth_token.AuthTokenMiddleware(app,
conf=dict(conf),
public_api_routes=public_routes)

View File

@ -58,3 +58,12 @@ class AuthTokenMiddleware(auth_token.AuthProtocol):
return self._app(env, start_response) return self._app(env, start_response)
return super(AuthTokenMiddleware, self).__call__(env, start_response) return super(AuthTokenMiddleware, self).__call__(env, start_response)
@classmethod
def factory(cls, global_config, **local_conf):
public_routes = local_conf.get('acl_public_routes', '')
public_api_routes = [path.strip() for path in public_routes.split(',')]
def _factory(app):
return cls(app, global_config, public_api_routes=public_api_routes)
return _factory

View File

@ -40,7 +40,7 @@ def main():
# Enable object backporting via the conductor # Enable object backporting via the conductor
base.MagnumObject.indirection_api = base.MagnumObjectIndirectionAPI() base.MagnumObject.indirection_api = base.MagnumObjectIndirectionAPI()
app = api_app.setup_app() app = api_app.load_app()
# Create the WSGI server and start it # Create the WSGI server and start it
host, port = cfg.CONF.api.host, cfg.CONF.api.port host, port = cfg.CONF.api.host, cfg.CONF.api.port

View File

@ -16,7 +16,6 @@
import itertools import itertools
import magnum.api.app import magnum.api.app
import magnum.api.auth
import magnum.api.validation import magnum.api.validation
import magnum.common.cert_manager import magnum.common.cert_manager
from magnum.common.cert_manager import local_cert_manager from magnum.common.cert_manager import local_cert_manager
@ -35,8 +34,7 @@ import magnum.db
def list_opts(): def list_opts():
return [ return [
('DEFAULT', ('DEFAULT',
itertools.chain(magnum.api.auth.AUTH_OPTS, itertools.chain(magnum.common.paths.PATH_OPTS,
magnum.common.paths.PATH_OPTS,
magnum.common.utils.UTILS_OPTS, magnum.common.utils.UTILS_OPTS,
magnum.common.rpc_service.periodic_opts, magnum.common.rpc_service.periodic_opts,
magnum.common.service.service_opts, magnum.common.service.service_opts,

View File

@ -54,8 +54,6 @@ class FunctionalTest(base.DbTestCase):
'modules': ['magnum.api'], 'modules': ['magnum.api'],
'static_root': '%s/public' % root_dir, 'static_root': '%s/public' % root_dir,
'template_path': '%s/api/templates' % root_dir, 'template_path': '%s/api/templates' % root_dir,
'enable_acl': False,
'acl_public_routes': ['/', '/v1'],
'hooks': [ 'hooks': [
hooks.ContextHook(), hooks.ContextHook(),
hooks.RPCHook(), hooks.RPCHook(),
@ -83,12 +81,10 @@ class FunctionalTest(base.DbTestCase):
for attr in attrs: for attr in attrs:
verify_method(attr, response) verify_method(attr, response)
def _make_app(self, config=None, enable_acl=False): def _make_app(self, config=None):
if not config: if not config:
config = self.config config = self.config
config["app"]["enable_acl"] = enable_acl
return pecan.testing.load_test_app(config) return pecan.testing.load_test_app(config)
def _request_json(self, path, params, expect_errors=False, headers=None, def _request_json(self, path, params, expect_errors=False, headers=None,

View File

@ -0,0 +1,18 @@
[pipeline:main]
pipeline = cors request_id authtoken api_v1
[app:api_v1]
paste.app_factory = magnum.api.app:app_factory
[filter:authtoken]
paste.filter_factory = magnum.api.middleware.auth_token:AuthTokenMiddleware.factory
[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = magnum
latent_allow_methods = GET, PUT, POST, DELETE, PATCH
latent_allow_headers = X-Auth-Token, X-Identity-Status, X-Roles, X-Service-Catalog, X-User-Id, X-Tenant-Id, X-OpenStack-Request-ID
latent_expose_headers = X-Auth-Token, X-Subject-Token, X-Service-Token, X-OpenStack-Request-ID

View File

@ -0,0 +1,19 @@
[pipeline:main]
pipeline = cors request_id authtoken api_v1
[app:api_v1]
paste.app_factory = magnum.api.app:app_factory
[filter:authtoken]
acl_public_routes = /
paste.filter_factory = magnum.api.middleware.auth_token:AuthTokenMiddleware.factory
[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = magnum
latent_allow_methods = GET, PUT, POST, DELETE, PATCH
latent_allow_headers = X-Auth-Token, X-Identity-Status, X-Roles, X-Service-Catalog, X-User-Id, X-Tenant-Id, X-OpenStack-Request-ID
latent_expose_headers = X-Auth-Token, X-Subject-Token, X-Service-Token, X-OpenStack-Request-ID

View File

@ -0,0 +1,19 @@
[pipeline:main]
pipeline = cors request_id authtoken api_v1
[app:api_v1]
paste.app_factory = magnum.api.app:app_factory
[filter:authtoken]
acl_public_routes = /v1
paste.filter_factory = magnum.api.middleware.auth_token:AuthTokenMiddleware.factory
[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = magnum
latent_allow_methods = GET, PUT, POST, DELETE, PATCH
latent_allow_headers = X-Auth-Token, X-Identity-Status, X-Roles, X-Service-Catalog, X-User-Id, X-Tenant-Id, X-OpenStack-Request-ID
latent_expose_headers = X-Auth-Token, X-Subject-Token, X-Service-Token, X-OpenStack-Request-ID

View File

@ -0,0 +1,19 @@
[pipeline:main]
pipeline = cors request_id api_v1
[app:api_v1]
paste.app_factory = magnum.api.app:app_factory
[filter:authtoken]
acl_public_routes = /
paste.filter_factory = magnum.api.middleware.auth_token:AuthTokenMiddleware.factory
[filter:request_id]
paste.filter_factory = oslo_middleware:RequestId.factory
[filter:cors]
paste.filter_factory = oslo_middleware.cors:filter_factory
oslo_config_project = magnum
latent_allow_methods = GET, PUT, POST, DELETE, PATCH
latent_allow_headers = X-Auth-Token, X-Identity-Status, X-Roles, X-Service-Catalog, X-User-Id, X-Tenant-Id, X-OpenStack-Request-ID
latent_expose_headers = X-Auth-Token, X-Subject-Token, X-Service-Token, X-OpenStack-Request-ID

View File

@ -10,12 +10,13 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import copy
import mock import mock
from oslo_config import cfg from oslo_config import cfg
from webob import exc as webob_exc from webob import exc as webob_exc
import webtest
from magnum.api import app
from magnum.api.controllers import v1 as v1_api from magnum.api.controllers import v1 as v1_api
from magnum.tests import base as test_base from magnum.tests import base as test_base
from magnum.tests.unit.api import base as api_base from magnum.tests.unit.api import base as api_base
@ -84,6 +85,11 @@ class TestRootController(api_base.FunctionalTest):
{u'href': u'http://localhost/mservices/', {u'href': u'http://localhost/mservices/',
u'rel': u'bookmark'}]} u'rel': u'bookmark'}]}
def make_app(self, paste_file):
file_name = self.get_path(paste_file)
cfg.CONF.set_override("api_paste_config", file_name, group="api")
return webtest.TestApp(app.load_app())
def test_version(self): def test_version(self):
response = self.app.get('/') response = self.app.get('/')
self.assertEqual(self.root_expected, response.json) self.assertEqual(self.root_expected, response.json)
@ -96,13 +102,10 @@ class TestRootController(api_base.FunctionalTest):
response = self.app.get('/a/bogus/url', expect_errors=True) response = self.app.get('/a/bogus/url', expect_errors=True)
assert response.status_int == 404 assert response.status_int == 404
def test_acl_access_with_all(self): def test_noauth(self):
cfg.CONF.set_override("enable_authentication", True) # Don't need to auth
paste_file = "magnum/tests/unit/api/controllers/noauth-paste.ini"
config = copy.deepcopy(self.config) app = self.make_app(paste_file)
# Both / and /v1 and access without auth
config["app"]["acl_public_routes"] = ['/', '/v1']
app = self._make_app(config=config)
response = app.get('/') response = app.get('/')
self.assertEqual(self.root_expected, response.json) self.assertEqual(self.root_expected, response.json)
@ -110,13 +113,24 @@ class TestRootController(api_base.FunctionalTest):
response = app.get('/v1/') response = app.get('/v1/')
self.assertEqual(self.v1_expected, response.json) self.assertEqual(self.v1_expected, response.json)
def test_acl_access_with_root(self): response = app.get('/v1/baymodels')
cfg.CONF.set_override("enable_authentication", True) self.assertEqual(200, response.status_int)
config = copy.deepcopy(self.config) def test_auth_with_no_public_routes(self):
# All apis need auth when access
paste_file = "magnum/tests/unit/api/controllers/auth-paste.ini"
app = self.make_app(paste_file)
response = app.get('/', expect_errors=True)
self.assertEqual(401, response.status_int)
response = app.get('/v1/', expect_errors=True)
self.assertEqual(401, response.status_int)
def test_auth_with_root_access(self):
# Only / can access without auth # Only / can access without auth
config["app"]["acl_public_routes"] = ['/'] paste_file = "magnum/tests/unit/api/controllers/auth-root-access.ini"
app = self._make_app(config=config) app = self.make_app(paste_file)
response = app.get('/') response = app.get('/')
self.assertEqual(self.root_expected, response.json) self.assertEqual(self.root_expected, response.json)
@ -124,13 +138,13 @@ class TestRootController(api_base.FunctionalTest):
response = app.get('/v1/', expect_errors=True) response = app.get('/v1/', expect_errors=True)
self.assertEqual(401, response.status_int) self.assertEqual(401, response.status_int)
def test_acl_access_with_v1(self): response = app.get('/v1/baymodels', expect_errors=True)
cfg.CONF.set_override("enable_authentication", True) self.assertEqual(401, response.status_int)
config = copy.deepcopy(self.config) def test_auth_with_v1_access(self):
# Only /v1 can access without auth # Only /v1 can access without auth
config["app"]["acl_public_routes"] = ['/v1'] paste_file = "magnum/tests/unit/api/controllers/auth-v1-access.ini"
app = self._make_app(config=config) app = self.make_app(paste_file)
response = app.get('/', expect_errors=True) response = app.get('/', expect_errors=True)
self.assertEqual(401, response.status_int) self.assertEqual(401, response.status_int)
@ -138,17 +152,7 @@ class TestRootController(api_base.FunctionalTest):
response = app.get('/v1/') response = app.get('/v1/')
self.assertEqual(self.v1_expected, response.json) self.assertEqual(self.v1_expected, response.json)
def test_acl_with_neither(self): response = app.get('/v1/baymodels', expect_errors=True)
cfg.CONF.set_override("enable_authentication", True)
config = copy.deepcopy(self.config)
config["app"]["acl_public_routes"] = []
app = self._make_app(config=config)
response = app.get('/', expect_errors=True)
self.assertEqual(401, response.status_int)
response = app.get('/v1/', expect_errors=True)
self.assertEqual(401, response.status_int) self.assertEqual(401, response.status_int)

View File

@ -1,41 +0,0 @@
# Copyright 2014
# The Cloudscaling Group, 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.
import mock
from oslo_config import fixture
from magnum.api import auth
from magnum.tests import base
from magnum.tests import fakes
@mock.patch('magnum.api.middleware.auth_token.AuthTokenMiddleware',
new_callable=fakes.FakeAuthProtocol)
class TestAuth(base.BaseTestCase):
def setUp(self):
super(TestAuth, self).setUp()
self.CONF = self.useFixture(fixture.Config())
self.app = fakes.FakeApp()
def test_check_auth_option_enabled(self, mock_auth):
self.CONF.config(enable_authentication=True)
result = auth.install(self.app, self.CONF.conf, ['/'])
self.assertIsInstance(result, fakes.FakeAuthProtocol)
def test_check_auth_option_disabled(self, mock_auth):
self.CONF.config(enable_authentication=False)
result = auth.install(self.app, self.CONF.conf, ['/'])
self.assertIsInstance(result, fakes.FakeApp)

View File

@ -31,8 +31,6 @@ from magnum.tests import base
CONF = cfg.CONF CONF = cfg.CONF
CONF.import_opt('enable_authentication', 'magnum.api.auth')
_DB_CACHE = None _DB_CACHE = None
@ -88,7 +86,6 @@ class Database(fixtures.Fixture):
class DbTestCase(base.TestCase): class DbTestCase(base.TestCase):
def setUp(self): def setUp(self):
cfg.CONF.set_override("enable_authentication", False)
super(DbTestCase, self).setUp() super(DbTestCase, self).setUp()
self.dbapi = dbapi.get_instance() self.dbapi = dbapi.get_instance()