diff options
Diffstat (limited to 'keystone-moon/keystone/contrib/ec2')
-rw-r--r-- | keystone-moon/keystone/contrib/ec2/__init__.py | 18 | ||||
-rw-r--r-- | keystone-moon/keystone/contrib/ec2/controllers.py | 415 | ||||
-rw-r--r-- | keystone-moon/keystone/contrib/ec2/core.py | 34 | ||||
-rw-r--r-- | keystone-moon/keystone/contrib/ec2/routers.py | 95 |
4 files changed, 562 insertions, 0 deletions
diff --git a/keystone-moon/keystone/contrib/ec2/__init__.py b/keystone-moon/keystone/contrib/ec2/__init__.py new file mode 100644 index 00000000..88622e53 --- /dev/null +++ b/keystone-moon/keystone/contrib/ec2/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.contrib.ec2 import controllers # noqa +from keystone.contrib.ec2.core import * # noqa +from keystone.contrib.ec2.routers import Ec2Extension # noqa +from keystone.contrib.ec2.routers import Ec2ExtensionV3 # noqa diff --git a/keystone-moon/keystone/contrib/ec2/controllers.py b/keystone-moon/keystone/contrib/ec2/controllers.py new file mode 100644 index 00000000..6e6d3268 --- /dev/null +++ b/keystone-moon/keystone/contrib/ec2/controllers.py @@ -0,0 +1,415 @@ +# Copyright 2013 OpenStack Foundation +# +# 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. + +"""Main entry point into the EC2 Credentials service. + +This service allows the creation of access/secret credentials used for +the ec2 interop layer of OpenStack. + +A user can create as many access/secret pairs, each of which map to a +specific project. This is required because OpenStack supports a user +belonging to multiple projects, whereas the signatures created on ec2-style +requests don't allow specification of which project the user wishes to act +upon. + +To complete the cycle, we provide a method that OpenStack services can +use to validate a signature and get a corresponding OpenStack token. This +token allows method calls to other services within the context the +access/secret was created. As an example, Nova requests Keystone to validate +the signature of a request, receives a token, and then makes a request to +Glance to list images needed to perform the requested task. + +""" + +import abc +import sys +import uuid + +from keystoneclient.contrib.ec2 import utils as ec2_utils +from oslo_serialization import jsonutils +import six + +from keystone.common import controller +from keystone.common import dependency +from keystone.common import utils +from keystone.common import wsgi +from keystone import exception +from keystone.i18n import _ +from keystone.models import token_model + + +@dependency.requires('assignment_api', 'catalog_api', 'credential_api', + 'identity_api', 'resource_api', 'role_api', + 'token_provider_api') +@six.add_metaclass(abc.ABCMeta) +class Ec2ControllerCommon(object): + def check_signature(self, creds_ref, credentials): + signer = ec2_utils.Ec2Signer(creds_ref['secret']) + signature = signer.generate(credentials) + if utils.auth_str_equal(credentials['signature'], signature): + return + # NOTE(vish): Some libraries don't use the port when signing + # requests, so try again without port. + elif ':' in credentials['signature']: + hostname, _port = credentials['host'].split(':') + credentials['host'] = hostname + signature = signer.generate(credentials) + if not utils.auth_str_equal(credentials.signature, signature): + raise exception.Unauthorized(message='Invalid EC2 signature.') + else: + raise exception.Unauthorized(message='EC2 signature not supplied.') + + @abc.abstractmethod + def authenticate(self, context, credentials=None, ec2Credentials=None): + """Validate a signed EC2 request and provide a token. + + Other services (such as Nova) use this **admin** call to determine + if a request they signed received is from a valid user. + + If it is a valid signature, an OpenStack token that maps + to the user/tenant is returned to the caller, along with + all the other details returned from a normal token validation + call. + + The returned token is useful for making calls to other + OpenStack services within the context of the request. + + :param context: standard context + :param credentials: dict of ec2 signature + :param ec2Credentials: DEPRECATED dict of ec2 signature + :returns: token: OpenStack token equivalent to access key along + with the corresponding service catalog and roles + """ + raise exception.NotImplemented() + + def _authenticate(self, credentials=None, ec2credentials=None): + """Common code shared between the V2 and V3 authenticate methods. + + :returns: user_ref, tenant_ref, metadata_ref, roles_ref, catalog_ref + """ + + # FIXME(ja): validate that a service token was used! + + # NOTE(termie): backwards compat hack + if not credentials and ec2credentials: + credentials = ec2credentials + + if 'access' not in credentials: + raise exception.Unauthorized(message='EC2 signature not supplied.') + + creds_ref = self._get_credentials(credentials['access']) + self.check_signature(creds_ref, credentials) + + # TODO(termie): don't create new tokens every time + # TODO(termie): this is copied from TokenController.authenticate + tenant_ref = self.resource_api.get_project(creds_ref['tenant_id']) + user_ref = self.identity_api.get_user(creds_ref['user_id']) + metadata_ref = {} + metadata_ref['roles'] = ( + self.assignment_api.get_roles_for_user_and_project( + user_ref['id'], tenant_ref['id'])) + + trust_id = creds_ref.get('trust_id') + if trust_id: + metadata_ref['trust_id'] = trust_id + metadata_ref['trustee_user_id'] = user_ref['id'] + + # Validate that the auth info is valid and nothing is disabled + try: + self.identity_api.assert_user_enabled( + user_id=user_ref['id'], user=user_ref) + self.resource_api.assert_domain_enabled( + domain_id=user_ref['domain_id']) + self.resource_api.assert_project_enabled( + project_id=tenant_ref['id'], project=tenant_ref) + except AssertionError as e: + six.reraise(exception.Unauthorized, exception.Unauthorized(e), + sys.exc_info()[2]) + + roles = metadata_ref.get('roles', []) + if not roles: + raise exception.Unauthorized(message='User not valid for tenant.') + roles_ref = [self.role_api.get_role(role_id) for role_id in roles] + + catalog_ref = self.catalog_api.get_catalog( + user_ref['id'], tenant_ref['id']) + + return user_ref, tenant_ref, metadata_ref, roles_ref, catalog_ref + + def create_credential(self, context, user_id, tenant_id): + """Create a secret/access pair for use with ec2 style auth. + + Generates a new set of credentials that map the user/tenant + pair. + + :param context: standard context + :param user_id: id of user + :param tenant_id: id of tenant + :returns: credential: dict of ec2 credential + """ + + self.identity_api.get_user(user_id) + self.resource_api.get_project(tenant_id) + trust_id = self._get_trust_id_for_request(context) + blob = {'access': uuid.uuid4().hex, + 'secret': uuid.uuid4().hex, + 'trust_id': trust_id} + credential_id = utils.hash_access_key(blob['access']) + cred_ref = {'user_id': user_id, + 'project_id': tenant_id, + 'blob': jsonutils.dumps(blob), + 'id': credential_id, + 'type': 'ec2'} + self.credential_api.create_credential(credential_id, cred_ref) + return {'credential': self._convert_v3_to_ec2_credential(cred_ref)} + + def get_credentials(self, user_id): + """List all credentials for a user. + + :param user_id: id of user + :returns: credentials: list of ec2 credential dicts + """ + + self.identity_api.get_user(user_id) + credential_refs = self.credential_api.list_credentials_for_user( + user_id) + return {'credentials': + [self._convert_v3_to_ec2_credential(credential) + for credential in credential_refs]} + + def get_credential(self, user_id, credential_id): + """Retrieve a user's access/secret pair by the access key. + + Grab the full access/secret pair for a given access key. + + :param user_id: id of user + :param credential_id: access key for credentials + :returns: credential: dict of ec2 credential + """ + + self.identity_api.get_user(user_id) + return {'credential': self._get_credentials(credential_id)} + + def delete_credential(self, user_id, credential_id): + """Delete a user's access/secret pair. + + Used to revoke a user's access/secret pair + + :param user_id: id of user + :param credential_id: access key for credentials + :returns: bool: success + """ + + self.identity_api.get_user(user_id) + self._get_credentials(credential_id) + ec2_credential_id = utils.hash_access_key(credential_id) + return self.credential_api.delete_credential(ec2_credential_id) + + @staticmethod + def _convert_v3_to_ec2_credential(credential): + # Prior to bug #1259584 fix, blob was stored unserialized + # but it should be stored as a json string for compatibility + # with the v3 credentials API. Fall back to the old behavior + # for backwards compatibility with existing DB contents + try: + blob = jsonutils.loads(credential['blob']) + except TypeError: + blob = credential['blob'] + return {'user_id': credential.get('user_id'), + 'tenant_id': credential.get('project_id'), + 'access': blob.get('access'), + 'secret': blob.get('secret'), + 'trust_id': blob.get('trust_id')} + + def _get_credentials(self, credential_id): + """Return credentials from an ID. + + :param credential_id: id of credential + :raises exception.Unauthorized: when credential id is invalid + :returns: credential: dict of ec2 credential. + """ + ec2_credential_id = utils.hash_access_key(credential_id) + creds = self.credential_api.get_credential(ec2_credential_id) + if not creds: + raise exception.Unauthorized(message='EC2 access key not found.') + return self._convert_v3_to_ec2_credential(creds) + + +@dependency.requires('policy_api', 'token_provider_api') +class Ec2Controller(Ec2ControllerCommon, controller.V2Controller): + + @controller.v2_deprecated + def authenticate(self, context, credentials=None, ec2Credentials=None): + (user_ref, tenant_ref, metadata_ref, roles_ref, + catalog_ref) = self._authenticate(credentials=credentials, + ec2credentials=ec2Credentials) + + # NOTE(morganfainberg): Make sure the data is in correct form since it + # might be consumed external to Keystone and this is a v2.0 controller. + # The token provider does not explicitly care about user_ref version + # in this case, but the data is stored in the token itself and should + # match the version + user_ref = self.v3_to_v2_user(user_ref) + auth_token_data = dict(user=user_ref, + tenant=tenant_ref, + metadata=metadata_ref, + id='placeholder') + (token_id, token_data) = self.token_provider_api.issue_v2_token( + auth_token_data, roles_ref, catalog_ref) + return token_data + + @controller.v2_deprecated + def get_credential(self, context, user_id, credential_id): + if not self._is_admin(context): + self._assert_identity(context, user_id) + return super(Ec2Controller, self).get_credential(user_id, + credential_id) + + @controller.v2_deprecated + def get_credentials(self, context, user_id): + if not self._is_admin(context): + self._assert_identity(context, user_id) + return super(Ec2Controller, self).get_credentials(user_id) + + @controller.v2_deprecated + def create_credential(self, context, user_id, tenant_id): + if not self._is_admin(context): + self._assert_identity(context, user_id) + return super(Ec2Controller, self).create_credential(context, user_id, + tenant_id) + + @controller.v2_deprecated + def delete_credential(self, context, user_id, credential_id): + if not self._is_admin(context): + self._assert_identity(context, user_id) + self._assert_owner(user_id, credential_id) + return super(Ec2Controller, self).delete_credential(user_id, + credential_id) + + def _assert_identity(self, context, user_id): + """Check that the provided token belongs to the user. + + :param context: standard context + :param user_id: id of user + :raises exception.Forbidden: when token is invalid + + """ + try: + token_data = self.token_provider_api.validate_token( + context['token_id']) + except exception.TokenNotFound as e: + raise exception.Unauthorized(e) + + token_ref = token_model.KeystoneToken(token_id=context['token_id'], + token_data=token_data) + + if token_ref.user_id != user_id: + raise exception.Forbidden(_('Token belongs to another user')) + + def _is_admin(self, context): + """Wrap admin assertion error return statement. + + :param context: standard context + :returns: bool: success + + """ + try: + # NOTE(morganfainberg): policy_api is required for assert_admin + # to properly perform policy enforcement. + self.assert_admin(context) + return True + except exception.Forbidden: + return False + + def _assert_owner(self, user_id, credential_id): + """Ensure the provided user owns the credential. + + :param user_id: expected credential owner + :param credential_id: id of credential object + :raises exception.Forbidden: on failure + + """ + ec2_credential_id = utils.hash_access_key(credential_id) + cred_ref = self.credential_api.get_credential(ec2_credential_id) + if user_id != cred_ref['user_id']: + raise exception.Forbidden(_('Credential belongs to another user')) + + +@dependency.requires('policy_api', 'token_provider_api') +class Ec2ControllerV3(Ec2ControllerCommon, controller.V3Controller): + + member_name = 'project' + + def __init__(self): + super(Ec2ControllerV3, self).__init__() + self.get_member_from_driver = self.credential_api.get_credential + + def _check_credential_owner_and_user_id_match(self, context, prep_info, + user_id, credential_id): + # NOTE(morganfainberg): this method needs to capture the arguments of + # the method that is decorated with @controller.protected() (with + # exception of the first argument ('context') since the protected + # method passes in *args, **kwargs. In this case, it is easier to see + # the expected input if the argspec is `user_id` and `credential_id` + # explicitly (matching the :class:`.ec2_delete_credential()` method + # below). + ref = {} + credential_id = utils.hash_access_key(credential_id) + ref['credential'] = self.credential_api.get_credential(credential_id) + # NOTE(morganfainberg): policy_api is required for this + # check_protection to properly be able to perform policy enforcement. + self.check_protection(context, prep_info, ref) + + def authenticate(self, context, credentials=None, ec2Credentials=None): + (user_ref, project_ref, metadata_ref, roles_ref, + catalog_ref) = self._authenticate(credentials=credentials, + ec2credentials=ec2Credentials) + + method_names = ['ec2credential'] + + token_id, token_data = self.token_provider_api.issue_v3_token( + user_ref['id'], method_names, project_id=project_ref['id'], + metadata_ref=metadata_ref) + return render_token_data_response(token_id, token_data) + + @controller.protected(callback=_check_credential_owner_and_user_id_match) + def ec2_get_credential(self, context, user_id, credential_id): + return super(Ec2ControllerV3, self).get_credential(user_id, + credential_id) + + @controller.protected() + def ec2_list_credentials(self, context, user_id): + return super(Ec2ControllerV3, self).get_credentials(user_id) + + @controller.protected() + def ec2_create_credential(self, context, user_id, tenant_id): + return super(Ec2ControllerV3, self).create_credential(context, user_id, + tenant_id) + + @controller.protected(callback=_check_credential_owner_and_user_id_match) + def ec2_delete_credential(self, context, user_id, credential_id): + return super(Ec2ControllerV3, self).delete_credential(user_id, + credential_id) + + +def render_token_data_response(token_id, token_data): + """Render token data HTTP response. + + Stash token ID into the X-Subject-Token header. + + """ + headers = [('X-Subject-Token', token_id)] + + return wsgi.render_response(body=token_data, + status=(200, 'OK'), headers=headers) diff --git a/keystone-moon/keystone/contrib/ec2/core.py b/keystone-moon/keystone/contrib/ec2/core.py new file mode 100644 index 00000000..77857af8 --- /dev/null +++ b/keystone-moon/keystone/contrib/ec2/core.py @@ -0,0 +1,34 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 keystone.common import extension + + +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) diff --git a/keystone-moon/keystone/contrib/ec2/routers.py b/keystone-moon/keystone/contrib/ec2/routers.py new file mode 100644 index 00000000..7b6bf115 --- /dev/null +++ b/keystone-moon/keystone/contrib/ec2/routers.py @@ -0,0 +1,95 @@ +# Copyright 2013 OpenStack Foundation +# +# 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 functools + +from keystone.common import json_home +from keystone.common import wsgi +from keystone.contrib.ec2 import controllers + + +build_resource_relation = functools.partial( + json_home.build_v3_extension_resource_relation, extension_name='OS-EC2', + extension_version='1.0') + +build_parameter_relation = functools.partial( + json_home.build_v3_extension_parameter_relation, extension_name='OS-EC2', + extension_version='1.0') + + +class Ec2Extension(wsgi.ExtensionRouter): + def add_routes(self, mapper): + ec2_controller = controllers.Ec2Controller() + # validation + mapper.connect( + '/ec2tokens', + controller=ec2_controller, + action='authenticate', + conditions=dict(method=['POST'])) + + # crud + mapper.connect( + '/users/{user_id}/credentials/OS-EC2', + controller=ec2_controller, + action='create_credential', + conditions=dict(method=['POST'])) + mapper.connect( + '/users/{user_id}/credentials/OS-EC2', + controller=ec2_controller, + action='get_credentials', + conditions=dict(method=['GET'])) + mapper.connect( + '/users/{user_id}/credentials/OS-EC2/{credential_id}', + controller=ec2_controller, + action='get_credential', + conditions=dict(method=['GET'])) + mapper.connect( + '/users/{user_id}/credentials/OS-EC2/{credential_id}', + controller=ec2_controller, + action='delete_credential', + conditions=dict(method=['DELETE'])) + + +class Ec2ExtensionV3(wsgi.V3ExtensionRouter): + + def add_routes(self, mapper): + ec2_controller = controllers.Ec2ControllerV3() + # validation + self._add_resource( + mapper, ec2_controller, + path='/ec2tokens', + post_action='authenticate', + rel=build_resource_relation(resource_name='ec2tokens')) + + # crud + self._add_resource( + mapper, ec2_controller, + path='/users/{user_id}/credentials/OS-EC2', + get_action='ec2_list_credentials', + post_action='ec2_create_credential', + rel=build_resource_relation(resource_name='user_credentials'), + path_vars={ + 'user_id': json_home.Parameters.USER_ID, + }) + self._add_resource( + mapper, ec2_controller, + path='/users/{user_id}/credentials/OS-EC2/{credential_id}', + get_action='ec2_get_credential', + delete_action='ec2_delete_credential', + rel=build_resource_relation(resource_name='user_credential'), + path_vars={ + 'credential_id': + build_parameter_relation(parameter_name='credential_id'), + 'user_id': json_home.Parameters.USER_ID, + }) |