aboutsummaryrefslogtreecommitdiffstats
path: root/keystone-moon/keystone/token/provider.py
diff options
context:
space:
mode:
Diffstat (limited to 'keystone-moon/keystone/token/provider.py')
-rw-r--r--keystone-moon/keystone/token/provider.py584
1 files changed, 584 insertions, 0 deletions
diff --git a/keystone-moon/keystone/token/provider.py b/keystone-moon/keystone/token/provider.py
new file mode 100644
index 00000000..fb41d4bb
--- /dev/null
+++ b/keystone-moon/keystone/token/provider.py
@@ -0,0 +1,584 @@
+# 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.
+
+"""Token provider interface."""
+
+import abc
+import base64
+import datetime
+import sys
+import uuid
+
+from keystoneclient.common import cms
+from oslo_config import cfg
+from oslo_log import log
+from oslo_utils import timeutils
+import six
+
+from keystone.common import cache
+from keystone.common import dependency
+from keystone.common import manager
+from keystone import exception
+from keystone.i18n import _, _LE
+from keystone.models import token_model
+from keystone import notifications
+from keystone.token import persistence
+
+
+CONF = cfg.CONF
+LOG = log.getLogger(__name__)
+MEMOIZE = cache.get_memoization_decorator(section='token')
+
+# NOTE(morganfainberg): This is for compatibility in case someone was relying
+# on the old location of the UnsupportedTokenVersionException for their code.
+UnsupportedTokenVersionException = exception.UnsupportedTokenVersionException
+
+# supported token versions
+V2 = token_model.V2
+V3 = token_model.V3
+VERSIONS = token_model.VERSIONS
+
+
+def base64_encode(s):
+ """Encode a URL-safe string."""
+ return base64.urlsafe_b64encode(s).rstrip('=')
+
+
+def random_urlsafe_str():
+ """Generate a random URL-safe string."""
+ # chop the padding (==) off the end of the encoding to save space
+ return base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2]
+
+
+def random_urlsafe_str_to_bytes(s):
+ """Convert a string generated by ``random_urlsafe_str()`` to bytes."""
+ # restore the padding (==) at the end of the string
+ return base64.urlsafe_b64decode(s + '==')
+
+
+def default_expire_time():
+ """Determine when a fresh token should expire.
+
+ Expiration time varies based on configuration (see ``[token] expiration``).
+
+ :returns: a naive UTC datetime.datetime object
+
+ """
+ expire_delta = datetime.timedelta(seconds=CONF.token.expiration)
+ return timeutils.utcnow() + expire_delta
+
+
+def audit_info(parent_audit_id):
+ """Build the audit data for a token.
+
+ If ``parent_audit_id`` is None, the list will be one element in length
+ containing a newly generated audit_id.
+
+ If ``parent_audit_id`` is supplied, the list will be two elements in length
+ containing a newly generated audit_id and the ``parent_audit_id``. The
+ ``parent_audit_id`` will always be element index 1 in the resulting
+ list.
+
+ :param parent_audit_id: the audit of the original token in the chain
+ :type parent_audit_id: str
+ :returns: Keystone token audit data
+ """
+ audit_id = random_urlsafe_str()
+ if parent_audit_id is not None:
+ return [audit_id, parent_audit_id]
+ return [audit_id]
+
+
+@dependency.provider('token_provider_api')
+@dependency.requires('assignment_api', 'revoke_api')
+class Manager(manager.Manager):
+ """Default pivot point for the token provider backend.
+
+ See :mod:`keystone.common.manager.Manager` for more details on how this
+ dynamically calls the backend.
+
+ """
+
+ V2 = V2
+ V3 = V3
+ VERSIONS = VERSIONS
+ INVALIDATE_PROJECT_TOKEN_PERSISTENCE = 'invalidate_project_tokens'
+ INVALIDATE_USER_TOKEN_PERSISTENCE = 'invalidate_user_tokens'
+ _persistence_manager = None
+
+ def __init__(self):
+ super(Manager, self).__init__(CONF.token.provider)
+ self._register_callback_listeners()
+
+ def _register_callback_listeners(self):
+ # This is used by the @dependency.provider decorator to register the
+ # provider (token_provider_api) manager to listen for trust deletions.
+ callbacks = {
+ notifications.ACTIONS.deleted: [
+ ['OS-TRUST:trust', self._trust_deleted_event_callback],
+ ['user', self._delete_user_tokens_callback],
+ ['domain', self._delete_domain_tokens_callback],
+ ],
+ notifications.ACTIONS.disabled: [
+ ['user', self._delete_user_tokens_callback],
+ ['domain', self._delete_domain_tokens_callback],
+ ['project', self._delete_project_tokens_callback],
+ ],
+ notifications.ACTIONS.internal: [
+ [notifications.INVALIDATE_USER_TOKEN_PERSISTENCE,
+ self._delete_user_tokens_callback],
+ [notifications.INVALIDATE_USER_PROJECT_TOKEN_PERSISTENCE,
+ self._delete_user_project_tokens_callback],
+ [notifications.INVALIDATE_USER_OAUTH_CONSUMER_TOKENS,
+ self._delete_user_oauth_consumer_tokens_callback],
+ ]
+ }
+
+ for event, cb_info in six.iteritems(callbacks):
+ for resource_type, callback_fns in cb_info:
+ notifications.register_event_callback(event, resource_type,
+ callback_fns)
+
+ @property
+ def _needs_persistence(self):
+ return self.driver.needs_persistence()
+
+ @property
+ def _persistence(self):
+ # NOTE(morganfainberg): This should not be handled via __init__ to
+ # avoid dependency injection oddities circular dependencies (where
+ # the provider manager requires the token persistence manager, which
+ # requires the token provider manager).
+ if self._persistence_manager is None:
+ self._persistence_manager = persistence.PersistenceManager()
+ return self._persistence_manager
+
+ def unique_id(self, token_id):
+ """Return a unique ID for a token.
+
+ The returned value is useful as the primary key of a database table,
+ memcache store, or other lookup table.
+
+ :returns: Given a PKI token, returns it's hashed value. Otherwise,
+ returns the passed-in value (such as a UUID token ID or an
+ existing hash).
+ """
+ return cms.cms_hash_token(token_id, mode=CONF.token.hash_algorithm)
+
+ def _create_token(self, token_id, token_data):
+ try:
+ if isinstance(token_data['expires'], six.string_types):
+ token_data['expires'] = timeutils.normalize_time(
+ timeutils.parse_isotime(token_data['expires']))
+ self._persistence.create_token(token_id, token_data)
+ except Exception:
+ exc_info = sys.exc_info()
+ # an identical token may have been created already.
+ # if so, return the token_data as it is also identical
+ try:
+ self._persistence.get_token(token_id)
+ except exception.TokenNotFound:
+ six.reraise(*exc_info)
+
+ def validate_token(self, token_id, belongs_to=None):
+ unique_id = self.unique_id(token_id)
+ # NOTE(morganfainberg): Ensure we never use the long-form token_id
+ # (PKI) as part of the cache_key.
+ token = self._validate_token(unique_id)
+ self._token_belongs_to(token, belongs_to)
+ self._is_valid_token(token)
+ return token
+
+ def check_revocation_v2(self, token):
+ try:
+ token_data = token['access']
+ except KeyError:
+ raise exception.TokenNotFound(_('Failed to validate token'))
+
+ token_values = self.revoke_api.model.build_token_values_v2(
+ token_data, CONF.identity.default_domain_id)
+ self.revoke_api.check_token(token_values)
+
+ def validate_v2_token(self, token_id, belongs_to=None):
+ unique_id = self.unique_id(token_id)
+ if self._needs_persistence:
+ # NOTE(morganfainberg): Ensure we never use the long-form token_id
+ # (PKI) as part of the cache_key.
+ token_ref = self._persistence.get_token(unique_id)
+ else:
+ token_ref = token_id
+ token = self._validate_v2_token(token_ref)
+ self._token_belongs_to(token, belongs_to)
+ self._is_valid_token(token)
+ return token
+
+ def check_revocation_v3(self, token):
+ try:
+ token_data = token['token']
+ except KeyError:
+ raise exception.TokenNotFound(_('Failed to validate token'))
+ token_values = self.revoke_api.model.build_token_values(token_data)
+ self.revoke_api.check_token(token_values)
+
+ def check_revocation(self, token):
+ version = self.driver.get_token_version(token)
+ if version == V2:
+ return self.check_revocation_v2(token)
+ else:
+ return self.check_revocation_v3(token)
+
+ def validate_v3_token(self, token_id):
+ unique_id = self.unique_id(token_id)
+ # NOTE(lbragstad): Only go to persistent storage if we have a token to
+ # fetch from the backend. If the Fernet token provider is being used
+ # this step isn't necessary. The Fernet token reference is persisted in
+ # the token_id, so in this case set the token_ref as the identifier of
+ # the token.
+ if not self._needs_persistence:
+ token_ref = token_id
+ else:
+ # NOTE(morganfainberg): Ensure we never use the long-form token_id
+ # (PKI) as part of the cache_key.
+ token_ref = self._persistence.get_token(unique_id)
+ token = self._validate_v3_token(token_ref)
+ self._is_valid_token(token)
+ return token
+
+ @MEMOIZE
+ def _validate_token(self, token_id):
+ if not self._needs_persistence:
+ return self.driver.validate_v3_token(token_id)
+ token_ref = self._persistence.get_token(token_id)
+ version = self.driver.get_token_version(token_ref)
+ if version == self.V3:
+ return self.driver.validate_v3_token(token_ref)
+ elif version == self.V2:
+ return self.driver.validate_v2_token(token_ref)
+ raise exception.UnsupportedTokenVersionException()
+
+ @MEMOIZE
+ def _validate_v2_token(self, token_id):
+ return self.driver.validate_v2_token(token_id)
+
+ @MEMOIZE
+ def _validate_v3_token(self, token_id):
+ return self.driver.validate_v3_token(token_id)
+
+ def _is_valid_token(self, token):
+ """Verify the token is valid format and has not expired."""
+
+ current_time = timeutils.normalize_time(timeutils.utcnow())
+
+ try:
+ # Get the data we need from the correct location (V2 and V3 tokens
+ # differ in structure, Try V3 first, fall back to V2 second)
+ token_data = token.get('token', token.get('access'))
+ expires_at = token_data.get('expires_at',
+ token_data.get('expires'))
+ if not expires_at:
+ expires_at = token_data['token']['expires']
+ expiry = timeutils.normalize_time(
+ timeutils.parse_isotime(expires_at))
+ except Exception:
+ LOG.exception(_LE('Unexpected error or malformed token '
+ 'determining token expiry: %s'), token)
+ raise exception.TokenNotFound(_('Failed to validate token'))
+
+ if current_time < expiry:
+ self.check_revocation(token)
+ # Token has not expired and has not been revoked.
+ return None
+ else:
+ raise exception.TokenNotFound(_('Failed to validate token'))
+
+ def _token_belongs_to(self, token, belongs_to):
+ """Check if the token belongs to the right tenant.
+
+ This is only used on v2 tokens. The structural validity of the token
+ will have already been checked before this method is called.
+
+ """
+ if belongs_to:
+ token_data = token['access']['token']
+ if ('tenant' not in token_data or
+ token_data['tenant']['id'] != belongs_to):
+ raise exception.Unauthorized()
+
+ def issue_v2_token(self, token_ref, roles_ref=None, catalog_ref=None):
+ token_id, token_data = self.driver.issue_v2_token(
+ token_ref, roles_ref, catalog_ref)
+
+ if self._needs_persistence:
+ data = dict(key=token_id,
+ id=token_id,
+ expires=token_data['access']['token']['expires'],
+ user=token_ref['user'],
+ tenant=token_ref['tenant'],
+ metadata=token_ref['metadata'],
+ token_data=token_data,
+ bind=token_ref.get('bind'),
+ trust_id=token_ref['metadata'].get('trust_id'),
+ token_version=self.V2)
+ self._create_token(token_id, data)
+
+ return token_id, token_data
+
+ def issue_v3_token(self, user_id, method_names, expires_at=None,
+ project_id=None, domain_id=None, auth_context=None,
+ trust=None, metadata_ref=None, include_catalog=True,
+ parent_audit_id=None):
+ token_id, token_data = self.driver.issue_v3_token(
+ user_id, method_names, expires_at, project_id, domain_id,
+ auth_context, trust, metadata_ref, include_catalog,
+ parent_audit_id)
+
+ if metadata_ref is None:
+ metadata_ref = {}
+
+ if 'project' in token_data['token']:
+ # project-scoped token, fill in the v2 token data
+ # all we care are the role IDs
+
+ # FIXME(gyee): is there really a need to store roles in metadata?
+ role_ids = [r['id'] for r in token_data['token']['roles']]
+ metadata_ref = {'roles': role_ids}
+
+ if trust:
+ metadata_ref.setdefault('trust_id', trust['id'])
+ metadata_ref.setdefault('trustee_user_id',
+ trust['trustee_user_id'])
+
+ data = dict(key=token_id,
+ id=token_id,
+ expires=token_data['token']['expires_at'],
+ user=token_data['token']['user'],
+ tenant=token_data['token'].get('project'),
+ metadata=metadata_ref,
+ token_data=token_data,
+ trust_id=trust['id'] if trust else None,
+ token_version=self.V3)
+ if self._needs_persistence:
+ self._create_token(token_id, data)
+ return token_id, token_data
+
+ 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._validate_token.invalidate(self, token_id)
+ self._validate_v2_token.invalidate(self, token_id)
+ self._validate_v3_token.invalidate(self, token_id)
+
+ def revoke_token(self, token_id, revoke_chain=False):
+ revoke_by_expires = False
+ project_id = None
+ domain_id = None
+
+ token_ref = token_model.KeystoneToken(
+ token_id=token_id,
+ token_data=self.validate_token(token_id))
+
+ user_id = token_ref.user_id
+ expires_at = token_ref.expires
+ audit_id = token_ref.audit_id
+ audit_chain_id = token_ref.audit_chain_id
+ if token_ref.project_scoped:
+ project_id = token_ref.project_id
+ if token_ref.domain_scoped:
+ domain_id = token_ref.domain_id
+
+ if audit_id is None and not revoke_chain:
+ LOG.debug('Received token with no audit_id.')
+ revoke_by_expires = True
+
+ if audit_chain_id is None and revoke_chain:
+ LOG.debug('Received token with no audit_chain_id.')
+ revoke_by_expires = True
+
+ if revoke_by_expires:
+ self.revoke_api.revoke_by_expiration(user_id, expires_at,
+ project_id=project_id,
+ domain_id=domain_id)
+ elif revoke_chain:
+ self.revoke_api.revoke_by_audit_chain_id(audit_chain_id,
+ project_id=project_id,
+ domain_id=domain_id)
+ else:
+ self.revoke_api.revoke_by_audit_id(audit_id)
+
+ if CONF.token.revoke_by_id and self._needs_persistence:
+ self._persistence.delete_token(token_id=token_id)
+
+ def list_revoked_tokens(self):
+ return self._persistence.list_revoked_tokens()
+
+ def _trust_deleted_event_callback(self, service, resource_type, operation,
+ payload):
+ if CONF.token.revoke_by_id:
+ trust_id = payload['resource_info']
+ trust = self.trust_api.get_trust(trust_id, deleted=True)
+ self._persistence.delete_tokens(user_id=trust['trustor_user_id'],
+ trust_id=trust_id)
+
+ def _delete_user_tokens_callback(self, service, resource_type, operation,
+ payload):
+ if CONF.token.revoke_by_id:
+ user_id = payload['resource_info']
+ self._persistence.delete_tokens_for_user(user_id)
+
+ def _delete_domain_tokens_callback(self, service, resource_type,
+ operation, payload):
+ if CONF.token.revoke_by_id:
+ domain_id = payload['resource_info']
+ self._persistence.delete_tokens_for_domain(domain_id=domain_id)
+
+ def _delete_user_project_tokens_callback(self, service, resource_type,
+ operation, payload):
+ if CONF.token.revoke_by_id:
+ user_id = payload['resource_info']['user_id']
+ project_id = payload['resource_info']['project_id']
+ self._persistence.delete_tokens_for_user(user_id=user_id,
+ project_id=project_id)
+
+ def _delete_project_tokens_callback(self, service, resource_type,
+ operation, payload):
+ if CONF.token.revoke_by_id:
+ project_id = payload['resource_info']
+ self._persistence.delete_tokens_for_users(
+ self.assignment_api.list_user_ids_for_project(project_id),
+ project_id=project_id)
+
+ def _delete_user_oauth_consumer_tokens_callback(self, service,
+ resource_type, operation,
+ payload):
+ if CONF.token.revoke_by_id:
+ user_id = payload['resource_info']['user_id']
+ consumer_id = payload['resource_info']['consumer_id']
+ self._persistence.delete_tokens(user_id=user_id,
+ consumer_id=consumer_id)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class Provider(object):
+ """Interface description for a Token provider."""
+
+ @abc.abstractmethod
+ def needs_persistence(self):
+ """Determine if the token should be persisted.
+
+ If the token provider requires that the token be persisted to a
+ backend this should return True, otherwise return False.
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def get_token_version(self, token_data):
+ """Return the version of the given token data.
+
+ If the given token data is unrecognizable,
+ UnsupportedTokenVersionException is raised.
+
+ :param token_data: token_data
+ :type token_data: dict
+ :returns: token version string
+ :raises: keystone.token.provider.UnsupportedTokenVersionException
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def issue_v2_token(self, token_ref, roles_ref=None, catalog_ref=None):
+ """Issue a V2 token.
+
+ :param token_ref: token data to generate token from
+ :type token_ref: dict
+ :param roles_ref: optional roles list
+ :type roles_ref: dict
+ :param catalog_ref: optional catalog information
+ :type catalog_ref: dict
+ :returns: (token_id, token_data)
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def issue_v3_token(self, user_id, method_names, expires_at=None,
+ project_id=None, domain_id=None, auth_context=None,
+ trust=None, metadata_ref=None, include_catalog=True,
+ parent_audit_id=None):
+ """Issue a V3 Token.
+
+ :param user_id: identity of the user
+ :type user_id: string
+ :param method_names: names of authentication methods
+ :type method_names: list
+ :param expires_at: optional time the token will expire
+ :type expires_at: string
+ :param project_id: optional project identity
+ :type project_id: string
+ :param domain_id: optional domain identity
+ :type domain_id: string
+ :param auth_context: optional context from the authorization plugins
+ :type auth_context: dict
+ :param trust: optional trust reference
+ :type trust: dict
+ :param metadata_ref: optional metadata reference
+ :type metadata_ref: dict
+ :param include_catalog: optional, include the catalog in token data
+ :type include_catalog: boolean
+ :param parent_audit_id: optional, the audit id of the parent token
+ :type parent_audit_id: string
+ :returns: (token_id, token_data)
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def validate_v2_token(self, token_ref):
+ """Validate the given V2 token and return the token data.
+
+ Must raise Unauthorized exception if unable to validate token.
+
+ :param token_ref: the token reference
+ :type token_ref: dict
+ :returns: token data
+ :raises: keystone.exception.TokenNotFound
+
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def validate_v3_token(self, token_ref):
+ """Validate the given V3 token and return the token_data.
+
+ :param token_ref: the token reference
+ :type token_ref: dict
+ :returns: token data
+ :raises: keystone.exception.TokenNotFound
+ """
+ raise exception.NotImplemented() # pragma: no cover
+
+ @abc.abstractmethod
+ def _get_token_id(self, token_data):
+ """Generate the token_id based upon the data in token_data.
+
+ :param token_data: token information
+ :type token_data: dict
+ returns: token identifier
+ """
+ raise exception.NotImplemented() # pragma: no cover