From 37f59f4b0b82756167aeb63842420b90622de9bd Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Tue, 19 Aug 2014 17:21:40 -0700 Subject: [PATCH] Update AuthContextMiddleware to not use token_api AuthContextMiddleware and the fall-through in the controller base _build_policy_check_credentials now utilizes the KeystoneToken model and uses token_provider_api instead of token_api. In support of this change, the token auth plugin, the auth_context builder, token bind check, and token controller have all been updated to utilize the KeystoneToken model. Support for the federation data has been added to the KeystoneToken model so that it can be used in the auth context and associated code. Associated tests that passed a raw token_ref to methods that now expect the KeystoneToken model have been updated. This includes an update to the revocation model to guard against users without domain data (the federated user case). Change-Id: I81da15137a0ab3778d835c8de1ec8ed9e5b301f6 bp: non-persistent-tokens --- keystone/auth/plugins/token.py | 35 ++--- keystone/common/authorization.py | 127 ++++++------------ keystone/common/controller.py | 27 ++-- keystone/common/wsgi.py | 29 ++-- keystone/contrib/revoke/model.py | 4 +- keystone/middleware/core.py | 16 +-- keystone/models/token_model.py | 57 +++++++- keystone/tests/test_auth.py | 8 +- keystone/tests/test_content_types.py | 13 ++ keystone/tests/test_token_bind.py | 71 ++++++---- keystone/tests/unit/token/test_token_model.py | 43 ++++++ keystone/token/controllers.py | 61 +++------ 12 files changed, 269 insertions(+), 222 deletions(-) diff --git a/keystone/auth/plugins/token.py b/keystone/auth/plugins/token.py index 501a00812..2e8f5040b 100644 --- a/keystone/auth/plugins/token.py +++ b/keystone/auth/plugins/token.py @@ -12,12 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo.utils import timeutils - from keystone import auth from keystone.common import dependency from keystone.common import wsgi from keystone import exception +from keystone.models import token_model from keystone.openstack.common import log @@ -36,21 +35,14 @@ class Token(auth.AuthMethodHandler): target=self.method) token_id = auth_payload['id'] response = self.token_provider_api.validate_token(token_id) - # For V3 tokens, the essential data is under the 'token' value. - # For V2, the comparable data was nested under 'access'. - token_ref = response.get('token', response.get('access')) + token_ref = token_model.KeystoneToken(token_id=token_id, + token_data=response) # Do not allow tokens used for delegation to # create another token, or perform any changes of # state in Keystone. To do so is to invite elevation of # privilege attacks - if 'OS-TRUST:trust' in token_ref: - raise exception.Forbidden() - if 'trust' in token_ref: - raise exception.Forbidden() - if 'trust_id' in token_ref.get('metadata', {}): - raise exception.Forbidden() - if 'OS-OAUTH1' in token_ref: + if token_ref.oauth_scoped or token_ref.trust_scoped: raise exception.Forbidden() wsgi.validate_token_bind(context, token_ref) @@ -68,22 +60,13 @@ class Token(auth.AuthMethodHandler): # issued prior to audit id existing, the chain is not tracked. token_audit_id = None - # New tokens are not allowed to extend the expiration - # time of an old token, otherwise, they could be extened - # forever. The expiration value was stored at different - # locations in v2 and v3 tokens. - expires_at = token_ref.get('expires_at') - if not expires_at: - expires_at = token_ref.get('expires') - if not expires_at: - expires_at = timeutils.normalize_time( - timeutils.parse_isotime(token_ref['token']['expires'])) - - user_context.setdefault('expires_at', expires_at) + user_context.setdefault('expires_at', token_ref.expires) user_context['audit_id'] = token_audit_id - user_context.setdefault('user_id', token_ref['user']['id']) + user_context.setdefault('user_id', token_ref.user_id) + # TODO(morganfainberg: determine if token 'extras' can be removed + # from the user_context user_context['extras'].update(token_ref.get('extras', {})) - user_context['method_names'].extend(token_ref.get('methods', [])) + user_context['method_names'].extend(token_ref.methods) except AssertionError as e: LOG.error(e) diff --git a/keystone/common/authorization.py b/keystone/common/authorization.py index 0916d40b4..65dae93a4 100644 --- a/keystone/common/authorization.py +++ b/keystone/common/authorization.py @@ -16,9 +16,9 @@ # License for the specific language governing permissions and limitations # under the License. -from keystone.contrib import federation from keystone import exception from keystone.i18n import _ +from keystone.models import token_model from keystone.openstack.common import log @@ -41,89 +41,46 @@ It is a dictionary with the following attributes: LOG = log.getLogger(__name__) -def is_v3_token(token): - # V3 token data are encapsulated into "token" key while - # V2 token data are encapsulated into "access" key. - return 'token' in token - - -def v3_token_to_auth_context(token): - creds = {'is_delegated_auth': False} - token_data = token['token'] - try: - creds['user_id'] = token_data['user']['id'] - except AttributeError: - LOG.warning(_('RBAC: Invalid user data in v3 token')) - raise exception.Unauthorized() - if 'project' in token_data: - creds['project_id'] = token_data['project']['id'] - else: - LOG.debug('RBAC: Proceeding without project') - if 'domain' in token_data: - creds['domain_id'] = token_data['domain']['id'] - if 'roles' in token_data: - creds['roles'] = [] - for role in token_data['roles']: - creds['roles'].append(role['name']) - creds['group_ids'] = [ - g['id'] for g in token_data['user'].get(federation.FEDERATION, {}).get( - 'groups', [])] - - trust = token_data.get('OS-TRUST:trust') - if trust is None: - creds['trust_id'] = None - creds['trustor_id'] = None - creds['trustee_id'] = None - else: - creds['trust_id'] = trust['id'] - creds['trustor_id'] = trust['trustor_user']['id'] - creds['trustee_id'] = trust['trustee_user']['id'] - creds['is_delegated_auth'] = True - - oauth1 = token_data.get('OS-OAUTH1') - if oauth1 is None: - creds['consumer_id'] = None - creds['access_token_id'] = None - else: - creds['consumer_id'] = oauth1['consumer_id'] - creds['access_token_id'] = oauth1['access_token_id'] - creds['is_delegated_auth'] = True - return creds - - -def v2_token_to_auth_context(token): - creds = {'is_delegated_auth': False} - token_data = token['access'] - try: - creds['user_id'] = token_data['user']['id'] - except AttributeError: - LOG.warning(_('RBAC: Invalid user data in v2 token')) - raise exception.Unauthorized() - if 'tenant' in token_data['token']: - creds['project_id'] = token_data['token']['tenant']['id'] - else: - LOG.debug('RBAC: Proceeding without tenant') - if 'roles' in token_data['user']: - creds['roles'] = [role['name'] for - role in token_data['user']['roles']] - - trust = token_data.get('trust') - if trust is None: - creds['trust_id'] = None - creds['trustor_id'] = None - creds['trustee_id'] = None - else: - creds['trust_id'] = trust.get('id') - creds['trustor_id'] = trust.get('trustor_id') - creds['trustee_id'] = trust.get('trustee_id') - creds['is_delegated_auth'] = True - - return creds - - def token_to_auth_context(token): - if is_v3_token(token): - creds = v3_token_to_auth_context(token) + if not isinstance(token, token_model.KeystoneToken): + raise exception.UnexpectedError(_('token reference must be a ' + 'KeystoneToken type, got: %s') % + type(token)) + auth_context = {'token': token, + 'is_delegated_auth': False} + try: + auth_context['user_id'] = token.user_id + except KeyError: + LOG.warning(_('RBAC: Invalid user data in token')) + raise exception.Unauthorized() + + if token.project_scoped: + auth_context['project_id'] = token.project_id + elif token.domain_scoped: + auth_context['domain_id'] = token.domain_id else: - creds = v2_token_to_auth_context(token) - return creds + LOG.debug('RBAC: Proceeding without project or domain scope') + + if token.trust_scoped: + auth_context['is_delegated_auth'] = True + auth_context['trust_id'] = token.trust_id + auth_context['trustor_id'] = token.trustor_user_id + auth_context['trustee_id'] = token.trustee_user_id + else: + auth_context['trust_id'] = None + auth_context['trustor_id'] = None + auth_context['trustee_id'] = None + + roles = token.role_names + if roles: + auth_context['roles'] = roles + + if token.oauth_scoped: + auth_context['is_delegated_auth'] = True + auth_context['consumer_id'] = token.oauth_consumer_id + auth_context['access_token_id'] = token.oauth_access_token_id + + if token.is_federated_user: + auth_context['group_ids'] = token.federation_group_ids + + return auth_context diff --git a/keystone/common/controller.py b/keystone/common/controller.py index 4cef07cb1..df9049c1b 100644 --- a/keystone/common/controller.py +++ b/keystone/common/controller.py @@ -23,6 +23,7 @@ from keystone.common import wsgi from keystone import config from keystone import exception from keystone.i18n import _ +from keystone.models import token_model from keystone.openstack.common import log @@ -59,26 +60,24 @@ def _build_policy_check_credentials(self, action, context, kwargs): LOG.debug('RBAC: using auth context from the request environment') return context['environment'].get(authorization.AUTH_CONTEXT_ENV) - # now build the auth context from the incoming auth token + # There is no current auth context, build it from the incoming token. + # TODO(morganfainberg): Collapse this logic with AuthContextMiddleware + # in sane manner as this just mirrors the logic in AuthContextMiddleware try: LOG.debug('RBAC: building auth context from the incoming auth token') - # TODO(ayoung): These two functions return the token in different - # formats. However, the call - # to get_token hits the caching layer, and does not validate the - # token. This should be reduced to one call - if not CONF.token.revoke_by_id: - self.token_api.token_provider_api.validate_token( - context['token_id']) - token_ref = self.token_api.get_token(context['token_id']) + token_ref = token_model.KeystoneToken( + token_id=context['token_id'], + token_data=self.token_provider_api.validate_token( + context['token_id'])) + # NOTE(jamielennox): whilst this maybe shouldn't be within this + # function it would otherwise need to reload the token_ref from + # backing store. + wsgi.validate_token_bind(context, token_ref) except exception.TokenNotFound: LOG.warning(_('RBAC: Invalid token')) raise exception.Unauthorized() - # NOTE(jamielennox): whilst this maybe shouldn't be within this function - # it would otherwise need to reload the token_ref from backing store. - wsgi.validate_token_bind(context, token_ref) - - auth_context = authorization.token_to_auth_context(token_ref['token_data']) + auth_context = authorization.token_to_auth_context(token_ref) return auth_context diff --git a/keystone/common/wsgi.py b/keystone/common/wsgi.py index 9ff8f98f7..3aefea16b 100644 --- a/keystone/common/wsgi.py +++ b/keystone/common/wsgi.py @@ -18,6 +18,8 @@ """Utility methods for working with WSGI servers.""" +import copy + from oslo import i18n import routes.middleware import six @@ -52,7 +54,11 @@ def validate_token_bind(context, token_ref): if bind_mode == 'disabled': return - bind = token_ref.get('bind', {}) + if not isinstance(token_ref, token_model.KeystoneToken): + raise exception.UnexpectedError(_('token reference must be a ' + 'KeystoneToken type, got: %s') % + type(token_ref)) + bind = token_ref.bind # permissive and strict modes don't require there to be a bind permissive = bind_mode in ('permissive', 'strict') @@ -264,28 +270,29 @@ class Application(BaseApplication): def assert_admin(self, context): if not context['is_admin']: try: - user_token_ref = self.token_api.get_token(context['token_id']) + user_token_ref = token_model.KeystoneToken( + token_id=context['token_id'], + token_data=self.token_provider_api.validate_token( + context['token_id'])) except exception.TokenNotFound as e: raise exception.Unauthorized(e) validate_token_bind(context, user_token_ref) - creds = user_token_ref['metadata'].copy() + creds = copy.deepcopy(user_token_ref.metadata) try: - creds['user_id'] = user_token_ref['user'].get('id') - except AttributeError: + creds['user_id'] = user_token_ref.user_id + except exception.UnexpectedError: LOG.debug('Invalid user') raise exception.Unauthorized() - try: - creds['tenant_id'] = user_token_ref['tenant'].get('id') - except AttributeError: + if user_token_ref.project_scoped: + creds['tenant_id'] = user_token_ref.project_id + else: LOG.debug('Invalid tenant') raise exception.Unauthorized() - # NOTE(vish): this is pretty inefficient - creds['roles'] = [self.assignment_api.get_role(role)['name'] - for role in creds.get('roles', [])] + creds['roles'] = user_token_ref.role_names # Accept either is_admin or the admin role self.policy_api.enforce(creds, 'admin_required', {}) diff --git a/keystone/contrib/revoke/model.py b/keystone/contrib/revoke/model.py index dc913e6ee..e848e4d73 100644 --- a/keystone/contrib/revoke/model.py +++ b/keystone/contrib/revoke/model.py @@ -318,7 +318,9 @@ def build_token_values(token_data): user = token_data.get('user') if user is not None: token_values['user_id'] = user['id'] - token_values['identity_domain_id'] = user['domain']['id'] + # Federated users do not have a domain, be defensive and get the user + # domain set to None in the federated user case. + token_values['identity_domain_id'] = user.get('domain', {}).get('id') else: token_values['user_id'] = None token_values['identity_domain_id'] = None diff --git a/keystone/middleware/core.py b/keystone/middleware/core.py index d1b54237c..3d831b13c 100644 --- a/keystone/middleware/core.py +++ b/keystone/middleware/core.py @@ -22,6 +22,7 @@ from keystone.common import utils from keystone.common import wsgi from keystone import exception from keystone.i18n import _ +from keystone.models import token_model from keystone.openstack.common import jsonutils from keystone.openstack.common import log from keystone.openstack.common import versionutils @@ -253,20 +254,13 @@ class AuthContextMiddleware(wsgi.Middleware): context['environment'] = request.environ try: - token_ref = self.token_api.get_token(token_id) - # TODO(ayoung): These two functions return the token in different - # formats instead of two calls, only make one. However, the call - # to get_token hits the caching layer, and does not validate the - # token. In the future, this should be reduced to one call. - if not CONF.token.revoke_by_id: - self.token_api.token_provider_api.validate_token( - context['token_id']) - + token_ref = token_model.KeystoneToken( + token_id=token_id, + token_data=self.token_provider_api.validate_token(token_id)) # TODO(gyee): validate_token_bind should really be its own # middleware wsgi.validate_token_bind(context, token_ref) - return authorization.token_to_auth_context( - token_ref['token_data']) + return authorization.token_to_auth_context(token_ref) except exception.TokenNotFound: LOG.warning(_('RBAC: Invalid token')) raise exception.Unauthorized() diff --git a/keystone/models/token_model.py b/keystone/models/token_model.py index e446a5f35..7658fc825 100644 --- a/keystone/models/token_model.py +++ b/keystone/models/token_model.py @@ -12,10 +12,12 @@ """Unified in-memory token model.""" +from keystoneclient.common import cms from oslo.utils import timeutils import six from keystone.common import config +from keystone.contrib import federation from keystone import exception from keystone.i18n import _ @@ -52,6 +54,8 @@ class KeystoneToken(dict): else: raise exception.UnsupportedTokenVersionException() self.token_id = token_id + self.short_id = cms.cms_hash_token(token_id, + mode=CONF.token.hash_algorithm) if self.project_scoped and self.domain_scoped: raise exception.UnexpectedError(_('Found invalid token: scoped to ' @@ -238,16 +242,20 @@ class KeystoneToken(dict): else: return self.get('trust', {}).get('trustor_user_id') + @property + def oauth_scoped(self): + return 'OS-OAUTH1' in self + @property def oauth_access_token_id(self): - if self.version is V3: - return self.get('OS-OAUTH1', {}).get('access_token_id') + if self.version is V3 and self.oauth_scoped: + return self['OS-OAUTH1']['access_token_id'] return None @property def oauth_consumer_id(self): - if self.version is V3: - return self.get('OS-OAUTH1', {}).get('consumer_id') + if self.version is V3 and self.oauth_scoped: + return self['OS-OAUTH1']['consumer_id'] return None @property @@ -269,3 +277,44 @@ class KeystoneToken(dict): if self.version is V3: return self.get('bind') return self.get('token', {}).get('bind') + + @property + def is_federated_user(self): + try: + return self.version is V3 and federation.FEDERATION in self['user'] + except KeyError: + raise exception.UnexpectedError() + + @property + def federation_group_ids(self): + if self.is_federated_user: + if self.version is V3: + try: + groups = self['user'][federation.FEDERATION].get( + 'groups', []) + return [g['id'] for g in groups] + except KeyError: + raise exception.UnexpectedError() + return [] + + @property + def federation_idp_id(self): + if self.version is not V3 or not self.is_federated_user: + return None + return self['user'][federation.FEDERATION]['identity_provider']['id'] + + @property + def federation_protocol_id(self): + if self.version is V3 and self.is_federated_user: + return self['user'][federation.FEDERATION]['protocol']['id'] + return None + + @property + def metadata(self): + return self.get('metadata', {}) + + @property + def methods(self): + if self.version is V3: + return self.get('methods', []) + return [] diff --git a/keystone/tests/test_auth.py b/keystone/tests/test_auth.py index 22547f638..b6a934cba 100644 --- a/keystone/tests/test_auth.py +++ b/keystone/tests/test_auth.py @@ -26,6 +26,7 @@ from keystone.common import authorization from keystone.common import environment from keystone import config from keystone import exception +from keystone.models import token_model from keystone import tests from keystone.tests import default_fixtures from keystone.tests.ksfixtures import database @@ -811,9 +812,10 @@ class AuthWithTrust(AuthTest): self.config_fixture.config(group='trust', enabled=True) def _create_auth_context(self, token_id): - token_ref = self.token_api.get_token(token_id) - auth_context = authorization.token_to_auth_context( - token_ref['token_data']) + token_ref = token_model.KeystoneToken( + token_id=token_id, + token_data=self.token_provider_api.validate_token(token_id)) + auth_context = authorization.token_to_auth_context(token_ref) return {'environment': {authorization.AUTH_CONTEXT_ENV: auth_context}, 'token_id': token_id, 'host_url': HOST_URL} diff --git a/keystone/tests/test_content_types.py b/keystone/tests/test_content_types.py index ab7b1d821..201fc8280 100644 --- a/keystone/tests/test_content_types.py +++ b/keystone/tests/test_content_types.py @@ -13,6 +13,7 @@ # under the License. import json +import time import uuid from keystoneclient.common import cms @@ -1216,7 +1217,19 @@ class JsonTestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests): token1 = self.get_scoped_token() + # TODO(morganfainberg): Because this is making a restful call to the + # app a change to UTCNOW via mock.patch will not affect the returned + # token. The only surefire way to ensure there is not a transient bug + # based upon when the second token is issued is with a sleep. This + # issue all stems from the limited resolution (no microseconds) on the + # expiry time of tokens and the way revocation events utilizes token + # expiry to revoke individual tokens. This is a stop-gap until all + # associated issues with resolution on expiration and revocation events + # are resolved. + time.sleep(1) + token2 = self.get_scoped_token() + self.admin_request(method='DELETE', path='/v2.0/tokens/%s' % token2, token=token1) diff --git a/keystone/tests/test_token_bind.py b/keystone/tests/test_token_bind.py index e91c2e6c2..a18a6f7e4 100644 --- a/keystone/tests/test_token_bind.py +++ b/keystone/tests/test_token_bind.py @@ -12,20 +12,18 @@ # License for the specific language governing permissions and limitations # under the License. +import copy +import uuid + from keystone.common import wsgi from keystone import exception +from keystone.models import token_model from keystone import tests +from keystone.tests import test_token_provider KERBEROS_BIND = 'USER@REALM' - -# the only thing the function checks for is the presence of bind -TOKEN_BIND_KERB = {'bind': {'kerberos': KERBEROS_BIND}} -TOKEN_BIND_UNKNOWN = {'bind': {'FOO': 'BAR'}} -TOKEN_BIND_NONE = {} - ANY = 'any' -ALL_TOKENS = [TOKEN_BIND_KERB, TOKEN_BIND_UNKNOWN, TOKEN_BIND_NONE] class BindTest(tests.TestCase): @@ -35,6 +33,20 @@ class BindTest(tests.TestCase): will apply to all future binding mechanisms. """ + def setUp(self): + super(BindTest, self).setUp() + self.TOKEN_BIND_KERB = copy.deepcopy( + test_token_provider.SAMPLE_V3_TOKEN) + self.TOKEN_BIND_KERB['token']['bind'] = {'kerberos': KERBEROS_BIND} + self.TOKEN_BIND_UNKNOWN = copy.deepcopy( + test_token_provider.SAMPLE_V3_TOKEN) + self.TOKEN_BIND_UNKNOWN['token']['bind'] = {'FOO': 'BAR'} + self.TOKEN_BIND_NONE = copy.deepcopy( + test_token_provider.SAMPLE_V3_TOKEN) + + self.ALL_TOKENS = [self.TOKEN_BIND_KERB, self.TOKEN_BIND_UNKNOWN, + self.TOKEN_BIND_NONE] + def assert_kerberos_bind(self, tokens, bind_level, use_kerberos=True, success=True): if not isinstance(tokens, dict): @@ -55,17 +67,22 @@ class BindTest(tests.TestCase): context['environment']['REMOTE_USER'] = KERBEROS_BIND context['environment']['AUTH_TYPE'] = 'Negotiate' + # NOTE(morganfainberg): This assumes a V3 token. + token_ref = token_model.KeystoneToken( + token_id=uuid.uuid4().hex, + token_data=tokens) + if not success: self.assertRaises(exception.Unauthorized, wsgi.validate_token_bind, - context, tokens) + context, token_ref) else: - wsgi.validate_token_bind(context, tokens) + wsgi.validate_token_bind(context, token_ref) # DISABLED def test_bind_disabled_with_kerb_user(self): - self.assert_kerberos_bind(ALL_TOKENS, + self.assert_kerberos_bind(self.ALL_TOKENS, bind_level='disabled', use_kerberos=ANY, success=True) @@ -73,25 +90,25 @@ class BindTest(tests.TestCase): # PERMISSIVE def test_bind_permissive_with_kerb_user(self): - self.assert_kerberos_bind(TOKEN_BIND_KERB, + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, bind_level='permissive', use_kerberos=True, success=True) def test_bind_permissive_with_regular_token(self): - self.assert_kerberos_bind(TOKEN_BIND_NONE, + self.assert_kerberos_bind(self.TOKEN_BIND_NONE, bind_level='permissive', use_kerberos=ANY, success=True) def test_bind_permissive_without_kerb_user(self): - self.assert_kerberos_bind(TOKEN_BIND_KERB, + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, bind_level='permissive', use_kerberos=False, success=False) def test_bind_permissive_with_unknown_bind(self): - self.assert_kerberos_bind(TOKEN_BIND_UNKNOWN, + self.assert_kerberos_bind(self.TOKEN_BIND_UNKNOWN, bind_level='permissive', use_kerberos=ANY, success=True) @@ -99,25 +116,25 @@ class BindTest(tests.TestCase): # STRICT def test_bind_strict_with_regular_token(self): - self.assert_kerberos_bind(TOKEN_BIND_NONE, + self.assert_kerberos_bind(self.TOKEN_BIND_NONE, bind_level='strict', use_kerberos=ANY, success=True) def test_bind_strict_with_kerb_user(self): - self.assert_kerberos_bind(TOKEN_BIND_KERB, + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, bind_level='strict', use_kerberos=True, success=True) def test_bind_strict_without_kerb_user(self): - self.assert_kerberos_bind(TOKEN_BIND_KERB, + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, bind_level='strict', use_kerberos=False, success=False) def test_bind_strict_with_unknown_bind(self): - self.assert_kerberos_bind(TOKEN_BIND_UNKNOWN, + self.assert_kerberos_bind(self.TOKEN_BIND_UNKNOWN, bind_level='strict', use_kerberos=ANY, success=False) @@ -125,25 +142,25 @@ class BindTest(tests.TestCase): # REQUIRED def test_bind_required_with_regular_token(self): - self.assert_kerberos_bind(TOKEN_BIND_NONE, + self.assert_kerberos_bind(self.TOKEN_BIND_NONE, bind_level='required', use_kerberos=ANY, success=False) def test_bind_required_with_kerb_user(self): - self.assert_kerberos_bind(TOKEN_BIND_KERB, + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, bind_level='required', use_kerberos=True, success=True) def test_bind_required_without_kerb_user(self): - self.assert_kerberos_bind(TOKEN_BIND_KERB, + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, bind_level='required', use_kerberos=False, success=False) def test_bind_required_with_unknown_bind(self): - self.assert_kerberos_bind(TOKEN_BIND_UNKNOWN, + self.assert_kerberos_bind(self.TOKEN_BIND_UNKNOWN, bind_level='required', use_kerberos=ANY, success=False) @@ -151,31 +168,31 @@ class BindTest(tests.TestCase): # NAMED def test_bind_named_with_regular_token(self): - self.assert_kerberos_bind(TOKEN_BIND_NONE, + self.assert_kerberos_bind(self.TOKEN_BIND_NONE, bind_level='kerberos', use_kerberos=ANY, success=False) def test_bind_named_with_kerb_user(self): - self.assert_kerberos_bind(TOKEN_BIND_KERB, + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, bind_level='kerberos', use_kerberos=True, success=True) def test_bind_named_without_kerb_user(self): - self.assert_kerberos_bind(TOKEN_BIND_KERB, + self.assert_kerberos_bind(self.TOKEN_BIND_KERB, bind_level='kerberos', use_kerberos=False, success=False) def test_bind_named_with_unknown_bind(self): - self.assert_kerberos_bind(TOKEN_BIND_UNKNOWN, + self.assert_kerberos_bind(self.TOKEN_BIND_UNKNOWN, bind_level='kerberos', use_kerberos=ANY, success=False) def test_bind_named_with_unknown_scheme(self): - self.assert_kerberos_bind(ALL_TOKENS, + self.assert_kerberos_bind(self.ALL_TOKENS, bind_level='unknown', use_kerberos=ANY, success=False) diff --git a/keystone/tests/unit/token/test_token_model.py b/keystone/tests/unit/token/test_token_model.py index 2160d2cd1..6f7b909c2 100644 --- a/keystone/tests/unit/token/test_token_model.py +++ b/keystone/tests/unit/token/test_token_model.py @@ -114,6 +114,49 @@ class TestKeystoneTokenModel(core.TestCase): self.assertIsNone(token_data.audit_id) self.assertIsNone(token_data.audit_chain_id) + def test_token_model_v3_federated_user(self): + token_data = token_model.KeystoneToken(token_id=uuid.uuid4().hex, + token_data=self.v3_sample_token) + federation_data = {'identity_provider': {'id': uuid.uuid4().hex}, + 'protocol': {'id': 'saml2'}, + 'groups': [{'id': uuid.uuid4().hex} + for x in range(1, 5)]} + + self.assertFalse(token_data.is_federated_user) + self.assertEqual([], token_data.federation_group_ids) + self.assertIsNone(token_data.federation_protocol_id) + self.assertIsNone(token_data.federation_idp_id) + + token_data['user'][token_model.federation.FEDERATION] = federation_data + + self.assertTrue(token_data.is_federated_user) + self.assertEqual([x['id'] for x in federation_data['groups']], + token_data.federation_group_ids) + self.assertEqual(federation_data['protocol']['id'], + token_data.federation_protocol_id) + self.assertEqual(federation_data['identity_provider']['id'], + token_data.federation_idp_id) + + def test_token_model_v2_federated_user(self): + token_data = token_model.KeystoneToken(token_id=uuid.uuid4().hex, + token_data=self.v2_sample_token) + federation_data = {'identity_provider': {'id': uuid.uuid4().hex}, + 'protocol': {'id': 'saml2'}, + 'groups': [{'id': uuid.uuid4().hex} + for x in range(1, 5)]} + self.assertFalse(token_data.is_federated_user) + self.assertEqual([], token_data.federation_group_ids) + self.assertIsNone(token_data.federation_protocol_id) + self.assertIsNone(token_data.federation_idp_id) + + token_data['user'][token_model.federation.FEDERATION] = federation_data + + # Federated users should not exist in V2, the data should remain empty + self.assertFalse(token_data.is_federated_user) + self.assertEqual([], token_data.federation_group_ids) + self.assertIsNone(token_data.federation_protocol_id) + self.assertIsNone(token_data.federation_idp_id) + def test_token_model_v2(self): token_data = token_model.KeystoneToken(uuid.uuid4().hex, self.v2_sample_token) diff --git a/keystone/token/controllers.py b/keystone/token/controllers.py index 47b458ded..37edd0ec4 100644 --- a/keystone/token/controllers.py +++ b/keystone/token/controllers.py @@ -25,6 +25,7 @@ from keystone.common import wsgi from keystone import config from keystone import exception from keystone.i18n import _ +from keystone.models import token_model from keystone.openstack.common import jsonutils from keystone.openstack.common import log from keystone.token import provider @@ -40,7 +41,7 @@ class ExternalAuthNotApplicable(Exception): @dependency.requires('assignment_api', 'catalog_api', 'identity_api', - 'token_api', 'token_provider_api', 'trust_api') + 'token_provider_api', 'trust_api') class Auth(controller.V2Controller): @controller.v2_deprecated @@ -169,20 +170,19 @@ class Auth(controller.V2Controller): size=CONF.max_token_size) try: - old_token_ref = self.token_api.get_token(old_token) + token_model_ref = token_model.KeystoneToken( + token_id=old_token, + token_data=self.token_provider_api.validate_token(old_token)) except exception.NotFound as e: raise exception.Unauthorized(e) - wsgi.validate_token_bind(context, old_token_ref) + wsgi.validate_token_bind(context, token_model_ref) # A trust token cannot be used to get another token - if 'trust' in old_token_ref: - raise exception.Forbidden() - if 'trust_id' in old_token_ref['metadata']: + if token_model_ref.trust_scoped: raise exception.Forbidden() - user_ref = old_token_ref['user'] - user_id = user_ref['id'] + user_id = token_model_ref.user_id tenant_id = self._get_project_id_from_auth(auth) if not CONF.trust.enabled and 'trust_id' in auth: @@ -222,7 +222,7 @@ class Auth(controller.V2Controller): tenant_ref, metadata_ref['roles'] = self._get_project_roles_and_ref( user_id, tenant_id) - expiry = old_token_ref['expires'] + expiry = token_model_ref.expires if CONF.trust.enabled and 'trust_id' in auth: trust_id = auth['trust_id'] trust_roles = [] @@ -241,29 +241,8 @@ class Auth(controller.V2Controller): metadata_ref['trustee_user_id'] = trust_ref['trustee_user_id'] metadata_ref['trust_id'] = trust_id - bind = old_token_ref.get('bind') - # TODO(morganfainberg): Convert this over to using the KeystoneToken - # model when removing dependency on token_api. - token_data = old_token_ref.get('token_data') - audit_id = None - if token_data: - # NOTE(morganfainberg): The token audit field will always contain - # the token's direct audit id at index 0, index 1 will exist and - # contain the audit chain id (audit id of the original token in - # the chain), so always lookup the last element of the audit field - # to determine the id to pass on. - try: - if 'access' in token_data: - audit_id = token_data['access']['token'].get( - 'audit_ids', [])[-1] - else: - audit_id = token_data['token'].get('audit_ids', [])[-1] - except IndexError: - # NOTE(morganfainberg): When transitioning from tokens without - # audit_ids to tokens with audit ids it some tokens may not - # have an audit_id, and the lookup will cause an IndexError - # to be raised. - pass + bind = token_model_ref.bind + audit_id = token_model_ref.audit_chain_id return (current_user_ref, tenant_ref, metadata_ref, expiry, bind, audit_id) @@ -422,15 +401,17 @@ class Auth(controller.V2Controller): Optionally, limited to a token owned by a specific tenant. """ - data = self.token_api.get_token(token_id) + token_ref = token_model.KeystoneToken( + token_id=token_id, + token_data=self.token_provider_api.validate_token(token_id)) if belongs_to: - if data.get('tenant') is None: + if not token_ref.project_scoped: raise exception.Unauthorized( _('Token does not belong to specified tenant.')) - if data['tenant'].get('id') != belongs_to: + if token_ref.project_id != belongs_to: raise exception.Unauthorized( _('Token does not belong to specified tenant.')) - return data + return token_ref @controller.v2_deprecated @controller.protected() @@ -497,11 +478,11 @@ class Auth(controller.V2Controller): token_ref = self._get_token_ref(token_id) catalog_ref = None - if token_ref.get('tenant'): + if token_ref.project_id: catalog_ref = self.catalog_api.get_catalog( - token_ref['user']['id'], - token_ref['tenant']['id'], - token_ref['metadata']) + token_ref.user_id, + token_ref.project_id, + token_ref.metadata) return Auth.format_endpoint_list(catalog_ref)