diff options
Diffstat (limited to 'keystone-moon/keystone/token/providers/common.py')
-rw-r--r-- | keystone-moon/keystone/token/providers/common.py | 248 |
1 files changed, 159 insertions, 89 deletions
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 |