diff --git a/keystone/conf/token.py b/keystone/conf/token.py index 055a1dbfde..dd1d15cb85 100644 --- a/keystone/conf/token.py +++ b/keystone/conf/token.py @@ -157,6 +157,13 @@ directly assigned to the token's scope, but are instead linked implicitly to other role assignments. """)) +cache_on_issue = cfg.BoolOpt( + 'cache_on_issue', + default=False, + help=utils.fmt(""" +Enable storing issued token data to token validation cache so that first token +validation doesn't actually cause full validation cycle. +""")) GROUP_NAME = __name__.split('.')[-1] ALL_OPTS = [ @@ -171,6 +178,7 @@ ALL_OPTS = [ allow_rescope_scoped_token, hash_algorithm, infer_roles, + cache_on_issue, ] diff --git a/keystone/tests/unit/test_auth.py b/keystone/tests/unit/test_auth.py index 3e0af59d7c..6aa66010ea 100644 --- a/keystone/tests/unit/test_auth.py +++ b/keystone/tests/unit/test_auth.py @@ -1328,7 +1328,8 @@ class FernetAuthWithTrust(AuthWithTrust, AuthTest): def config_overrides(self): super(FernetAuthWithTrust, self).config_overrides() - self.config_fixture.config(group='token', provider='fernet') + self.config_fixture.config(group='token', provider='fernet', + cache_on_issue=True) self.useFixture( ksfixtures.KeyRepository( self.config_fixture, @@ -1346,6 +1347,14 @@ class FernetAuthWithTrust(AuthWithTrust, AuthTest): msg = 'The Fernet token provider does not support token persistence' self.skipTest(msg) + def test_delete_trust_revokes_token(self): + # NOTE(amakarov): have to override this for Fernet as TokenNotFound + # can't be raised for non-persistent token, but deleted trust will + # cause TrustNotFound exception. + self.assertRaises( + exception.TrustNotFound, + super(FernetAuthWithTrust, self).test_delete_trust_revokes_token) + class TokenExpirationTest(AuthTest): diff --git a/keystone/tests/unit/test_v3_auth.py b/keystone/tests/unit/test_v3_auth.py index 42dcd8491b..2b3de453c4 100644 --- a/keystone/tests/unit/test_v3_auth.py +++ b/keystone/tests/unit/test_v3_auth.py @@ -2426,7 +2426,8 @@ class TestFernetTokenAPIs(test_v3.RestfulTestCase, TokenAPITests, TokenDataTests): def config_overrides(self): super(TestFernetTokenAPIs, self).config_overrides() - self.config_fixture.config(group='token', provider='fernet') + self.config_fixture.config(group='token', provider='fernet', + cache_on_issue=True) self.useFixture( ksfixtures.KeyRepository( self.config_fixture, @@ -2504,6 +2505,13 @@ class TestFernetTokenAPIs(test_v3.RestfulTestCase, TokenAPITests, self.v3_create_token(auth_data, expected_status=http_client.NOT_IMPLEMENTED) + def test_trust_scoped_token_is_invalid_after_disabling_trustor(self): + # NOTE(amakarov): have to override this test for non-persistent tokens + # as TokenNotFound exception makes no sense for those. + self.assertRaises( + exception.Forbidden, super(TestFernetTokenAPIs, self) + .test_trust_scoped_token_is_invalid_after_disabling_trustor) + class TestTokenRevokeSelfAndAdmin(test_v3.RestfulTestCase): """Test token revoke using v3 Identity API by token owner and admin.""" diff --git a/keystone/token/provider.py b/keystone/token/provider.py index fa418eb435..8e3d06731b 100644 --- a/keystone/token/provider.py +++ b/keystone/token/provider.py @@ -16,6 +16,7 @@ import abc import base64 +import copy import datetime import sys import uuid @@ -379,6 +380,16 @@ class Manager(manager.Manager): token_version=self.V2) self._create_token(token_id, data) + # NOTE(amakarov): TOKENS_REGION is to be passed to serve as + # required positional "self" argument. It's ignored, so I've put + # it here for convenience - any placeholder is fine. + # NOTE(amakarov): v3 token data can be converted to v2.0 version, + # so v2.0 token validation cache can also be populated. However it + # isn't reflexive: there is no way to populate v3 validation cache + # on issuing a token using v2.0 API. + if CONF.token.cache_on_issue: + self._validate_v2_token.set(token_data, TOKENS_REGION, token_id) + return token_id, token_data def issue_v3_token(self, user_id, method_names, expires_at=None, @@ -419,6 +430,27 @@ class Manager(manager.Manager): token_version=self.V3) if self._needs_persistence: self._create_token(token_id, data) + + if CONF.token.cache_on_issue: + # NOTE(amakarov): here and above TOKENS_REGION is to be passed + # to serve as required positional "self" argument. It's ignored, + # so I've put it here for convenience - any placeholder is fine. + self._validate_v3_token.set(token_data, TOKENS_REGION, token_id) + self._validate_token.set(token_data, TOKENS_REGION, token_id) + self.validate_non_persistent_token.set( + token_data, TOKENS_REGION, token_id) + + try: + v2_helper = providers.common.V2TokenDataHelper() + v2_token_data = v2_helper.v3_to_v2_token( + copy.deepcopy(token_data), token_id) + except exception.Unauthorized: + # Ignore trust and oauth tokens + pass + else: + self._validate_v2_token.set( + v2_token_data, TOKENS_REGION, token_id) + return token_id, token_data def invalidate_individual_token_cache(self, token_id): @@ -467,18 +499,27 @@ class Manager(manager.Manager): trust = self.trust_api.get_trust(trust_id, deleted=True) self._persistence.delete_tokens(user_id=trust['trustor_user_id'], trust_id=trust_id) + if CONF.token.cache_on_issue: + # NOTE(amakarov): preserving behavior + TOKENS_REGION.invalidate() def _delete_user_tokens_callback(self, service, resource_type, operation, payload): if CONF.token.revoke_by_id: user_id = payload['resource_info'] self._persistence.delete_tokens_for_user(user_id) + if CONF.token.cache_on_issue: + # NOTE(amakarov): preserving behavior + TOKENS_REGION.invalidate() def _delete_domain_tokens_callback(self, service, resource_type, operation, payload): if CONF.token.revoke_by_id: domain_id = payload['resource_info'] self._persistence.delete_tokens_for_domain(domain_id=domain_id) + if CONF.token.cache_on_issue: + # NOTE(amakarov): preserving behavior + TOKENS_REGION.invalidate() def _delete_user_project_tokens_callback(self, service, resource_type, operation, payload): @@ -487,6 +528,9 @@ class Manager(manager.Manager): project_id = payload['resource_info']['project_id'] self._persistence.delete_tokens_for_user(user_id=user_id, project_id=project_id) + if CONF.token.cache_on_issue: + # NOTE(amakarov): preserving behavior + TOKENS_REGION.invalidate() def _delete_project_tokens_callback(self, service, resource_type, operation, payload): @@ -495,6 +539,9 @@ class Manager(manager.Manager): self._persistence.delete_tokens_for_users( self.assignment_api.list_user_ids_for_project(project_id), project_id=project_id) + if CONF.token.cache_on_issue: + # NOTE(amakarov): preserving behavior + TOKENS_REGION.invalidate() def _delete_user_oauth_consumer_tokens_callback(self, service, resource_type, operation, @@ -504,6 +551,9 @@ class Manager(manager.Manager): consumer_id = payload['resource_info']['consumer_id'] self._persistence.delete_tokens(user_id=user_id, consumer_id=consumer_id) + if CONF.token.cache_on_issue: + # NOTE(amakarov): preserving behavior + TOKENS_REGION.invalidate() @six.add_metaclass(abc.ABCMeta) diff --git a/releasenotes/notes/pre-cache-tokens-73450934918af26b.yaml b/releasenotes/notes/pre-cache-tokens-73450934918af26b.yaml new file mode 100644 index 0000000000..cf7dada0ec --- /dev/null +++ b/releasenotes/notes/pre-cache-tokens-73450934918af26b.yaml @@ -0,0 +1,7 @@ +--- +prelude: > + Tokens can now be cached when issued. +features: + - Add ``cache_on_issue`` flag to ``[token]`` section that enables + placing issued tokens to validation cache thus reducing the first + validation time as if token is already validated and token data cached.