diff --git a/barbican/api/app.py b/barbican/api/app.py index d2ee1478e..63277522b 100644 --- a/barbican/api/app.py +++ b/barbican/api/app.py @@ -26,11 +26,6 @@ except ImportError: from oslo_log import log -from barbican.api.controllers import cas -from barbican.api.controllers import containers -from barbican.api.controllers import orders -from barbican.api.controllers import secrets -from barbican.api.controllers import transportkeys from barbican.api.controllers import versions from barbican.api import hooks from barbican.common import config @@ -44,16 +39,6 @@ if newrelic_loaded: newrelic.agent.initialize('/etc/newrelic/newrelic.ini') -class RootController(object): - def __init__(self): - # Adding the controllers at runtime to due config loading issues - self.secrets = secrets.SecretsController() - self.orders = orders.OrdersController() - self.containers = containers.ContainersController() - self.transport_keys = transportkeys.TransportKeysController() - self.cas = cas.CertificateAuthoritiesController() - - def build_wsgi_app(controller=None, transactional=False): """WSGI application creation helper @@ -68,42 +53,47 @@ def build_wsgi_app(controller=None, transactional=False): # Create WSGI app wsgi_app = pecan.Pecan( - controller or RootController(), + controller or versions.AVAILABLE_VERSIONS[versions.DEFAULT_VERSION](), hooks=request_hooks, force_canonical=False ) return wsgi_app -def create_main_app(global_config, **local_conf): +def main_app(func): + def _wrapper(global_config, **local_conf): + # Queuing initialization + queue.init(CONF, is_server_side=False) + + # Configure oslo logging and configuration services. + log.setup(CONF, 'barbican') + + config.setup_remote_pydev_debug() + + # Initializing the database engine and session factory before the app + # starts ensures we don't lose requests due to lazy initialiation of db + # connections. + repositories.setup_database_engine_and_factory() + + wsgi_app = func(global_config, **local_conf) + + if newrelic_loaded: + wsgi_app = newrelic.agent.WSGIApplicationWrapper(wsgi_app) + LOG = log.getLogger(__name__) + LOG.info(u._LI('Barbican app created and initialized')) + return wsgi_app + return _wrapper + + +@main_app +def create_main_app_v1(global_config, **local_conf): """uWSGI factory method for the Barbican-API application.""" - - # Queuing initialization - queue.init(CONF, is_server_side=False) - - # Configure oslo logging and configuration services. - log.setup(CONF, 'barbican') - config.setup_remote_pydev_debug() - - # Initializing the database engine and session factory before the app - # starts ensures we don't lose requests due to lazy initialiation of db - # connections. - repositories.setup_database_engine_and_factory() - # Setup app with transactional hook enabled - wsgi_app = build_wsgi_app(transactional=True) - - if newrelic_loaded: - wsgi_app = newrelic.agent.WSGIApplicationWrapper(wsgi_app) - - LOG = log.getLogger(__name__) - LOG.info(u._LI('Barbican app created and initialized')) - - return wsgi_app + return build_wsgi_app(versions.V1Controller(), transactional=True) def create_admin_app(global_config, **local_conf): - wsgi_app = pecan.make_app(versions.VersionController()) + wsgi_app = pecan.make_app(versions.VersionsController()) return wsgi_app diff --git a/barbican/api/controllers/versions.py b/barbican/api/controllers/versions.py index 1660b56e2..7d3d80cc3 100644 --- a/barbican/api/controllers/versions.py +++ b/barbican/api/controllers/versions.py @@ -11,8 +11,14 @@ # under the License. import pecan +from six.moves.urllib import parse from barbican.api import controllers +from barbican.api.controllers import cas +from barbican.api.controllers import containers +from barbican.api.controllers import orders +from barbican.api.controllers import secrets +from barbican.api.controllers import transportkeys from barbican.common import utils from barbican import i18n as u from barbican import version @@ -20,21 +26,145 @@ from barbican import version LOG = utils.getLogger(__name__) -class VersionController(object): +MIME_TYPE_JSON = 'application/json' +MIME_TYPE_JSON_HOME = 'application/json-home' +MEDIA_TYPE_JSON = 'application/vnd.openstack.keymanagement-%s+json' + + +def _version_not_found(): + """Throw exception indicating version not found.""" + pecan.abort(404, u._("The version you requested wasn't found")) + + +def _get_versioned_url(full_url, version): + parsed_url, _ = parse.urldefrag(full_url) + + if version[-1] != '/': + version += '/' + return parse.urljoin(parsed_url, version) + + +class BaseVersionController(object): + """Base class for the version-specific controllers""" + + @classmethod + def get_version_info(cls, request): + return { + 'id': cls.version_id, + 'status': 'stable', + 'updated': cls.last_updated, + 'links': [ + { + 'rel': 'self', + 'href': _get_versioned_url(request.url, + cls.version_string), + }, { + 'rel': 'describedby', + 'type': 'text/html', + 'href': 'http://docs.openstack.org/' + } + ], + 'media-types': [ + { + 'base': MIME_TYPE_JSON, + 'type': MEDIA_TYPE_JSON % cls.version_string + } + ] + } + + +class V1Controller(BaseVersionController): + """Root controller for the v1 API""" + + version_string = 'v1' + + # NOTE(jaosorior): We might start using decimals in the future, meanwhile + # this is the same as the version string. + version_id = 'v1' + + last_updated = '2015-04-28T00:00:00Z' def __init__(self): - LOG.debug('=== Creating VersionController ===') + LOG.debug('=== Creating V1Controller ===') + self.secrets = secrets.SecretsController() + self.orders = orders.OrdersController() + self.containers = containers.ContainersController() + self.transport_keys = transportkeys.TransportKeysController() + self.cas = cas.CertificateAuthoritiesController() + + self.__controllers = [ + self.secrets, + self.orders, + self.containers, + self.transport_keys, + self.cas, + ] @pecan.expose(generic=True) def index(self): pecan.abort(405) # HTTP 405 Method Not Allowed as default @index.when(method='GET', template='json') + @utils.allow_certain_content_types(MIME_TYPE_JSON, MIME_TYPE_JSON_HOME) @controllers.handle_exceptions(u._('Version retrieval')) def on_get(self): - body = { - 'v1': 'current', - 'build': version.__version__ + return {'version': self.get_version_info(pecan.request)} + + +AVAILABLE_VERSIONS = { + V1Controller.version_string: V1Controller, +} + +DEFAULT_VERSION = V1Controller.version_string + + +class VersionsController(object): + + def __init__(self): + LOG.debug('=== Creating VersionsController ===') + + @pecan.expose(generic=True) + def index(self, **kwargs): + pecan.abort(405) # HTTP 405 Method Not Allowed as default + + @index.when(method='GET', template='json') + @utils.allow_certain_content_types(MIME_TYPE_JSON, MIME_TYPE_JSON_HOME) + def on_get(self, **kwargs): + """The list of versions is dependent on the context.""" + self._redirect_to_default_json_home_if_needed(pecan.request) + + if 'build' in kwargs: + return {'build': version.__version__} + + versions_info = [version_class.get_version_info(pecan.request) + for version_class in AVAILABLE_VERSIONS.itervalues()] + + version_output = { + 'versions': { + 'values': versions_info + } } - LOG.info(u._LI('Retrieved version')) - return body + + # Since we are returning all the versions available, the proper status + # code is Multiple Choices (300) + pecan.response.status = 300 + return version_output + + def _redirect_to_default_json_home_if_needed(self, request): + if self._mime_best_match(request.accept) == MIME_TYPE_JSON_HOME: + url = _get_versioned_url(request.url, DEFAULT_VERSION) + LOG.debug("Redirecting Request to " + url) + # NOTE(jaosorior): This issues an "external" redirect because of + # two reasons: + # * This module doesn't require authorization, and accessing + # specific version info needs that. + # * The resource is a separate app_factory and won't be found + # internally + pecan.redirect(url, request=request) + + def _mime_best_match(self, accept): + if not accept: + return MIME_TYPE_JSON + + SUPPORTED_TYPES = [MIME_TYPE_JSON, MIME_TYPE_JSON_HOME] + return accept.best_match(SUPPORTED_TYPES) diff --git a/barbican/common/utils.py b/barbican/common/utils.py index e43d05d50..59aa6f17d 100644 --- a/barbican/common/utils.py +++ b/barbican/common/utils.py @@ -35,13 +35,23 @@ CONF = config.CONF API_VERSION = 'v1' -def allow_all_content_types(f): - # Pecan decorator to not limit content types for controller routes - cfg = pecan.util._cfg(f) +def _do_allow_certain_content_types(func, content_types_list=[]): + # Allows you to bypass pecan's content-type restrictions + cfg = pecan.util._cfg(func) cfg.setdefault('content_types', {}) cfg['content_types'].update((value, '') - for value in mimetypes.types_map.values()) - return f + for value in content_types_list) + return func + + +def allow_certain_content_types(*content_types_list): + def _wrapper(func): + return _do_allow_certain_content_types(func, content_types_list) + return _wrapper + + +def allow_all_content_types(f): + return _do_allow_certain_content_types(f, mimetypes.types_map.values()) def hostname_for_refs(resource=None): diff --git a/barbican/tests/api/controllers/test_versions.py b/barbican/tests/api/controllers/test_versions.py index 81f427d77..5ac525f7b 100644 --- a/barbican/tests/api/controllers/test_versions.py +++ b/barbican/tests/api/controllers/test_versions.py @@ -16,15 +16,32 @@ from barbican.api import controllers from barbican.tests import utils -class WhenTestingVersionResource(utils.BarbicanAPIBaseTestCase): - root_controller = controllers.versions.VersionController() +class WhenTestingVersionsResource(utils.BarbicanAPIBaseTestCase): + root_controller = controllers.versions.VersionsController() - def test_should_return_200_on_get(self): + def test_should_return_multiple_choices_on_get(self): resp = self.app.get('/') - self.assertEqual(200, resp.status_int) + self.assertEqual(300, resp.status_int) + + def test_should_return_multiple_choices_on_get_if_json_accept_header(self): + headers = {'Accept': 'application/json'} + resp = self.app.get('/', headers=headers) + self.assertEqual(300, resp.status_int) + + def test_should_redirect_if_json_home_accept_header_present(self): + headers = {'Accept': 'application/json-home'} + resp = self.app.get('/', headers=headers) + self.assertEqual(302, resp.status_int) def test_should_return_version_json(self): resp = self.app.get('/') - self.assertTrue('v1' in resp.json) - self.assertEqual(resp.json.get('v1'), 'current') + versions_response = resp.json['versions']['values'] + v1_info = versions_response[0] + + # NOTE(jaosorior): I used assertIn instead of assertEqual because we + # might start using decimal numbers in the future. So when that happens + # this test will still be valid. + self.assertIn('v1', v1_info['id']) + self.assertEqual(len(v1_info['media-types']), 1) + self.assertEqual(v1_info['media-types'][0]['base'], 'application/json') diff --git a/barbican/tests/api/test_resources_policy.py b/barbican/tests/api/test_resources_policy.py index 053e7db2e..9e0042f1c 100644 --- a/barbican/tests/api/test_resources_policy.py +++ b/barbican/tests/api/test_resources_policy.py @@ -69,8 +69,8 @@ class TestableResource(object): return self.controller.on_delete(*args, **kwargs) -class VersionResource(TestableResource): - controller_cls = versions.VersionController +class VersionsResource(TestableResource): + controller_cls = versions.VersionsController class SecretsResource(TestableResource): @@ -203,32 +203,32 @@ class BaseTestCase(utils.BaseTestCase, utils.MockModelRepositoryMixin): self.assertEqual(403, exception.status_int) -class WhenTestingVersionResource(BaseTestCase): - """RBAC tests for the barbican.api.resources.VersionResource class.""" +class WhenTestingVersionsResource(BaseTestCase): + """RBAC tests for the barbican.api.resources.VersionsResource class.""" def setUp(self): - super(WhenTestingVersionResource, self).setUp() + super(WhenTestingVersionsResource, self).setUp() - self.resource = VersionResource() + self.resource = VersionsResource() def test_rules_should_be_loaded(self): self.assertIsNotNone(self.policy_enforcer.rules) - def test_should_pass_get_version(self): + def test_should_pass_get_versions(self): # Can't use base method that short circuits post-RBAC processing here, # as version GET is trivial for role in ['admin', 'observer', 'creator', 'audit']: self.req = self._generate_req(roles=[role] if role else []) self._invoke_on_get() - def test_should_pass_get_version_with_bad_roles(self): + def test_should_pass_get_versions_with_bad_roles(self): self.req = self._generate_req(roles=[None, 'bunkrolehere']) self._invoke_on_get() - def test_should_pass_get_version_with_no_roles(self): + def test_should_pass_get_versions_with_no_roles(self): self.req = self._generate_req() self._invoke_on_get() - def test_should_pass_get_version_multiple_roles(self): + def test_should_pass_get_versions_multiple_roles(self): self.req = self._generate_req(roles=['admin', 'observer', 'creator', 'audit']) self._invoke_on_get() diff --git a/contrib/devstack/lib/barbican b/contrib/devstack/lib/barbican index a23636f1a..a4b65c3fe 100755 --- a/contrib/devstack/lib/barbican +++ b/contrib/devstack/lib/barbican @@ -130,7 +130,7 @@ function configure_barbican { ## Set up keystone # Turn on the middleware - iniset $BARBICAN_PASTE_CONF 'pipeline:barbican_api' pipeline 'keystone_authtoken context apiapp' + iniset $BARBICAN_PASTE_CONF 'pipeline:barbican_api' pipeline 'keystone_authtoken context apiapp_v1' # Set the keystone parameters iniset $BARBICAN_PASTE_CONF 'filter:keystone_authtoken' auth_protocol $KEYSTONE_AUTH_PROTOCOL diff --git a/doc/source/setup/keystone.rst b/doc/source/setup/keystone.rst index 1caf03fd2..b9b38aa6b 100644 --- a/doc/source/setup/keystone.rst +++ b/doc/source/setup/keystone.rst @@ -32,7 +32,7 @@ the get version call. .. code-block:: ini [pipeline:barbican_api] - pipeline = keystone_authtoken context apiapp + pipeline = keystone_authtoken context apiapp_v1 2. Replace ``keystone_authtoken`` filter values to match your Keystone setup diff --git a/etc/barbican/barbican-api-paste.ini b/etc/barbican/barbican-api-paste.ini index 204f87a21..b887654c7 100644 --- a/etc/barbican/barbican-api-paste.ini +++ b/etc/barbican/barbican-api-paste.ini @@ -9,21 +9,20 @@ pipeline = versionapp # Use this pipeline for Barbican API - DEFAULT no authentication [pipeline:barbican_api] -pipeline = unauthenticated-context apiapp -####pipeline = simple apiapp -#pipeline = keystone_authtoken context apiapp +pipeline = unauthenticated-context apiapp_v1 +#pipeline = keystone_authtoken context apiapp_v1 #Use this pipeline to activate a repoze.profile middleware and HTTP port, # to provide profiling information for the REST API processing. [pipeline:barbican-profile] -pipeline = unauthenticated-context egg:Paste#cgitb egg:Paste#httpexceptions profile apiapp +pipeline = unauthenticated-context egg:Paste#cgitb egg:Paste#httpexceptions profile apiapp_v1 #Use this pipeline for keystone auth [pipeline:barbican-api-keystone] -pipeline = keystone_authtoken context apiapp +pipeline = keystone_authtoken context apiapp_v1 -[app:apiapp] -paste.app_factory = barbican.api.app:create_main_app +[app:apiapp_v1] +paste.app_factory = barbican.api.app:create_main_app_v1 [app:versionapp] paste.app_factory = barbican.api.app:create_version_app diff --git a/functionaltests/api/v1/smoke/test_versions.py b/functionaltests/api/v1/smoke/test_versions.py index b5afe5fff..df89c6746 100644 --- a/functionaltests/api/v1/smoke/test_versions.py +++ b/functionaltests/api/v1/smoke/test_versions.py @@ -35,6 +35,13 @@ class VersionDiscoveryTestCase(base.TestCase): resp = self.client.get(url_without_version, use_auth=use_auth) body = resp.json() - self.assertEqual(resp.status_code, 200) - self.assertEqual(body.get('v1'), 'current') - self.assertGreater(len(body.get('build')), 1) + self.assertEqual(resp.status_code, 300) + versions_response = body['versions']['values'] + v1_info = versions_response[0] + + # NOTE(jaosorior): I used assertIn instead of assertEqual because we + # might start using decimal numbers in the future. So when that happens + # this test will still be valid. + self.assertIn('v1', v1_info['id']) + self.assertEqual(len(v1_info['media-types']), 1) + self.assertEqual(v1_info['media-types'][0]['base'], 'application/json')