338 lines
13 KiB
Python
338 lines
13 KiB
Python
# Copyright 2012 OpenStack Foundation
|
|
#
|
|
# 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.
|
|
|
|
"""Main entry point into the Token persistence service."""
|
|
|
|
import abc
|
|
import copy
|
|
|
|
import six
|
|
|
|
from keystone.common import cache
|
|
from keystone.common import dependency
|
|
from keystone.common import manager
|
|
from keystone import config
|
|
from keystone import exception
|
|
from keystone.openstack.common import log
|
|
from keystone.openstack.common import timeutils
|
|
from keystone.openstack.common import versionutils
|
|
|
|
|
|
CONF = config.CONF
|
|
LOG = log.getLogger(__name__)
|
|
SHOULD_CACHE = cache.should_cache_fn('token')
|
|
|
|
# NOTE(blk-u): The config options are not available at import time.
|
|
EXPIRATION_TIME = lambda: CONF.token.cache_time
|
|
REVOCATION_CACHE_EXPIRATION_TIME = lambda: CONF.token.revocation_cache_time
|
|
|
|
|
|
@dependency.requires('assignment_api', 'identity_api', 'token_provider_api',
|
|
'trust_api')
|
|
@dependency.provider('token_api')
|
|
class Manager(manager.Manager):
|
|
"""Default pivot point for the Token backend.
|
|
|
|
See :mod:`keystone.common.manager.Manager` for more details on how this
|
|
dynamically calls the backend.
|
|
|
|
"""
|
|
|
|
def __init__(self):
|
|
super(Manager, self).__init__(CONF.token.driver)
|
|
|
|
@versionutils.deprecated(as_of=versionutils.deprecated.JUNO,
|
|
in_favor_of='token_provider_api.unique_id',
|
|
remove_in=+1,
|
|
what='token_api.unique_id')
|
|
def unique_id(self, token_id):
|
|
return self.token_provider_api.unique_id(token_id)
|
|
|
|
def _assert_valid(self, token_id, token_ref):
|
|
"""Raise TokenNotFound if the token is expired."""
|
|
current_time = timeutils.normalize_time(timeutils.utcnow())
|
|
expires = token_ref.get('expires')
|
|
if not expires or current_time > timeutils.normalize_time(expires):
|
|
raise exception.TokenNotFound(token_id=token_id)
|
|
|
|
def get_token(self, token_id):
|
|
if not token_id:
|
|
# NOTE(morganfainberg): There are cases when the
|
|
# context['token_id'] will in-fact be None. This also saves
|
|
# a round-trip to the backend if we don't have a token_id.
|
|
raise exception.TokenNotFound(token_id='')
|
|
unique_id = self.token_provider_api.unique_id(token_id)
|
|
token_ref = self._get_token(unique_id)
|
|
# NOTE(morganfainberg): Lift expired checking to the manager, there is
|
|
# no reason to make the drivers implement this check. With caching,
|
|
# self._get_token could return an expired token. Make sure we behave
|
|
# as expected and raise TokenNotFound on those instances.
|
|
self._assert_valid(token_id, token_ref)
|
|
return token_ref
|
|
|
|
@cache.on_arguments(should_cache_fn=SHOULD_CACHE,
|
|
expiration_time=EXPIRATION_TIME)
|
|
def _get_token(self, token_id):
|
|
# Only ever use the "unique" id in the cache key.
|
|
return self.driver.get_token(token_id)
|
|
|
|
def create_token(self, token_id, data):
|
|
unique_id = self.token_provider_api.unique_id(token_id)
|
|
data_copy = copy.deepcopy(data)
|
|
data_copy['id'] = unique_id
|
|
ret = self.driver.create_token(unique_id, data_copy)
|
|
if SHOULD_CACHE(ret):
|
|
# NOTE(morganfainberg): when doing a cache set, you must pass the
|
|
# same arguments through, the same as invalidate (this includes
|
|
# "self"). First argument is always the value to be cached
|
|
self._get_token.set(ret, self, unique_id)
|
|
return ret
|
|
|
|
def delete_token(self, token_id):
|
|
if not CONF.token.revoke_by_id:
|
|
return
|
|
unique_id = self.token_provider_api.unique_id(token_id)
|
|
self.driver.delete_token(unique_id)
|
|
self._invalidate_individual_token_cache(unique_id)
|
|
self.invalidate_revocation_list()
|
|
|
|
def delete_tokens(self, user_id, tenant_id=None, trust_id=None,
|
|
consumer_id=None):
|
|
if not CONF.token.revoke_by_id:
|
|
return
|
|
token_list = self.driver._list_tokens(user_id, tenant_id, trust_id,
|
|
consumer_id)
|
|
self.driver.delete_tokens(user_id, tenant_id, trust_id, consumer_id)
|
|
for token_id in token_list:
|
|
unique_id = self.token_provider_api.unique_id(token_id)
|
|
self._invalidate_individual_token_cache(unique_id)
|
|
self.invalidate_revocation_list()
|
|
|
|
@cache.on_arguments(should_cache_fn=SHOULD_CACHE,
|
|
expiration_time=REVOCATION_CACHE_EXPIRATION_TIME)
|
|
def list_revoked_tokens(self):
|
|
return self.driver.list_revoked_tokens()
|
|
|
|
def invalidate_revocation_list(self):
|
|
# NOTE(morganfainberg): Note that ``self`` needs to be passed to
|
|
# invalidate() because of the way the invalidation method works on
|
|
# determining cache-keys.
|
|
self.list_revoked_tokens.invalidate(self)
|
|
|
|
def delete_tokens_for_domain(self, domain_id):
|
|
"""Delete all tokens for a given domain.
|
|
|
|
It will delete all the project-scoped tokens for the projects
|
|
that are owned by the given domain, as well as any tokens issued
|
|
to users that are owned by this domain.
|
|
|
|
However, deletion of domain_scoped tokens will still need to be
|
|
implemented as stated in TODO below.
|
|
"""
|
|
if not CONF.token.revoke_by_id:
|
|
return
|
|
projects = self.assignment_api.list_projects()
|
|
for project in projects:
|
|
if project['domain_id'] == domain_id:
|
|
for user_id in self.assignment_api.list_user_ids_for_project(
|
|
project['id']):
|
|
self.delete_tokens_for_user(user_id, project['id'])
|
|
# TODO(morganfainberg): implement deletion of domain_scoped tokens.
|
|
|
|
users = self.identity_api.list_users(domain_id)
|
|
user_ids = (user['id'] for user in users)
|
|
self.delete_tokens_for_users(user_ids)
|
|
|
|
def delete_tokens_for_user(self, user_id, project_id=None):
|
|
"""Delete all tokens for a given user or user-project combination.
|
|
|
|
This method adds in the extra logic for handling trust-scoped token
|
|
revocations in a single call instead of needing to explicitly handle
|
|
trusts in the caller's logic.
|
|
"""
|
|
if not CONF.token.revoke_by_id:
|
|
return
|
|
self.delete_tokens(user_id, tenant_id=project_id)
|
|
for trust in self.trust_api.list_trusts_for_trustee(user_id):
|
|
# Ensure we revoke tokens associated to the trust / project
|
|
# user_id combination.
|
|
self.delete_tokens(user_id, trust_id=trust['id'],
|
|
tenant_id=project_id)
|
|
for trust in self.trust_api.list_trusts_for_trustor(user_id):
|
|
# Ensure we revoke tokens associated to the trust / project /
|
|
# user_id combination where the user_id is the trustor.
|
|
|
|
# NOTE(morganfainberg): This revocation is a bit coarse, but it
|
|
# covers a number of cases such as disabling of the trustor user,
|
|
# deletion of the trustor user (for any number of reasons). It
|
|
# might make sense to refine this and be more surgical on the
|
|
# deletions (e.g. don't revoke tokens for the trusts when the
|
|
# trustor changes password). For now, to maintain previous
|
|
# functionality, this will continue to be a bit overzealous on
|
|
# revocations.
|
|
self.delete_tokens(trust['trustee_user_id'], trust_id=trust['id'],
|
|
tenant_id=project_id)
|
|
|
|
def delete_tokens_for_users(self, user_ids, project_id=None):
|
|
"""Delete all tokens for a list of user_ids.
|
|
|
|
:param user_ids: list of user identifiers
|
|
:param project_id: optional project identifier
|
|
"""
|
|
if not CONF.token.revoke_by_id:
|
|
return
|
|
for user_id in user_ids:
|
|
self.delete_tokens_for_user(user_id, project_id=project_id)
|
|
|
|
def _invalidate_individual_token_cache(self, token_id):
|
|
# NOTE(morganfainberg): invalidate takes the exact same arguments as
|
|
# the normal method, this means we need to pass "self" in (which gets
|
|
# stripped off).
|
|
|
|
# FIXME(morganfainberg): Does this cache actually need to be
|
|
# invalidated? We maintain a cached revocation list, which should be
|
|
# consulted before accepting a token as valid. For now we will
|
|
# do the explicit individual token invalidation.
|
|
self._get_token.invalidate(self, token_id)
|
|
self.token_provider_api.invalidate_individual_token_cache(token_id)
|
|
|
|
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class Driver(object):
|
|
"""Interface description for a Token driver."""
|
|
|
|
@abc.abstractmethod
|
|
def get_token(self, token_id):
|
|
"""Get a token by id.
|
|
|
|
:param token_id: identity of the token
|
|
:type token_id: string
|
|
:returns: token_ref
|
|
:raises: keystone.exception.TokenNotFound
|
|
|
|
"""
|
|
raise exception.NotImplemented() # pragma: no cover
|
|
|
|
@abc.abstractmethod
|
|
def create_token(self, token_id, data):
|
|
"""Create a token by id and data.
|
|
|
|
:param token_id: identity of the token
|
|
:type token_id: string
|
|
:param data: dictionary with additional reference information
|
|
|
|
::
|
|
|
|
{
|
|
expires=''
|
|
id=token_id,
|
|
user=user_ref,
|
|
tenant=tenant_ref,
|
|
metadata=metadata_ref
|
|
}
|
|
|
|
:type data: dict
|
|
:returns: token_ref or None.
|
|
|
|
"""
|
|
raise exception.NotImplemented() # pragma: no cover
|
|
|
|
@abc.abstractmethod
|
|
def delete_token(self, token_id):
|
|
"""Deletes a token by id.
|
|
|
|
:param token_id: identity of the token
|
|
:type token_id: string
|
|
:returns: None.
|
|
:raises: keystone.exception.TokenNotFound
|
|
|
|
"""
|
|
raise exception.NotImplemented() # pragma: no cover
|
|
|
|
@abc.abstractmethod
|
|
def delete_tokens(self, user_id, tenant_id=None, trust_id=None,
|
|
consumer_id=None):
|
|
"""Deletes tokens by user.
|
|
|
|
If the tenant_id is not None, only delete the tokens by user id under
|
|
the specified tenant.
|
|
|
|
If the trust_id is not None, it will be used to query tokens and the
|
|
user_id will be ignored.
|
|
|
|
If the consumer_id is not None, only delete the tokens by consumer id
|
|
that match the specified consumer id.
|
|
|
|
:param user_id: identity of user
|
|
:type user_id: string
|
|
:param tenant_id: identity of the tenant
|
|
:type tenant_id: string
|
|
:param trust_id: identity of the trust
|
|
:type trust_id: string
|
|
:param consumer_id: identity of the consumer
|
|
:type consumer_id: string
|
|
:returns: None.
|
|
:raises: keystone.exception.TokenNotFound
|
|
|
|
"""
|
|
if not CONF.token.revoke_by_id:
|
|
return
|
|
token_list = self._list_tokens(user_id,
|
|
tenant_id=tenant_id,
|
|
trust_id=trust_id,
|
|
consumer_id=consumer_id)
|
|
|
|
for token in token_list:
|
|
try:
|
|
self.delete_token(token)
|
|
except exception.NotFound:
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def _list_tokens(self, user_id, tenant_id=None, trust_id=None,
|
|
consumer_id=None):
|
|
"""Returns a list of current token_id's for a user
|
|
|
|
This is effectively a private method only used by the ``delete_tokens``
|
|
method and should not be called by anything outside of the
|
|
``token_api`` manager or the token driver itself.
|
|
|
|
:param user_id: identity of the user
|
|
:type user_id: string
|
|
:param tenant_id: identity of the tenant
|
|
:type tenant_id: string
|
|
:param trust_id: identity of the trust
|
|
:type trust_id: string
|
|
:param consumer_id: identity of the consumer
|
|
:type consumer_id: string
|
|
:returns: list of token_id's
|
|
|
|
"""
|
|
raise exception.NotImplemented() # pragma: no cover
|
|
|
|
@abc.abstractmethod
|
|
def list_revoked_tokens(self):
|
|
"""Returns a list of all revoked tokens
|
|
|
|
:returns: list of token_id's
|
|
|
|
"""
|
|
raise exception.NotImplemented() # pragma: no cover
|
|
|
|
@abc.abstractmethod
|
|
def flush_expired_tokens(self):
|
|
"""Archive or delete tokens that have expired.
|
|
"""
|
|
raise exception.NotImplemented() # pragma: no cover
|