From 2e7b4f2027a1147ca28301e4f88adf8274b39a1f Mon Sep 17 00:00:00 2001 From: DUVAL Thomas Date: Thu, 9 Jun 2016 09:11:50 +0200 Subject: Update Keystone core to Mitaka. Change-Id: Ia10d6add16f4a9d25d1f42d420661c46332e69db --- keystone-moon/keystone/token/providers/common.py | 248 ++++++---- .../keystone/token/providers/fernet/core.py | 239 +++------ .../token/providers/fernet/token_formatters.py | 543 ++++++++++----------- .../keystone/token/providers/fernet/utils.py | 41 +- keystone-moon/keystone/token/providers/pki.py | 5 + keystone-moon/keystone/token/providers/pkiz.py | 5 + 6 files changed, 523 insertions(+), 558 deletions(-) (limited to 'keystone-moon/keystone/token/providers') diff --git a/keystone-moon/keystone/token/providers/common.py b/keystone-moon/keystone/token/providers/common.py index b71458cd..94729178 100644 --- a/keystone-moon/keystone/token/providers/common.py +++ b/keystone-moon/keystone/token/providers/common.py @@ -14,7 +14,6 @@ from oslo_config import cfg from oslo_log import log -from oslo_log import versionutils from oslo_serialization import jsonutils import six from six.moves.urllib import parse @@ -22,8 +21,8 @@ from six.moves.urllib import parse from keystone.common import controller as common_controller from keystone.common import dependency from keystone.common import utils -from keystone.contrib.federation import constants as federation_constants from keystone import exception +from keystone.federation import constants as federation_constants from keystone.i18n import _, _LE from keystone import token from keystone.token import provider @@ -33,72 +32,69 @@ LOG = log.getLogger(__name__) CONF = cfg.CONF -@dependency.requires('catalog_api', 'resource_api') +@dependency.requires('catalog_api', 'resource_api', 'assignment_api') class V2TokenDataHelper(object): """Creates V2 token data.""" def v3_to_v2_token(self, v3_token_data): + """Convert v3 token data into v2.0 token data. + + This method expects a dictionary generated from + V3TokenDataHelper.get_token_data() and converts it to look like a v2.0 + token dictionary. + + :param v3_token_data: dictionary formatted for v3 tokens + :returns: dictionary formatted for v2 tokens + :raises keystone.exception.Unauthorized: If a specific token type is + not supported in v2. + + """ token_data = {} # Build v2 token v3_token = v3_token_data['token'] + # NOTE(lbragstad): Version 2.0 tokens don't know about any domain other + # than the default domain specified in the configuration. + domain_id = v3_token.get('domain', {}).get('id') + if domain_id and CONF.identity.default_domain_id != domain_id: + msg = ('Unable to validate domain-scoped tokens outside of the ' + 'default domain') + raise exception.Unauthorized(msg) + token = {} token['expires'] = v3_token.get('expires_at') token['issued_at'] = v3_token.get('issued_at') token['audit_ids'] = v3_token.get('audit_ids') - # Bail immediately if this is a domain-scoped token, which is not - # supported by the v2 API at all. - if 'domain' in v3_token: - raise exception.Unauthorized(_( - 'Domains are not supported by the v2 API. Please use the v3 ' - 'API instead.')) - - # Bail if this is a project-scoped token outside the default domain, - # which may result in a namespace collision with a project inside the - # default domain. if 'project' in v3_token: - if (v3_token['project']['domain']['id'] != - CONF.identity.default_domain_id): - raise exception.Unauthorized(_( - 'Project not found in the default domain (please use the ' - 'v3 API instead): %s') % v3_token['project']['id']) - # v3 token_data does not contain all tenant attributes tenant = self.resource_api.get_project( v3_token['project']['id']) - token['tenant'] = common_controller.V2Controller.filter_domain_id( + # Drop domain specific fields since v2 calls are not domain-aware. + token['tenant'] = common_controller.V2Controller.v3_to_v2_project( tenant) token_data['token'] = token # Build v2 user v3_user = v3_token['user'] - # Bail if this is a token outside the default domain, - # which may result in a namespace collision with a project inside the - # default domain. - if ('domain' in v3_user and v3_user['domain']['id'] != - CONF.identity.default_domain_id): - raise exception.Unauthorized(_( - 'User not found in the default domain (please use the v3 API ' - 'instead): %s') % v3_user['id']) - user = common_controller.V2Controller.v3_to_v2_user(v3_user) - # Maintain Trust Data if 'OS-TRUST:trust' in v3_token: - v3_trust_data = v3_token['OS-TRUST:trust'] - token_data['trust'] = { - 'trustee_user_id': v3_trust_data['trustee_user']['id'], - 'id': v3_trust_data['id'], - 'trustor_user_id': v3_trust_data['trustor_user']['id'], - 'impersonation': v3_trust_data['impersonation'] - } + msg = ('Unable to validate trust-scoped tokens using version v2.0 ' + 'API.') + raise exception.Unauthorized(msg) + + if 'OS-OAUTH1' in v3_token: + msg = ('Unable to validate Oauth tokens using the version v2.0 ' + 'API.') + raise exception.Unauthorized(msg) # Set user roles user['roles'] = [] role_ids = [] for role in v3_token.get('roles', []): + role_ids.append(role.pop('id')) user['roles'].append(role) user['roles_links'] = [] @@ -145,7 +141,7 @@ class V2TokenDataHelper(object): o = {'access': {'token': {'id': token_ref['id'], 'expires': expires, - 'issued_at': utils.strtime(), + 'issued_at': utils.isotime(subsecond=True), 'audit_ids': audit_info }, 'user': {'id': user_ref['id'], @@ -186,7 +182,8 @@ class V2TokenDataHelper(object): @classmethod def format_catalog(cls, catalog_ref): - """Munge catalogs from internal to output format + """Munge catalogs from internal to output format. + Internal catalogs look like:: {$REGION: { @@ -235,6 +232,7 @@ class V2TokenDataHelper(object): 'identity_api', 'resource_api', 'role_api', 'trust_api') class V3TokenDataHelper(object): """Token data helper.""" + def __init__(self): # Keep __init__ around to ensure dependency injection works. super(V3TokenDataHelper, self).__init__() @@ -248,8 +246,12 @@ class V3TokenDataHelper(object): filtered_project = { 'id': project_ref['id'], 'name': project_ref['name']} - filtered_project['domain'] = self._get_filtered_domain( - project_ref['domain_id']) + if project_ref['domain_id'] is not None: + filtered_project['domain'] = ( + self._get_filtered_domain(project_ref['domain_id'])) + else: + # Projects acting as a domain do not have a domain_id attribute + filtered_project['domain'] = None return filtered_project def _populate_scope(self, token_data, domain_id, project_id): @@ -262,6 +264,18 @@ class V3TokenDataHelper(object): if project_id: token_data['project'] = self._get_filtered_project(project_id) + def _populate_is_admin_project(self, token_data): + # TODO(ayoung): Support the ability for a project acting as a domain + # to be the admin project once the rest of the code for projects + # acting as domains is merged. Code will likely be: + # (r.admin_project_name == None and project['is_domain'] == True + # and project['name'] == r.admin_project_domain_name) + project = token_data['project'] + r = CONF.resource + if (project['name'] == r.admin_project_name and + project['domain']['name'] == r.admin_project_domain_name): + token_data['is_admin_project'] = True + def _get_roles_for_user(self, user_id, domain_id, project_id): roles = [] if domain_id: @@ -282,12 +296,12 @@ class V3TokenDataHelper(object): place. :param token_data: a dictionary used for building token response - :group_ids: list of group IDs a user is a member of - :project_id: project ID to scope to - :domain_id: domain ID to scope to - :user_id: user ID + :param group_ids: list of group IDs a user is a member of + :param project_id: project ID to scope to + :param domain_id: domain ID to scope to + :param user_id: user ID - :raises: exception.Unauthorized - when no roles were found for a + :raises keystone.exception.Unauthorized: when no roles were found for a (group_ids, project_id) or (group_ids, domain_id) pairs. """ @@ -370,7 +384,16 @@ class V3TokenDataHelper(object): return if CONF.trust.enabled and trust: - token_user_id = trust['trustor_user_id'] + # If redelegated_trust_id is set, then we must traverse the + # trust_chain in order to determine who the original trustor is. We + # need to do this because the user ID of the original trustor helps + # us determine scope in the redelegated context. + if trust.get('redelegated_trust_id'): + trust_chain = self.trust_api.get_trust_pedigree(trust['id']) + token_user_id = trust_chain[-1]['trustor_user_id'] + else: + token_user_id = trust['trustor_user_id'] + token_project_id = trust['project_id'] # trusts do not support domains yet token_domain_id = None @@ -380,21 +403,39 @@ class V3TokenDataHelper(object): token_domain_id = domain_id if token_domain_id or token_project_id: - roles = self._get_roles_for_user(token_user_id, - token_domain_id, - token_project_id) filtered_roles = [] if CONF.trust.enabled and trust: - for trust_role in trust['roles']: - match_roles = [x for x in roles - if x['id'] == trust_role['id']] + # First expand out any roles that were in the trust to include + # any implied roles, whether global or domain specific + refs = [{'role_id': role['id']} for role in trust['roles']] + effective_trust_roles = ( + self.assignment_api.add_implied_roles(refs)) + # Now get the current role assignments for the trustor, + # including any domain specific roles. + assignment_list = self.assignment_api.list_role_assignments( + user_id=token_user_id, + project_id=token_project_id, + effective=True, strip_domain_roles=False) + current_effective_trustor_roles = ( + list(set([x['role_id'] for x in assignment_list]))) + # Go through each of the effective trust roles, making sure the + # trustor still has them, if any have been removed, then we + # will treat the trust as invalid + for trust_role in effective_trust_roles: + + match_roles = [x for x in current_effective_trustor_roles + if x == trust_role['role_id']] if match_roles: - filtered_roles.append(match_roles[0]) + role = self.role_api.get_role(match_roles[0]) + if role['domain_id'] is None: + filtered_roles.append(role) else: raise exception.Forbidden( _('Trustee has no delegated roles.')) else: - for role in roles: + for role in self._get_roles_for_user(token_user_id, + token_domain_id, + token_project_id): filtered_roles.append({'id': role['id'], 'name': role['name']}) @@ -426,7 +467,6 @@ class V3TokenDataHelper(object): if project_id or domain_id: service_catalog = self.catalog_api.get_v3_catalog( user_id, project_id) - # TODO(ayoung): Enforce Endpoints for trust token_data['catalog'] = service_catalog def _populate_service_providers(self, token_data): @@ -458,20 +498,11 @@ class V3TokenDataHelper(object): LOG.error(msg) raise exception.UnexpectedError(msg) - def get_token_data(self, user_id, method_names, extras=None, - domain_id=None, project_id=None, expires=None, - trust=None, token=None, include_catalog=True, - bind=None, access_token=None, issued_at=None, - audit_info=None): - if extras is None: - extras = {} - if extras: - versionutils.deprecated( - what='passing token data with "extras"', - as_of=versionutils.deprecated.KILO, - in_favor_of='well-defined APIs')(lambda: None)() - token_data = {'methods': method_names, - 'extras': extras} + def get_token_data(self, user_id, method_names, domain_id=None, + project_id=None, expires=None, trust=None, token=None, + include_catalog=True, bind=None, access_token=None, + issued_at=None, audit_info=None): + token_data = {'methods': method_names} # We've probably already written these to the token if token: @@ -479,14 +510,12 @@ class V3TokenDataHelper(object): if x in token: token_data[x] = token[x] - if CONF.trust.enabled and trust: - if user_id != trust['trustee_user_id']: - raise exception.Forbidden(_('User is not a trustee.')) - if bind: token_data['bind'] = bind self._populate_scope(token_data, domain_id, project_id) + if token_data.get('project'): + self._populate_is_admin_project(token_data) self._populate_user(token_data, user_id, trust) self._populate_roles(token_data, user_id, domain_id, project_id, trust, access_token) @@ -527,6 +556,11 @@ class BaseProvider(provider.Provider): def issue_v2_token(self, token_ref, roles_ref=None, catalog_ref=None): + if token_ref.get('bind') and not self._supports_bind_authentication: + msg = _('The configured token provider does not support bind ' + 'authentication.') + raise exception.NotImplemented(message=msg) + metadata_ref = token_ref['metadata'] trust_ref = None if CONF.trust.enabled and metadata_ref and 'trust_id' in metadata_ref: @@ -559,6 +593,10 @@ class BaseProvider(provider.Provider): 'trust_id' in metadata_ref): trust = self.trust_api.get_trust(metadata_ref['trust_id']) + if CONF.trust.enabled and trust: + if user_id != trust['trustee_user_id']: + raise exception.Forbidden(_('User is not a trustee.')) + token_ref = None if auth_context and self._is_mapped_token(auth_context): token_ref = self._handle_mapped_tokens( @@ -572,7 +610,6 @@ class BaseProvider(provider.Provider): token_data = self.v3_token_data_helper.get_token_data( user_id, method_names, - auth_context.get('extras') if auth_context else None, domain_id=domain_id, project_id=project_id, expires=expires_at, @@ -636,21 +673,10 @@ class BaseProvider(provider.Provider): token.provider.V3): # this is a V3 token msg = _('Non-default domain is not supported') - # user in a non-default is prohibited - if (token_ref['token_data']['token']['user']['domain']['id'] != - CONF.identity.default_domain_id): - raise exception.Unauthorized(msg) # domain scoping is prohibited if token_ref['token_data']['token'].get('domain'): raise exception.Unauthorized( _('Domain scoped token is not supported')) - # project in non-default domain is prohibited - if token_ref['token_data']['token'].get('project'): - project = token_ref['token_data']['token']['project'] - project_domain_id = project['domain']['id'] - # scoped to project in non-default domain is prohibited - if project_domain_id != CONF.identity.default_domain_id: - raise exception.Unauthorized(msg) # if token is scoped to trust, both trustor and trustee must # be in the default domain. Furthermore, the delegated project # must also be in the default domain @@ -693,14 +719,58 @@ class BaseProvider(provider.Provider): trust_id = token_data['access'].get('trust', {}).get('id') if trust_id: - # token trust validation - self.trust_api.get_trust(trust_id) + msg = ('Unable to validate trust-scoped tokens using version ' + 'v2.0 API.') + raise exception.Unauthorized(msg) return token_data - except exception.ValidationError as e: + except exception.ValidationError: LOG.exception(_LE('Failed to validate token')) + token_id = token_ref['token_data']['access']['token']['id'] + raise exception.TokenNotFound(token_id=token_id) + + def validate_non_persistent_token(self, token_id): + try: + (user_id, methods, audit_ids, domain_id, project_id, trust_id, + federated_info, access_token_id, created_at, expires_at) = ( + self.token_formatter.validate_token(token_id)) + except exception.ValidationError as e: raise exception.TokenNotFound(e) + token_dict = None + trust_ref = None + if federated_info: + # NOTE(lbragstad): We need to rebuild information about the + # federated token as well as the federated token roles. This is + # because when we validate a non-persistent token, we don't have a + # token reference to pull the federated token information out of. + # As a result, we have to extract it from the token itself and + # rebuild the federated context. These private methods currently + # live in the keystone.token.providers.fernet.Provider() class. + token_dict = self._rebuild_federated_info(federated_info, user_id) + if project_id or domain_id: + self._rebuild_federated_token_roles(token_dict, federated_info, + user_id, project_id, + domain_id) + if trust_id: + trust_ref = self.trust_api.get_trust(trust_id) + + access_token = None + if access_token_id: + access_token = self.oauth_api.get_access_token(access_token_id) + + return self.v3_token_data_helper.get_token_data( + user_id, + method_names=methods, + domain_id=domain_id, + project_id=project_id, + issued_at=created_at, + expires=expires_at, + trust=trust_ref, + token=token_dict, + access_token=access_token, + audit_info=audit_ids) + def validate_v3_token(self, token_ref): # FIXME(gyee): performance or correctness? Should we return the # cached token or reconstruct it? Obviously if we are going with diff --git a/keystone-moon/keystone/token/providers/fernet/core.py b/keystone-moon/keystone/token/providers/fernet/core.py index a71c375b..ff6fe9cc 100644 --- a/keystone-moon/keystone/token/providers/fernet/core.py +++ b/keystone-moon/keystone/token/providers/fernet/core.py @@ -11,23 +11,18 @@ # under the License. from oslo_config import cfg -from oslo_log import log from keystone.common import dependency from keystone.common import utils as ks_utils -from keystone.contrib.federation import constants as federation_constants -from keystone import exception -from keystone.i18n import _ -from keystone.token import provider +from keystone.federation import constants as federation_constants from keystone.token.providers import common from keystone.token.providers.fernet import token_formatters as tf CONF = cfg.CONF -LOG = log.getLogger(__name__) -@dependency.requires('trust_api') +@dependency.requires('trust_api', 'oauth_api') class Provider(common.BaseProvider): def __init__(self, *args, **kwargs): super(Provider, self).__init__(*args, **kwargs) @@ -38,65 +33,10 @@ class Provider(common.BaseProvider): """Should the token be written to a backend.""" return False - def issue_v2_token(self, token_ref, roles_ref=None, catalog_ref=None): - """Issue a V2 formatted token. - - :param token_ref: reference describing the token - :param roles_ref: reference describing the roles for the token - :param catalog_ref: reference describing the token's catalog - :returns: tuple containing the ID of the token and the token data - - """ - # TODO(lbragstad): Currently, Fernet tokens don't support bind in the - # token format. Raise a 501 if we're dealing with bind. - if token_ref.get('bind'): - raise exception.NotImplemented() - - user_id = token_ref['user']['id'] - # Default to password since methods not provided by token_ref - method_names = ['password'] - project_id = None - # Verify that tenant is not None in token_ref - if token_ref.get('tenant'): - project_id = token_ref['tenant']['id'] - - # maintain expiration time across rescopes - expires = token_ref.get('expires') - - parent_audit_id = token_ref.get('parent_audit_id') - # If parent_audit_id is defined then a token authentication was made - if parent_audit_id: - method_names.append('token') - - audit_ids = provider.audit_info(parent_audit_id) - - # Get v3 token data and exclude building v3 specific catalog. This is - # due to the fact that the V2TokenDataHelper.format_token() method - # doesn't build any of the token_reference from other Keystone APIs. - # Instead, it builds it from what is persisted in the token reference. - # Here we are going to leverage the V3TokenDataHelper.get_token_data() - # method written for V3 because it goes through and populates the token - # reference dynamically. Once we have a V3 token reference, we can - # attempt to convert it to a V2 token response. - v3_token_data = self.v3_token_data_helper.get_token_data( - user_id, - method_names, - project_id=project_id, - token=token_ref, - include_catalog=False, - audit_info=audit_ids, - expires=expires) - - expires_at = v3_token_data['token']['expires_at'] - token_id = self.token_formatter.create_token(user_id, expires_at, - audit_ids, - methods=method_names, - project_id=project_id) - self._build_issued_at_info(token_id, v3_token_data) - # Convert v3 to v2 token data and build v2 catalog - token_data = self.v2_token_data_helper.v3_to_v2_token(v3_token_data) - token_data['access']['token']['id'] = token_id - + def issue_v2_token(self, *args, **kwargs): + token_id, token_data = super(Provider, self).issue_v2_token( + *args, **kwargs) + self._build_issued_at_info(token_id, token_data) return token_id, token_data def issue_v3_token(self, *args, **kwargs): @@ -117,8 +57,12 @@ class Provider(common.BaseProvider): # that we have to rely on when we validate the token. fernet_creation_datetime_obj = self.token_formatter.creation_time( token_id) - token_data['token']['issued_at'] = ks_utils.isotime( - at=fernet_creation_datetime_obj, subsecond=True) + if token_data.get('access'): + token_data['access']['token']['issued_at'] = ks_utils.isotime( + at=fernet_creation_datetime_obj, subsecond=True) + else: + token_data['token']['issued_at'] = ks_utils.isotime( + at=fernet_creation_datetime_obj, subsecond=True) def _build_federated_info(self, token_data): """Extract everything needed for federated tokens. @@ -127,18 +71,18 @@ class Provider(common.BaseProvider): the values and build federated Fernet tokens. """ - idp_id = token_data['token'].get('user', {}).get( - federation_constants.FEDERATION, {}).get( - 'identity_provider', {}).get('id') - protocol_id = token_data['token'].get('user', {}).get( - federation_constants.FEDERATION, {}).get('protocol', {}).get('id') - # If we don't have an identity provider ID and a protocol ID, it's safe - # to assume we aren't dealing with a federated token. - if not (idp_id and protocol_id): - return None - - group_ids = token_data['token'].get('user', {}).get( - federation_constants.FEDERATION, {}).get('groups') + token_data = token_data['token'] + try: + user = token_data['user'] + federation = user[federation_constants.FEDERATION] + idp_id = federation['identity_provider']['id'] + protocol_id = federation['protocol']['id'] + except KeyError: + # The token data doesn't have federated info, so we aren't dealing + # with a federated token and no federated info to build. + return + + group_ids = federation.get('groups') return {'group_ids': group_ids, 'idp_id': idp_id, @@ -195,96 +139,66 @@ class Provider(common.BaseProvider): self.v3_token_data_helper.populate_roles_for_groups( token_dict, group_ids, project_id, domain_id, user_id) - def validate_v2_token(self, token_ref): - """Validate a V2 formatted token. - - :param token_ref: reference describing the token to validate - :returns: the token data - :raises keystone.exception.TokenNotFound: if token format is invalid - :raises keystone.exception.Unauthorized: if v3 token is used - - """ - try: - (user_id, methods, - audit_ids, domain_id, - project_id, trust_id, - federated_info, created_at, - expires_at) = self.token_formatter.validate_token(token_ref) - except exception.ValidationError as e: - raise exception.TokenNotFound(e) - - if trust_id or domain_id or federated_info: - msg = _('This is not a v2.0 Fernet token. Use v3 for trust, ' - 'domain, or federated tokens.') - raise exception.Unauthorized(msg) - - v3_token_data = self.v3_token_data_helper.get_token_data( - user_id, - methods, - project_id=project_id, - expires=expires_at, - issued_at=created_at, - token=token_ref, - include_catalog=False, - audit_info=audit_ids) - token_data = self.v2_token_data_helper.v3_to_v2_token(v3_token_data) - token_data['access']['token']['id'] = token_ref - return token_data - - def validate_v3_token(self, token): - """Validate a V3 formatted token. - - :param token: a string describing the token to validate - :returns: the token data - :raises keystone.exception.TokenNotFound: if token format version isn't - supported - - """ - try: - (user_id, methods, audit_ids, domain_id, project_id, trust_id, - federated_info, created_at, expires_at) = ( - self.token_formatter.validate_token(token)) - except exception.ValidationError as e: - raise exception.TokenNotFound(e) - - token_dict = None - trust_ref = None - if federated_info: - token_dict = self._rebuild_federated_info(federated_info, user_id) - if project_id or domain_id: - self._rebuild_federated_token_roles(token_dict, federated_info, - user_id, project_id, - domain_id) - if trust_id: - trust_ref = self.trust_api.get_trust(trust_id) - - return self.v3_token_data_helper.get_token_data( - user_id, - method_names=methods, - domain_id=domain_id, - project_id=project_id, - issued_at=created_at, - expires=expires_at, - trust=trust_ref, - token=token_dict, - audit_info=audit_ids) + def _extract_v2_token_data(self, token_data): + user_id = token_data['access']['user']['id'] + expires_at = token_data['access']['token']['expires'] + audit_ids = token_data['access']['token'].get('audit_ids') + methods = ['password'] + if len(audit_ids) > 1: + methods.append('token') + project_id = token_data['access']['token'].get('tenant', {}).get('id') + domain_id = None + trust_id = None + access_token_id = None + federated_info = None + return (user_id, expires_at, audit_ids, methods, domain_id, project_id, + trust_id, access_token_id, federated_info) + + def _extract_v3_token_data(self, token_data): + """Extract information from a v3 token reference.""" + user_id = token_data['token']['user']['id'] + expires_at = token_data['token']['expires_at'] + audit_ids = token_data['token']['audit_ids'] + methods = token_data['token'].get('methods') + domain_id = token_data['token'].get('domain', {}).get('id') + project_id = token_data['token'].get('project', {}).get('id') + trust_id = token_data['token'].get('OS-TRUST:trust', {}).get('id') + access_token_id = token_data['token'].get('OS-OAUTH1', {}).get( + 'access_token_id') + federated_info = self._build_federated_info(token_data) + + return (user_id, expires_at, audit_ids, methods, domain_id, project_id, + trust_id, access_token_id, federated_info) def _get_token_id(self, token_data): """Generate the token_id based upon the data in token_data. :param token_data: token information :type token_data: dict - :raises keystone.exception.NotImplemented: when called + :rtype: six.text_type + """ + # NOTE(lbragstad): Only v2.0 token responses include an 'access' + # attribute. + if token_data.get('access'): + (user_id, expires_at, audit_ids, methods, domain_id, project_id, + trust_id, access_token_id, federated_info) = ( + self._extract_v2_token_data(token_data)) + else: + (user_id, expires_at, audit_ids, methods, domain_id, project_id, + trust_id, access_token_id, federated_info) = ( + self._extract_v3_token_data(token_data)) + return self.token_formatter.create_token( - token_data['token']['user']['id'], - token_data['token']['expires_at'], - token_data['token']['audit_ids'], - methods=token_data['token'].get('methods'), - domain_id=token_data['token'].get('domain', {}).get('id'), - project_id=token_data['token'].get('project', {}).get('id'), - trust_id=token_data['token'].get('OS-TRUST:trust', {}).get('id'), - federated_info=self._build_federated_info(token_data) + user_id, + expires_at, + audit_ids, + methods=methods, + domain_id=domain_id, + project_id=project_id, + trust_id=trust_id, + federated_info=federated_info, + access_token_id=access_token_id ) @property @@ -292,5 +206,6 @@ class Provider(common.BaseProvider): """Return if the token provider supports bind authentication methods. :returns: False + """ return False diff --git a/keystone-moon/keystone/token/providers/fernet/token_formatters.py b/keystone-moon/keystone/token/providers/fernet/token_formatters.py index dbfee6dd..dfdd06e8 100644 --- a/keystone-moon/keystone/token/providers/fernet/token_formatters.py +++ b/keystone-moon/keystone/token/providers/fernet/token_formatters.py @@ -20,7 +20,6 @@ import msgpack from oslo_config import cfg from oslo_log import log from oslo_utils import timeutils -import six from six.moves import map from six.moves import urllib @@ -66,14 +65,22 @@ class TokenFormatter(object): return fernet.MultiFernet(fernet_instances) def pack(self, payload): - """Pack a payload for transport as a token.""" + """Pack a payload for transport as a token. + + :type payload: six.binary_type + :rtype: six.text_type + + """ # base64 padding (if any) is not URL-safe - return self.crypto.encrypt(payload).rstrip('=') + return self.crypto.encrypt(payload).rstrip(b'=').decode('utf-8') def unpack(self, token): - """Unpack a token, and validate the payload.""" - token = six.binary_type(token) + """Unpack a token, and validate the payload. + :type token: six.text_type + :rtype: six.binary_type + + """ # TODO(lbragstad): Restore padding on token before decoding it. # Initially in Kilo, Fernet tokens were returned to the user with # padding appended to the token. Later in Liberty this padding was @@ -89,16 +96,17 @@ class TokenFormatter(object): token = TokenFormatter.restore_padding(token) try: - return self.crypto.decrypt(token) + return self.crypto.decrypt(token.encode('utf-8')) except fernet.InvalidToken: raise exception.ValidationError( - _('This is not a recognized Fernet token')) + _('This is not a recognized Fernet token %s') % token) @classmethod def restore_padding(cls, token): """Restore padding based on token size. :param token: token to restore padding on + :type token: six.text_type :returns: token with correct padding """ @@ -106,21 +114,22 @@ class TokenFormatter(object): mod_returned = len(token) % 4 if mod_returned: missing_padding = 4 - mod_returned - token += b'=' * missing_padding + token += '=' * missing_padding return token @classmethod def creation_time(cls, fernet_token): - """Returns the creation time of a valid Fernet token.""" - # tokens may be transmitted as Unicode, but they're just ASCII - # (pypi/cryptography will refuse to operate on Unicode input) - fernet_token = six.binary_type(fernet_token) + """Returns the creation time of a valid Fernet token. - # Restore padding on token before decoding it + :type fernet_token: six.text_type + + """ fernet_token = TokenFormatter.restore_padding(fernet_token) + # fernet_token is six.text_type - # fernet tokens are base64 encoded, so we need to unpack them first - token_bytes = base64.urlsafe_b64decode(fernet_token) + # Fernet tokens are base64 encoded, so we need to unpack them first + # urlsafe_b64decode() requires six.binary_type + token_bytes = base64.urlsafe_b64decode(fernet_token.encode('utf-8')) # slice into the byte array to get just the timestamp timestamp_bytes = token_bytes[TIMESTAMP_START:TIMESTAMP_END] @@ -136,66 +145,20 @@ class TokenFormatter(object): def create_token(self, user_id, expires_at, audit_ids, methods=None, domain_id=None, project_id=None, trust_id=None, - federated_info=None): + federated_info=None, access_token_id=None): """Given a set of payload attributes, generate a Fernet token.""" - if trust_id: - version = TrustScopedPayload.version - payload = TrustScopedPayload.assemble( - user_id, - methods, - project_id, - expires_at, - audit_ids, - trust_id) - elif project_id and federated_info: - version = FederatedProjectScopedPayload.version - payload = FederatedProjectScopedPayload.assemble( - user_id, - methods, - project_id, - expires_at, - audit_ids, - federated_info) - elif domain_id and federated_info: - version = FederatedDomainScopedPayload.version - payload = FederatedDomainScopedPayload.assemble( - user_id, - methods, - domain_id, - expires_at, - audit_ids, - federated_info) - elif federated_info: - version = FederatedUnscopedPayload.version - payload = FederatedUnscopedPayload.assemble( - user_id, - methods, - expires_at, - audit_ids, - federated_info) - elif project_id: - version = ProjectScopedPayload.version - payload = ProjectScopedPayload.assemble( - user_id, - methods, - project_id, - expires_at, - audit_ids) - elif domain_id: - version = DomainScopedPayload.version - payload = DomainScopedPayload.assemble( - user_id, - methods, - domain_id, - expires_at, - audit_ids) - else: - version = UnscopedPayload.version - payload = UnscopedPayload.assemble( - user_id, - methods, - expires_at, - audit_ids) + for payload_class in PAYLOAD_CLASSES: + if payload_class.create_arguments_apply( + project_id=project_id, domain_id=domain_id, + trust_id=trust_id, federated_info=federated_info, + access_token_id=access_token_id): + break + + version = payload_class.version + payload = payload_class.assemble( + user_id, methods, project_id, domain_id, expires_at, audit_ids, + trust_id, federated_info, access_token_id + ) versioned_payload = (version,) + payload serialized_payload = msgpack.packb(versioned_payload) @@ -215,44 +178,21 @@ class TokenFormatter(object): return token def validate_token(self, token): - """Validates a Fernet token and returns the payload attributes.""" - # Convert v2 unicode token to a string - if not isinstance(token, six.binary_type): - token = token.encode('ascii') + """Validates a Fernet token and returns the payload attributes. + :type token: six.text_type + + """ serialized_payload = self.unpack(token) versioned_payload = msgpack.unpackb(serialized_payload) version, payload = versioned_payload[0], versioned_payload[1:] - # depending on the formatter, these may or may not be defined - domain_id = None - project_id = None - trust_id = None - federated_info = None - - if version == UnscopedPayload.version: - (user_id, methods, expires_at, audit_ids) = ( - UnscopedPayload.disassemble(payload)) - elif version == DomainScopedPayload.version: - (user_id, methods, domain_id, expires_at, audit_ids) = ( - DomainScopedPayload.disassemble(payload)) - elif version == ProjectScopedPayload.version: - (user_id, methods, project_id, expires_at, audit_ids) = ( - ProjectScopedPayload.disassemble(payload)) - elif version == TrustScopedPayload.version: - (user_id, methods, project_id, expires_at, audit_ids, trust_id) = ( - TrustScopedPayload.disassemble(payload)) - elif version == FederatedUnscopedPayload.version: - (user_id, methods, expires_at, audit_ids, federated_info) = ( - FederatedUnscopedPayload.disassemble(payload)) - elif version == FederatedProjectScopedPayload.version: - (user_id, methods, project_id, expires_at, audit_ids, - federated_info) = FederatedProjectScopedPayload.disassemble( - payload) - elif version == FederatedDomainScopedPayload.version: - (user_id, methods, domain_id, expires_at, audit_ids, - federated_info) = FederatedDomainScopedPayload.disassemble( - payload) + for payload_class in PAYLOAD_CLASSES: + if version == payload_class.version: + (user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id) = ( + payload_class.disassemble(payload)) + break else: # If the token_format is not recognized, raise ValidationError. raise exception.ValidationError(_( @@ -267,7 +207,7 @@ class TokenFormatter(object): expires_at = ks_utils.isotime(at=expires_at, subsecond=True) return (user_id, methods, audit_ids, domain_id, project_id, trust_id, - federated_info, created_at, expires_at) + federated_info, access_token_id, created_at, expires_at) class BasePayload(object): @@ -275,10 +215,32 @@ class BasePayload(object): version = None @classmethod - def assemble(cls, *args): + def create_arguments_apply(cls, **kwargs): + """Check the arguments to see if they apply to this payload variant. + + :returns: True if the arguments indicate that this payload class is + needed for the token otherwise returns False. + :rtype: bool + + """ + raise NotImplementedError() + + @classmethod + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): """Assemble the payload of a token. - :param args: whatever data should go into the payload + :param user_id: identifier of the user in the token request + :param methods: list of authentication methods used + :param project_id: ID of the project to scope to + :param domain_id: ID of the domain to scope to + :param expires_at: datetime of the token's expiration + :param audit_ids: list of the token's audit IDs + :param trust_id: ID of the trust in effect + :param federated_info: dictionary containing group IDs, the identity + provider ID, protocol ID, and federated domain + ID + :param access_token_id: ID of the secret in OAuth1 authentication :returns: the payload of a token """ @@ -288,6 +250,17 @@ class BasePayload(object): def disassemble(cls, payload): """Disassemble an unscoped payload into the component data. + The tuple consists of:: + + (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) + + * ``methods`` are the auth methods. + * federated_info is a dict contains the group IDs, the identity + provider ID, the protocol ID, and the federated domain ID + + Fields will be set to None if they didn't apply to this payload type. + :param payload: this variant of payload :returns: a tuple of the payloads component data @@ -302,9 +275,6 @@ class BasePayload(object): :returns: a byte representation of the uuid """ - # TODO(lbragstad): Wrap this in an exception. Not sure what the case - # would be where we couldn't handle what we've been given but incase - # the integrity of the token has been compromised. uuid_obj = uuid.UUID(uuid_string) return uuid_obj.bytes @@ -316,18 +286,15 @@ class BasePayload(object): :returns: uuid hex formatted string """ - # TODO(lbragstad): Wrap this in an exception. Not sure what the case - # would be where we couldn't handle what we've been given but incase - # the integrity of the token has been compromised. uuid_obj = uuid.UUID(bytes=uuid_byte_string) return uuid_obj.hex @classmethod - def _convert_time_string_to_int(cls, time_string): - """Convert a time formatted string to a timestamp integer. + def _convert_time_string_to_float(cls, time_string): + """Convert a time formatted string to a float. :param time_string: time formatted string - :returns: an integer timestamp + :returns: a timestamp as a float """ time_object = timeutils.parse_isotime(time_string) @@ -335,14 +302,14 @@ class BasePayload(object): datetime.datetime.utcfromtimestamp(0)).total_seconds() @classmethod - def _convert_int_to_time_string(cls, time_int): - """Convert a timestamp integer to a string. + def _convert_float_to_time_string(cls, time_float): + """Convert a floating point timestamp to a string. - :param time_int: integer representing timestamp + :param time_float: integer representing timestamp :returns: a time formatted strings """ - time_object = datetime.datetime.utcfromtimestamp(time_int) + time_object = datetime.datetime.utcfromtimestamp(time_float) return ks_utils.isotime(time_object, subsecond=True) @classmethod @@ -361,74 +328,51 @@ class BasePayload(object): # federation) return (False, value) - @classmethod - def attempt_convert_uuid_bytes_to_hex(cls, value): - """Attempt to convert value to hex or return value. - - :param value: value to attempt to convert to hex - :returns: uuid value in hex or value - - """ - try: - return cls.convert_uuid_bytes_to_hex(value) - except ValueError: - return value - class UnscopedPayload(BasePayload): version = 0 @classmethod - def assemble(cls, user_id, methods, expires_at, audit_ids): - """Assemble the payload of an unscoped token. - - :param user_id: identifier of the user in the token request - :param methods: list of authentication methods used - :param expires_at: datetime of the token's expiration - :param audit_ids: list of the token's audit IDs - :returns: the payload of an unscoped token + def create_arguments_apply(cls, **kwargs): + return True - """ + @classmethod + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) - expires_at_int = cls._convert_time_string_to_int(expires_at) + expires_at_int = cls._convert_time_string_to_float(expires_at) b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, audit_ids)) return (b_user_id, methods, expires_at_int, b_audit_ids) @classmethod def disassemble(cls, payload): - """Disassemble an unscoped payload into the component data. - - :param payload: the payload of an unscoped token - :return: a tuple containing the user_id, auth methods, expires_at, and - audit_ids - - """ (is_stored_as_bytes, user_id) = payload[0] if is_stored_as_bytes: - user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id) + user_id = cls.convert_uuid_bytes_to_hex(user_id) methods = auth_plugins.convert_integer_to_method_list(payload[1]) - expires_at_str = cls._convert_int_to_time_string(payload[2]) + expires_at_str = cls._convert_float_to_time_string(payload[2]) audit_ids = list(map(provider.base64_encode, payload[3])) - return (user_id, methods, expires_at_str, audit_ids) + project_id = None + domain_id = None + trust_id = None + federated_info = None + access_token_id = None + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) class DomainScopedPayload(BasePayload): version = 1 @classmethod - def assemble(cls, user_id, methods, domain_id, expires_at, audit_ids): - """Assemble the payload of a domain-scoped token. + def create_arguments_apply(cls, **kwargs): + return kwargs['domain_id'] - :param user_id: ID of the user in the token request - :param methods: list of authentication methods used - :param domain_id: ID of the domain to scope to - :param expires_at: datetime of the token's expiration - :param audit_ids: list of the token's audit IDs - :returns: the payload of a domain-scoped token - - """ + @classmethod + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) try: @@ -439,23 +383,16 @@ class DomainScopedPayload(BasePayload): b_domain_id = domain_id else: raise - expires_at_int = cls._convert_time_string_to_int(expires_at) + expires_at_int = cls._convert_time_string_to_float(expires_at) b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, audit_ids)) return (b_user_id, methods, b_domain_id, expires_at_int, b_audit_ids) @classmethod def disassemble(cls, payload): - """Disassemble a payload into the component data. - - :param payload: the payload of a token - :return: a tuple containing the user_id, auth methods, domain_id, - expires_at_str, and audit_ids - - """ (is_stored_as_bytes, user_id) = payload[0] if is_stored_as_bytes: - user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id) + user_id = cls.convert_uuid_bytes_to_hex(user_id) methods = auth_plugins.convert_integer_to_method_list(payload[1]) try: domain_id = cls.convert_uuid_bytes_to_hex(payload[2]) @@ -465,79 +402,68 @@ class DomainScopedPayload(BasePayload): domain_id = payload[2] else: raise - expires_at_str = cls._convert_int_to_time_string(payload[3]) + expires_at_str = cls._convert_float_to_time_string(payload[3]) audit_ids = list(map(provider.base64_encode, payload[4])) - - return (user_id, methods, domain_id, expires_at_str, audit_ids) + project_id = None + trust_id = None + federated_info = None + access_token_id = None + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) class ProjectScopedPayload(BasePayload): version = 2 @classmethod - def assemble(cls, user_id, methods, project_id, expires_at, audit_ids): - """Assemble the payload of a project-scoped token. + def create_arguments_apply(cls, **kwargs): + return kwargs['project_id'] - :param user_id: ID of the user in the token request - :param methods: list of authentication methods used - :param project_id: ID of the project to scope to - :param expires_at: datetime of the token's expiration - :param audit_ids: list of the token's audit IDs - :returns: the payload of a project-scoped token - - """ + @classmethod + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id) - expires_at_int = cls._convert_time_string_to_int(expires_at) + expires_at_int = cls._convert_time_string_to_float(expires_at) b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, audit_ids)) return (b_user_id, methods, b_project_id, expires_at_int, b_audit_ids) @classmethod def disassemble(cls, payload): - """Disassemble a payload into the component data. - - :param payload: the payload of a token - :return: a tuple containing the user_id, auth methods, project_id, - expires_at_str, and audit_ids - - """ (is_stored_as_bytes, user_id) = payload[0] if is_stored_as_bytes: - user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id) + user_id = cls.convert_uuid_bytes_to_hex(user_id) methods = auth_plugins.convert_integer_to_method_list(payload[1]) (is_stored_as_bytes, project_id) = payload[2] if is_stored_as_bytes: - project_id = cls.attempt_convert_uuid_bytes_to_hex(project_id) - expires_at_str = cls._convert_int_to_time_string(payload[3]) + project_id = cls.convert_uuid_bytes_to_hex(project_id) + expires_at_str = cls._convert_float_to_time_string(payload[3]) audit_ids = list(map(provider.base64_encode, payload[4])) - - return (user_id, methods, project_id, expires_at_str, audit_ids) + domain_id = None + trust_id = None + federated_info = None + access_token_id = None + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) class TrustScopedPayload(BasePayload): version = 3 @classmethod - def assemble(cls, user_id, methods, project_id, expires_at, audit_ids, - trust_id): - """Assemble the payload of a trust-scoped token. - - :param user_id: ID of the user in the token request - :param methods: list of authentication methods used - :param project_id: ID of the project to scope to - :param expires_at: datetime of the token's expiration - :param audit_ids: list of the token's audit IDs - :param trust_id: ID of the trust in effect - :returns: the payload of a trust-scoped token + def create_arguments_apply(cls, **kwargs): + return kwargs['trust_id'] - """ + @classmethod + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id) b_trust_id = cls.convert_uuid_hex_to_bytes(trust_id) - expires_at_int = cls._convert_time_string_to_int(expires_at) + expires_at_int = cls._convert_time_string_to_float(expires_at) b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, audit_ids)) @@ -546,31 +472,30 @@ class TrustScopedPayload(BasePayload): @classmethod def disassemble(cls, payload): - """Validate a trust-based payload. - - :param token_string: a string representing the token - :returns: a tuple containing the user_id, auth methods, project_id, - expires_at_str, audit_ids, and trust_id - - """ (is_stored_as_bytes, user_id) = payload[0] if is_stored_as_bytes: - user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id) + user_id = cls.convert_uuid_bytes_to_hex(user_id) methods = auth_plugins.convert_integer_to_method_list(payload[1]) (is_stored_as_bytes, project_id) = payload[2] if is_stored_as_bytes: - project_id = cls.attempt_convert_uuid_bytes_to_hex(project_id) - expires_at_str = cls._convert_int_to_time_string(payload[3]) + project_id = cls.convert_uuid_bytes_to_hex(project_id) + expires_at_str = cls._convert_float_to_time_string(payload[3]) audit_ids = list(map(provider.base64_encode, payload[4])) trust_id = cls.convert_uuid_bytes_to_hex(payload[5]) - - return (user_id, methods, project_id, expires_at_str, audit_ids, - trust_id) + domain_id = None + federated_info = None + access_token_id = None + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) class FederatedUnscopedPayload(BasePayload): version = 4 + @classmethod + def create_arguments_apply(cls, **kwargs): + return kwargs['federated_info'] + @classmethod def pack_group_id(cls, group_dict): return cls.attempt_convert_uuid_hex_to_bytes(group_dict['id']) @@ -579,24 +504,12 @@ class FederatedUnscopedPayload(BasePayload): def unpack_group_id(cls, group_id_in_bytes): (is_stored_as_bytes, group_id) = group_id_in_bytes if is_stored_as_bytes: - group_id = cls.attempt_convert_uuid_bytes_to_hex(group_id) + group_id = cls.convert_uuid_bytes_to_hex(group_id) return {'id': group_id} @classmethod - def assemble(cls, user_id, methods, expires_at, audit_ids, federated_info): - """Assemble the payload of a federated token. - - :param user_id: ID of the user in the token request - :param methods: list of authentication methods used - :param expires_at: datetime of the token's expiration - :param audit_ids: list of the token's audit IDs - :param federated_info: dictionary containing group IDs, the identity - provider ID, protocol ID, and federated domain - ID - :returns: the payload of a federated token - - """ - + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) b_group_ids = list(map(cls.pack_group_id, @@ -604,7 +517,7 @@ class FederatedUnscopedPayload(BasePayload): b_idp_id = cls.attempt_convert_uuid_hex_to_bytes( federated_info['idp_id']) protocol_id = federated_info['protocol_id'] - expires_at_int = cls._convert_time_string_to_int(expires_at) + expires_at_int = cls._convert_time_string_to_float(expires_at) b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, audit_ids)) @@ -613,59 +526,43 @@ class FederatedUnscopedPayload(BasePayload): @classmethod def disassemble(cls, payload): - """Validate a federated payload. - - :param token_string: a string representing the token - :return: a tuple containing the user_id, auth methods, audit_ids, and a - dictionary containing federated information such as the group - IDs, the identity provider ID, the protocol ID, and the - federated domain ID - - """ - (is_stored_as_bytes, user_id) = payload[0] if is_stored_as_bytes: - user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id) + user_id = cls.convert_uuid_bytes_to_hex(user_id) methods = auth_plugins.convert_integer_to_method_list(payload[1]) group_ids = list(map(cls.unpack_group_id, payload[2])) (is_stored_as_bytes, idp_id) = payload[3] if is_stored_as_bytes: - idp_id = cls.attempt_convert_uuid_bytes_to_hex(idp_id) + idp_id = cls.convert_uuid_bytes_to_hex(idp_id) protocol_id = payload[4] - expires_at_str = cls._convert_int_to_time_string(payload[5]) + expires_at_str = cls._convert_float_to_time_string(payload[5]) audit_ids = list(map(provider.base64_encode, payload[6])) federated_info = dict(group_ids=group_ids, idp_id=idp_id, protocol_id=protocol_id) - return (user_id, methods, expires_at_str, audit_ids, federated_info) + project_id = None + domain_id = None + trust_id = None + access_token_id = None + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) class FederatedScopedPayload(FederatedUnscopedPayload): version = None @classmethod - def assemble(cls, user_id, methods, scope_id, expires_at, audit_ids, - federated_info): - """Assemble the project-scoped payload of a federated token. - - :param user_id: ID of the user in the token request - :param methods: list of authentication methods used - :param scope_id: ID of the project or domain ID to scope to - :param expires_at: datetime of the token's expiration - :param audit_ids: list of the token's audit IDs - :param federated_info: dictionary containing the identity provider ID, - protocol ID, federated domain ID and group IDs - :returns: the payload of a federated token - - """ + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) methods = auth_plugins.convert_method_list_to_integer(methods) - b_scope_id = cls.attempt_convert_uuid_hex_to_bytes(scope_id) + b_scope_id = cls.attempt_convert_uuid_hex_to_bytes( + project_id or domain_id) b_group_ids = list(map(cls.pack_group_id, federated_info['group_ids'])) b_idp_id = cls.attempt_convert_uuid_hex_to_bytes( federated_info['idp_id']) protocol_id = federated_info['protocol_id'] - expires_at_int = cls._convert_time_string_to_int(expires_at) + expires_at_int = cls._convert_time_string_to_float(expires_at) b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, audit_ids)) @@ -674,39 +571,107 @@ class FederatedScopedPayload(FederatedUnscopedPayload): @classmethod def disassemble(cls, payload): - """Validate a project-scoped federated payload. - - :param token_string: a string representing the token - :returns: a tuple containing the user_id, auth methods, scope_id, - expiration time (as str), audit_ids, and a dictionary - containing federated information such as the the identity - provider ID, the protocol ID, the federated domain ID and - group IDs - - """ (is_stored_as_bytes, user_id) = payload[0] if is_stored_as_bytes: - user_id = cls.attempt_convert_uuid_bytes_to_hex(user_id) + user_id = cls.convert_uuid_bytes_to_hex(user_id) methods = auth_plugins.convert_integer_to_method_list(payload[1]) (is_stored_as_bytes, scope_id) = payload[2] if is_stored_as_bytes: - scope_id = cls.attempt_convert_uuid_bytes_to_hex(scope_id) + scope_id = cls.convert_uuid_bytes_to_hex(scope_id) + project_id = ( + scope_id + if cls.version == FederatedProjectScopedPayload.version else None) + domain_id = ( + scope_id + if cls.version == FederatedDomainScopedPayload.version else None) group_ids = list(map(cls.unpack_group_id, payload[3])) (is_stored_as_bytes, idp_id) = payload[4] if is_stored_as_bytes: - idp_id = cls.attempt_convert_uuid_bytes_to_hex(idp_id) + idp_id = cls.convert_uuid_bytes_to_hex(idp_id) protocol_id = payload[5] - expires_at_str = cls._convert_int_to_time_string(payload[6]) + expires_at_str = cls._convert_float_to_time_string(payload[6]) audit_ids = list(map(provider.base64_encode, payload[7])) federated_info = dict(idp_id=idp_id, protocol_id=protocol_id, group_ids=group_ids) - return (user_id, methods, scope_id, expires_at_str, audit_ids, - federated_info) + trust_id = None + access_token_id = None + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) class FederatedProjectScopedPayload(FederatedScopedPayload): version = 5 + @classmethod + def create_arguments_apply(cls, **kwargs): + return kwargs['project_id'] and kwargs['federated_info'] + class FederatedDomainScopedPayload(FederatedScopedPayload): version = 6 + + @classmethod + def create_arguments_apply(cls, **kwargs): + return kwargs['domain_id'] and kwargs['federated_info'] + + +class OauthScopedPayload(BasePayload): + version = 7 + + @classmethod + def create_arguments_apply(cls, **kwargs): + return kwargs['access_token_id'] + + @classmethod + def assemble(cls, user_id, methods, project_id, domain_id, expires_at, + audit_ids, trust_id, federated_info, access_token_id): + b_user_id = cls.attempt_convert_uuid_hex_to_bytes(user_id) + methods = auth_plugins.convert_method_list_to_integer(methods) + b_project_id = cls.attempt_convert_uuid_hex_to_bytes(project_id) + expires_at_int = cls._convert_time_string_to_float(expires_at) + b_audit_ids = list(map(provider.random_urlsafe_str_to_bytes, + audit_ids)) + b_access_token_id = cls.attempt_convert_uuid_hex_to_bytes( + access_token_id) + return (b_user_id, methods, b_project_id, b_access_token_id, + expires_at_int, b_audit_ids) + + @classmethod + def disassemble(cls, payload): + (is_stored_as_bytes, user_id) = payload[0] + if is_stored_as_bytes: + user_id = cls.convert_uuid_bytes_to_hex(user_id) + methods = auth_plugins.convert_integer_to_method_list(payload[1]) + (is_stored_as_bytes, project_id) = payload[2] + if is_stored_as_bytes: + project_id = cls.convert_uuid_bytes_to_hex(project_id) + (is_stored_as_bytes, access_token_id) = payload[3] + if is_stored_as_bytes: + access_token_id = cls.convert_uuid_bytes_to_hex(access_token_id) + expires_at_str = cls._convert_float_to_time_string(payload[4]) + audit_ids = list(map(provider.base64_encode, payload[5])) + domain_id = None + trust_id = None + federated_info = None + + return (user_id, methods, project_id, domain_id, expires_at_str, + audit_ids, trust_id, federated_info, access_token_id) + + +# For now, the order of the classes in the following list is important. This +# is because the way they test that the payload applies to them in +# the create_arguments_apply method requires that the previous ones rejected +# the payload arguments. For example, UnscopedPayload must be last since it's +# the catch-all after all the other payloads have been checked. +# TODO(blk-u): Clean up the create_arguments_apply methods so that they don't +# depend on the previous classes then these can be in any order. +PAYLOAD_CLASSES = [ + OauthScopedPayload, + TrustScopedPayload, + FederatedProjectScopedPayload, + FederatedDomainScopedPayload, + FederatedUnscopedPayload, + ProjectScopedPayload, + DomainScopedPayload, + UnscopedPayload, +] diff --git a/keystone-moon/keystone/token/providers/fernet/utils.py b/keystone-moon/keystone/token/providers/fernet/utils.py index 4235eda8..1c3552d4 100644 --- a/keystone-moon/keystone/token/providers/fernet/utils.py +++ b/keystone-moon/keystone/token/providers/fernet/utils.py @@ -25,29 +25,33 @@ LOG = log.getLogger(__name__) CONF = cfg.CONF -def validate_key_repository(): +def validate_key_repository(requires_write=False): """Validate permissions on the key repository directory.""" # NOTE(lbragstad): We shouldn't need to check if the directory was passed # in as None because we don't set allow_no_values to True. - # ensure current user has full access to the key repository - if (not os.access(CONF.fernet_tokens.key_repository, os.R_OK) or not - os.access(CONF.fernet_tokens.key_repository, os.W_OK) or not - os.access(CONF.fernet_tokens.key_repository, os.X_OK)): + # ensure current user has sufficient access to the key repository + is_valid = (os.access(CONF.fernet_tokens.key_repository, os.R_OK) and + os.access(CONF.fernet_tokens.key_repository, os.X_OK)) + if requires_write: + is_valid = (is_valid and + os.access(CONF.fernet_tokens.key_repository, os.W_OK)) + + if not is_valid: LOG.error( _LE('Either [fernet_tokens] key_repository does not exist or ' 'Keystone does not have sufficient permission to access it: ' '%s'), CONF.fernet_tokens.key_repository) - return False - - # ensure the key repository isn't world-readable - stat_info = os.stat(CONF.fernet_tokens.key_repository) - if stat_info.st_mode & stat.S_IROTH or stat_info.st_mode & stat.S_IXOTH: - LOG.warning(_LW( - '[fernet_tokens] key_repository is world readable: %s'), - CONF.fernet_tokens.key_repository) + else: + # ensure the key repository isn't world-readable + stat_info = os.stat(CONF.fernet_tokens.key_repository) + if(stat_info.st_mode & stat.S_IROTH or + stat_info.st_mode & stat.S_IXOTH): + LOG.warning(_LW( + '[fernet_tokens] key_repository is world readable: %s'), + CONF.fernet_tokens.key_repository) - return True + return is_valid def _convert_to_integers(id_value): @@ -99,7 +103,7 @@ def _create_new_key(keystone_user_id, keystone_group_id): Create a new key that is readable by the Keystone group and Keystone user. """ - key = fernet.Fernet.generate_key() + key = fernet.Fernet.generate_key() # key is bytes # This ensures the key created is not world-readable old_umask = os.umask(0o177) @@ -117,7 +121,7 @@ def _create_new_key(keystone_user_id, keystone_group_id): key_file = os.path.join(CONF.fernet_tokens.key_repository, '0') try: with open(key_file, 'w') as f: - f.write(key) + f.write(key.decode('utf-8')) # convert key to str for the file. finally: # After writing the key, set the umask back to it's original value. Do # the same with group and user identifiers if a Keystone group or user @@ -176,7 +180,7 @@ def rotate_keys(keystone_user_id=None, keystone_group_id=None): if os.path.isfile(path): try: key_id = int(filename) - except ValueError: + except ValueError: # nosec : name isn't a number, ignore the file. pass else: key_files[key_id] = path @@ -243,7 +247,8 @@ def load_keys(): with open(path, 'r') as key_file: try: key_id = int(filename) - except ValueError: + except ValueError: # nosec : filename isn't a number, ignore + # this file since it's not a key. pass else: keys[key_id] = key_file.read() diff --git a/keystone-moon/keystone/token/providers/pki.py b/keystone-moon/keystone/token/providers/pki.py index af8dc739..6a5a2999 100644 --- a/keystone-moon/keystone/token/providers/pki.py +++ b/keystone-moon/keystone/token/providers/pki.py @@ -17,6 +17,7 @@ from keystoneclient.common import cms from oslo_config import cfg from oslo_log import log +from oslo_log import versionutils from oslo_serialization import jsonutils from keystone.common import environment @@ -31,6 +32,10 @@ CONF = cfg.CONF LOG = log.getLogger(__name__) +@versionutils.deprecated( + as_of=versionutils.deprecated.MITAKA, + what='the PKI token provider', + in_favor_of='the Fernet or UUID token providers') class Provider(common.BaseProvider): def _get_token_id(self, token_data): try: diff --git a/keystone-moon/keystone/token/providers/pkiz.py b/keystone-moon/keystone/token/providers/pkiz.py index b4e31918..3e78d2e4 100644 --- a/keystone-moon/keystone/token/providers/pkiz.py +++ b/keystone-moon/keystone/token/providers/pkiz.py @@ -15,6 +15,7 @@ from keystoneclient.common import cms from oslo_config import cfg from oslo_log import log +from oslo_log import versionutils from oslo_serialization import jsonutils from keystone.common import environment @@ -30,6 +31,10 @@ LOG = log.getLogger(__name__) ERROR_MESSAGE = _('Unable to sign token.') +@versionutils.deprecated( + as_of=versionutils.deprecated.MITAKA, + what='the PKIZ token provider', + in_favor_of='the Fernet or UUID token providers') class Provider(common.BaseProvider): def _get_token_id(self, token_data): try: -- cgit 1.2.3-korg