From db378a0ee5e1f11d8aceb352c6be97a058c854a9 Mon Sep 17 00:00:00 2001 From: Aaron-DH Date: Fri, 4 Mar 2016 14:34:14 +0800 Subject: [PATCH] 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 --- devstack/lib/magnum | 8 +++ etc/magnum/api-paste.ini | 19 ++++++ magnum/api/app.py | 45 ++++++++----- magnum/api/auth.py | 46 ------------- magnum/api/middleware/auth_token.py | 9 +++ magnum/cmd/api.py | 2 +- magnum/opts.py | 4 +- magnum/tests/unit/api/base.py | 6 +- .../tests/unit/api/controllers/auth-paste.ini | 18 ++++++ .../unit/api/controllers/auth-root-access.ini | 19 ++++++ .../unit/api/controllers/auth-v1-access.ini | 19 ++++++ .../unit/api/controllers/noauth-paste.ini | 19 ++++++ .../tests/unit/api/controllers/test_root.py | 64 ++++++++++--------- magnum/tests/unit/api/test_auth.py | 41 ------------ magnum/tests/unit/db/base.py | 3 - 15 files changed, 177 insertions(+), 145 deletions(-) create mode 100644 etc/magnum/api-paste.ini delete mode 100644 magnum/api/auth.py create mode 100644 magnum/tests/unit/api/controllers/auth-paste.ini create mode 100644 magnum/tests/unit/api/controllers/auth-root-access.ini create mode 100644 magnum/tests/unit/api/controllers/auth-v1-access.ini create mode 100644 magnum/tests/unit/api/controllers/noauth-paste.ini delete mode 100644 magnum/tests/unit/api/test_auth.py diff --git a/devstack/lib/magnum b/devstack/lib/magnum index 94ae0a587c..c9eb2761c7 100644 --- a/devstack/lib/magnum +++ b/devstack/lib/magnum @@ -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) diff --git a/etc/magnum/api-paste.ini b/etc/magnum/api-paste.ini new file mode 100644 index 0000000000..fe36640cde --- /dev/null +++ b/etc/magnum/api-paste.ini @@ -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 diff --git a/magnum/api/app.py b/magnum/api/app.py index ff2d561315..b7e5596fb4 100644 --- a/magnum/api/app.py +++ b/magnum/api/app.py @@ -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() diff --git a/magnum/api/auth.py b/magnum/api/auth.py deleted file mode 100644 index ab36cb62cf..0000000000 --- a/magnum/api/auth.py +++ /dev/null @@ -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) diff --git a/magnum/api/middleware/auth_token.py b/magnum/api/middleware/auth_token.py index 40ec9abf91..788eb0b1b6 100644 --- a/magnum/api/middleware/auth_token.py +++ b/magnum/api/middleware/auth_token.py @@ -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 diff --git a/magnum/cmd/api.py b/magnum/cmd/api.py index 3155190b19..390d3a75a1 100644 --- a/magnum/cmd/api.py +++ b/magnum/cmd/api.py @@ -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 diff --git a/magnum/opts.py b/magnum/opts.py index aa2bbf7a82..782b1db49e 100644 --- a/magnum/opts.py +++ b/magnum/opts.py @@ -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, diff --git a/magnum/tests/unit/api/base.py b/magnum/tests/unit/api/base.py index 0f72e8d298..6ac06f64d6 100644 --- a/magnum/tests/unit/api/base.py +++ b/magnum/tests/unit/api/base.py @@ -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, diff --git a/magnum/tests/unit/api/controllers/auth-paste.ini b/magnum/tests/unit/api/controllers/auth-paste.ini new file mode 100644 index 0000000000..498fb0b74d --- /dev/null +++ b/magnum/tests/unit/api/controllers/auth-paste.ini @@ -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 diff --git a/magnum/tests/unit/api/controllers/auth-root-access.ini b/magnum/tests/unit/api/controllers/auth-root-access.ini new file mode 100644 index 0000000000..37971ec03b --- /dev/null +++ b/magnum/tests/unit/api/controllers/auth-root-access.ini @@ -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 diff --git a/magnum/tests/unit/api/controllers/auth-v1-access.ini b/magnum/tests/unit/api/controllers/auth-v1-access.ini new file mode 100644 index 0000000000..4f88187934 --- /dev/null +++ b/magnum/tests/unit/api/controllers/auth-v1-access.ini @@ -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 diff --git a/magnum/tests/unit/api/controllers/noauth-paste.ini b/magnum/tests/unit/api/controllers/noauth-paste.ini new file mode 100644 index 0000000000..6c87f6464f --- /dev/null +++ b/magnum/tests/unit/api/controllers/noauth-paste.ini @@ -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 diff --git a/magnum/tests/unit/api/controllers/test_root.py b/magnum/tests/unit/api/controllers/test_root.py index 9d4480745b..8f09c1951c 100644 --- a/magnum/tests/unit/api/controllers/test_root.py +++ b/magnum/tests/unit/api/controllers/test_root.py @@ -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) diff --git a/magnum/tests/unit/api/test_auth.py b/magnum/tests/unit/api/test_auth.py deleted file mode 100644 index 168b091dbb..0000000000 --- a/magnum/tests/unit/api/test_auth.py +++ /dev/null @@ -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) diff --git a/magnum/tests/unit/db/base.py b/magnum/tests/unit/db/base.py index cd420bde3d..4abca0ce9d 100644 --- a/magnum/tests/unit/db/base.py +++ b/magnum/tests/unit/db/base.py @@ -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()