Don't automatically enable revocation events.

Cuts any direct imports of revoke.model as that
triggers the dependency registration.

In order to fix a dependecy resolution issue, changes the syncronziation to
using the same sort of chaching mechanism as the other drivers.

Adds the ability to Lazy activate providers for future or optional dependency
resolution.

Closes-Bug: 1291099
Related-Bug: 1292283
Change-Id: I0db36b295c2040ec1fb248cf75dc55c44c059211
This commit is contained in:
Adam Young 2014-03-13 21:11:54 -04:00 committed by Morgan Fainberg
parent 358674aada
commit 7c9746c49b
12 changed files with 128 additions and 133 deletions

View File

@ -110,40 +110,40 @@
# Auto-delete queues in amqp. (boolean value)
#amqp_auto_delete=false
# Size of RPC connection pool (integer value)
# Size of RPC connection pool. (integer value)
#rpc_conn_pool_size=30
# Modules of exceptions that are permitted to be recreatedupon
# receiving exception data from an rpc call. (list value)
# Modules of exceptions that are permitted to be recreated
# upon receiving exception data from an rpc call. (list value)
#allowed_rpc_exception_modules=oslo.messaging.exceptions,nova.exception,cinder.exception,exceptions
# Qpid broker hostname (string value)
# Qpid broker hostname. (string value)
#qpid_hostname=localhost
# Qpid broker port (integer value)
# Qpid broker port. (integer value)
#qpid_port=5672
# Qpid HA cluster host:port pairs (list value)
# Qpid HA cluster host:port pairs. (list value)
#qpid_hosts=$qpid_hostname:$qpid_port
# Username for Qpid connection (string value)
# Username for Qpid connection. (string value)
#qpid_username=
# Password for Qpid connection (string value)
# Password for Qpid connection. (string value)
#qpid_password=
# Space separated list of SASL mechanisms to use for auth
# Space separated list of SASL mechanisms to use for auth.
# (string value)
#qpid_sasl_mechanisms=
# Seconds between connection keepalive heartbeats (integer
# Seconds between connection keepalive heartbeats. (integer
# value)
#qpid_heartbeat=60
# Transport to use, either 'tcp' or 'ssl' (string value)
# Transport to use, either 'tcp' or 'ssl'. (string value)
#qpid_protocol=tcp
# Disable Nagle algorithm (boolean value)
# Whether to disable the Nagle algorithm. (boolean value)
#qpid_tcp_nodelay=true
# The qpid topology version to use. Version 1 is what was
@ -156,52 +156,59 @@
# SSL version to use (valid only if SSL enabled). valid values
# are TLSv1, SSLv23 and SSLv3. SSLv2 may be available on some
# distributions (string value)
# distributions. (string value)
#kombu_ssl_version=
# SSL key file (valid only if SSL enabled) (string value)
# SSL key file (valid only if SSL enabled). (string value)
#kombu_ssl_keyfile=
# SSL cert file (valid only if SSL enabled) (string value)
# SSL cert file (valid only if SSL enabled). (string value)
#kombu_ssl_certfile=
# SSL certification authority file (valid only if SSL enabled)
# (string value)
# SSL certification authority file (valid only if SSL
# enabled). (string value)
#kombu_ssl_ca_certs=
# The RabbitMQ broker address where a single node is used
# How long to wait before reconnecting in response to an AMQP
# consumer cancel notification. (floating point value)
#kombu_reconnect_delay=1.0
# The RabbitMQ broker address where a single node is used.
# (string value)
#rabbit_host=localhost
# The RabbitMQ broker port where a single node is used
# The RabbitMQ broker port where a single node is used.
# (integer value)
#rabbit_port=5672
# RabbitMQ HA cluster host:port pairs (list value)
# RabbitMQ HA cluster host:port pairs. (list value)
#rabbit_hosts=$rabbit_host:$rabbit_port
# Connect over SSL for RabbitMQ (boolean value)
# Connect over SSL for RabbitMQ. (boolean value)
#rabbit_use_ssl=false
# The RabbitMQ userid (string value)
# The RabbitMQ userid. (string value)
#rabbit_userid=guest
# The RabbitMQ password (string value)
# The RabbitMQ password. (string value)
#rabbit_password=guest
# The RabbitMQ virtual host (string value)
# the RabbitMQ login method (string value)
#rabbit_login_method=AMQPLAIN
# The RabbitMQ virtual host. (string value)
#rabbit_virtual_host=/
# How frequently to retry connecting with RabbitMQ (integer
# How frequently to retry connecting with RabbitMQ. (integer
# value)
#rabbit_retry_interval=1
# How long to backoff for between retries when connecting to
# RabbitMQ (integer value)
# RabbitMQ. (integer value)
#rabbit_retry_backoff=2
# Maximum number of RabbitMQ connection retries. Default is 0
# (infinite retry count) (integer value)
# (infinite retry count). (integer value)
#rabbit_max_retries=0
# Use HA queues in RabbitMQ (x-ha-policy: all). If you change
@ -209,7 +216,7 @@
# value)
#rabbit_ha_queues=false
# If passed, use a fake RabbitMQ provider (boolean value)
# If passed, use a fake RabbitMQ provider. (boolean value)
#fake_rabbit=false
# ZeroMQ bind address. Should be a wildcard (*), an ethernet
@ -217,20 +224,20 @@
# to this address. (string value)
#rpc_zmq_bind_address=*
# MatchMaker driver (string value)
# MatchMaker driver. (string value)
#rpc_zmq_matchmaker=oslo.messaging._drivers.matchmaker.MatchMakerLocalhost
# ZeroMQ receiver listening port (integer value)
# ZeroMQ receiver listening port. (integer value)
#rpc_zmq_port=9501
# Number of ZeroMQ contexts, defaults to 1 (integer value)
# Number of ZeroMQ contexts, defaults to 1. (integer value)
#rpc_zmq_contexts=1
# Maximum number of ingress messages to locally buffer per
# topic. Default is unlimited. (integer value)
#rpc_zmq_topic_backlog=<None>
# Directory for holding IPC sockets (string value)
# Directory for holding IPC sockets. (string value)
#rpc_zmq_ipc_dir=/var/run/openstack
# Name of this node. Must be a valid hostname, FQDN, or IP
@ -242,33 +249,33 @@
# by impl_zmq. (integer value)
#rpc_cast_timeout=30
# Heartbeat frequency (integer value)
# Heartbeat frequency. (integer value)
#matchmaker_heartbeat_freq=300
# Heartbeat time-to-live. (integer value)
#matchmaker_heartbeat_ttl=600
# Host to locate redis (string value)
# Host to locate redis. (string value)
#host=127.0.0.1
# Use this port to connect to redis host. (integer value)
#port=6379
# Password for Redis server. (optional) (string value)
# Password for Redis server (optional). (string value)
#password=<None>
# Size of RPC greenthread pool (integer value)
# Size of RPC greenthread pool. (integer value)
#rpc_thread_pool_size=64
# Driver or drivers to handle sending notifications (multi
# Driver or drivers to handle sending notifications. (multi
# valued)
#notification_driver=
# AMQP topic used for OpenStack notifications (list value)
# AMQP topic used for OpenStack notifications. (list value)
# Deprecated group/name - [rpc_notifier2]/topics
#notification_topics=notifications
# Seconds to wait for a response from a call (integer value)
# Seconds to wait for a response from a call. (integer value)
#rpc_response_timeout=60
# A URL representing the messaging driver to use and its full
@ -1037,7 +1044,7 @@
# Options defined in oslo.messaging
#
# Matchmaker ring file (JSON) (string value)
# Matchmaker ring file (JSON). (string value)
# Deprecated group/name - [DEFAULT]/matchmaker_ringfile
#ringfile=/etc/oslo/matchmaker_ring.json
@ -1126,6 +1133,10 @@
# backend. (integer value)
#expiration_buffer=1800
# Toggle for revocation event cacheing. This has no effect
# unless global caching is enabled. (boolean value)
#caching=true
[signing]
@ -1200,23 +1211,6 @@
#cert_subject=/C=US/ST=Unset/L=Unset/O=Unset/CN=localhost
#
# Options defined in keystone.openstack.common.sslutils
#
# CA certificate file to use to verify connecting clients
# (string value)
#ca_file=<None>
# Certificate file to use when starting the server securely
# (string value)
#cert_file=<None>
# Private key file to use when starting the server securely
# (string value)
#key_file=<None>
[stats]
#
@ -1260,7 +1254,8 @@
# global caching is enabled. (boolean value)
#caching=true
# Time to cache the revocation list (in seconds). This has no
# Time to cache the revocation list and the revocation events
# if revoke extension is enabled (in seconds). This has no
# effect unless global and token caching are enabled. (integer
# value)
#revocation_cache_time=3600

View File

@ -180,7 +180,8 @@ FILE_OPTIONS = {
help='Toggle for token system cacheing. This has no '
'effect unless global caching is enabled.'),
cfg.IntOpt('revocation_cache_time', default=3600,
help='Time to cache the revocation list (in seconds). '
help='Time to cache the revocation list and the revocation '
'events if revoke extension is enabled (in seconds). '
'This has no effect unless global and token '
'caching are enabled.'),
cfg.IntOpt('cache_time', default=None,
@ -205,7 +206,9 @@ FILE_OPTIONS = {
help='This value (calculated in seconds) is added to token '
'expiration before a revocation event may be removed '
'from the backend.'),
cfg.BoolOpt('caching', default=True,
help='Toggle for revocation event cacheing. This has no '
'effect unless global caching is enabled.'),
],
'cache': [
cfg.StrOpt('config_prefix', default='cache.keystone',

View File

@ -27,12 +27,14 @@ See also:
import six
from keystone import notifications
from keystone.openstack.common.gettextutils import _ # flake8: noqa
REGISTRY = {}
_future_dependencies = {}
_future_optionals = {}
_factories = {}
class UnresolvableDependencyException(Exception):
@ -110,8 +112,8 @@ def provider(name):
return __wrapped_init__
cls.__init__ = wrapped(cls.__init__)
_factories[name] = cls
return cls
return wrapper
@ -221,6 +223,7 @@ def resolve_future_dependencies(provider_name=None):
the optional argument is used internally, and should be treated as an
implementation detail.
"""
new_providers = dict()
if provider_name:
# A provider was registered, so take care of any objects depending on
# it.
@ -234,25 +237,37 @@ def resolve_future_dependencies(provider_name=None):
# Resolve optional dependencies, sets the attribute to None if there's no
# provider registered.
for dependency, targets in six.iteritems(_future_optionals):
for dependency, targets in six.iteritems(_future_optionals.copy()):
provider = REGISTRY.get(dependency)
if provider is None:
factory = _factories.get(dependency)
if factory:
provider = factory()
REGISTRY[dependency] = provider
new_providers[dependency] = provider
for target in targets:
setattr(target, dependency, provider)
_future_optionals.clear()
# Resolve optional dependencies, raises UnresolvableDependencyException if
# Resolve future dependencies, raises UnresolvableDependencyException if
# there's no provider registered.
try:
for dependency, targets in six.iteritems(_future_dependencies):
for dependency, targets in six.iteritems(_future_dependencies.copy()):
if dependency not in REGISTRY:
raise UnresolvableDependencyException(dependency)
# a Class was registered that could fulfill the dependency, but
# it has not yet been initialized.
factory = _factories.get(dependency)
if factory:
provider = factory()
REGISTRY[dependency] = provider
new_providers[dependency] = provider
else:
raise UnresolvableDependencyException(dependency)
for target in targets:
setattr(target, dependency, REGISTRY[dependency])
finally:
_future_dependencies.clear()
return new_providers
def reset():
"""Reset the registry of providers.

View File

@ -28,6 +28,7 @@ import six
from keystone.common import config
from keystone import exception
from keystone.openstack.common.gettextutils import _ # flake8: noqa
from keystone.openstack.common import importutils
from keystone.openstack.common import log

View File

@ -15,9 +15,9 @@ import datetime
import six
from keystone.common import cache
from keystone.common import dependency
from keystone.common import extension
from keystone.common import kvs
from keystone.common import manager
from keystone import config
from keystone.contrib.revoke import model
@ -50,6 +50,10 @@ EXTENSION_DATA = {
extension.register_admin_extension(EXTENSION_DATA['alias'], EXTENSION_DATA)
extension.register_public_extension(EXTENSION_DATA['alias'], EXTENSION_DATA)
SHOULD_CACHE = cache.should_cache_fn('revoke')
# TODO(ayoung): migrate from the token section
REVOCATION_CACHE_EXPIRATION_TIME = lambda: CONF.token.revocation_cache_time
def revoked_before_cutoff_time():
expire_delta = datetime.timedelta(
@ -58,34 +62,6 @@ def revoked_before_cutoff_time():
return oldest
_TREE_KEY = 'os-revoke-tree'
_KVS_BACKEND = 'openstack.kvs.Memory'
class _Cache(object):
def __init__(self, **kwargs):
self._store = kvs.get_key_value_store('os-revoke-synchonize')
self._store.configure(backing_store=_KVS_BACKEND, **kwargs)
self._last_fetch = None
self._current_events = []
self.revoke_map = model.RevokeTree()
def synchronize_revoke_map(self, driver):
cutoff = revoked_before_cutoff_time()
with self._store.get_lock(_TREE_KEY):
for e in self._current_events:
if e.revoked_at < cutoff:
self.revoke_map.remove_event(e)
self._current_events.remove(e)
else:
break
events = driver.get_events(last_fetch=self._last_fetch)
self._last_fetch = timeutils.utcnow()
self.revoke_map.add_events(events)
self._current_events = self._current_events + events
@dependency.provider('revoke_api')
class Manager(manager.Manager):
"""Revoke API Manager.
@ -97,7 +73,7 @@ class Manager(manager.Manager):
def __init__(self):
super(Manager, self).__init__(CONF.revoke.driver)
self._register_listeners()
self._cache = _Cache()
self.model = model
def _user_callback(self, service, resource_type, operation,
payload):
@ -105,32 +81,32 @@ class Manager(manager.Manager):
def _role_callback(self, service, resource_type, operation,
payload):
self.driver.revoke(
self.revoke(
model.RevokeEvent(role_id=payload['resource_info']))
def _project_callback(self, service, resource_type, operation,
payload):
self.driver.revoke(
self.revoke(
model.RevokeEvent(project_id=payload['resource_info']))
def _domain_callback(self, service, resource_type, operation,
payload):
self.driver.revoke(
self.revoke(
model.RevokeEvent(domain_id=payload['resource_info']))
def _trust_callback(self, service, resource_type, operation,
payload):
self.driver.revoke(
self.revoke(
model.RevokeEvent(trust_id=payload['resource_info']))
def _consumer_callback(self, service, resource_type, operation,
payload):
self.driver.revoke(
self.revoke(
model.RevokeEvent(consumer_id=payload['resource_info']))
def _access_token_callback(self, service, resource_type, operation,
payload):
self.driver.revoke(
self.revoke(
model.RevokeEvent(access_token_id=payload['resource_info']))
def _register_listeners(self):
@ -149,33 +125,38 @@ class Manager(manager.Manager):
notifications.register_event_callback(*cb)
def revoke_by_user(self, user_id):
return self.driver.revoke(model.RevokeEvent(user_id=user_id))
return self.revoke(model.RevokeEvent(user_id=user_id))
def revoke_by_expiration(self, user_id, expires_at):
self.driver.revoke(
self.revoke(
model.RevokeEvent(user_id=user_id,
expires_at=expires_at))
def revoke_by_grant(self, role_id, user_id=None,
domain_id=None, project_id=None):
self.driver.revoke(
self.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.driver.revoke(
model.RevokeEvent(project_id=project_id,
user_id=user_id))
self.revoke(
model.RevokeEvent(project_id=project_id, user_id=user_id))
def revoke_by_project_role_assignment(self, project_id, role_id):
self.driver.revoke(model.RevokeEvent(project_id=project_id,
role_id=role_id))
self.revoke(model.RevokeEvent(project_id=project_id, role_id=role_id))
def revoke_by_domain_role_assignment(self, domain_id, role_id):
self.driver.revoke(model.RevokeEvent(domain_id=domain_id,
role_id=role_id))
self.revoke(model.RevokeEvent(domain_id=domain_id, role_id=role_id))
@cache.on_arguments(should_cache_fn=SHOULD_CACHE,
expiration_time=REVOCATION_CACHE_EXPIRATION_TIME)
def _get_revoke_tree(self):
events = self.driver.get_events()
revoke_tree = model.RevokeTree(revoke_events=events)
return revoke_tree
def check_token(self, token_values):
"""Checks the values from a token against the revocation list
@ -187,10 +168,13 @@ class Manager(manager.Manager):
:raises exception.TokenNotFound: if the token is invalid
"""
self._cache.synchronize_revoke_map(self.driver)
if self._cache.revoke_map.is_revoked(token_values):
if self._get_revoke_tree().is_revoked(token_values):
raise exception.TokenNotFound(_('Failed to validate token'))
def revoke(self, event):
self.driver.revoke(event)
self._get_revoke_tree.invalidate(self)
@six.add_metaclass(abc.ABCMeta)
class Driver(object):

View File

@ -15,6 +15,7 @@
import six
from keystone.common import config
from keystone.openstack.common.gettextutils import _ # flake8: noqa
from keystone.openstack.common import log
from keystone.openstack.common import strutils

View File

@ -24,7 +24,6 @@ from keystone.common import cache
from keystone.common import wsgi
from keystone import config
from keystone.contrib import endpoint_filter
from keystone.contrib import revoke
from keystone import controllers
from keystone import credential
from keystone import identity
@ -56,7 +55,6 @@ def load_backends():
endpoint_filter_api=endpoint_filter.Manager(),
identity_api=_IDENTITY_API,
policy_api=policy.Manager(),
revoke_api=revoke.Manager(),
token_api=token.Manager(),
trust_api=trust.Manager(),
token_provider_api=token.provider.Manager())

View File

@ -348,6 +348,7 @@ class BaseTestCase(testtools.TestCase):
return cleanup
@dependency.optional('revoke_api')
class TestCase(BaseTestCase):
_config_file_list = [dirs.etc('keystone.conf.sample'),
@ -426,15 +427,7 @@ class TestCase(BaseTestCase):
self.clear_auth_plugin_registry()
drivers = service.load_backends()
# TODO(stevemar): currently, load oauth1 driver as well, eventually
# we need to have this as optional.
from keystone.contrib import oauth1
drivers['oauth1_api'] = oauth1.Manager()
from keystone.contrib import federation
drivers['federation_api'] = federation.Manager()
dependency.resolve_future_dependencies()
drivers.update(dependency.resolve_future_dependencies())
for manager_name, manager in six.iteritems(drivers):
setattr(self, manager_name, manager)

View File

@ -20,6 +20,7 @@ import uuid
from keystoneclient.common import cms
from keystone import auth
from keystone.common import dependency
from keystone import config
from keystone import exception
from keystone.openstack.common import timeutils
@ -567,6 +568,7 @@ class TestTokenRevokeSelfAndAdmin(test_v3.RestfulTestCase):
token=adminB_token)
@dependency.requires('revoke_api')
class TestTokenRevokeById(test_v3.RestfulTestCase):
"""Test token revocation on the v3 Identity API."""
@ -1182,6 +1184,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase):
self.head(role_path, expected_status=404)
@dependency.requires('revoke_api')
class TestTokenRevokeApi(TestTokenRevokeById):
EXTENSION_NAME = 'revoke'
EXTENSION_TO_ADD = 'revoke_extension'
@ -2259,6 +2262,7 @@ class TestTrustOptional(test_v3.RestfulTestCase):
self.post('/auth/tokens', body=auth_data, expected_status=403)
@dependency.requires('revoke_api')
class TestTrustAuth(TestAuthInfo):
EXTENSION_NAME = 'revoke'
EXTENSION_TO_ADD = 'revoke_extension'

View File

@ -14,6 +14,7 @@ import random
import uuid
from keystone.auth import controllers as auth_controllers
from keystone.common import dependency
from keystone.common import sql
from keystone.common.sql import migration_helpers
from keystone import config
@ -36,6 +37,7 @@ def dummy_validator(*args, **kwargs):
pass
@dependency.requires('federation_api')
class FederationTests(test_v3.RestfulTestCase):
EXTENSION_NAME = 'federation'

View File

@ -22,7 +22,6 @@ from keystone.common import cache
from keystone.common import dependency
from keystone.common import manager
from keystone import config
from keystone.contrib.revoke import model as revoke_model
from keystone import exception
from keystone.openstack.common import log
@ -124,9 +123,9 @@ class Manager(manager.Manager):
except KeyError:
raise exception.TokenNotFound(_('Failed to validate token'))
token_values = revoke_model.build_token_values_v2(
token_data, CONF.identity.default_domain_id)
if self.revoke_api is not None:
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):
@ -144,8 +143,8 @@ class Manager(manager.Manager):
token_data = token['token']
except KeyError:
raise exception.TokenNotFound(_('Failed to validate token'))
token_values = revoke_model.build_token_values(token_data)
if self.revoke_api is not None:
token_values = self.revoke_api.model.build_token_values(token_data)
self.revoke_api.check_token(token_values)
def check_revocation(self, token):

View File

@ -354,9 +354,9 @@ class V3TokenDataHelper(object):
return {'token': token_data}
@dependency.optional('oauth_api')
@dependency.optional('oauth_api', 'revoke_api')
@dependency.requires('assignment_api', 'catalog_api', 'identity_api',
'revoke_api', 'token_api', 'trust_api')
'token_api', 'trust_api')
class BaseProvider(provider.Provider):
def __init__(self, *args, **kwargs):
super(BaseProvider, self).__init__(*args, **kwargs)