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:
parent
0563743f4d
commit
db378a0ee5
@ -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
19
etc/magnum/api-paste.ini
Normal 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
|
@ -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()
|
||||
|
@ -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)
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
18
magnum/tests/unit/api/controllers/auth-paste.ini
Normal file
18
magnum/tests/unit/api/controllers/auth-paste.ini
Normal 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
|
19
magnum/tests/unit/api/controllers/auth-root-access.ini
Normal file
19
magnum/tests/unit/api/controllers/auth-root-access.ini
Normal 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
|
19
magnum/tests/unit/api/controllers/auth-v1-access.ini
Normal file
19
magnum/tests/unit/api/controllers/auth-v1-access.ini
Normal 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
|
19
magnum/tests/unit/api/controllers/noauth-paste.ini
Normal file
19
magnum/tests/unit/api/controllers/noauth-paste.ini
Normal 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
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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)
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user