diff --git a/keystone/common/cache/_context_cache.py b/keystone/common/cache/_context_cache.py new file mode 100644 index 0000000000..d9d3d43b7e --- /dev/null +++ b/keystone/common/cache/_context_cache.py @@ -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) diff --git a/keystone/common/cache/core.py b/keystone/common/cache/core.py index 03dbf80a57..6bb0af5122 100644 --- a/keystone/common/cache/core.py +++ b/keystone/common/cache/core.py @@ -18,6 +18,9 @@ from dogpile.cache import api from oslo_cache import core as cache from oslo_config import cfg +from keystone.common.cache import _context_cache + + CONF = cfg.CONF CACHE_REGION = cache.create_region() @@ -25,7 +28,15 @@ CACHE_REGION = cache.create_region() def configure_cache(region=None): if region is None: 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) + # 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): diff --git a/keystone/middleware/auth.py b/keystone/middleware/auth.py index 909675243f..eea0ba0647 100644 --- a/keystone/middleware/auth.py +++ b/keystone/middleware/auth.py @@ -214,6 +214,7 @@ class AuthContextMiddleware(wsgi.Middleware): request_context.domain = auth_context.get('domain_id') request_context.user_domain = auth_context.get('user_domain_id') request_context.project_domain = auth_context.get('project_domain_id') + request_context.update_store() LOG.debug('RBAC: auth_context: %s', auth_context) request.environ[authorization.AUTH_CONTEXT_ENV] = auth_context diff --git a/keystone/models/revoke_model.py b/keystone/models/revoke_model.py new file mode 100644 index 0000000000..5217e9c62d --- /dev/null +++ b/keystone/models/revoke_model.py @@ -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 diff --git a/keystone/revoke/backends/sql.py b/keystone/revoke/backends/sql.py index 67c7caab8d..fa62e34c03 100644 --- a/keystone/revoke/backends/sql.py +++ b/keystone/revoke/backends/sql.py @@ -13,13 +13,13 @@ import uuid from keystone.common import sql +from keystone.models import revoke_model from keystone import revoke -from keystone.revoke import model class RevocationEvent(sql.ModelBase, sql.ModelDictMixin): __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. # It is, however, necessary for SQLAlchemy. @@ -88,13 +88,13 @@ class Revoke(revoke.RevokeDriverV8): if 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 def revoke(self, event): kwargs = dict() - for attr in model.REVOKE_KEYS: + for attr in revoke_model.REVOKE_KEYS: kwargs[attr] = getattr(event, attr) kwargs['id'] = uuid.uuid4().hex record = RevocationEvent(**kwargs) diff --git a/keystone/revoke/core.py b/keystone/revoke/core.py index 0b2bc9403e..64d2e998ad 100644 --- a/keystone/revoke/core.py +++ b/keystone/revoke/core.py @@ -26,8 +26,8 @@ from keystone.common import extension from keystone.common import manager from keystone import exception from keystone.i18n import _ +from keystone.models import revoke_model from keystone import notifications -from keystone.revoke import model CONF = cfg.CONF @@ -77,7 +77,7 @@ class Manager(manager.Manager): def __init__(self): super(Manager, self).__init__(CONF.revoke.driver) self._register_listeners() - self.model = model + self.model = revoke_model def _user_callback(self, service, resource_type, operation, payload): @@ -86,32 +86,32 @@ class Manager(manager.Manager): def _role_callback(self, service, resource_type, operation, payload): 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, payload): 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, payload): 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, payload): 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, payload): 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, payload): 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, payload): @@ -148,7 +148,7 @@ class Manager(manager.Manager): callback_fns) 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, project_id=None): @@ -167,13 +167,13 @@ class Manager(manager.Manager): project_id=project_id) self.revoke( - model.RevokeEvent(user_id=user_id, - expires_at=expires_at, - domain_id=domain_id, - project_id=project_id)) + revoke_model.RevokeEvent(user_id=user_id, + expires_at=expires_at, + domain_id=domain_id, + project_id=project_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, domain_id=None): @@ -181,32 +181,34 @@ class Manager(manager.Manager): self._assert_not_domain_and_project_scoped(domain_id=domain_id, project_id=project_id) - self.revoke(model.RevokeEvent(audit_chain_id=audit_chain_id, - domain_id=domain_id, - project_id=project_id)) + self.revoke(revoke_model.RevokeEvent(audit_chain_id=audit_chain_id, + domain_id=domain_id, + project_id=project_id)) def revoke_by_grant(self, role_id, user_id=None, domain_id=None, project_id=None): self.revoke( - model.RevokeEvent(user_id=user_id, - role_id=role_id, - domain_id=domain_id, - project_id=project_id)) + revoke_model.RevokeEvent(user_id=user_id, + role_id=role_id, + domain_id=domain_id, + project_id=project_id)) def revoke_by_user_and_project(self, user_id, project_id): 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): - 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): - 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 def _get_revoke_tree(self): events = self.driver.list_events() - revoke_tree = model.RevokeTree(revoke_events=events) + revoke_tree = revoke_model.RevokeTree(revoke_events=events) return revoke_tree diff --git a/keystone/revoke/model.py b/keystone/revoke/model.py index 5217e9c62d..28a8d07f71 100644 --- a/keystone/revoke/model.py +++ b/keystone/revoke/model.py @@ -10,362 +10,4 @@ # 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 +from keystone.models.revoke_model import * # noqa diff --git a/keystone/tests/unit/core.py b/keystone/tests/unit/core.py index 0b655f2640..2db5925643 100644 --- a/keystone/tests/unit/core.py +++ b/keystone/tests/unit/core.py @@ -31,6 +31,8 @@ import warnings import fixtures from oslo_config import cfg 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 log from oslo_utils import timeutils @@ -510,6 +512,10 @@ class BaseTestCase(oslotest.BaseTestCase): module='^keystone\\.') warnings.simplefilter('error', exc.SAWarning) 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): """Create a function suitable for use with self.addCleanup. diff --git a/keystone/tests/unit/test_revoke.py b/keystone/tests/unit/test_revoke.py index f10d264c62..82c0125a00 100644 --- a/keystone/tests/unit/test_revoke.py +++ b/keystone/tests/unit/test_revoke.py @@ -21,7 +21,7 @@ from testtools import matchers from keystone.common import utils from keystone import exception -from keystone.revoke import model +from keystone.models import revoke_model from keystone.tests import unit from keystone.tests.unit import test_backend_sql from keystone.token import provider @@ -46,7 +46,7 @@ def _past_time(): def _sample_blank_token(): issued_delta = datetime.timedelta(minutes=-2) 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 @@ -134,7 +134,7 @@ class RevokeTests(object): user_id = 1 self.revoke_api.revoke_by_expiration(user_id, _future_time()) self.assertEqual(1, len(self.revoke_api.list_events())) - event = model.RevokeEvent() + event = revoke_model.RevokeEvent() event.revoked_at = _past_time() self.revoke_api.revoke(event) self.assertEqual(1, len(self.revoke_api.list_events())) @@ -194,7 +194,7 @@ class RevokeTreeTests(unit.TestCase): def setUp(self): super(RevokeTreeTests, self).setUp() self.events = [] - self.tree = model.RevokeTree() + self.tree = revoke_model.RevokeTree() self._sample_data() def _sample_data(self): @@ -248,20 +248,20 @@ class RevokeTreeTests(unit.TestCase): def _revoke_by_user(self, user_id): 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): event = self.tree.add_event( - model.RevokeEvent(audit_id=audit_id)) + revoke_model.RevokeEvent(audit_id=audit_id)) self.events.append(event) return event def _revoke_by_audit_chain_id(self, audit_chain_id, project_id=None, domain_id=None): event = self.tree.add_event( - model.RevokeEvent(audit_chain_id=audit_chain_id, - project_id=project_id, - domain_id=domain_id) + revoke_model.RevokeEvent(audit_chain_id=audit_chain_id, + project_id=project_id, + domain_id=domain_id) ) self.events.append(event) return event @@ -269,46 +269,47 @@ class RevokeTreeTests(unit.TestCase): def _revoke_by_expiration(self, user_id, expires_at, project_id=None, domain_id=None): event = self.tree.add_event( - model.RevokeEvent(user_id=user_id, - expires_at=expires_at, - project_id=project_id, - domain_id=domain_id)) + revoke_model.RevokeEvent(user_id=user_id, + expires_at=expires_at, + project_id=project_id, + domain_id=domain_id)) self.events.append(event) return event def _revoke_by_grant(self, role_id, user_id=None, domain_id=None, project_id=None): event = self.tree.add_event( - model.RevokeEvent(user_id=user_id, - role_id=role_id, - domain_id=domain_id, - project_id=project_id)) + revoke_model.RevokeEvent(user_id=user_id, + role_id=role_id, + domain_id=domain_id, + project_id=project_id)) self.events.append(event) return event def _revoke_by_user_and_project(self, user_id, project_id): event = self.tree.add_event( - model.RevokeEvent(project_id=project_id, - user_id=user_id)) + revoke_model.RevokeEvent(project_id=project_id, + user_id=user_id)) self.events.append(event) return event def _revoke_by_project_role_assignment(self, project_id, role_id): event = self.tree.add_event( - model.RevokeEvent(project_id=project_id, - role_id=role_id)) + revoke_model.RevokeEvent(project_id=project_id, + role_id=role_id)) self.events.append(event) return event def _revoke_by_domain_role_assignment(self, domain_id, role_id): event = self.tree.add_event( - model.RevokeEvent(domain_id=domain_id, - role_id=role_id)) + revoke_model.RevokeEvent(domain_id=domain_id, + role_id=role_id)) self.events.append(event) return event 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) def _user_field_test(self, field_name): diff --git a/keystone/tests/unit/test_v3_os_revoke.py b/keystone/tests/unit/test_v3_os_revoke.py index 1ad614d937..5fb5387a03 100644 --- a/keystone/tests/unit/test_v3_os_revoke.py +++ b/keystone/tests/unit/test_v3_os_revoke.py @@ -19,7 +19,7 @@ from six.moves import http_client from testtools import matchers 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.token import provider @@ -90,7 +90,7 @@ class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin): sample['project_id'] = six.text_type(project_id) before_time = timeutils.utcnow() self.revoke_api.revoke( - model.RevokeEvent(project_id=project_id)) + revoke_model.RevokeEvent(project_id=project_id)) resp = self.get('/OS-REVOKE/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) before_time = timeutils.utcnow() self.revoke_api.revoke( - model.RevokeEvent(domain_id=domain_id)) + revoke_model.RevokeEvent(domain_id=domain_id)) resp = self.get('/OS-REVOKE/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) self.revoke_api.revoke( - model.RevokeEvent(domain_id=domain_id)) + revoke_model.RevokeEvent(domain_id=domain_id)) resp = self.get('/OS-REVOKE/events') events = resp.json_body['events']