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=$MAGNUM_CONF_DIR/magnum.conf
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
MAGNUM_SERVICE_PROTOCOL="https"
@ -95,6 +96,8 @@ function configure_magnum {
# Rebuild the config file from scratch
create_magnum_conf
create_api_paste_conf
update_heat_policy
}
@ -206,6 +209,11 @@ function create_magnum_conf {
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 {
# enable stacks globel_index search so that magnum can use
# 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.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
from oslo_config import cfg
from oslo_middleware import cors
from oslo_log import log
from paste import deploy
import pecan
from magnum.api import auth
from magnum.api import config as api_config
from magnum.api import middleware
from magnum.i18n import _
# Register options for the service
API_SERVICE_OPTS = [
@ -29,7 +31,11 @@ API_SERVICE_OPTS = [
cfg.IntOpt('max_limit',
default=1000,
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
@ -38,6 +44,8 @@ opt_group = cfg.OptGroup(name='api',
CONF.register_group(opt_group)
CONF.register_opts(API_SERVICE_OPTS, opt_group)
LOG = log.getLogger(__name__)
def get_pecan_config():
# Set up the pecan configuration
@ -58,17 +66,22 @@ def setup_app(config=None):
**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
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 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
base.MagnumObject.indirection_api = base.MagnumObjectIndirectionAPI()
app = api_app.setup_app()
app = api_app.load_app()
# Create the WSGI server and start it
host, port = cfg.CONF.api.host, cfg.CONF.api.port

View File

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

View File

@ -54,8 +54,6 @@ class FunctionalTest(base.DbTestCase):
'modules': ['magnum.api'],
'static_root': '%s/public' % root_dir,
'template_path': '%s/api/templates' % root_dir,
'enable_acl': False,
'acl_public_routes': ['/', '/v1'],
'hooks': [
hooks.ContextHook(),
hooks.RPCHook(),
@ -83,12 +81,10 @@ class FunctionalTest(base.DbTestCase):
for attr in attrs:
verify_method(attr, response)
def _make_app(self, config=None, enable_acl=False):
def _make_app(self, config=None):
if not config:
config = self.config
config["app"]["enable_acl"] = enable_acl
return pecan.testing.load_test_app(config)
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
# limitations under the License.
import copy
import mock
from oslo_config import cfg
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.tests import base as test_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'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):
response = self.app.get('/')
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)
assert response.status_int == 404
def test_acl_access_with_all(self):
cfg.CONF.set_override("enable_authentication", True)
config = copy.deepcopy(self.config)
# Both / and /v1 and access without auth
config["app"]["acl_public_routes"] = ['/', '/v1']
app = self._make_app(config=config)
def test_noauth(self):
# Don't need to auth
paste_file = "magnum/tests/unit/api/controllers/noauth-paste.ini"
app = self.make_app(paste_file)
response = app.get('/')
self.assertEqual(self.root_expected, response.json)
@ -110,13 +113,24 @@ class TestRootController(api_base.FunctionalTest):
response = app.get('/v1/')
self.assertEqual(self.v1_expected, response.json)
def test_acl_access_with_root(self):
cfg.CONF.set_override("enable_authentication", True)
response = app.get('/v1/baymodels')
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
config["app"]["acl_public_routes"] = ['/']
app = self._make_app(config=config)
paste_file = "magnum/tests/unit/api/controllers/auth-root-access.ini"
app = self.make_app(paste_file)
response = app.get('/')
self.assertEqual(self.root_expected, response.json)
@ -124,13 +138,13 @@ class TestRootController(api_base.FunctionalTest):
response = app.get('/v1/', expect_errors=True)
self.assertEqual(401, response.status_int)
def test_acl_access_with_v1(self):
cfg.CONF.set_override("enable_authentication", True)
response = app.get('/v1/baymodels', expect_errors=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
config["app"]["acl_public_routes"] = ['/v1']
app = self._make_app(config=config)
paste_file = "magnum/tests/unit/api/controllers/auth-v1-access.ini"
app = self.make_app(paste_file)
response = app.get('/', expect_errors=True)
self.assertEqual(401, response.status_int)
@ -138,17 +152,7 @@ class TestRootController(api_base.FunctionalTest):
response = app.get('/v1/')
self.assertEqual(self.v1_expected, response.json)
def test_acl_with_neither(self):
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)
response = app.get('/v1/baymodels', expect_errors=True)
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.import_opt('enable_authentication', 'magnum.api.auth')
_DB_CACHE = None
@ -88,7 +86,6 @@ class Database(fixtures.Fixture):
class DbTestCase(base.TestCase):
def setUp(self):
cfg.CONF.set_override("enable_authentication", False)
super(DbTestCase, self).setUp()
self.dbapi = dbapi.get_instance()