From c5900d0f43e5c76566059895d515477d8b716df0 Mon Sep 17 00:00:00 2001 From: Adam Young Date: Tue, 9 Jul 2013 19:09:50 -0400 Subject: [PATCH] Register Extensions Extensions register themselves with keystone/common/extension.py as either public, admin, or both, and they show up in the extensions collection on http://:/v2.0/extensions/ Bug 1177531 Change-Id: Ic0b5c84e28342e96c3197c1b46f8b1656e2d7050 --- keystone/common/extension.py | 47 ++++++++++++++++++++++++++++ keystone/contrib/admin_crud/core.py | 21 +++++++++++++ keystone/contrib/ec2/core.py | 20 ++++++++++++ keystone/contrib/s3/core.py | 18 +++++++++++ keystone/contrib/stats/core.py | 18 +++++++++++ keystone/contrib/user_crud/core.py | 20 ++++++++++++ keystone/controllers.py | 40 +++++++----------------- tests/test_content_types.py | 48 ++++++++++++++++------------- 8 files changed, 182 insertions(+), 50 deletions(-) create mode 100644 keystone/common/extension.py diff --git a/keystone/common/extension.py b/keystone/common/extension.py new file mode 100644 index 0000000000..f176a1970d --- /dev/null +++ b/keystone/common/extension.py @@ -0,0 +1,47 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack LLC +# +# 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. + + +ADMIN_EXTENSIONS = {} +PUBLIC_EXTENSIONS = {} + + +def register_admin_extension(url_prefix, extension_data): + """Register extension with collection of admin extensions. + + Extensions register the information here that will show + up in the /extensions page as a way to indicate that the extension is + active. + + url_prefix: unique key for the extension that will appear in the + urls generated by the extension. + + extension_data is a dictionary. The expected fields are: + 'name': short, human readable name of the extnsion + 'namespace': xml namespace + 'alias': identifier for the extension + 'updated': date the extension was last updated + 'description': text description of the extension + 'links': hyperlinks to documents describing the extension + + """ + ADMIN_EXTENSIONS[url_prefix] = extension_data + + +def register_public_extension(url_prefix, extension_data): + """Same as register_admin_extension but for public extensions.""" + + PUBLIC_EXTENSIONS[url_prefix] = extension_data diff --git a/keystone/contrib/admin_crud/core.py b/keystone/contrib/admin_crud/core.py index c06afcf7fe..f98397aeeb 100644 --- a/keystone/contrib/admin_crud/core.py +++ b/keystone/contrib/admin_crud/core.py @@ -14,10 +14,31 @@ # License for the specific language governing permissions and limitations # under the License. from keystone import catalog +from keystone.common import extension from keystone.common import wsgi from keystone import identity +extension.register_admin_extension( + 'OS-KSADM', { + 'name': 'OpenStack Keystone Admin', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 'OS-KSADM/v1.0', + 'alias': 'OS-KSADM', + 'updated': '2013-07-11T17:14:00-00:00', + 'description': 'OpenStack extensions to Keystone v2.0 API ' + 'enabling Administrative Operations.', + 'links': [ + { + 'rel': 'describedby', + # TODO(dolph): link needs to be revised after + # bug 928059 merges + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api', + } + ]}) + + class CrudExtension(wsgi.ExtensionRouter): """Previously known as the OS-KSADM extension. diff --git a/keystone/contrib/ec2/core.py b/keystone/contrib/ec2/core.py index 5254b53fc8..7e6c04f532 100644 --- a/keystone/contrib/ec2/core.py +++ b/keystone/contrib/ec2/core.py @@ -40,6 +40,7 @@ from keystone.common import controller from keystone.common import dependency +from keystone.common import extension from keystone.common import manager from keystone.common import utils from keystone.common import wsgi @@ -51,6 +52,25 @@ CONF = config.CONF +EXTENSION_DATA = { + 'name': 'OpenStack EC2 API', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 'OS-EC2/v1.0', + 'alias': 'OS-EC2', + 'updated': '2013-07-07T12:00:0-00:00', + 'description': 'OpenStack EC2 Credentials backend.', + 'links': [ + { + 'rel': 'describedby', + # TODO(ayoung): needs a description + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api', + } + ]} +extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) +extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) + + @dependency.provider('ec2_api') class Manager(manager.Manager): """Default pivot point for the EC2 Credentials backend. diff --git a/keystone/contrib/s3/core.py b/keystone/contrib/s3/core.py index 44b038d412..29ea4fe291 100644 --- a/keystone/contrib/s3/core.py +++ b/keystone/contrib/s3/core.py @@ -27,6 +27,7 @@ import hashlib import hmac +from keystone.common import extension from keystone.common import utils from keystone.common import wsgi from keystone import config @@ -35,6 +36,23 @@ CONF = config.CONF +EXTENSION_DATA = { + 'name': 'OpenStack S3 API', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 's3tokens/v1.0', + 'alias': 's3tokens', + 'updated': '2013-07-07T12:00:0-00:00', + 'description': 'OpenStack S3 API.', + 'links': [ + { + 'rel': 'describedby', + # TODO(ayoung): needs a description + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api', + } + ]} +extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA) + class S3Extension(wsgi.ExtensionRouter): def add_routes(self, mapper): diff --git a/keystone/contrib/stats/core.py b/keystone/contrib/stats/core.py index 0325c4fa8a..1d7b2cdf28 100644 --- a/keystone/contrib/stats/core.py +++ b/keystone/contrib/stats/core.py @@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. +from keystone.common import extension from keystone.common import logging from keystone.common import manager from keystone.common import wsgi @@ -27,6 +28,23 @@ CONF = config.CONF LOG = logging.getLogger(__name__) +extension_data = { + 'name': 'Openstack Keystone Stats API', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 'OS-STATS/v1.0', + 'alias': 'OS-STATS', + 'updated': '2013-07-07T12:00:0-00:00', + 'description': 'Openstack Keystone Stats API.', + 'links': [ + { + 'rel': 'describedby', + # TODO(ayoung): needs a description + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api', + } + ]} +extension.register_admin_extension(extension_data['alias'], extension_data) + class Manager(manager.Manager): """Default pivot point for the Stats backend. diff --git a/keystone/contrib/user_crud/core.py b/keystone/contrib/user_crud/core.py index 7b3f459f50..f9f09b89f6 100644 --- a/keystone/contrib/user_crud/core.py +++ b/keystone/contrib/user_crud/core.py @@ -17,6 +17,7 @@ import copy import uuid +from keystone.common import extension from keystone.common import logging from keystone.common import wsgi from keystone import exception @@ -26,6 +27,25 @@ LOG = logging.getLogger(__name__) +extension.register_public_extension( + 'OS-KSCRUD', { + 'name': 'OpenStack Keystone User CRUD', + 'namespace': 'http://docs.openstack.org/identity/api/ext/' + 'OS-KSCRUD/v1.0', + 'alias': 'OS-KSCRUD', + 'updated': '2013-07-07T12:00:0-00:00', + 'description': 'OpenStack extensions to Keystone v2.0 API ' + 'enabling User Operations.', + 'links': [ + { + 'rel': 'describedby', + # TODO(ayoung): needs a description + 'type': 'text/html', + 'href': 'https://github.com/openstack/identity-api', + } + ]}) + + class UserController(identity.controllers.User): def set_user_password(self, context, user_id, user): token_id = context.get('token_id') diff --git a/keystone/controllers.py b/keystone/controllers.py index 6dd303e1f2..8ffa073a1b 100644 --- a/keystone/controllers.py +++ b/keystone/controllers.py @@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. +from keystone.common import extension from keystone.common import logging from keystone.common import wsgi from keystone import config @@ -32,10 +33,10 @@ class Extensions(wsgi.Application): """Base extensions controller to be extended by public and admin API's.""" - def __init__(self, extensions=None): - super(Extensions, self).__init__() - - self.extensions = extensions or {} + #extend in subclass to specify the set of extensions + @property + def extensions(self): + return None def get_extensions_info(self, context): return {'extensions': {'values': self.extensions.values()}} @@ -48,34 +49,15 @@ def get_extension_info(self, context, extension_alias): class AdminExtensions(Extensions): - def __init__(self, *args, **kwargs): - super(AdminExtensions, self).__init__(*args, **kwargs) - - # TODO(dolph): Extensions should obviously provide this information - # themselves, but hardcoding it here allows us to match - # the API spec in the short term with minimal complexity. - self.extensions['OS-KSADM'] = { - 'name': 'Openstack Keystone Admin', - 'namespace': 'http://docs.openstack.org/identity/api/ext/' - 'OS-KSADM/v1.0', - 'alias': 'OS-KSADM', - 'updated': '2011-08-19T13:25:27-06:00', - 'description': 'Openstack extensions to Keystone v2.0 API ' - 'enabling Admin Operations.', - 'links': [ - { - 'rel': 'describedby', - # TODO(dolph): link needs to be revised after - # bug 928059 merges - 'type': 'text/html', - 'href': 'https://github.com/openstack/identity-api', - } - ] - } + @property + def extensions(self): + return extension.ADMIN_EXTENSIONS class PublicExtensions(Extensions): - pass + @property + def extensions(self): + return extension.PUBLIC_EXTENSIONS def register_version(version): diff --git a/tests/test_content_types.py b/tests/test_content_types.py index 278a109894..3213656cd5 100644 --- a/tests/test_content_types.py +++ b/tests/test_content_types.py @@ -23,6 +23,7 @@ from keystone import test +from keystone.common import extension from keystone.common import serializer from keystone.openstack.common import jsonutils @@ -334,14 +335,14 @@ def test_admin_version(self): self.assertValidVersionResponse(r) def test_public_extensions(self): - self.public_request(path='/v2.0/extensions',) - - # TODO(dolph): can't test this without any public extensions defined - # self.assertValidExtensionListResponse(r) + r = self.public_request(path='/v2.0/extensions') + self.assertValidExtensionListResponse(r, + extension.PUBLIC_EXTENSIONS) def test_admin_extensions(self): - r = self.admin_request(path='/v2.0/extensions',) - self.assertValidExtensionListResponse(r) + r = self.admin_request(path='/v2.0/extensions') + self.assertValidExtensionListResponse(r, + extension.ADMIN_EXTENSIONS) def test_admin_extensions_404(self): self.admin_request(path='/v2.0/extensions/invalid-extension', @@ -353,7 +354,8 @@ def test_public_osksadm_extension_404(self): def test_admin_osksadm_extension(self): r = self.admin_request(path='/v2.0/extensions/OS-KSADM') - self.assertValidExtensionResponse(r) + self.assertValidExtensionResponse(r, + extension.ADMIN_EXTENSIONS) def test_authenticate(self): r = self.public_request( @@ -611,24 +613,26 @@ def assertValidErrorResponse(self, r): self.assertValidError(r.result['error']) self.assertEqual(r.result['error']['code'], r.status_code) - def assertValidExtension(self, extension): + def assertValidExtension(self, extension, expected): super(JsonTestCase, self).assertValidExtension(extension) - - self.assertIsNotNone(extension.get('description')) + descriptions = [ext['description'] for ext in expected.itervalues()] + description = extension.get('description') + self.assertIsNotNone(description) + self.assertIn(description, descriptions) self.assertIsNotNone(extension.get('links')) self.assertNotEmpty(extension.get('links')) for link in extension.get('links'): self.assertValidExtensionLink(link) - def assertValidExtensionListResponse(self, r): + def assertValidExtensionListResponse(self, r, expected): self.assertIsNotNone(r.result.get('extensions')) self.assertIsNotNone(r.result['extensions'].get('values')) self.assertNotEmpty(r.result['extensions'].get('values')) for extension in r.result['extensions']['values']: - self.assertValidExtension(extension) + self.assertValidExtension(extension, expected) - def assertValidExtensionResponse(self, r): - self.assertValidExtension(r.result.get('extension')) + def assertValidExtensionResponse(self, r, expected): + self.assertValidExtension(r.result.get('extension'), expected) def assertValidAuthenticationResponse(self, r, require_service_catalog=False): @@ -850,29 +854,31 @@ def assertValidErrorResponse(self, r): self.assertValidError(xml) self.assertEqual(xml.get('code'), str(r.status_code)) - def assertValidExtension(self, extension): + def assertValidExtension(self, extension, expected): super(XmlTestCase, self).assertValidExtension(extension) self.assertIsNotNone(extension.find(self._tag('description'))) self.assertTrue(extension.find(self._tag('description')).text) links = extension.find(self._tag('links')) self.assertNotEmpty(links.findall(self._tag('link'))) + descriptions = [ext['description'] for ext in expected.itervalues()] + description = extension.find(self._tag('description')).text + self.assertIn(description, descriptions) for link in links.findall(self._tag('link')): self.assertValidExtensionLink(link) - def assertValidExtensionListResponse(self, r): + def assertValidExtensionListResponse(self, r, expected): xml = r.result self.assertEqual(xml.tag, self._tag('extensions')) - self.assertNotEmpty(xml.findall(self._tag('extension'))) - for extension in xml.findall(self._tag('extension')): - self.assertValidExtension(extension) + for ext in xml.findall(self._tag('extension')): + self.assertValidExtension(ext, expected) - def assertValidExtensionResponse(self, r): + def assertValidExtensionResponse(self, r, expected): xml = r.result self.assertEqual(xml.tag, self._tag('extension')) - self.assertValidExtension(xml) + self.assertValidExtension(xml, expected) def assertValidVersion(self, version): super(XmlTestCase, self).assertValidVersion(version)