¡@

Home 

OpenStack Study: kvs.py

OpenStack Index

**** CubicPower OpenStack Study ****

# -*- coding: utf-8 -*-

# Copyright 2013 Metacloud, Inc.

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

from __future__ import absolute_import

import copy

import six

from keystone.common import kvs

from keystone import config

from keystone import exception

from keystone.openstack.common.gettextutils import _

from keystone.openstack.common import log

from keystone.openstack.common import timeutils

from keystone import token

CONF = config.CONF

LOG = log.getLogger(__name__)

**** CubicPower OpenStack Study ****

class Token(token.Driver):

"""KeyValueStore backend for tokens.

This is the base implementation for any/all key-value-stores (e.g.

memcached) for the Token backend. It is recommended to only use the base

in-memory implementation for testing purposes.

"""

revocation_key = 'revocation-list'

kvs_backend = 'openstack.kvs.Memory'

**** CubicPower OpenStack Study ****

    def __init__(self, backing_store=None, **kwargs):

        super(Token, self).__init__()

        self._store = kvs.get_key_value_store('token-driver')

        if backing_store is not None:

            self.kvs_backend = backing_store

        self._store.configure(backing_store=self.kvs_backend, **kwargs)

        if self.__class__ == Token:

            # NOTE(morganfainberg): Only warn if the base KVS implementation

            # is instantiated.

            LOG.warn(_('It is recommended to only use the base '

                       'key-value-store implementation for the token driver '

                       'for testing purposes.  '

                       'Please use keystone.token.backends.memcache.Token '

                       'or keystone.token.backends.sql.Token instead.'))

**** CubicPower OpenStack Study ****

    def _prefix_token_id(self, token_id):

        return 'token-%s' % token_id.encode('utf-8')

**** CubicPower OpenStack Study ****

    def _prefix_user_id(self, user_id):

        return 'usertokens-%s' % user_id.encode('utf-8')

**** CubicPower OpenStack Study ****

    def _get_key_or_default(self, key, default=None):

        try:

            return self._store.get(key)

        except exception.NotFound:

            return default

**** CubicPower OpenStack Study ****

    def _get_key(self, key):

        return self._store.get(key)

**** CubicPower OpenStack Study ****

    def _set_key(self, key, value, lock=None):

        self._store.set(key, value, lock)

**** CubicPower OpenStack Study ****

    def _delete_key(self, key):

        return self._store.delete(key)

**** CubicPower OpenStack Study ****

    def get_token(self, token_id):

        ptk = self._prefix_token_id(token_id)

        try:

            token_ref = self._get_key(ptk)

        except exception.NotFound:

            raise exception.TokenNotFound(token_id=token_id)

        return token_ref

**** CubicPower OpenStack Study ****

    def create_token(self, token_id, data):

        """Create a token by id and data.

        It is assumed the caller has performed data validation on the "data"

        parameter.

        """

        data_copy = copy.deepcopy(data)

        ptk = self._prefix_token_id(token_id)

        if not data_copy.get('expires'):

            data_copy['expires'] = token.default_expire_time()

        if not data_copy.get('user_id'):

            data_copy['user_id'] = data_copy['user']['id']

        # NOTE(morganfainberg): for ease of manipulating the data without

        # concern about the backend, always store the value(s) in the

        # index as the isotime (string) version so this is where the string is

        # built.

        expires_str = timeutils.isotime(data_copy['expires'], subsecond=True)

        self._set_key(ptk, data_copy)

        user_id = data['user']['id']

        user_key = self._prefix_user_id(user_id)

        self._update_user_token_list(user_key, token_id, expires_str)

        if CONF.trust.enabled and data.get('trust_id'):

            # NOTE(morganfainberg): If trusts are enabled and this is a trust

            # scoped token, we add the token to the trustee list as well.  This

            # allows password changes of the trustee to also expire the token.

            # There is no harm in placing the token in multiple lists, as

            # _list_tokens is smart enough to handle almost any case of

            # valid/invalid/expired for a given token.

            token_data = data_copy['token_data']

            if data_copy['token_version'] == token.provider.V2:

                trustee_user_id = token_data['access']['trust'][

                    'trustee_user_id']

            elif data_copy['token_version'] == token.provider.V3:

                trustee_user_id = token_data['OS-TRUST:trust'][

                    'trustee_user_id']

            else:

                raise token.provider.UnsupportedTokenVersionException(

                    _('Unknown token version %s') %

                    data_copy.get('token_version'))

            trustee_key = self._prefix_user_id(trustee_user_id)

            self._update_user_token_list(trustee_key, token_id, expires_str)

        return data_copy

**** CubicPower OpenStack Study ****

    def _get_user_token_list_with_expiry(self, user_key):

        """Return a list of tuples in the format (token_id, token_expiry) for

        the user_key.

        """

        return self._get_key_or_default(user_key, default=[])

**** CubicPower OpenStack Study ****

    def _get_user_token_list(self, user_key):

        """Return a list of token_ids for the user_key."""

        token_list = self._get_user_token_list_with_expiry(user_key)

        # Each element is a tuple of (token_id, token_expiry). Most code does

        # not care about the expiry, it is stripped out and only a

        # list of token_ids are returned.

        return [t[0] for t in token_list]

**** CubicPower OpenStack Study ****

    def _update_user_token_list(self, user_key, token_id, expires_isotime_str):

        current_time = self._get_current_time()

        revoked_token_list = set([t['id'] for t in

                                  self.list_revoked_tokens()])

        with self._store.get_lock(user_key) as lock:

            filtered_list = []

            token_list = self._get_user_token_list_with_expiry(user_key)

            for item in token_list:

                try:

                    item_id, expires = self._format_token_index_item(item)

                except (ValueError, TypeError):

                    # NOTE(morganfainberg): Skip on expected errors

                    # possibilities from the `_format_token_index_item` method.

                    continue

                if expires < current_time:

                    LOG.debug(_('Token `%(token_id)s` is expired, removing '

                                'from `%(user_key)s`.'),

                              {'token_id': item_id, 'user_key': user_key})

                    continue

                if item_id in revoked_token_list:

                    # NOTE(morganfainberg): If the token has been revoked, it

                    # can safely be removed from this list.  This helps to keep

                    # the user_token_list as reasonably small as possible.

                    LOG.debug(_('Token `%(token_id)s` is revoked, removing '

                                'from `%(user_key)s`.'),

                              {'token_id': item_id, 'user_key': user_key})

                    continue

                filtered_list.append(item)

            filtered_list.append((token_id, expires_isotime_str))

            self._set_key(user_key, filtered_list, lock)

            return filtered_list

**** CubicPower OpenStack Study ****

    def _get_current_time(self):

        return timeutils.normalize_time(timeutils.utcnow())

**** CubicPower OpenStack Study ****

    def _add_to_revocation_list(self, data, lock):

        filtered_list = []

        revoked_token_data = {}

        current_time = self._get_current_time()

        expires = data['expires']

        if isinstance(expires, six.string_types):

            expires = timeutils.parse_isotime(expires)

        expires = timeutils.normalize_time(expires)

        if expires < current_time:

            LOG.warning(_('Token `%s` is expired, not adding to the '

                          'revocation list.'), data['id'])

            return

        revoked_token_data['expires'] = timeutils.isotime(expires,

                                                          subsecond=True)

        revoked_token_data['id'] = data['id']

        token_list = self._get_key_or_default(self.revocation_key, default=[])

        if not isinstance(token_list, list):

            # NOTE(morganfainberg): In the case that the revocation list is not

            # in a format we understand, reinitialize it. This is an attempt to

            # not allow the revocation list to be completely broken if

            # somehow the key is changed outside of keystone (e.g. memcache

            # that is shared by multiple applications). Logging occurs at error

            # level so that the cloud administrators have some awareness that

            # the revocation_list needed to be cleared out. In all, this should

            # be recoverable. Keystone cannot control external applications

            # from changing a key in some backends, however, it is possible to

            # gracefully handle and notify of this event.

            LOG.error(_('Reinitializing revocation list due to error '

                        'in loading revocation list from backend.  '

                        'Expected `list` type got `%(type)s`. Old '

                        'revocation list data: %(list)r'),

                      {'type': type(token_list), 'list': token_list})

            token_list = []

        # NOTE(morganfainberg): on revocation, cleanup the expired entries, try

        # to keep the list of tokens revoked at the minimum.

        for token_data in token_list:

            try:

                expires_at = timeutils.normalize_time(

                    timeutils.parse_isotime(token_data['expires']))

            except ValueError:

                LOG.warning(_('Removing `%s` from revocation list due to '

                              'invalid expires data in revocation list.'),

                            token_data.get('id', 'INVALID_TOKEN_DATA'))

                continue

            if expires_at > current_time:

                filtered_list.append(token_data)

        filtered_list.append(revoked_token_data)

        self._set_key(self.revocation_key, filtered_list, lock)

**** CubicPower OpenStack Study ****

    def delete_token(self, token_id):

        # Test for existence

        with self._store.get_lock(self.revocation_key) as lock:

            data = self.get_token(token_id)

            ptk = self._prefix_token_id(token_id)

            result = self._delete_key(ptk)

            self._add_to_revocation_list(data, lock)

        return result

**** CubicPower OpenStack Study ****

    def delete_tokens(self, user_id, tenant_id=None, trust_id=None,

                      consumer_id=None):

        return super(Token, self).delete_tokens(

            user_id=user_id,

            tenant_id=tenant_id,

            trust_id=trust_id,

            consumer_id=consumer_id,

        )

**** CubicPower OpenStack Study ****

    def _format_token_index_item(self, item):

        try:

            token_id, expires = item

        except (TypeError, ValueError):

            LOG.debug(_('Invalid token entry expected tuple of '

                        '`()` got: `%(item)r`'),

                      dict(item=item))

            raise

        try:

            expires = timeutils.normalize_time(

                timeutils.parse_isotime(expires))

        except ValueError:

            LOG.debug(_('Invalid expires time on token `%(token_id)s`:'

                        ' %(expires)r'),

                      dict(token_id=token_id, expires=expires))

            raise

        return token_id, expires

**** CubicPower OpenStack Study ****

    def _token_match_tenant(self, token_ref, tenant_id):

        if token_ref.get('tenant'):

            return token_ref['tenant'].get('id') == tenant_id

        return False

**** CubicPower OpenStack Study ****

    def _token_match_trust(self, token_ref, trust_id):

        if not token_ref.get('trust_id'):

            return False

        return token_ref['trust_id'] == trust_id

**** CubicPower OpenStack Study ****

    def _token_match_consumer(self, token_ref, consumer_id):

        try:

            oauth = token_ref['token_data']['token']['OS-OAUTH1']

            return oauth.get('consumer_id') == consumer_id

        except KeyError:

            return False

**** CubicPower OpenStack Study ****

    def _list_tokens(self, user_id, tenant_id=None, trust_id=None,

                     consumer_id=None):

        # This function is used to generate the list of tokens that should be

        # revoked when revoking by token identifiers.  This approach will be

        # deprecated soon, probably in the Juno release.  Setting revoke_by_id

        # to False indicates that this kind of recording should not be

        # performed.  In order to test the revocation events, tokens shouldn't

        # be deleted from the backends.  This check ensures that tokens are

        # still recorded.

        if not CONF.token.revoke_by_id:

            return []

        tokens = []

        user_key = self._prefix_user_id(user_id)

        token_list = self._get_user_token_list_with_expiry(user_key)

        current_time = self._get_current_time()

        for item in token_list:

            try:

                token_id, expires = self._format_token_index_item(item)

            except (TypeError, ValueError):

                # NOTE(morganfainberg): Skip on expected error possibilities

                # from the `_format_token_index_item` method.

                continue

            if expires < current_time:

                continue

            try:

                token_ref = self.get_token(token_id)

            except exception.TokenNotFound:

                # NOTE(morganfainberg): Token doesn't exist, skip it.

                continue

            if token_ref:

                if tenant_id is not None:

                    if not self._token_match_tenant(token_ref, tenant_id):

                        continue

                if trust_id is not None:

                    if not self._token_match_trust(token_ref, trust_id):

                        continue

                if consumer_id is not None:

                    if not self._token_match_consumer(token_ref, consumer_id):

                        continue

                tokens.append(token_id)

        return tokens

**** CubicPower OpenStack Study ****

    def list_revoked_tokens(self):

        revoked_token_list = self._get_key_or_default(self.revocation_key,

                                                      default=[])

        if isinstance(revoked_token_list, list):

            return revoked_token_list

        return []

**** CubicPower OpenStack Study ****

    def flush_expired_tokens(self):

        """Archive or delete tokens that have expired."""

        raise exception.NotImplemented()