Use requst local in-process cache per request
Use a request local cache, where possible, to offload requests to the backend. Change-Id: I8b744f75f21e9dd669a735a7717fd0d044d6d8ed Depends-On: I6d1d28f5b974e79d44d1e86ea53c666e3f5771df
This commit is contained in:
parent
f699ca93fc
commit
0eba0e0477
|
@ -0,0 +1,127 @@
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""A dogpile.cache proxy that caches objects in the request local cache."""
|
||||||
|
from dogpile.cache import api
|
||||||
|
from dogpile.cache import proxy
|
||||||
|
from oslo_context import context as oslo_context
|
||||||
|
from oslo_serialization import msgpackutils
|
||||||
|
|
||||||
|
from keystone.models import revoke_model
|
||||||
|
|
||||||
|
|
||||||
|
class _RevokeModelHandler(object):
|
||||||
|
# NOTE(morganfainberg): There needs to be reserved "registry" entries set
|
||||||
|
# in oslo_serialization for application-specific handlers. We picked 127
|
||||||
|
# here since it's waaaaaay far out before oslo_serialization will use it.
|
||||||
|
identity = 127
|
||||||
|
handles = (revoke_model.RevokeTree,)
|
||||||
|
|
||||||
|
def __init__(self, registry):
|
||||||
|
self._registry = registry
|
||||||
|
|
||||||
|
def serialize(self, obj):
|
||||||
|
return msgpackutils.dumps(obj.revoke_map,
|
||||||
|
registry=self._registry)
|
||||||
|
|
||||||
|
def deserialize(self, data):
|
||||||
|
revoke_map = msgpackutils.loads(data, registry=self._registry)
|
||||||
|
revoke_tree = revoke_model.RevokeTree()
|
||||||
|
revoke_tree.revoke_map = revoke_map
|
||||||
|
return revoke_tree
|
||||||
|
|
||||||
|
|
||||||
|
# Register our new handler.
|
||||||
|
_registry = msgpackutils.default_registry
|
||||||
|
_registry.frozen = False
|
||||||
|
_registry.register(_RevokeModelHandler(registry=_registry))
|
||||||
|
_registry.frozen = True
|
||||||
|
|
||||||
|
|
||||||
|
class _ResponseCacheProxy(proxy.ProxyBackend):
|
||||||
|
|
||||||
|
__key_pfx = '_request_cache_%s'
|
||||||
|
|
||||||
|
def _get_request_context(self):
|
||||||
|
# Return the current context or a new/empty context.
|
||||||
|
return oslo_context.get_current() or oslo_context.RequestContext()
|
||||||
|
|
||||||
|
def _get_request_key(self, key):
|
||||||
|
return self.__key_pfx % key
|
||||||
|
|
||||||
|
def _set_local_cache(self, key, value, ctx=None):
|
||||||
|
# Set a serialized version of the returned value in local cache for
|
||||||
|
# subsequent calls to the memoized method.
|
||||||
|
if not ctx:
|
||||||
|
ctx = self._get_request_context()
|
||||||
|
serialize = {'payload': value.payload, 'metadata': value.metadata}
|
||||||
|
setattr(ctx, self._get_request_key(key), msgpackutils.dumps(serialize))
|
||||||
|
ctx.update_store()
|
||||||
|
|
||||||
|
def _get_local_cache(self, key):
|
||||||
|
# Return the version from our local request cache if it exists.
|
||||||
|
ctx = self._get_request_context()
|
||||||
|
try:
|
||||||
|
value = getattr(ctx, self._get_request_key(key))
|
||||||
|
except AttributeError:
|
||||||
|
return api.NO_VALUE
|
||||||
|
|
||||||
|
value = msgpackutils.loads(value)
|
||||||
|
return api.CachedValue(payload=value['payload'],
|
||||||
|
metadata=value['metadata'])
|
||||||
|
|
||||||
|
def _delete_local_cache(self, key):
|
||||||
|
# On invalidate/delete remove the value from the local request cache
|
||||||
|
ctx = self._get_request_context()
|
||||||
|
try:
|
||||||
|
delattr(ctx, self._get_request_key(key))
|
||||||
|
ctx.update_store()
|
||||||
|
except AttributeError: # nosec
|
||||||
|
# NOTE(morganfainberg): We will simply pass here, this value has
|
||||||
|
# not been cached locally in the request.
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
value = self._get_local_cache(key)
|
||||||
|
if value is api.NO_VALUE:
|
||||||
|
value = self.proxied.get(key)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
self._set_local_cache(key, value)
|
||||||
|
self.proxied.set(key, value)
|
||||||
|
|
||||||
|
def delete(self, key):
|
||||||
|
self._delete_local_cache(key)
|
||||||
|
self.proxied.delete(key)
|
||||||
|
|
||||||
|
def get_multi(self, keys):
|
||||||
|
values = {}
|
||||||
|
for key in keys:
|
||||||
|
v = self._get_local_cache(key)
|
||||||
|
if v is not api.NO_VALUE:
|
||||||
|
values[key] = v
|
||||||
|
query_keys = set(keys).difference(set(values.keys()))
|
||||||
|
values.update(dict(
|
||||||
|
zip(query_keys, self.proxied.get_multi(query_keys))))
|
||||||
|
return [values[k] for k in keys]
|
||||||
|
|
||||||
|
def set_multi(self, mapping):
|
||||||
|
ctx = self._get_request_context()
|
||||||
|
for k, v in mapping.items():
|
||||||
|
self._set_local_cache(k, v, ctx)
|
||||||
|
self.proxied.set_multi(mapping)
|
||||||
|
|
||||||
|
def delete_multi(self, keys):
|
||||||
|
for k in keys:
|
||||||
|
self._delete_local_cache(k)
|
||||||
|
self.proxied.delete_multi(keys)
|
|
@ -18,6 +18,9 @@ from dogpile.cache import api
|
||||||
from oslo_cache import core as cache
|
from oslo_cache import core as cache
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from keystone.common.cache import _context_cache
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
CACHE_REGION = cache.create_region()
|
CACHE_REGION = cache.create_region()
|
||||||
|
|
||||||
|
@ -25,7 +28,15 @@ CACHE_REGION = cache.create_region()
|
||||||
def configure_cache(region=None):
|
def configure_cache(region=None):
|
||||||
if region is None:
|
if region is None:
|
||||||
region = CACHE_REGION
|
region = CACHE_REGION
|
||||||
|
# NOTE(morganfainberg): running cache.configure_cache_region()
|
||||||
|
# sets region.is_configured, this must be captured before
|
||||||
|
# cache.configure_cache_region is called.
|
||||||
|
configured = region.is_configured
|
||||||
cache.configure_cache_region(CONF, region)
|
cache.configure_cache_region(CONF, region)
|
||||||
|
# Only wrap the region if it was not configured. This should be pushed
|
||||||
|
# to oslo_cache lib somehow.
|
||||||
|
if not configured:
|
||||||
|
region.wrap(_context_cache._ResponseCacheProxy)
|
||||||
|
|
||||||
|
|
||||||
def get_memoization_decorator(group, expiration_group=None, region=None):
|
def get_memoization_decorator(group, expiration_group=None, region=None):
|
||||||
|
|
|
@ -214,6 +214,7 @@ class AuthContextMiddleware(wsgi.Middleware):
|
||||||
request_context.domain = auth_context.get('domain_id')
|
request_context.domain = auth_context.get('domain_id')
|
||||||
request_context.user_domain = auth_context.get('user_domain_id')
|
request_context.user_domain = auth_context.get('user_domain_id')
|
||||||
request_context.project_domain = auth_context.get('project_domain_id')
|
request_context.project_domain = auth_context.get('project_domain_id')
|
||||||
|
request_context.update_store()
|
||||||
|
|
||||||
LOG.debug('RBAC: auth_context: %s', auth_context)
|
LOG.debug('RBAC: auth_context: %s', auth_context)
|
||||||
request.environ[authorization.AUTH_CONTEXT_ENV] = auth_context
|
request.environ[authorization.AUTH_CONTEXT_ENV] = auth_context
|
||||||
|
|
|
@ -0,0 +1,371 @@
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
from six.moves import map
|
||||||
|
|
||||||
|
from keystone.common import utils
|
||||||
|
|
||||||
|
|
||||||
|
# The set of attributes common between the RevokeEvent
|
||||||
|
# and the dictionaries created from the token Data.
|
||||||
|
_NAMES = ['trust_id',
|
||||||
|
'consumer_id',
|
||||||
|
'access_token_id',
|
||||||
|
'audit_id',
|
||||||
|
'audit_chain_id',
|
||||||
|
'expires_at',
|
||||||
|
'domain_id',
|
||||||
|
'project_id',
|
||||||
|
'user_id',
|
||||||
|
'role_id']
|
||||||
|
|
||||||
|
|
||||||
|
# Additional arguments for creating a RevokeEvent
|
||||||
|
_EVENT_ARGS = ['issued_before', 'revoked_at']
|
||||||
|
|
||||||
|
# Names of attributes in the RevocationEvent, including "virtual" attributes.
|
||||||
|
# Virtual attributes are those added based on other values.
|
||||||
|
_EVENT_NAMES = _NAMES + ['domain_scope_id']
|
||||||
|
|
||||||
|
# Values that will be in the token data but not in the event.
|
||||||
|
# These will compared with event values that have different names.
|
||||||
|
# For example: both trustor_id and trustee_id are compared against user_id
|
||||||
|
_TOKEN_KEYS = ['identity_domain_id',
|
||||||
|
'assignment_domain_id',
|
||||||
|
'issued_at',
|
||||||
|
'trustor_id',
|
||||||
|
'trustee_id']
|
||||||
|
|
||||||
|
# Alternative names to be checked in token for every field in
|
||||||
|
# revoke tree.
|
||||||
|
ALTERNATIVES = {
|
||||||
|
'user_id': ['user_id', 'trustor_id', 'trustee_id'],
|
||||||
|
'domain_id': ['identity_domain_id', 'assignment_domain_id'],
|
||||||
|
# For a domain-scoped token, the domain is in assignment_domain_id.
|
||||||
|
'domain_scope_id': ['assignment_domain_id', ],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
REVOKE_KEYS = _NAMES + _EVENT_ARGS
|
||||||
|
|
||||||
|
|
||||||
|
def blank_token_data(issued_at):
|
||||||
|
token_data = dict()
|
||||||
|
for name in _NAMES:
|
||||||
|
token_data[name] = None
|
||||||
|
for name in _TOKEN_KEYS:
|
||||||
|
token_data[name] = None
|
||||||
|
# required field
|
||||||
|
token_data['issued_at'] = issued_at
|
||||||
|
return token_data
|
||||||
|
|
||||||
|
|
||||||
|
class RevokeEvent(object):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
for k in REVOKE_KEYS:
|
||||||
|
v = kwargs.get(k)
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
if self.domain_id and self.expires_at:
|
||||||
|
# This is revoking a domain-scoped token.
|
||||||
|
self.domain_scope_id = self.domain_id
|
||||||
|
self.domain_id = None
|
||||||
|
else:
|
||||||
|
# This is revoking all tokens for a domain.
|
||||||
|
self.domain_scope_id = None
|
||||||
|
|
||||||
|
if self.expires_at is not None:
|
||||||
|
# Trim off the expiration time because MySQL timestamps are only
|
||||||
|
# accurate to the second.
|
||||||
|
self.expires_at = self.expires_at.replace(microsecond=0)
|
||||||
|
|
||||||
|
if self.revoked_at is None:
|
||||||
|
self.revoked_at = timeutils.utcnow()
|
||||||
|
if self.issued_before is None:
|
||||||
|
self.issued_before = self.revoked_at
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
keys = ['user_id',
|
||||||
|
'role_id',
|
||||||
|
'domain_id',
|
||||||
|
'domain_scope_id',
|
||||||
|
'project_id',
|
||||||
|
'audit_id',
|
||||||
|
'audit_chain_id',
|
||||||
|
]
|
||||||
|
event = {key: self.__dict__[key] for key in keys
|
||||||
|
if self.__dict__[key] is not None}
|
||||||
|
if self.trust_id is not None:
|
||||||
|
event['OS-TRUST:trust_id'] = self.trust_id
|
||||||
|
if self.consumer_id is not None:
|
||||||
|
event['OS-OAUTH1:consumer_id'] = self.consumer_id
|
||||||
|
if self.consumer_id is not None:
|
||||||
|
event['OS-OAUTH1:access_token_id'] = self.access_token_id
|
||||||
|
if self.expires_at is not None:
|
||||||
|
event['expires_at'] = utils.isotime(self.expires_at)
|
||||||
|
if self.issued_before is not None:
|
||||||
|
event['issued_before'] = utils.isotime(self.issued_before,
|
||||||
|
subsecond=True)
|
||||||
|
return event
|
||||||
|
|
||||||
|
def key_for_name(self, name):
|
||||||
|
return "%s=%s" % (name, getattr(self, name) or '*')
|
||||||
|
|
||||||
|
|
||||||
|
def attr_keys(event):
|
||||||
|
return list(map(event.key_for_name, _EVENT_NAMES))
|
||||||
|
|
||||||
|
|
||||||
|
class RevokeTree(object):
|
||||||
|
"""Fast Revocation Checking Tree Structure
|
||||||
|
|
||||||
|
The Tree is an index to quickly match tokens against events.
|
||||||
|
Each node is a hashtable of key=value combinations from revocation events.
|
||||||
|
The
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, revoke_events=None):
|
||||||
|
self.revoke_map = dict()
|
||||||
|
self.add_events(revoke_events)
|
||||||
|
|
||||||
|
def add_event(self, event):
|
||||||
|
"""Updates the tree based on a revocation event.
|
||||||
|
|
||||||
|
Creates any necessary internal nodes in the tree corresponding to the
|
||||||
|
fields of the revocation event. The leaf node will always be set to
|
||||||
|
the latest 'issued_before' for events that are otherwise identical.
|
||||||
|
|
||||||
|
:param: Event to add to the tree
|
||||||
|
|
||||||
|
:returns: the event that was passed in.
|
||||||
|
|
||||||
|
"""
|
||||||
|
revoke_map = self.revoke_map
|
||||||
|
for key in attr_keys(event):
|
||||||
|
revoke_map = revoke_map.setdefault(key, {})
|
||||||
|
revoke_map['issued_before'] = max(
|
||||||
|
event.issued_before, revoke_map.get(
|
||||||
|
'issued_before', event.issued_before))
|
||||||
|
return event
|
||||||
|
|
||||||
|
def remove_event(self, event):
|
||||||
|
"""Update the tree based on the removal of a Revocation Event
|
||||||
|
|
||||||
|
Removes empty nodes from the tree from the leaf back to the root.
|
||||||
|
|
||||||
|
If multiple events trace the same path, but have different
|
||||||
|
'issued_before' values, only the last is ever stored in the tree.
|
||||||
|
So only an exact match on 'issued_before' ever triggers a removal
|
||||||
|
|
||||||
|
:param: Event to remove from the tree
|
||||||
|
|
||||||
|
"""
|
||||||
|
stack = []
|
||||||
|
revoke_map = self.revoke_map
|
||||||
|
for name in _EVENT_NAMES:
|
||||||
|
key = event.key_for_name(name)
|
||||||
|
nxt = revoke_map.get(key)
|
||||||
|
if nxt is None:
|
||||||
|
break
|
||||||
|
stack.append((revoke_map, key, nxt))
|
||||||
|
revoke_map = nxt
|
||||||
|
else:
|
||||||
|
if event.issued_before == revoke_map['issued_before']:
|
||||||
|
revoke_map.pop('issued_before')
|
||||||
|
for parent, key, child in reversed(stack):
|
||||||
|
if not any(child):
|
||||||
|
del parent[key]
|
||||||
|
|
||||||
|
def add_events(self, revoke_events):
|
||||||
|
return list(map(self.add_event, revoke_events or []))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _next_level_keys(name, token_data):
|
||||||
|
"""Generate keys based on current field name and token data
|
||||||
|
|
||||||
|
Generate all keys to look for in the next iteration of revocation
|
||||||
|
event tree traversal.
|
||||||
|
"""
|
||||||
|
yield '*'
|
||||||
|
if name == 'role_id':
|
||||||
|
# Roles are very special since a token has a list of them.
|
||||||
|
# If the revocation event matches any one of them,
|
||||||
|
# revoke the token.
|
||||||
|
for role_id in token_data.get('roles', []):
|
||||||
|
yield role_id
|
||||||
|
else:
|
||||||
|
# For other fields we try to get any branch that concur
|
||||||
|
# with any alternative field in the token.
|
||||||
|
for alt_name in ALTERNATIVES.get(name, [name]):
|
||||||
|
yield token_data[alt_name]
|
||||||
|
|
||||||
|
def _search(self, revoke_map, names, token_data):
|
||||||
|
"""Search for revocation event by token_data
|
||||||
|
|
||||||
|
Traverse the revocation events tree looking for event matching token
|
||||||
|
data issued after the token.
|
||||||
|
"""
|
||||||
|
if not names:
|
||||||
|
# The last (leaf) level is checked in a special way because we
|
||||||
|
# verify issued_at field differently.
|
||||||
|
try:
|
||||||
|
return revoke_map['issued_before'] >= token_data['issued_at']
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
name, remaining_names = names[0], names[1:]
|
||||||
|
|
||||||
|
for key in self._next_level_keys(name, token_data):
|
||||||
|
subtree = revoke_map.get('%s=%s' % (name, key))
|
||||||
|
if subtree and self._search(subtree, remaining_names, token_data):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# If we made it out of the loop then no element in revocation tree
|
||||||
|
# corresponds to our token and it is good.
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_revoked(self, token_data):
|
||||||
|
"""Check if a token matches the revocation event
|
||||||
|
|
||||||
|
Compare the values for each level of the tree with the values from
|
||||||
|
the token, accounting for attributes that have alternative
|
||||||
|
keys, and for wildcard matches.
|
||||||
|
if there is a match, continue down the tree.
|
||||||
|
if there is no match, exit early.
|
||||||
|
|
||||||
|
token_data is a map based on a flattened view of token.
|
||||||
|
The required fields are:
|
||||||
|
|
||||||
|
'expires_at','user_id', 'project_id', 'identity_domain_id',
|
||||||
|
'assignment_domain_id', 'trust_id', 'trustor_id', 'trustee_id'
|
||||||
|
'consumer_id', 'access_token_id'
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self._search(self.revoke_map, _EVENT_NAMES, token_data)
|
||||||
|
|
||||||
|
|
||||||
|
def build_token_values_v2(access, default_domain_id):
|
||||||
|
token_data = access['token']
|
||||||
|
|
||||||
|
token_expires_at = timeutils.parse_isotime(token_data['expires'])
|
||||||
|
|
||||||
|
# Trim off the microseconds because the revocation event only has
|
||||||
|
# expirations accurate to the second.
|
||||||
|
token_expires_at = token_expires_at.replace(microsecond=0)
|
||||||
|
|
||||||
|
token_values = {
|
||||||
|
'expires_at': timeutils.normalize_time(token_expires_at),
|
||||||
|
'issued_at': timeutils.normalize_time(
|
||||||
|
timeutils.parse_isotime(token_data['issued_at'])),
|
||||||
|
'audit_id': token_data.get('audit_ids', [None])[0],
|
||||||
|
'audit_chain_id': token_data.get('audit_ids', [None])[-1],
|
||||||
|
}
|
||||||
|
|
||||||
|
token_values['user_id'] = access.get('user', {}).get('id')
|
||||||
|
|
||||||
|
project = token_data.get('tenant')
|
||||||
|
if project is not None:
|
||||||
|
token_values['project_id'] = project['id']
|
||||||
|
else:
|
||||||
|
token_values['project_id'] = None
|
||||||
|
|
||||||
|
token_values['identity_domain_id'] = default_domain_id
|
||||||
|
token_values['assignment_domain_id'] = default_domain_id
|
||||||
|
|
||||||
|
trust = token_data.get('trust')
|
||||||
|
if trust is None:
|
||||||
|
token_values['trust_id'] = None
|
||||||
|
token_values['trustor_id'] = None
|
||||||
|
token_values['trustee_id'] = None
|
||||||
|
else:
|
||||||
|
token_values['trust_id'] = trust['id']
|
||||||
|
token_values['trustor_id'] = trust['trustor_id']
|
||||||
|
token_values['trustee_id'] = trust['trustee_id']
|
||||||
|
|
||||||
|
token_values['consumer_id'] = None
|
||||||
|
token_values['access_token_id'] = None
|
||||||
|
|
||||||
|
role_list = []
|
||||||
|
# Roles are by ID in metadata and by name in the user section
|
||||||
|
roles = access.get('metadata', {}).get('roles', [])
|
||||||
|
for role in roles:
|
||||||
|
role_list.append(role)
|
||||||
|
token_values['roles'] = role_list
|
||||||
|
return token_values
|
||||||
|
|
||||||
|
|
||||||
|
def build_token_values(token_data):
|
||||||
|
|
||||||
|
token_expires_at = timeutils.parse_isotime(token_data['expires_at'])
|
||||||
|
|
||||||
|
# Trim off the microseconds because the revocation event only has
|
||||||
|
# expirations accurate to the second.
|
||||||
|
token_expires_at = token_expires_at.replace(microsecond=0)
|
||||||
|
|
||||||
|
token_values = {
|
||||||
|
'expires_at': timeutils.normalize_time(token_expires_at),
|
||||||
|
'issued_at': timeutils.normalize_time(
|
||||||
|
timeutils.parse_isotime(token_data['issued_at'])),
|
||||||
|
'audit_id': token_data.get('audit_ids', [None])[0],
|
||||||
|
'audit_chain_id': token_data.get('audit_ids', [None])[-1],
|
||||||
|
}
|
||||||
|
|
||||||
|
user = token_data.get('user')
|
||||||
|
if user is not None:
|
||||||
|
token_values['user_id'] = user['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
|
||||||
|
|
||||||
|
project = token_data.get('project', token_data.get('tenant'))
|
||||||
|
if project is not None:
|
||||||
|
token_values['project_id'] = project['id']
|
||||||
|
token_values['assignment_domain_id'] = project['domain']['id']
|
||||||
|
else:
|
||||||
|
token_values['project_id'] = None
|
||||||
|
|
||||||
|
domain = token_data.get('domain')
|
||||||
|
if domain is not None:
|
||||||
|
token_values['assignment_domain_id'] = domain['id']
|
||||||
|
else:
|
||||||
|
token_values['assignment_domain_id'] = None
|
||||||
|
|
||||||
|
role_list = []
|
||||||
|
roles = token_data.get('roles')
|
||||||
|
if roles is not None:
|
||||||
|
for role in roles:
|
||||||
|
role_list.append(role['id'])
|
||||||
|
token_values['roles'] = role_list
|
||||||
|
|
||||||
|
trust = token_data.get('OS-TRUST:trust')
|
||||||
|
if trust is None:
|
||||||
|
token_values['trust_id'] = None
|
||||||
|
token_values['trustor_id'] = None
|
||||||
|
token_values['trustee_id'] = None
|
||||||
|
else:
|
||||||
|
token_values['trust_id'] = trust['id']
|
||||||
|
token_values['trustor_id'] = trust['trustor_user']['id']
|
||||||
|
token_values['trustee_id'] = trust['trustee_user']['id']
|
||||||
|
|
||||||
|
oauth1 = token_data.get('OS-OAUTH1')
|
||||||
|
if oauth1 is None:
|
||||||
|
token_values['consumer_id'] = None
|
||||||
|
token_values['access_token_id'] = None
|
||||||
|
else:
|
||||||
|
token_values['consumer_id'] = oauth1['consumer_id']
|
||||||
|
token_values['access_token_id'] = oauth1['access_token_id']
|
||||||
|
return token_values
|
|
@ -13,13 +13,13 @@
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from keystone.common import sql
|
from keystone.common import sql
|
||||||
|
from keystone.models import revoke_model
|
||||||
from keystone import revoke
|
from keystone import revoke
|
||||||
from keystone.revoke import model
|
|
||||||
|
|
||||||
|
|
||||||
class RevocationEvent(sql.ModelBase, sql.ModelDictMixin):
|
class RevocationEvent(sql.ModelBase, sql.ModelDictMixin):
|
||||||
__tablename__ = 'revocation_event'
|
__tablename__ = 'revocation_event'
|
||||||
attributes = model.REVOKE_KEYS
|
attributes = revoke_model.REVOKE_KEYS
|
||||||
|
|
||||||
# The id field is not going to be exposed to the outside world.
|
# The id field is not going to be exposed to the outside world.
|
||||||
# It is, however, necessary for SQLAlchemy.
|
# It is, however, necessary for SQLAlchemy.
|
||||||
|
@ -88,13 +88,13 @@ class Revoke(revoke.RevokeDriverV8):
|
||||||
if last_fetch:
|
if last_fetch:
|
||||||
query = query.filter(RevocationEvent.revoked_at > last_fetch)
|
query = query.filter(RevocationEvent.revoked_at > last_fetch)
|
||||||
|
|
||||||
events = [model.RevokeEvent(**e.to_dict()) for e in query]
|
events = [revoke_model.RevokeEvent(**e.to_dict()) for e in query]
|
||||||
|
|
||||||
return events
|
return events
|
||||||
|
|
||||||
def revoke(self, event):
|
def revoke(self, event):
|
||||||
kwargs = dict()
|
kwargs = dict()
|
||||||
for attr in model.REVOKE_KEYS:
|
for attr in revoke_model.REVOKE_KEYS:
|
||||||
kwargs[attr] = getattr(event, attr)
|
kwargs[attr] = getattr(event, attr)
|
||||||
kwargs['id'] = uuid.uuid4().hex
|
kwargs['id'] = uuid.uuid4().hex
|
||||||
record = RevocationEvent(**kwargs)
|
record = RevocationEvent(**kwargs)
|
||||||
|
|
|
@ -26,8 +26,8 @@ from keystone.common import extension
|
||||||
from keystone.common import manager
|
from keystone.common import manager
|
||||||
from keystone import exception
|
from keystone import exception
|
||||||
from keystone.i18n import _
|
from keystone.i18n import _
|
||||||
|
from keystone.models import revoke_model
|
||||||
from keystone import notifications
|
from keystone import notifications
|
||||||
from keystone.revoke import model
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
@ -77,7 +77,7 @@ class Manager(manager.Manager):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(Manager, self).__init__(CONF.revoke.driver)
|
super(Manager, self).__init__(CONF.revoke.driver)
|
||||||
self._register_listeners()
|
self._register_listeners()
|
||||||
self.model = model
|
self.model = revoke_model
|
||||||
|
|
||||||
def _user_callback(self, service, resource_type, operation,
|
def _user_callback(self, service, resource_type, operation,
|
||||||
payload):
|
payload):
|
||||||
|
@ -86,32 +86,32 @@ class Manager(manager.Manager):
|
||||||
def _role_callback(self, service, resource_type, operation,
|
def _role_callback(self, service, resource_type, operation,
|
||||||
payload):
|
payload):
|
||||||
self.revoke(
|
self.revoke(
|
||||||
model.RevokeEvent(role_id=payload['resource_info']))
|
revoke_model.RevokeEvent(role_id=payload['resource_info']))
|
||||||
|
|
||||||
def _project_callback(self, service, resource_type, operation,
|
def _project_callback(self, service, resource_type, operation,
|
||||||
payload):
|
payload):
|
||||||
self.revoke(
|
self.revoke(
|
||||||
model.RevokeEvent(project_id=payload['resource_info']))
|
revoke_model.RevokeEvent(project_id=payload['resource_info']))
|
||||||
|
|
||||||
def _domain_callback(self, service, resource_type, operation,
|
def _domain_callback(self, service, resource_type, operation,
|
||||||
payload):
|
payload):
|
||||||
self.revoke(
|
self.revoke(
|
||||||
model.RevokeEvent(domain_id=payload['resource_info']))
|
revoke_model.RevokeEvent(domain_id=payload['resource_info']))
|
||||||
|
|
||||||
def _trust_callback(self, service, resource_type, operation,
|
def _trust_callback(self, service, resource_type, operation,
|
||||||
payload):
|
payload):
|
||||||
self.revoke(
|
self.revoke(
|
||||||
model.RevokeEvent(trust_id=payload['resource_info']))
|
revoke_model.RevokeEvent(trust_id=payload['resource_info']))
|
||||||
|
|
||||||
def _consumer_callback(self, service, resource_type, operation,
|
def _consumer_callback(self, service, resource_type, operation,
|
||||||
payload):
|
payload):
|
||||||
self.revoke(
|
self.revoke(
|
||||||
model.RevokeEvent(consumer_id=payload['resource_info']))
|
revoke_model.RevokeEvent(consumer_id=payload['resource_info']))
|
||||||
|
|
||||||
def _access_token_callback(self, service, resource_type, operation,
|
def _access_token_callback(self, service, resource_type, operation,
|
||||||
payload):
|
payload):
|
||||||
self.revoke(
|
self.revoke(
|
||||||
model.RevokeEvent(access_token_id=payload['resource_info']))
|
revoke_model.RevokeEvent(access_token_id=payload['resource_info']))
|
||||||
|
|
||||||
def _role_assignment_callback(self, service, resource_type, operation,
|
def _role_assignment_callback(self, service, resource_type, operation,
|
||||||
payload):
|
payload):
|
||||||
|
@ -148,7 +148,7 @@ class Manager(manager.Manager):
|
||||||
callback_fns)
|
callback_fns)
|
||||||
|
|
||||||
def revoke_by_user(self, user_id):
|
def revoke_by_user(self, user_id):
|
||||||
return self.revoke(model.RevokeEvent(user_id=user_id))
|
return self.revoke(revoke_model.RevokeEvent(user_id=user_id))
|
||||||
|
|
||||||
def _assert_not_domain_and_project_scoped(self, domain_id=None,
|
def _assert_not_domain_and_project_scoped(self, domain_id=None,
|
||||||
project_id=None):
|
project_id=None):
|
||||||
|
@ -167,13 +167,13 @@ class Manager(manager.Manager):
|
||||||
project_id=project_id)
|
project_id=project_id)
|
||||||
|
|
||||||
self.revoke(
|
self.revoke(
|
||||||
model.RevokeEvent(user_id=user_id,
|
revoke_model.RevokeEvent(user_id=user_id,
|
||||||
expires_at=expires_at,
|
expires_at=expires_at,
|
||||||
domain_id=domain_id,
|
domain_id=domain_id,
|
||||||
project_id=project_id))
|
project_id=project_id))
|
||||||
|
|
||||||
def revoke_by_audit_id(self, audit_id):
|
def revoke_by_audit_id(self, audit_id):
|
||||||
self.revoke(model.RevokeEvent(audit_id=audit_id))
|
self.revoke(revoke_model.RevokeEvent(audit_id=audit_id))
|
||||||
|
|
||||||
def revoke_by_audit_chain_id(self, audit_chain_id, project_id=None,
|
def revoke_by_audit_chain_id(self, audit_chain_id, project_id=None,
|
||||||
domain_id=None):
|
domain_id=None):
|
||||||
|
@ -181,32 +181,34 @@ class Manager(manager.Manager):
|
||||||
self._assert_not_domain_and_project_scoped(domain_id=domain_id,
|
self._assert_not_domain_and_project_scoped(domain_id=domain_id,
|
||||||
project_id=project_id)
|
project_id=project_id)
|
||||||
|
|
||||||
self.revoke(model.RevokeEvent(audit_chain_id=audit_chain_id,
|
self.revoke(revoke_model.RevokeEvent(audit_chain_id=audit_chain_id,
|
||||||
domain_id=domain_id,
|
domain_id=domain_id,
|
||||||
project_id=project_id))
|
project_id=project_id))
|
||||||
|
|
||||||
def revoke_by_grant(self, role_id, user_id=None,
|
def revoke_by_grant(self, role_id, user_id=None,
|
||||||
domain_id=None, project_id=None):
|
domain_id=None, project_id=None):
|
||||||
self.revoke(
|
self.revoke(
|
||||||
model.RevokeEvent(user_id=user_id,
|
revoke_model.RevokeEvent(user_id=user_id,
|
||||||
role_id=role_id,
|
role_id=role_id,
|
||||||
domain_id=domain_id,
|
domain_id=domain_id,
|
||||||
project_id=project_id))
|
project_id=project_id))
|
||||||
|
|
||||||
def revoke_by_user_and_project(self, user_id, project_id):
|
def revoke_by_user_and_project(self, user_id, project_id):
|
||||||
self.revoke(
|
self.revoke(
|
||||||
model.RevokeEvent(project_id=project_id, user_id=user_id))
|
revoke_model.RevokeEvent(project_id=project_id, user_id=user_id))
|
||||||
|
|
||||||
def revoke_by_project_role_assignment(self, project_id, role_id):
|
def revoke_by_project_role_assignment(self, project_id, role_id):
|
||||||
self.revoke(model.RevokeEvent(project_id=project_id, role_id=role_id))
|
self.revoke(revoke_model.RevokeEvent(project_id=project_id,
|
||||||
|
role_id=role_id))
|
||||||
|
|
||||||
def revoke_by_domain_role_assignment(self, domain_id, role_id):
|
def revoke_by_domain_role_assignment(self, domain_id, role_id):
|
||||||
self.revoke(model.RevokeEvent(domain_id=domain_id, role_id=role_id))
|
self.revoke(revoke_model.RevokeEvent(domain_id=domain_id,
|
||||||
|
role_id=role_id))
|
||||||
|
|
||||||
@MEMOIZE
|
@MEMOIZE
|
||||||
def _get_revoke_tree(self):
|
def _get_revoke_tree(self):
|
||||||
events = self.driver.list_events()
|
events = self.driver.list_events()
|
||||||
revoke_tree = model.RevokeTree(revoke_events=events)
|
revoke_tree = revoke_model.RevokeTree(revoke_events=events)
|
||||||
|
|
||||||
return revoke_tree
|
return revoke_tree
|
||||||
|
|
||||||
|
|
|
@ -10,362 +10,4 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from oslo_utils import timeutils
|
from keystone.models.revoke_model import * # noqa
|
||||||
from six.moves import map
|
|
||||||
|
|
||||||
from keystone.common import utils
|
|
||||||
|
|
||||||
|
|
||||||
# The set of attributes common between the RevokeEvent
|
|
||||||
# and the dictionaries created from the token Data.
|
|
||||||
_NAMES = ['trust_id',
|
|
||||||
'consumer_id',
|
|
||||||
'access_token_id',
|
|
||||||
'audit_id',
|
|
||||||
'audit_chain_id',
|
|
||||||
'expires_at',
|
|
||||||
'domain_id',
|
|
||||||
'project_id',
|
|
||||||
'user_id',
|
|
||||||
'role_id']
|
|
||||||
|
|
||||||
|
|
||||||
# Additional arguments for creating a RevokeEvent
|
|
||||||
_EVENT_ARGS = ['issued_before', 'revoked_at']
|
|
||||||
|
|
||||||
# Names of attributes in the RevocationEvent, including "virtual" attributes.
|
|
||||||
# Virtual attributes are those added based on other values.
|
|
||||||
_EVENT_NAMES = _NAMES + ['domain_scope_id']
|
|
||||||
|
|
||||||
# Values that will be in the token data but not in the event.
|
|
||||||
# These will compared with event values that have different names.
|
|
||||||
# For example: both trustor_id and trustee_id are compared against user_id
|
|
||||||
_TOKEN_KEYS = ['identity_domain_id',
|
|
||||||
'assignment_domain_id',
|
|
||||||
'issued_at',
|
|
||||||
'trustor_id',
|
|
||||||
'trustee_id']
|
|
||||||
|
|
||||||
# Alternative names to be checked in token for every field in
|
|
||||||
# revoke tree.
|
|
||||||
ALTERNATIVES = {
|
|
||||||
'user_id': ['user_id', 'trustor_id', 'trustee_id'],
|
|
||||||
'domain_id': ['identity_domain_id', 'assignment_domain_id'],
|
|
||||||
# For a domain-scoped token, the domain is in assignment_domain_id.
|
|
||||||
'domain_scope_id': ['assignment_domain_id', ],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
REVOKE_KEYS = _NAMES + _EVENT_ARGS
|
|
||||||
|
|
||||||
|
|
||||||
def blank_token_data(issued_at):
|
|
||||||
token_data = dict()
|
|
||||||
for name in _NAMES:
|
|
||||||
token_data[name] = None
|
|
||||||
for name in _TOKEN_KEYS:
|
|
||||||
token_data[name] = None
|
|
||||||
# required field
|
|
||||||
token_data['issued_at'] = issued_at
|
|
||||||
return token_data
|
|
||||||
|
|
||||||
|
|
||||||
class RevokeEvent(object):
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
for k in REVOKE_KEYS:
|
|
||||||
v = kwargs.get(k)
|
|
||||||
setattr(self, k, v)
|
|
||||||
|
|
||||||
if self.domain_id and self.expires_at:
|
|
||||||
# This is revoking a domain-scoped token.
|
|
||||||
self.domain_scope_id = self.domain_id
|
|
||||||
self.domain_id = None
|
|
||||||
else:
|
|
||||||
# This is revoking all tokens for a domain.
|
|
||||||
self.domain_scope_id = None
|
|
||||||
|
|
||||||
if self.expires_at is not None:
|
|
||||||
# Trim off the expiration time because MySQL timestamps are only
|
|
||||||
# accurate to the second.
|
|
||||||
self.expires_at = self.expires_at.replace(microsecond=0)
|
|
||||||
|
|
||||||
if self.revoked_at is None:
|
|
||||||
self.revoked_at = timeutils.utcnow()
|
|
||||||
if self.issued_before is None:
|
|
||||||
self.issued_before = self.revoked_at
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
keys = ['user_id',
|
|
||||||
'role_id',
|
|
||||||
'domain_id',
|
|
||||||
'domain_scope_id',
|
|
||||||
'project_id',
|
|
||||||
'audit_id',
|
|
||||||
'audit_chain_id',
|
|
||||||
]
|
|
||||||
event = {key: self.__dict__[key] for key in keys
|
|
||||||
if self.__dict__[key] is not None}
|
|
||||||
if self.trust_id is not None:
|
|
||||||
event['OS-TRUST:trust_id'] = self.trust_id
|
|
||||||
if self.consumer_id is not None:
|
|
||||||
event['OS-OAUTH1:consumer_id'] = self.consumer_id
|
|
||||||
if self.consumer_id is not None:
|
|
||||||
event['OS-OAUTH1:access_token_id'] = self.access_token_id
|
|
||||||
if self.expires_at is not None:
|
|
||||||
event['expires_at'] = utils.isotime(self.expires_at)
|
|
||||||
if self.issued_before is not None:
|
|
||||||
event['issued_before'] = utils.isotime(self.issued_before,
|
|
||||||
subsecond=True)
|
|
||||||
return event
|
|
||||||
|
|
||||||
def key_for_name(self, name):
|
|
||||||
return "%s=%s" % (name, getattr(self, name) or '*')
|
|
||||||
|
|
||||||
|
|
||||||
def attr_keys(event):
|
|
||||||
return list(map(event.key_for_name, _EVENT_NAMES))
|
|
||||||
|
|
||||||
|
|
||||||
class RevokeTree(object):
|
|
||||||
"""Fast Revocation Checking Tree Structure
|
|
||||||
|
|
||||||
The Tree is an index to quickly match tokens against events.
|
|
||||||
Each node is a hashtable of key=value combinations from revocation events.
|
|
||||||
The
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, revoke_events=None):
|
|
||||||
self.revoke_map = dict()
|
|
||||||
self.add_events(revoke_events)
|
|
||||||
|
|
||||||
def add_event(self, event):
|
|
||||||
"""Updates the tree based on a revocation event.
|
|
||||||
|
|
||||||
Creates any necessary internal nodes in the tree corresponding to the
|
|
||||||
fields of the revocation event. The leaf node will always be set to
|
|
||||||
the latest 'issued_before' for events that are otherwise identical.
|
|
||||||
|
|
||||||
:param: Event to add to the tree
|
|
||||||
|
|
||||||
:returns: the event that was passed in.
|
|
||||||
|
|
||||||
"""
|
|
||||||
revoke_map = self.revoke_map
|
|
||||||
for key in attr_keys(event):
|
|
||||||
revoke_map = revoke_map.setdefault(key, {})
|
|
||||||
revoke_map['issued_before'] = max(
|
|
||||||
event.issued_before, revoke_map.get(
|
|
||||||
'issued_before', event.issued_before))
|
|
||||||
return event
|
|
||||||
|
|
||||||
def remove_event(self, event):
|
|
||||||
"""Update the tree based on the removal of a Revocation Event
|
|
||||||
|
|
||||||
Removes empty nodes from the tree from the leaf back to the root.
|
|
||||||
|
|
||||||
If multiple events trace the same path, but have different
|
|
||||||
'issued_before' values, only the last is ever stored in the tree.
|
|
||||||
So only an exact match on 'issued_before' ever triggers a removal
|
|
||||||
|
|
||||||
:param: Event to remove from the tree
|
|
||||||
|
|
||||||
"""
|
|
||||||
stack = []
|
|
||||||
revoke_map = self.revoke_map
|
|
||||||
for name in _EVENT_NAMES:
|
|
||||||
key = event.key_for_name(name)
|
|
||||||
nxt = revoke_map.get(key)
|
|
||||||
if nxt is None:
|
|
||||||
break
|
|
||||||
stack.append((revoke_map, key, nxt))
|
|
||||||
revoke_map = nxt
|
|
||||||
else:
|
|
||||||
if event.issued_before == revoke_map['issued_before']:
|
|
||||||
revoke_map.pop('issued_before')
|
|
||||||
for parent, key, child in reversed(stack):
|
|
||||||
if not any(child):
|
|
||||||
del parent[key]
|
|
||||||
|
|
||||||
def add_events(self, revoke_events):
|
|
||||||
return list(map(self.add_event, revoke_events or []))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _next_level_keys(name, token_data):
|
|
||||||
"""Generate keys based on current field name and token data
|
|
||||||
|
|
||||||
Generate all keys to look for in the next iteration of revocation
|
|
||||||
event tree traversal.
|
|
||||||
"""
|
|
||||||
yield '*'
|
|
||||||
if name == 'role_id':
|
|
||||||
# Roles are very special since a token has a list of them.
|
|
||||||
# If the revocation event matches any one of them,
|
|
||||||
# revoke the token.
|
|
||||||
for role_id in token_data.get('roles', []):
|
|
||||||
yield role_id
|
|
||||||
else:
|
|
||||||
# For other fields we try to get any branch that concur
|
|
||||||
# with any alternative field in the token.
|
|
||||||
for alt_name in ALTERNATIVES.get(name, [name]):
|
|
||||||
yield token_data[alt_name]
|
|
||||||
|
|
||||||
def _search(self, revoke_map, names, token_data):
|
|
||||||
"""Search for revocation event by token_data
|
|
||||||
|
|
||||||
Traverse the revocation events tree looking for event matching token
|
|
||||||
data issued after the token.
|
|
||||||
"""
|
|
||||||
if not names:
|
|
||||||
# The last (leaf) level is checked in a special way because we
|
|
||||||
# verify issued_at field differently.
|
|
||||||
try:
|
|
||||||
return revoke_map['issued_before'] >= token_data['issued_at']
|
|
||||||
except KeyError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
name, remaining_names = names[0], names[1:]
|
|
||||||
|
|
||||||
for key in self._next_level_keys(name, token_data):
|
|
||||||
subtree = revoke_map.get('%s=%s' % (name, key))
|
|
||||||
if subtree and self._search(subtree, remaining_names, token_data):
|
|
||||||
return True
|
|
||||||
|
|
||||||
# If we made it out of the loop then no element in revocation tree
|
|
||||||
# corresponds to our token and it is good.
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_revoked(self, token_data):
|
|
||||||
"""Check if a token matches the revocation event
|
|
||||||
|
|
||||||
Compare the values for each level of the tree with the values from
|
|
||||||
the token, accounting for attributes that have alternative
|
|
||||||
keys, and for wildcard matches.
|
|
||||||
if there is a match, continue down the tree.
|
|
||||||
if there is no match, exit early.
|
|
||||||
|
|
||||||
token_data is a map based on a flattened view of token.
|
|
||||||
The required fields are:
|
|
||||||
|
|
||||||
'expires_at','user_id', 'project_id', 'identity_domain_id',
|
|
||||||
'assignment_domain_id', 'trust_id', 'trustor_id', 'trustee_id'
|
|
||||||
'consumer_id', 'access_token_id'
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self._search(self.revoke_map, _EVENT_NAMES, token_data)
|
|
||||||
|
|
||||||
|
|
||||||
def build_token_values_v2(access, default_domain_id):
|
|
||||||
token_data = access['token']
|
|
||||||
|
|
||||||
token_expires_at = timeutils.parse_isotime(token_data['expires'])
|
|
||||||
|
|
||||||
# Trim off the microseconds because the revocation event only has
|
|
||||||
# expirations accurate to the second.
|
|
||||||
token_expires_at = token_expires_at.replace(microsecond=0)
|
|
||||||
|
|
||||||
token_values = {
|
|
||||||
'expires_at': timeutils.normalize_time(token_expires_at),
|
|
||||||
'issued_at': timeutils.normalize_time(
|
|
||||||
timeutils.parse_isotime(token_data['issued_at'])),
|
|
||||||
'audit_id': token_data.get('audit_ids', [None])[0],
|
|
||||||
'audit_chain_id': token_data.get('audit_ids', [None])[-1],
|
|
||||||
}
|
|
||||||
|
|
||||||
token_values['user_id'] = access.get('user', {}).get('id')
|
|
||||||
|
|
||||||
project = token_data.get('tenant')
|
|
||||||
if project is not None:
|
|
||||||
token_values['project_id'] = project['id']
|
|
||||||
else:
|
|
||||||
token_values['project_id'] = None
|
|
||||||
|
|
||||||
token_values['identity_domain_id'] = default_domain_id
|
|
||||||
token_values['assignment_domain_id'] = default_domain_id
|
|
||||||
|
|
||||||
trust = token_data.get('trust')
|
|
||||||
if trust is None:
|
|
||||||
token_values['trust_id'] = None
|
|
||||||
token_values['trustor_id'] = None
|
|
||||||
token_values['trustee_id'] = None
|
|
||||||
else:
|
|
||||||
token_values['trust_id'] = trust['id']
|
|
||||||
token_values['trustor_id'] = trust['trustor_id']
|
|
||||||
token_values['trustee_id'] = trust['trustee_id']
|
|
||||||
|
|
||||||
token_values['consumer_id'] = None
|
|
||||||
token_values['access_token_id'] = None
|
|
||||||
|
|
||||||
role_list = []
|
|
||||||
# Roles are by ID in metadata and by name in the user section
|
|
||||||
roles = access.get('metadata', {}).get('roles', [])
|
|
||||||
for role in roles:
|
|
||||||
role_list.append(role)
|
|
||||||
token_values['roles'] = role_list
|
|
||||||
return token_values
|
|
||||||
|
|
||||||
|
|
||||||
def build_token_values(token_data):
|
|
||||||
|
|
||||||
token_expires_at = timeutils.parse_isotime(token_data['expires_at'])
|
|
||||||
|
|
||||||
# Trim off the microseconds because the revocation event only has
|
|
||||||
# expirations accurate to the second.
|
|
||||||
token_expires_at = token_expires_at.replace(microsecond=0)
|
|
||||||
|
|
||||||
token_values = {
|
|
||||||
'expires_at': timeutils.normalize_time(token_expires_at),
|
|
||||||
'issued_at': timeutils.normalize_time(
|
|
||||||
timeutils.parse_isotime(token_data['issued_at'])),
|
|
||||||
'audit_id': token_data.get('audit_ids', [None])[0],
|
|
||||||
'audit_chain_id': token_data.get('audit_ids', [None])[-1],
|
|
||||||
}
|
|
||||||
|
|
||||||
user = token_data.get('user')
|
|
||||||
if user is not None:
|
|
||||||
token_values['user_id'] = user['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
|
|
||||||
|
|
||||||
project = token_data.get('project', token_data.get('tenant'))
|
|
||||||
if project is not None:
|
|
||||||
token_values['project_id'] = project['id']
|
|
||||||
token_values['assignment_domain_id'] = project['domain']['id']
|
|
||||||
else:
|
|
||||||
token_values['project_id'] = None
|
|
||||||
|
|
||||||
domain = token_data.get('domain')
|
|
||||||
if domain is not None:
|
|
||||||
token_values['assignment_domain_id'] = domain['id']
|
|
||||||
else:
|
|
||||||
token_values['assignment_domain_id'] = None
|
|
||||||
|
|
||||||
role_list = []
|
|
||||||
roles = token_data.get('roles')
|
|
||||||
if roles is not None:
|
|
||||||
for role in roles:
|
|
||||||
role_list.append(role['id'])
|
|
||||||
token_values['roles'] = role_list
|
|
||||||
|
|
||||||
trust = token_data.get('OS-TRUST:trust')
|
|
||||||
if trust is None:
|
|
||||||
token_values['trust_id'] = None
|
|
||||||
token_values['trustor_id'] = None
|
|
||||||
token_values['trustee_id'] = None
|
|
||||||
else:
|
|
||||||
token_values['trust_id'] = trust['id']
|
|
||||||
token_values['trustor_id'] = trust['trustor_user']['id']
|
|
||||||
token_values['trustee_id'] = trust['trustee_user']['id']
|
|
||||||
|
|
||||||
oauth1 = token_data.get('OS-OAUTH1')
|
|
||||||
if oauth1 is None:
|
|
||||||
token_values['consumer_id'] = None
|
|
||||||
token_values['access_token_id'] = None
|
|
||||||
else:
|
|
||||||
token_values['consumer_id'] = oauth1['consumer_id']
|
|
||||||
token_values['access_token_id'] = oauth1['access_token_id']
|
|
||||||
return token_values
|
|
||||||
|
|
|
@ -31,6 +31,8 @@ import warnings
|
||||||
import fixtures
|
import fixtures
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_config import fixture as config_fixture
|
from oslo_config import fixture as config_fixture
|
||||||
|
from oslo_context import context as oslo_context
|
||||||
|
from oslo_context import fixture as oslo_ctx_fixture
|
||||||
from oslo_log import fixture as log_fixture
|
from oslo_log import fixture as log_fixture
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
|
@ -510,6 +512,10 @@ class BaseTestCase(oslotest.BaseTestCase):
|
||||||
module='^keystone\\.')
|
module='^keystone\\.')
|
||||||
warnings.simplefilter('error', exc.SAWarning)
|
warnings.simplefilter('error', exc.SAWarning)
|
||||||
self.addCleanup(warnings.resetwarnings)
|
self.addCleanup(warnings.resetwarnings)
|
||||||
|
# Ensure we have an empty threadlocal context at the start of each
|
||||||
|
# test.
|
||||||
|
self.assertIsNone(oslo_context.get_current())
|
||||||
|
self.useFixture(oslo_ctx_fixture.ClearRequestContext())
|
||||||
|
|
||||||
def cleanup_instance(self, *names):
|
def cleanup_instance(self, *names):
|
||||||
"""Create a function suitable for use with self.addCleanup.
|
"""Create a function suitable for use with self.addCleanup.
|
||||||
|
|
|
@ -21,7 +21,7 @@ from testtools import matchers
|
||||||
|
|
||||||
from keystone.common import utils
|
from keystone.common import utils
|
||||||
from keystone import exception
|
from keystone import exception
|
||||||
from keystone.revoke import model
|
from keystone.models import revoke_model
|
||||||
from keystone.tests import unit
|
from keystone.tests import unit
|
||||||
from keystone.tests.unit import test_backend_sql
|
from keystone.tests.unit import test_backend_sql
|
||||||
from keystone.token import provider
|
from keystone.token import provider
|
||||||
|
@ -46,7 +46,7 @@ def _past_time():
|
||||||
def _sample_blank_token():
|
def _sample_blank_token():
|
||||||
issued_delta = datetime.timedelta(minutes=-2)
|
issued_delta = datetime.timedelta(minutes=-2)
|
||||||
issued_at = timeutils.utcnow() + issued_delta
|
issued_at = timeutils.utcnow() + issued_delta
|
||||||
token_data = model.blank_token_data(issued_at)
|
token_data = revoke_model.blank_token_data(issued_at)
|
||||||
return token_data
|
return token_data
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ class RevokeTests(object):
|
||||||
user_id = 1
|
user_id = 1
|
||||||
self.revoke_api.revoke_by_expiration(user_id, _future_time())
|
self.revoke_api.revoke_by_expiration(user_id, _future_time())
|
||||||
self.assertEqual(1, len(self.revoke_api.list_events()))
|
self.assertEqual(1, len(self.revoke_api.list_events()))
|
||||||
event = model.RevokeEvent()
|
event = revoke_model.RevokeEvent()
|
||||||
event.revoked_at = _past_time()
|
event.revoked_at = _past_time()
|
||||||
self.revoke_api.revoke(event)
|
self.revoke_api.revoke(event)
|
||||||
self.assertEqual(1, len(self.revoke_api.list_events()))
|
self.assertEqual(1, len(self.revoke_api.list_events()))
|
||||||
|
@ -194,7 +194,7 @@ class RevokeTreeTests(unit.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(RevokeTreeTests, self).setUp()
|
super(RevokeTreeTests, self).setUp()
|
||||||
self.events = []
|
self.events = []
|
||||||
self.tree = model.RevokeTree()
|
self.tree = revoke_model.RevokeTree()
|
||||||
self._sample_data()
|
self._sample_data()
|
||||||
|
|
||||||
def _sample_data(self):
|
def _sample_data(self):
|
||||||
|
@ -248,20 +248,20 @@ class RevokeTreeTests(unit.TestCase):
|
||||||
|
|
||||||
def _revoke_by_user(self, user_id):
|
def _revoke_by_user(self, user_id):
|
||||||
return self.tree.add_event(
|
return self.tree.add_event(
|
||||||
model.RevokeEvent(user_id=user_id))
|
revoke_model.RevokeEvent(user_id=user_id))
|
||||||
|
|
||||||
def _revoke_by_audit_id(self, audit_id):
|
def _revoke_by_audit_id(self, audit_id):
|
||||||
event = self.tree.add_event(
|
event = self.tree.add_event(
|
||||||
model.RevokeEvent(audit_id=audit_id))
|
revoke_model.RevokeEvent(audit_id=audit_id))
|
||||||
self.events.append(event)
|
self.events.append(event)
|
||||||
return event
|
return event
|
||||||
|
|
||||||
def _revoke_by_audit_chain_id(self, audit_chain_id, project_id=None,
|
def _revoke_by_audit_chain_id(self, audit_chain_id, project_id=None,
|
||||||
domain_id=None):
|
domain_id=None):
|
||||||
event = self.tree.add_event(
|
event = self.tree.add_event(
|
||||||
model.RevokeEvent(audit_chain_id=audit_chain_id,
|
revoke_model.RevokeEvent(audit_chain_id=audit_chain_id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
domain_id=domain_id)
|
domain_id=domain_id)
|
||||||
)
|
)
|
||||||
self.events.append(event)
|
self.events.append(event)
|
||||||
return event
|
return event
|
||||||
|
@ -269,46 +269,47 @@ class RevokeTreeTests(unit.TestCase):
|
||||||
def _revoke_by_expiration(self, user_id, expires_at, project_id=None,
|
def _revoke_by_expiration(self, user_id, expires_at, project_id=None,
|
||||||
domain_id=None):
|
domain_id=None):
|
||||||
event = self.tree.add_event(
|
event = self.tree.add_event(
|
||||||
model.RevokeEvent(user_id=user_id,
|
revoke_model.RevokeEvent(user_id=user_id,
|
||||||
expires_at=expires_at,
|
expires_at=expires_at,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
domain_id=domain_id))
|
domain_id=domain_id))
|
||||||
self.events.append(event)
|
self.events.append(event)
|
||||||
return event
|
return event
|
||||||
|
|
||||||
def _revoke_by_grant(self, role_id, user_id=None,
|
def _revoke_by_grant(self, role_id, user_id=None,
|
||||||
domain_id=None, project_id=None):
|
domain_id=None, project_id=None):
|
||||||
event = self.tree.add_event(
|
event = self.tree.add_event(
|
||||||
model.RevokeEvent(user_id=user_id,
|
revoke_model.RevokeEvent(user_id=user_id,
|
||||||
role_id=role_id,
|
role_id=role_id,
|
||||||
domain_id=domain_id,
|
domain_id=domain_id,
|
||||||
project_id=project_id))
|
project_id=project_id))
|
||||||
self.events.append(event)
|
self.events.append(event)
|
||||||
return event
|
return event
|
||||||
|
|
||||||
def _revoke_by_user_and_project(self, user_id, project_id):
|
def _revoke_by_user_and_project(self, user_id, project_id):
|
||||||
event = self.tree.add_event(
|
event = self.tree.add_event(
|
||||||
model.RevokeEvent(project_id=project_id,
|
revoke_model.RevokeEvent(project_id=project_id,
|
||||||
user_id=user_id))
|
user_id=user_id))
|
||||||
self.events.append(event)
|
self.events.append(event)
|
||||||
return event
|
return event
|
||||||
|
|
||||||
def _revoke_by_project_role_assignment(self, project_id, role_id):
|
def _revoke_by_project_role_assignment(self, project_id, role_id):
|
||||||
event = self.tree.add_event(
|
event = self.tree.add_event(
|
||||||
model.RevokeEvent(project_id=project_id,
|
revoke_model.RevokeEvent(project_id=project_id,
|
||||||
role_id=role_id))
|
role_id=role_id))
|
||||||
self.events.append(event)
|
self.events.append(event)
|
||||||
return event
|
return event
|
||||||
|
|
||||||
def _revoke_by_domain_role_assignment(self, domain_id, role_id):
|
def _revoke_by_domain_role_assignment(self, domain_id, role_id):
|
||||||
event = self.tree.add_event(
|
event = self.tree.add_event(
|
||||||
model.RevokeEvent(domain_id=domain_id,
|
revoke_model.RevokeEvent(domain_id=domain_id,
|
||||||
role_id=role_id))
|
role_id=role_id))
|
||||||
self.events.append(event)
|
self.events.append(event)
|
||||||
return event
|
return event
|
||||||
|
|
||||||
def _revoke_by_domain(self, domain_id):
|
def _revoke_by_domain(self, domain_id):
|
||||||
event = self.tree.add_event(model.RevokeEvent(domain_id=domain_id))
|
event = self.tree.add_event(
|
||||||
|
revoke_model.RevokeEvent(domain_id=domain_id))
|
||||||
self.events.append(event)
|
self.events.append(event)
|
||||||
|
|
||||||
def _user_field_test(self, field_name):
|
def _user_field_test(self, field_name):
|
||||||
|
|
|
@ -19,7 +19,7 @@ from six.moves import http_client
|
||||||
from testtools import matchers
|
from testtools import matchers
|
||||||
|
|
||||||
from keystone.common import utils
|
from keystone.common import utils
|
||||||
from keystone.revoke import model
|
from keystone.models import revoke_model
|
||||||
from keystone.tests.unit import test_v3
|
from keystone.tests.unit import test_v3
|
||||||
from keystone.token import provider
|
from keystone.token import provider
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin):
|
||||||
sample['project_id'] = six.text_type(project_id)
|
sample['project_id'] = six.text_type(project_id)
|
||||||
before_time = timeutils.utcnow()
|
before_time = timeutils.utcnow()
|
||||||
self.revoke_api.revoke(
|
self.revoke_api.revoke(
|
||||||
model.RevokeEvent(project_id=project_id))
|
revoke_model.RevokeEvent(project_id=project_id))
|
||||||
|
|
||||||
resp = self.get('/OS-REVOKE/events')
|
resp = self.get('/OS-REVOKE/events')
|
||||||
events = resp.json_body['events']
|
events = resp.json_body['events']
|
||||||
|
@ -103,7 +103,7 @@ class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin):
|
||||||
sample['domain_id'] = six.text_type(domain_id)
|
sample['domain_id'] = six.text_type(domain_id)
|
||||||
before_time = timeutils.utcnow()
|
before_time = timeutils.utcnow()
|
||||||
self.revoke_api.revoke(
|
self.revoke_api.revoke(
|
||||||
model.RevokeEvent(domain_id=domain_id))
|
revoke_model.RevokeEvent(domain_id=domain_id))
|
||||||
|
|
||||||
resp = self.get('/OS-REVOKE/events')
|
resp = self.get('/OS-REVOKE/events')
|
||||||
events = resp.json_body['events']
|
events = resp.json_body['events']
|
||||||
|
@ -125,7 +125,7 @@ class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin):
|
||||||
sample['domain_id'] = six.text_type(domain_id)
|
sample['domain_id'] = six.text_type(domain_id)
|
||||||
|
|
||||||
self.revoke_api.revoke(
|
self.revoke_api.revoke(
|
||||||
model.RevokeEvent(domain_id=domain_id))
|
revoke_model.RevokeEvent(domain_id=domain_id))
|
||||||
|
|
||||||
resp = self.get('/OS-REVOKE/events')
|
resp = self.get('/OS-REVOKE/events')
|
||||||
events = resp.json_body['events']
|
events = resp.json_body['events']
|
||||||
|
|
Loading…
Reference in New Issue