Pre-cache new tokens

Since tokens are most often used right after being created,
cache them to bypass redundant validation.
The patch uses dogpile.cache internal functionality so some
calls may look strange

Implements bp pre-cache-tokens

Change-Id: I2e720eed6b0066738181afd1cbf73c5ff4d876f5
This commit is contained in:
Alexander Makarov 2016-04-21 21:21:52 +03:00
parent 0ae6d0924f
commit 7260b55cfc
5 changed files with 84 additions and 2 deletions

View File

@ -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,
]

View File

@ -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):

View File

@ -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."""

View File

@ -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)

View File

@ -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.