diff --git a/etc/zun/api-paste.ini b/etc/zun/api-paste.ini index e69de29bb..786e42092 100644 --- a/etc/zun/api-paste.ini +++ b/etc/zun/api-paste.ini @@ -0,0 +1,16 @@ +[pipeline:main] +pipeline = cors request_id authtoken api_v1 + +[app:api_v1] +paste.app_factory = zun.api.app:app_factory + +[filter:authtoken] +acl_public_routes = /, /v1 +paste.filter_factory = zun.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 = zun diff --git a/requirements.txt b/requirements.txt index 5f8cab813..ddeae36ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,6 +14,7 @@ oslo.log>=1.14.0 # Apache-2.0 oslo.concurrency>=3.8.0 # Apache-2.0 oslo.config>=3.14.0 # Apache-2.0 oslo.messaging>=5.2.0 # Apache-2.0 +oslo.middleware>=3.0.0 # Apache-2.0 oslo.policy>=1.9.0 # Apache-2.0 oslo.serialization>=1.10.0 # Apache-2.0 oslo.service>=1.10.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 142da4d10..fd02330a0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,6 +54,9 @@ console_scripts = oslo.config.opts = zun = zun.opts:list_opts +oslo.config.opts.defaults = + zun = zun.common.config.set_cors_middleware_defaults + zun.database.migration_backend = sqlalchemy = zun.db.sqlalchemy.migration diff --git a/tools/zun-config-generator.conf b/tools/zun-config-generator.conf index fbbf45707..b384751a3 100644 --- a/tools/zun-config-generator.conf +++ b/tools/zun-config-generator.conf @@ -8,6 +8,7 @@ namespace = oslo.concurrency namespace = oslo.db namespace = oslo.log namespace = oslo.messaging +namespace = oslo.middleware.cors namespace = oslo.policy namespace = oslo.service.periodic_task namespace = oslo.service.service diff --git a/zun/api/app.py b/zun/api/app.py index dc0c96e89..e96c4f585 100644 --- a/zun/api/app.py +++ b/zun/api/app.py @@ -10,13 +10,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os + from oslo_config import cfg from oslo_log import log +from paste import deploy import pecan from zun.api import config as api_config from zun.api import middleware +from zun.common import config as common_config from zun.common.i18n import _ +from zun.common.i18n import _LI # Register options for the service @@ -41,7 +46,10 @@ 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 @@ -64,6 +72,7 @@ def setup_app(config=None): config = get_pecan_config() app_conf = dict(config.app) + common_config.set_config_defaults() app = pecan.make_app( app_conf.pop('root'), @@ -73,3 +82,21 @@ def setup_app(config=None): ) 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.api_paste_config]) + LOG.info(_LI("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/zun/common/config.py b/zun/common/config.py index 25f7a07b9..a55bf4836 100644 --- a/zun/common/config.py +++ b/zun/common/config.py @@ -16,6 +16,7 @@ # under the License. from oslo_config import cfg +from oslo_middleware import cors from zun.common import rpc from zun import version @@ -28,3 +29,31 @@ def parse_args(argv, default_config_files=None): version=version.version_info.release_string(), default_config_files=default_config_files) rpc.init(cfg.CONF) + + +def set_config_defaults(): + """This method updates all configuration default values.""" + set_cors_middleware_defaults() + + +def set_cors_middleware_defaults(): + """Update default configuration options for oslo.middleware.""" + cfg.set_defaults(cors.CORS_OPTS, + allow_headers=['X-Auth-Token', + 'X-Identity-Status', + 'X-Roles', + 'X-Service-Catalog', + 'X-User-Id', + 'X-Project-Id', + 'X-OpenStack-Request-ID', + 'X-Server-Management-Url'], + expose_headers=['X-Auth-Token', + 'X-Subject-Token', + 'X-Service-Token', + 'X-OpenStack-Request-ID', + 'X-Server-Management-Url'], + allow_methods=['GET', + 'PUT', + 'POST', + 'DELETE', + 'PATCH']) diff --git a/zun/common/service.py b/zun/common/service.py index c91633f3e..78684d548 100644 --- a/zun/common/service.py +++ b/zun/common/service.py @@ -49,6 +49,7 @@ def prepare_service(argv=None): argv = [] log.register_options(CONF) config.parse_args(argv) + config.set_config_defaults() log.setup(CONF, 'zun') # TODO(yuanying): Uncomment after objects are implemented # objects.register_all() @@ -69,7 +70,7 @@ class WSGIService(service.ServiceBase): :returns: None """ self.name = name - self.app = app.setup_app() + self.app = app.load_app() self.workers = (CONF.api.workers or processutils.get_worker_count()) if self.workers and self.workers < 1: raise exception.ConfigInvalid( diff --git a/zun/tests/unit/api/controllers/auth-paste.ini b/zun/tests/unit/api/controllers/auth-paste.ini new file mode 100644 index 000000000..3107a1ab3 --- /dev/null +++ b/zun/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 = zun.api.app:app_factory + +[filter:authtoken] +paste.filter_factory = zun.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 = zun +latent_allow_methods = GET, PUT, POST, DELETE +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/zun/tests/unit/api/controllers/auth-root-access.ini b/zun/tests/unit/api/controllers/auth-root-access.ini new file mode 100644 index 000000000..c467a037c --- /dev/null +++ b/zun/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 = zun.api.app:app_factory + +[filter:authtoken] +acl_public_routes = / +paste.filter_factory = zun.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 = zun +latent_allow_methods = GET, PUT, POST, DELETE +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/zun/tests/unit/api/controllers/auth-v1-access.ini b/zun/tests/unit/api/controllers/auth-v1-access.ini new file mode 100644 index 000000000..38d19d774 --- /dev/null +++ b/zun/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 = zun.api.app:app_factory + +[filter:authtoken] +acl_public_routes = /v1 +paste.filter_factory = zun.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 = zun +latent_allow_methods = GET, PUT, POST, DELETE +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/zun/tests/unit/api/controllers/noauth-paste.ini b/zun/tests/unit/api/controllers/noauth-paste.ini new file mode 100644 index 000000000..8bb6c94fa --- /dev/null +++ b/zun/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 = zun.api.app:app_factory + +[filter:authtoken] +acl_public_routes = / +paste.filter_factory = zun.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 = zun +latent_allow_methods = GET, PUT, POST, DELETE +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/zun/tests/unit/api/controllers/test_root.py b/zun/tests/unit/api/controllers/test_root.py new file mode 100644 index 000000000..0dd396bcc --- /dev/null +++ b/zun/tests/unit/api/controllers/test_root.py @@ -0,0 +1,123 @@ +# 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. + +from oslo_config import cfg + +import webtest + +from zun.api import app +from zun.tests.unit.api import base as api_base + + +class TestRootController(api_base.FunctionalTest): + def setUp(self): + super(TestRootController, self).setUp() + self.root_expected = { + u'default_version': + {u'id': u'v1', u'links': + [{u'href': u'http://localhost/v1/', u'rel': u'self'}]}, + u'description': u'Zun is an OpenStack project which ' + 'aims to provide container management.', + u'versions': [{u'id': u'v1', + u'links': + [{u'href': u'http://localhost/v1/', + u'rel': u'self'}]}]} + + self.v1_expected = { + u'media_types': + [{u'base': u'application/json', + u'type': u'application/vnd.openstack.zun.v1+json'}], + u'links': [{u'href': u'http://localhost/v1/', + u'rel': u'self'}, + {u'href': + u'http://docs.openstack.org/developer' + '/zun/dev/api-spec-v1.html', + u'type': u'text/html', u'rel': u'describedby'}], + u'services': [{u'href': u'http://localhost/v1/services/', + u'rel': u'self'}, + {u'href': u'http://localhost/services/', + u'rel': u'bookmark'}], + u'id': u'v1', + u'containers': [{u'href': u'http://localhost/v1/containers/', + u'rel': u'self'}, + {u'href': u'http://localhost/containers/', + 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) + + def test_v1_controller(self): + response = self.app.get('/v1/') + self.assertEqual(self.v1_expected, response.json) + + def test_get_not_found(self): + response = self.app.get('/a/bogus/url', expect_errors=True) + assert response.status_int == 404 + + def test_noauth(self): + # Don't need to auth + paste_file = "zun/tests/unit/api/controllers/noauth-paste.ini" + app = self.make_app(paste_file) + + response = app.get('/') + self.assertEqual(self.root_expected, response.json) + + response = app.get('/v1/') + self.assertEqual(self.v1_expected, response.json) + + response = app.get('/v1/containers/') + self.assertEqual(200, response.status_int) + + def test_auth_with_no_public_routes(self): + # All apis need auth when access + paste_file = "zun/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 + paste_file = "zun/tests/unit/api/controllers/auth-root-access.ini" + app = self.make_app(paste_file) + + response = app.get('/') + self.assertEqual(self.root_expected, response.json) + + response = app.get('/v1/', expect_errors=True) + self.assertEqual(401, response.status_int) + + response = app.get('/v1/containers', expect_errors=True) + self.assertEqual(401, response.status_int) + + def test_auth_with_v1_access(self): + # Only /v1 can access without auth + paste_file = "zun/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) + + response = app.get('/v1/') + self.assertEqual(self.v1_expected, response.json) + + response = app.get('/v1/containers', expect_errors=True) + self.assertEqual(401, response.status_int)